Compare commits

..

3 Commits

65 changed files with 2427 additions and 4065 deletions
@@ -1,4 +0,0 @@
-- Rollback: Remove requested_qty column from laying_transfer_sources table
ALTER TABLE laying_transfer_sources
DROP COLUMN IF EXISTS requested_qty;
@@ -1,9 +0,0 @@
-- Add requested_qty column to laying_transfer_sources table
-- This field stores the quantity requested by user during create/update
-- Separate from UsageQty (FIFO consumed) and PendingUsageQty (FIFO pending)
ALTER TABLE laying_transfer_sources
ADD COLUMN requested_qty NUMERIC(15,3) DEFAULT 0 NOT NULL;
-- Add comment for documentation
COMMENT ON COLUMN laying_transfer_sources.requested_qty IS 'Quantity requested by user during create/update';
@@ -1,3 +0,0 @@
ALTER TABLE recording_depletions
DROP COLUMN IF EXISTS pending_qty,
DROP COLUMN IF EXISTS source_product_warehouse_id;
@@ -1,17 +0,0 @@
ALTER TABLE recording_depletions
ADD COLUMN IF NOT EXISTS pending_qty numeric(15,3) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS source_product_warehouse_id bigint;
UPDATE recording_depletions rd
SET source_product_warehouse_id = src.product_warehouse_id
FROM recordings r
JOIN LATERAL (
SELECT pfp.product_warehouse_id
FROM project_chickins pc
JOIN project_flock_populations pfp ON pfp.project_chickin_id = pc.id
WHERE pc.project_flock_kandang_id = r.project_flock_kandangs_id
ORDER BY pfp.created_at ASC, pfp.id ASC
LIMIT 1
) AS src ON true
WHERE r.id = rd.recording_id
AND rd.source_product_warehouse_id IS NULL;
@@ -11,7 +11,6 @@ type LayingTransferSource struct {
LayingTransferId uint `gorm:"index;not null"` LayingTransferId uint `gorm:"index;not null"`
SourceProjectFlockKandangId uint `gorm:"not null"` SourceProjectFlockKandangId uint `gorm:"not null"`
ProductWarehouseId *uint `gorm:""` ProductWarehouseId *uint `gorm:""`
RequestedQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // Quantity requested by user
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
Note string `gorm:"type:text"` Note string `gorm:"type:text"`
+4 -6
View File
@@ -1,12 +1,10 @@
package entities package entities
type RecordingDepletion struct { type RecordingDepletion struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"` RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"` ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
SourceProductWarehouseId *uint `gorm:"column:source_product_warehouse_id"` Qty float64 `gorm:"column:qty;not null"`
Qty float64 `gorm:"column:qty;not null"`
PendingQty float64 `gorm:"column:pending_qty"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"` Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
-1
View File
@@ -52,7 +52,6 @@ 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,16 +14,14 @@ 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, closingKeuanganService service.ClosingKeuanganService) *ClosingController { func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService) *ClosingController {
return &ClosingController{ return &ClosingController{
ClosingService: closingService, ClosingService: closingService,
SapronakService: sapronakService, SapronakService: sapronakService,
ClosingKeuanganService: closingKeuanganService,
} }
} }
@@ -236,10 +234,9 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
} }
query := &validation.ClosingSapronakQuery{ query := &validation.ClosingSapronakQuery{
Type: strings.ToLower(c.Query("type")), Type: strings.ToLower(c.Query("type")),
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search"),
} }
if raw := c.Query("kandang_id"); raw != "" { if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw) kandangInt, convErr := strconv.Atoi(raw)
@@ -278,45 +275,6 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
}) })
} }
func (u *ClosingController) GetClosingSapronakSummary(c *fiber.Ctx) error {
param := c.Params("projectFlockId")
id, err := strconv.Atoi(param)
if err != nil || id <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
}
query := &validation.ClosingSapronakQuery{
Type: strings.ToLower(c.Query("type")),
Search: c.Query("search"),
}
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
if convErr != nil || kandangInt <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
kandangUint := uint(kandangInt)
query.KandangID = &kandangUint
}
if query.Type != validation.SapronakTypeIncoming && query.Type != validation.SapronakTypeOutgoing {
return fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
}
result, err := u.ClosingService.GetClosingSapronakSummary(c, uint(id), query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Retrieved closing report (sapronak summary) successfully",
Data: result,
})
}
func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error { func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
param := c.Params("project_flock_id") param := c.Params("project_flock_id")
flag := c.Query("flag", "") flag := c.Query("flag", "")
@@ -380,7 +338,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.ClosingKeuanganService.GetClosingKeuangan(c, uint(projectFlockID)) result, err := u.ClosingService.GetClosingKeuangan(c, uint(projectFlockID))
if err != nil { if err != nil {
return err return err
} }
@@ -394,34 +352,6 @@ func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
}) })
} }
func (u *ClosingController) GetClosingKeuanganByKandang(c *fiber.Ctx) error {
projectParam := c.Params("project_flock_id")
kandangParam := c.Params("project_flock_kandang_id")
projectFlockID, err := strconv.Atoi(projectParam)
if err != nil || projectFlockID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
pfkID, err := strconv.Atoi(kandangParam)
if err != nil || pfkID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
}
result, err := u.ClosingKeuanganService.GetClosingKeuanganByKandang(c, uint(projectFlockID), uint(pfkID))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing keuangan by kandang successfully",
Data: result,
})
}
func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error { func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error {
param := c.Params("project_flock_id") param := c.Params("project_flock_id")
+20 -20
View File
@@ -98,26 +98,26 @@ type ClosingEggSalesDTO struct {
} }
type ClosingPerformanceDTO struct { type ClosingPerformanceDTO struct {
Depletion float64 `json:"depletion"` Depletion float64 `json:"depletion"`
Age float64 `json:"age_day"` Age float64 `json:"age_day"`
MortalityStd float64 `json:"mor_std"` MortalityStd float64 `json:"mor_std"`
MortalityAct float64 `json:"mor_act"` MortalityAct float64 `json:"mor_act"`
DeffMortality float64 `json:"mor_diff"` DeffMortality float64 `json:"mor_diff"`
FcrStd float64 `json:"fcr_std"` FcrStd float64 `json:"fcr_std"`
FcrAct float64 `json:"fcr_act"` FcrAct float64 `json:"fcr_act"`
DeffFcr float64 `json:"fcr_diff"` DeffFcr float64 `json:"fcr_diff"`
AwgAct float64 `json:"awg_act"` AwgAct float64 `json:"awg_act"`
AwgStd float64 `json:"awg_std"` AwgStd float64 `json:"awg_std"`
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"` HendayStd *float64 `json:"hen_day_std,omitempty"`
EggMass float64 `json:"egg_mass,omitempty"` EggMass *float64 `json:"egg_mass,omitempty"`
EggMassStd float64 `json:"egg_mass_std"` EggMassStd *float64 `json:"egg_mass_std,omitempty"`
EggWeight float64 `json:"egg_weight,omitempty"` EggWeight *float64 `json:"egg_weight,omitempty"`
EggWeightStd float64 `json:"egg_weight_std"` EggWeightStd *float64 `json:"egg_weight_std,omitempty"`
HenHouseAct float64 `json:"hen_housed_act,omitempty"` HenHouseAct *float64 `json:"hen_housed_act,omitempty"`
HenHouseStd float64 `json:"hen_housed_std"` HenHouseStd *float64 `json:"hen_housed_std,omitempty"`
} }
type ClosingSalesGroupDTO struct { type ClosingSalesGroupDTO struct {
@@ -1,103 +1,135 @@
package dto package dto
// === CLOSING KEUANGAN CODES === import (
"slices"
"strings"
// Closing HPP Codes "gitlab.com/mbugroup/lti-api.git/internal/entities"
type ClosingHPPCode string "gitlab.com/mbugroup/lti-api.git/internal/utils"
const (
HPPCodePakan ClosingHPPCode = "PAKAN"
HPPCodeOVK ClosingHPPCode = "OVK"
HPPCodeDOC ClosingHPPCode = "DOC"
HPPCodeDepresiasi ClosingHPPCode = "DEPRESIASI"
HPPCodeOverhead ClosingHPPCode = "OVERHEAD"
HPPCodeEkspedisi ClosingHPPCode = "EKSPEDISI"
) )
// Closing Profit Loss Codes // === CONSTANTS ===
type ClosingProfitLossCode string
const ( const (
PLCodeSales ClosingProfitLossCode = "SALES" HPPGroupPengeluaran = "HPP dan Pengeluaran"
PLCodeSapronak ClosingProfitLossCode = "SAPRONAK" HPPGroupBahanBaku = "HPP dan Bahan Baku"
PLCodeOverhead ClosingProfitLossCode = "OVERHEAD" HPPLabelOverhead = "Pengeluaran Overhead"
PLCodeEkspedisi ClosingProfitLossCode = "EKSPEDISI" HPPLabelEkspedisi = "Beban Ekspedisi"
HPPSummaryLabel = "HPP"
PLSalesTypeChicken = "Penjualan Ayam Besar"
PLSalesTypeEgg = "Penjualan Telur"
PLItemTypeSapronak = "Pembelian Sapronak"
PLItemTypeOverhead = "Pengeluaran Overhead"
PLItemTypeEkspedisi = "Beban Ekspedisi"
PLSummaryLabelGrossProfit = "LABA RUGI BRUTTO"
PLSummaryLabelSubTotal = "SUB TOTAL"
PLSummaryLabelNetProfit = "LABA RUGI NETTO"
PurchaseLabelPrefix = "Pembelian "
) )
// === NEW CLOSING KEUANGAN DTO === // === CONTEXT STRUCTS ===
type CalculationContext struct {
TotalPopulation float64
TotalWeightProduced float64
TotalEggWeightKg float64
TotalDepletion float64
TotalWeightSold float64
ActualPopulation float64
}
type ClosingKeuanganInput struct {
ProjectFlockCategory string
PurchaseItems []entities.PurchaseItem
Budgets []entities.ProjectBudget
Realizations []entities.ExpenseRealization
DeliveryProducts []entities.MarketingDeliveryProduct
Chickins []entities.ProjectChickin
TotalWeightProduced float64
TotalEggWeightKg float64
TotalDepletion float64
}
// === BASE METRICS ===
// FinancialMetrics represents financial metrics with per unit and total amounts
type FinancialMetrics struct { 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"`
} }
// HPPItem represents an item in HPP section type Comparison struct {
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"`
} }
// HPPSummary represents summary for HPP section // === HPP PURCHASES PACKAGE ===
type HPPSummary struct {
Label string `json:"label"` type HppItem struct {
Budgeting FinancialMetrics `json:"budgeting"` Type string `json:"type"`
Realization FinancialMetrics `json:"realization"` Comparison
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
} }
// HPPSection represents HPP data section type HppGroup struct {
type HPPSection struct { GroupName string `json:"group_name"`
Items []HPPItem `json:"items"` Data []HppItem `json:"data"`
Summary HPPSummary `json:"summary"`
} }
// ProfitLossItem represents an item in Profit & Loss section type SummaryHpp struct {
type ProfitLossItem struct { Label string `json:"label"`
Code string `json:"code"` // "SALES", "PURCHASE_DOC", "OVERHEAD", "EKSPEDISI" Budgeting FinancialMetrics `json:"budgeting"`
Label string `json:"label"` Realization FinancialMetrics `json:"realization"`
Type string `json:"type"` // "income", "purchase", "overhead" EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
RpPerBird float64 `json:"rp_per_bird"` EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
RpPerKg float64 `json:"rp_per_kg"`
Amount float64 `json:"amount"`
} }
// ProfitLossSummary represents summary for Profit & Loss section type HppPurchasesSection struct {
type ProfitLossSummary struct { Hpp []HppGroup `json:"hpp"`
GrossProfit FinancialMetrics `json:"gross_profit"` SummaryHpp SummaryHpp `json:"summary_hpp"`
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 {
Items []ProfitLossItem `json:"items"` Data ProfitLossData `json:"data"`
Summary ProfitLossSummary `json:"summary"`
} }
// ClosingKeuanganData represents the main data structure // === RESPONSE DTO (ROOT) ===
type ClosingKeuanganData struct {
HPP HPPSection `json:"hpp"`
ProfitLoss ProfitLossSection `json:"profit_loss"`
}
// ClosingKeuanganResponse represents the full API response type ReportResponse struct {
type ClosingKeuanganResponse struct { HppPurchases HppPurchasesSection `json:"hpp_purchases"`
Code int `json:"code"` ProfitLoss ProfitLossSection `json:"profit_loss"`
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,
@@ -106,80 +138,451 @@ func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
} }
} }
// ToHPPItem creates HPP item func ToComparison(budgeting, realization FinancialMetrics) Comparison {
func ToHPPItem(id uint, category, code, label string, budgeting, realization FinancialMetrics) HPPItem { return Comparison{
return HPPItem{
ID: id,
Category: category,
Code: code,
Label: label,
Budgeting: budgeting, Budgeting: budgeting,
Realization: realization, Realization: realization,
} }
} }
// ToHPPSummary creates HPP summary // === HPP PENGELUARAN (from Purchase Items) ===
func ToHPPSummary(label string, budgeting, realization FinancialMetrics, eggBudgeting, eggRealization *FinancialMetrics) HPPSummary {
return HPPSummary{ func getFlagLabel(flagType utils.FlagType) string {
Label: label, return PurchaseLabelPrefix + string(flagType)
Budgeting: budgeting, }
Realization: realization,
EggBudgeting: eggBudgeting, func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, ctx CalculationContext) []HppItem {
EggRealization: eggRealization, flags := []utils.FlagType{
utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan,
utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher,
utils.FlagOVK, utils.FlagObat, utils.FlagVitamin, utils.FlagKimia,
}
items := []HppItem{}
seenFlags := make(map[utils.FlagType]bool)
for _, item := range purchaseItems {
if item.Product == nil || len(item.Product.Flags) == 0 {
continue
}
for _, flag := range item.Product.Flags {
flagType := utils.FlagType(flag.Name)
if slices.Contains(flags, flagType) && !seenFlags[flagType] {
amount := sumPurchasesByFlag(purchaseItems, flagType)
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.TotalPopulation, ctx.TotalWeightProduced)
items = append(items, HppItem{
Type: getFlagLabel(flagType),
Comparison: ToComparison(
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
),
})
seenFlags[flagType] = true
}
}
}
return items
}
// === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) ===
func createHppOverheadItem(budgetAmount, realizationAmount float64, ctx CalculationContext) HppItem {
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
return HppItem{
Type: HPPLabelOverhead,
Comparison: ToComparison(
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount),
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
),
} }
} }
// ToHPPSection creates HPP section func createHppEkspedisiItem(ekspedisiAmount float64, ctx CalculationContext) HppItem {
func ToHPPSection(items []HPPItem, summary HPPSummary) HPPSection { ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
return HPPSection{
Items: items, return HppItem{
Summary: summary, Type: HPPLabelEkspedisi,
Comparison: ToComparison(
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
),
} }
} }
// ToProfitLossItem creates Profit & Loss item func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppGroup {
func ToProfitLossItem(code, label, itemType string, rpPerBird, rpPerKg, amount float64) ProfitLossItem { items := []HppItem{}
return ProfitLossItem{
Code: code, budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
Label: label, realizationAmount := getOperationalExpenses(realizations)
Type: itemType,
RpPerBird: rpPerBird, if budgetAmount > 0 || realizationAmount > 0 {
RpPerKg: rpPerKg, items = append(items, createHppOverheadItem(budgetAmount, realizationAmount, ctx))
Amount: amount, }
ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
items = append(items, createHppEkspedisiItem(ekspedisiAmount, ctx))
return HppGroup{
GroupName: HPPGroupBahanBaku,
Data: items,
} }
} }
// ToProfitLossSummary creates Profit & Loss summary // === HPP SUMMARY ===
func ToProfitLossSummary(grossProfit, subTotal, netProfit FinancialMetrics) ProfitLossSummary {
return ProfitLossSummary{ func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) SummaryHpp {
GrossProfit: grossProfit, purchaseTotal := sumPurchaseTotal(purchaseItems)
SubTotal: subTotal, budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
NetProfit: netProfit, totalBudget := purchaseTotal + budgetTotal
totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true })
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, ctx.TotalPopulation, ctx.TotalWeightProduced)
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced)
summary := SummaryHpp{
Label: label,
Budgeting: ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
Realization: ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
}
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) && ctx.TotalEggWeightKg > 0 {
budgetEggRpPerKg, _ := calculatePerUnitMetrics(totalBudget, 0, ctx.TotalEggWeightKg)
realizationEggRpPerKg, _ := calculatePerUnitMetrics(totalRealization, 0, ctx.TotalEggWeightKg)
summary.EggBudgeting = &FinancialMetrics{
RpPerBird: 0,
RpPerKg: budgetEggRpPerKg,
Amount: totalBudget,
}
summary.EggRealization = &FinancialMetrics{
RpPerBird: 0,
RpPerKg: realizationEggRpPerKg,
Amount: totalRealization,
}
}
return summary
}
func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) HppPurchasesSection {
hppGroups := []HppGroup{
{
GroupName: HPPGroupPengeluaran,
Data: buildHppItemsByPurchaseFlags(purchaseItems, ctx),
},
ToHppBahanBakuGroup(budgets, realizations, ctx),
}
summaryHpp := ToSummaryHpp(HPPSummaryLabel, purchaseItems, budgets, realizations, projectFlockCategory, ctx)
return HppPurchasesSection{
Hpp: hppGroups,
SummaryHpp: summaryHpp,
} }
} }
// ToProfitLossSection creates Profit & Loss section // === PROFIT & LOSS ===
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{
Items: items, Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems),
Summary: summary,
} }
} }
// ToClosingKeuanganData creates complete closing keuangan data func aggregatePLItems(items []PLItem, label string) PLItem {
func ToClosingKeuanganData(hpp HPPSection, profitLoss ProfitLossSection) ClosingKeuanganData { totalAmount, totalPerBird := sumPLItems(items)
return ClosingKeuanganData{ return ToPLItem(label, ToFinancialMetrics(totalPerBird, 0, totalAmount))
HPP: hpp, }
ProfitLoss: profitLoss,
func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse {
return ReportResponse{
HppPurchases: hppPurchases,
ProfitLoss: profitLoss,
} }
} }
// ToSuccessClosingKeuanganResponse creates success response func ToClosingKeuanganReport(input ClosingKeuanganInput) ReportResponse {
func ToSuccessClosingKeuanganResponse(data ClosingKeuanganData) ClosingKeuanganResponse { var totalPopulation float64
return ClosingKeuanganResponse{ var totalWeightSold float64
Code: 200,
Status: "success", for _, chickin := range input.Chickins {
Message: "Get closing keuangan successfully", totalPopulation += chickin.UsageQty
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
}
@@ -0,0 +1,186 @@
package dto
// New Closing Keuangan Response DTO Structure
// Base metrics - digunakan di banyak tempat
type NewFinancialMetrics struct {
RpPerBird float64 `json:"rp_per_bird"`
RpPerKg float64 `json:"rp_per_kg"`
Amount float64 `json:"amount"`
}
// Comparison untuk Budgeting vs Realization
type NewComparison struct {
Budgeting NewFinancialMetrics `json:"budgeting"`
Realization NewFinancialMetrics `json:"realization"`
}
// HPP Purchase Section
type HppPurchase struct {
Pakan NewComparison `json:"pakan"`
OVK NewComparison `json:"OVK"`
DOC NewComparison `json:"DOC"`
Depresiasi NewComparison `json:"Depresiasi"`
}
// HPP Overhead Section
type HppOverhead struct {
Overhead NewComparison `json:"overhead"`
Ekspedisi NewComparison `json:"ekspedisi"`
}
// Summary HPP
type NewSummaryHpp struct {
Label string `json:"label"`
Budgeting NewFinancialMetrics `json:"budgeting"`
Realization NewFinancialMetrics `json:"realization"`
EggBudgeting *NewFinancialMetrics `json:"egg_budgeting,omitempty"`
EggRealization *NewFinancialMetrics `json:"egg_realization,omitempty"`
}
// HPP wrapper
type NewHpp struct {
HppPurchase HppPurchase `json:"hpp_purchase"`
HppOverhead HppOverhead `json:"hpp_overhead"`
SummaryHpp NewSummaryHpp `json:"summary_hpp"`
}
// Purchase Cost (dengan type field, embedded NewFinancialMetrics)
type PurchaseCost struct {
Type string `json:"type"`
NewFinancialMetrics
}
// PL Summary
type PLSummary struct {
GrossProfit NewFinancialMetrics `json:"gross_profit"`
SubTotal NewFinancialMetrics `json:"sub_total"`
NetProfit NewFinancialMetrics `json:"net_profit"`
}
// Profit Loss wrapper
type NewProfitLoss struct {
PenjualanTelur NewFinancialMetrics `json:"penjualan_telur"`
PurchaseCost PurchaseCost `json:"purchase_cost"`
Overhead NewFinancialMetrics `json:"overhead"`
Ekspedisi NewFinancialMetrics `json:"ekspedisi"`
Summary PLSummary `json:"summary"`
}
// Main Data structure
type NewClosingKeuanganData struct {
Hpp NewHpp `json:"hpp"`
ProfitLoss NewProfitLoss `json:"profit_loss"`
}
// Full Response DTO
type NewClosingKeuanganResponse struct {
Code int `json:"code"`
Status string `json:"status"`
Message string `json:"message"`
Data NewClosingKeuanganData `json:"data"`
}
// === MAPPER FUNCTIONS ===
// ToNewFinancialMetrics creates a new financial metrics
func ToNewFinancialMetrics(rpPerBird, rpPerKg, amount float64) NewFinancialMetrics {
return NewFinancialMetrics{
RpPerBird: rpPerBird,
RpPerKg: rpPerKg,
Amount: amount,
}
}
// ToNewComparison creates a new budgeting vs realization comparison
func ToNewComparison(budgetingRpPerBird, budgetingRpPerKg, budgetingAmount, realizationRpPerBird, realizationRpPerKg, realizationAmount float64) NewComparison {
return NewComparison{
Budgeting: ToNewFinancialMetrics(budgetingRpPerBird, budgetingRpPerKg, budgetingAmount),
Realization: ToNewFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
}
}
// ToHppPurchase creates HPP purchase section
func ToHppPurchase(pakan, oVK, dOC, depresiasi NewComparison) HppPurchase {
return HppPurchase{
Pakan: pakan,
OVK: oVK,
DOC: dOC,
Depresiasi: depresiasi,
}
}
// ToHppOverhead creates HPP overhead section
func ToHppOverhead(overhead, ekspedisi NewComparison) HppOverhead {
return HppOverhead{
Overhead: overhead,
Ekspedisi: ekspedisi,
}
}
// ToNewSummaryHpp creates HPP summary
func ToNewSummaryHpp(label string, budgeting, realization NewFinancialMetrics, eggBudgeting, eggRealization *NewFinancialMetrics) NewSummaryHpp {
return NewSummaryHpp{
Label: label,
Budgeting: budgeting,
Realization: realization,
EggBudgeting: eggBudgeting,
EggRealization: eggRealization,
}
}
// ToNewHpp creates complete HPP section
func ToNewHpp(hppPurchase HppPurchase, hppOverhead HppOverhead, summaryHpp NewSummaryHpp) NewHpp {
return NewHpp{
HppPurchase: hppPurchase,
HppOverhead: hppOverhead,
SummaryHpp: summaryHpp,
}
}
// ToPurchaseCost creates purchase cost item
func ToPurchaseCost(costType string, metrics NewFinancialMetrics) PurchaseCost {
return PurchaseCost{
Type: costType,
NewFinancialMetrics: metrics,
}
}
// ToPLSummary creates profit loss summary
func ToPLSummary(grossProfit, subTotal, netProfit NewFinancialMetrics) PLSummary {
return PLSummary{
GrossProfit: grossProfit,
SubTotal: subTotal,
NetProfit: netProfit,
}
}
// ToNewProfitLoss creates complete profit loss section
func ToNewProfitLoss(penjualanTelur, overhead, ekspedisi NewFinancialMetrics, purchaseCost PurchaseCost, summary PLSummary) NewProfitLoss {
return NewProfitLoss{
PenjualanTelur: penjualanTelur,
PurchaseCost: purchaseCost,
Overhead: overhead,
Ekspedisi: ekspedisi,
Summary: summary,
}
}
// ToNewClosingKeuanganData creates complete closing keuangan data
func ToNewClosingKeuanganData(hpp NewHpp, profitLoss NewProfitLoss) NewClosingKeuanganData {
return NewClosingKeuanganData{
Hpp: hpp,
ProfitLoss: profitLoss,
}
}
// ToSuccessNewClosingKeuanganResponse creates success response shortcut
func ToSuccessNewClosingKeuanganResponse(data NewClosingKeuanganData) NewClosingKeuanganResponse {
return NewClosingKeuanganResponse{
Code: 200,
Status: "success",
Message: "Get closing keuangan successfully",
Data: data,
}
}
@@ -114,17 +114,6 @@ type ClosingSapronakDTO struct {
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"` OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
} }
type ClosingSapronakSummaryItemDTO struct {
Category string `json:"category"`
TotalQty int64 `json:"total_qty"`
Uom UomSummaryDTO `json:"uom"`
}
type UomSummaryDTO struct {
ID uint `json:"id"`
Name string `json:"name"`
}
// === Mapper Functions for Aggregated Sapronak Response === // === Mapper Functions for Aggregated Sapronak Response ===
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO { func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
@@ -212,48 +201,18 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
switch strings.ToLower(item.JenisTransaksi) { switch strings.ToLower(item.JenisTransaksi) {
case "pembelian", "adjustment masuk", "mutasi masuk": case "pembelian", "adjustment masuk", "mutasi masuk":
row.QtyIn += item.QtyMasuk row.QtyIn += item.QtyMasuk
if row.UnitPrice == 0 { row.TotalAmount += item.Nilai
if item.QtyMasuk > 0 && item.Nilai > 0 {
row.UnitPrice = item.Nilai / item.QtyMasuk
} else if item.Harga > 0 {
row.UnitPrice = item.Harga
}
}
if strings.ToLower(item.JenisTransaksi) == "mutasi masuk" {
ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi))
if strings.HasPrefix(ref, "TL-") {
row.Notes = "TRANSFER LAYING"
} else if strings.HasPrefix(ref, "ST-") {
row.Notes = "TRANSFER STOCK"
}
}
case "pemakaian", "adjustment keluar": case "pemakaian", "adjustment keluar":
price := row.UnitPrice
if price == 0 {
price = item.Harga
}
row.QtyUsed += item.QtyKeluar row.QtyUsed += item.QtyKeluar
row.TotalAmount += item.QtyKeluar * price case "mutasi keluar":
case "mutasi keluar", "penjualan":
price := row.UnitPrice
if price == 0 {
price = item.Harga
}
row.QtyOut += item.QtyKeluar row.QtyOut += item.QtyKeluar
if strings.ToLower(item.JenisTransaksi) == "mutasi keluar" {
ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi))
if strings.HasPrefix(ref, "TL-") {
row.Notes = "TRANSFER LAYING"
} else if strings.HasPrefix(ref, "ST-") {
row.Notes = "TRANSFER STOCK"
}
}
default: default:
row.QtyIn += item.QtyMasuk row.QtyIn += item.QtyMasuk
row.TotalAmount += item.Nilai row.TotalAmount += item.Nilai
if row.QtyIn > 0 { }
row.UnitPrice = row.TotalAmount / row.QtyIn
} if row.QtyIn > 0 {
row.UnitPrice = row.TotalAmount / row.QtyIn
} }
} }
@@ -274,8 +233,8 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
total += r.TotalAmount total += r.TotalAmount
} }
avg := 0.0 avg := 0.0
if qtyUsed > 0 { if qtyIn > 0 {
avg = total / qtyUsed avg = total / qtyIn
} }
cat.Total = SapronakCategoryTotalDTO{ cat.Total = SapronakCategoryTotalDTO{
Label: label, Label: label,
+2 -3
View File
@@ -41,10 +41,9 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
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, closingKeuanganRepo, 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, closingKeuanganService) ClosingRoutes(router, userService, closingService, sapronakService)
} }
@@ -10,14 +10,12 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm" "gorm.io/gorm"
) )
type ClosingRepository interface { type ClosingRepository interface {
repository.BaseRepository[entity.ProjectFlock] repository.BaseRepository[entity.ProjectFlock]
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
GetSapronakSummary(ctx context.Context, params SapronakQueryParams) ([]SapronakSummaryRow, error)
SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error)
SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
@@ -25,7 +23,7 @@ type ClosingRepository interface {
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error)
GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error)
GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error)
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
@@ -34,8 +32,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)
FetchSapronakSales(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
} }
type ClosingRepositoryImpl struct { type ClosingRepositoryImpl struct {
@@ -61,30 +57,16 @@ type SapronakRow struct {
DestinationWarehouse string `gorm:"column:destination_warehouse"` DestinationWarehouse string `gorm:"column:destination_warehouse"`
Destination string `gorm:"column:destination"` Destination string `gorm:"column:destination"`
Quantity float64 `gorm:"column:quantity"` Quantity float64 `gorm:"column:quantity"`
UnitID uint `gorm:"column:unit_id"`
Unit string `gorm:"column:unit"` Unit string `gorm:"column:unit"`
Notes string `gorm:"column:notes"` Notes string `gorm:"column:notes"`
} }
type SapronakSummaryRow struct {
Category string `gorm:"column:category"`
TotalQty int64 `gorm:"column:total_qty"`
UomID uint `gorm:"column:uom_id"`
UomName string `gorm:"column:uom_name"`
}
type ExpeditionHPPRow struct {
SupplierName string `gorm:"column:supplier_name"`
TotalAmount float64 `gorm:"column:total_amount"`
}
type SapronakQueryParams struct { type SapronakQueryParams struct {
Type string Type string
WarehouseIDs []uint WarehouseIDs []uint
ProjectFlockKandangIDs []uint ProjectFlockKandangIDs []uint
Limit int Limit int
Offset int Offset int
Search string
} }
func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) { func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
@@ -120,36 +102,14 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
unionSQL := strings.Join(unionParts, " UNION ALL ") unionSQL := strings.Join(unionParts, " UNION ALL ")
search := strings.TrimSpace(params.Search)
searchClause := ""
var searchArgs []any
if search != "" {
searchClause = `
WHERE (
reference_number ILIKE ?
OR product_name ILIKE ?
OR product_category ILIKE ?
OR source_warehouse ILIKE ?
OR destination_warehouse ILIKE ?
OR CAST(quantity AS TEXT) ILIKE ?
OR unit ILIKE ?
OR notes ILIKE ?
OR transaction_type ILIKE ?
)`
like := "%" + search + "%"
searchArgs = append(searchArgs, like, like, like, like, like, like, like, like, like)
}
var totalResults int64 var totalResults int64
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined%s", unionSQL, searchClause) countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined", unionSQL)
countArgs := append(append([]any{}, args...), searchArgs...) if err := db.Raw(countSQL, args...).Scan(&totalResults).Error; err != nil {
if err := db.Raw(countSQL, countArgs...).Scan(&totalResults).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
dataArgs := append(append([]any{}, args...), searchArgs...) dataArgs := append(append([]any{}, args...), params.Limit, params.Offset)
dataArgs = append(dataArgs, params.Limit, params.Offset) dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL)
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined%s ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL, searchClause)
var rows []SapronakRow var rows []SapronakRow
if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil { if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil {
@@ -159,293 +119,6 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
return rows, totalResults, nil return rows, totalResults, nil
} }
func (r *ClosingRepositoryImpl) GetSapronakSummary(ctx context.Context, params SapronakQueryParams) ([]SapronakSummaryRow, error) {
db := r.DB().WithContext(ctx)
var (
unionParts []string
args []any
)
switch params.Type {
case validation.SapronakTypeIncoming:
if len(params.WarehouseIDs) == 0 {
return []SapronakSummaryRow{}, nil
}
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL)
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
case validation.SapronakTypeOutgoing:
if len(params.WarehouseIDs) > 0 {
unionParts = append(unionParts, sapronakOutgoingTransfersSQL)
args = append(args, params.WarehouseIDs)
}
if len(params.ProjectFlockKandangIDs) > 0 {
unionParts = append(unionParts, sapronakOutgoingMarketingsSQL)
args = append(args, params.ProjectFlockKandangIDs)
}
if len(unionParts) == 0 {
return []SapronakSummaryRow{}, nil
}
default:
return nil, fmt.Errorf("invalid sapronak type: %s", params.Type)
}
unionSQL := strings.Join(unionParts, " UNION ALL ")
search := strings.TrimSpace(params.Search)
searchClause := ""
var searchArgs []any
if search != "" {
searchClause = `
WHERE (
reference_number ILIKE ?
OR product_name ILIKE ?
OR product_category ILIKE ?
OR source_warehouse ILIKE ?
OR destination_warehouse ILIKE ?
OR CAST(quantity AS TEXT) ILIKE ?
OR unit ILIKE ?
OR notes ILIKE ?
OR transaction_type ILIKE ?
)`
like := "%" + search + "%"
searchArgs = append(searchArgs, like, like, like, like, like, like, like, like, like)
}
querySQL := fmt.Sprintf(`
SELECT
product_category AS category,
CAST(COALESCE(SUM(quantity), 0) AS BIGINT) AS total_qty,
unit_id AS uom_id,
unit AS uom_name
FROM (%s) AS combined%s
GROUP BY product_category, unit_id, unit
ORDER BY product_category ASC, unit ASC
`, unionSQL, searchClause)
queryArgs := append(append([]any{}, args...), searchArgs...)
var rows []SapronakSummaryRow
if err := db.Raw(querySQL, queryArgs...).Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, 0, nil
}
var purchaseAgg struct {
TotalIn float64 `gorm:"column:total_in"`
}
err := r.DB().WithContext(ctx).
Table("purchase_items pi").
Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'").
Where("f.name = ?", "PAKAN").
Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(pi.total_qty), 0) AS total_in").
Scan(&purchaseAgg).Error
if err != nil {
return 0, 0, err
}
var usageAgg struct {
TotalUsed float64 `gorm:"column:total_used"`
}
err = r.DB().WithContext(ctx).
Table("recording_stocks rs").
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name = ?", "PAKAN").
Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used").
Scan(&usageAgg).Error
if err != nil {
return 0, 0, err
}
return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil
}
func (r *ClosingRepositoryImpl) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
var total float64
if err := r.DB().WithContext(ctx).
Model(&entity.ProjectChickin{}).
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(usage_qty), 0)").
Scan(&total).Error; err != nil {
return 0, err
}
return total, nil
}
func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
var agg struct {
Total float64 `gorm:"column:total_culling"`
}
err := r.DB().WithContext(ctx).
Table("recording_depletions rd").
Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name = ?", utils.FlagAyamCulling).
Select("COALESCE(SUM(rd.qty), 0) AS total_culling").
Scan(&agg).Error
if err != nil {
return 0, err
}
return agg.Total, nil
}
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, 0, 0, nil
}
var agg struct {
TotalWeight float64 `gorm:"column:total_weight"`
TotalQty float64 `gorm:"column:total_qty"`
TotalPrice float64 `gorm:"column:total_price"`
}
err := r.DB().WithContext(ctx).
Table("marketing_products mp").
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
Scan(&agg).Error
if err != nil {
return 0, 0, 0, err
}
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
}
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) {
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
return 0, 0, 0, nil
}
var agg struct {
TotalWeight float64 `gorm:"column:total_weight"`
TotalQty float64 `gorm:"column:total_qty"`
TotalPrice float64 `gorm:"column:total_price"`
}
err := r.DB().WithContext(ctx).
Table("marketing_products mp").
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name IN ?", flagNames).
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
Scan(&agg).Error
if err != nil {
return 0, 0, 0, err
}
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
}
func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) {
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
return 0, nil
}
var agg struct {
TotalQty float64 `gorm:"column:total_qty"`
}
err := r.DB().WithContext(ctx).
Table("recording_eggs re").
Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name IN ?", flagNames).
Select("COALESCE(SUM(re.qty), 0) AS total_qty").
Scan(&agg).Error
if err != nil {
return 0, err
}
return agg.TotalQty, nil
}
func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) {
if fcrID == 0 {
return []entity.FcrStandard{}, nil
}
var standards []entity.FcrStandard
if err := r.DB().WithContext(ctx).
Where("fcr_id = ?", fcrID).
Order("weight ASC").
Find(&standards).Error; err != nil {
return nil, err
}
return standards, nil
}
func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) {
db := r.DB().WithContext(ctx)
if projectFlockID == 0 {
return nil, fmt.Errorf("invalid project flock id")
}
query := db.
Table("expense_realizations AS er").
Joins("JOIN expense_nonstocks ens ON ens.id = er.expense_nonstock_id").
Joins("JOIN expenses e ON e.id = ens.expense_id").
Joins("JOIN project_flock_kandangs pfk ON pfk.id = ens.project_flock_kandang_id").
Joins("JOIN nonstocks n ON n.id = ens.nonstock_id").
Joins("JOIN flags f ON f.flagable_id = n.id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
Joins("JOIN suppliers s ON s.id = e.supplier_id").
Where("pfk.project_flock_id = ?", projectFlockID).
Where("e.category = ?", "BOP").
Where("e.realization_date IS NOT NULL").
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi)))
if projectFlockKandangID != nil && *projectFlockKandangID != 0 {
query = query.Where("pfk.id = ?", *projectFlockKandangID)
}
var rows []ExpeditionHPPRow
err := query.
Select(
"e.supplier_id AS supplier_id, " +
"s.name AS supplier_name, " +
"SUM(er.qty * er.price) AS total_amount",
).
Group("e.supplier_id, s.name").
Scan(&rows).Error
if err != nil {
return nil, err
}
return rows, nil
}
const ( const (
sapronakIncomingPurchasesSQL = ` sapronakIncomingPurchasesSQL = `
SELECT SELECT
@@ -485,7 +158,6 @@ SELECT
w.name AS destination_warehouse, w.name AS destination_warehouse,
'' AS destination, '' AS destination,
pi.total_qty AS quantity, pi.total_qty AS quantity,
u.id AS unit_id,
u.name AS unit, u.name AS unit,
COALESCE(p.notes, '') AS notes COALESCE(p.notes, '') AS notes
FROM purchase_items pi FROM purchase_items pi
@@ -534,7 +206,6 @@ SELECT
COALESCE(tw.name, '') AS destination_warehouse, COALESCE(tw.name, '') AS destination_warehouse,
'' AS destination, '' AS destination,
std.usage_qty AS quantity, std.usage_qty AS quantity,
u.id AS unit_id,
u.name AS unit, u.name AS unit,
'Stock Refill' AS notes 'Stock Refill' AS notes
FROM stock_transfer_details std FROM stock_transfer_details std
@@ -581,10 +252,9 @@ 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,
COALESCE(tw.name, '') AS destination_warehouse, '' AS destination_warehouse,
'' AS destination, COALESCE(tw.name, '') AS destination,
std.usage_qty AS quantity, std.usage_qty AS quantity,
u.id AS unit_id,
u.name AS unit, u.name AS unit,
'Transfer to other unit' AS notes 'Transfer to other unit' AS notes
FROM stock_transfer_details std FROM stock_transfer_details std
@@ -631,27 +301,18 @@ 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,
COALESCE(c.name, '') AS destination_warehouse, '' AS destination_warehouse,
'' AS destination, 'RETAIL CUSTOMER' AS destination,
mp.qty AS quantity, mp.qty AS quantity,
u.id AS unit_id,
u.name AS unit, u.name AS unit,
m.notes AS notes m.notes AS notes
FROM marketing_products mp FROM marketing_products mp
JOIN marketings m ON m.id = mp.marketing_id JOIN marketings m ON m.id = mp.marketing_id
LEFT JOIN customers c ON c.id = m.customer_id
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
JOIN products prod ON prod.id = pw.product_id JOIN products prod ON prod.id = pw.product_id
JOIN uoms u ON u.id = prod.uom_id JOIN uoms u ON u.id = prod.uom_id
JOIN warehouses w ON w.id = pw.warehouse_id JOIN warehouses w ON w.id = pw.warehouse_id
WHERE pw.project_flock_kandang_id IN ? WHERE pw.project_flock_kandang_id IN ?
AND EXISTS (
SELECT 1
FROM flags f
WHERE f.flagable_id = pw.product_id
AND f.flagable_type = 'products'
AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET')
)
` `
) )
@@ -1007,156 +668,193 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka
} }
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) { func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
incomingQuery := r.withCtx(ctx). rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeTransfer), true)
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("LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_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("(fw.kandang_id IS NULL OR fw.kandang_id <> w.kandang_id)").
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 {
incomingLayingQuery := r.withCtx(ctx). if ref := strings.TrimSpace(row.MovementNumber); ref != "" {
Table("laying_transfer_targets AS ltt"). return ref
Select(` }
pw.product_id AS product_id, return fmt.Sprintf("TRF-%d", row.ID)
p.name AS product_name, })
f.name AS flag, return in, out, nil
lt.transfer_date::timestamp AS date,
COALESCE(lt.transfer_number, '') AS reference,
COALESCE(ltt.total_qty, 0) AS qty_in,
0 AS qty_out,
COALESCE(p.product_price, 0) AS price
`).
Joins("JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
Joins("LEFT JOIN laying_transfer_sources lts ON lts.laying_transfer_id = lt.id").
Joins("LEFT JOIN product_warehouses pw_source ON pw_source.id = lts.product_warehouse_id").
Joins("LEFT JOIN warehouses w_source ON w_source.id = pw_source.warehouse_id").
Joins("JOIN product_warehouses pw ON pw.id = ltt.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("w.kandang_id = ?", kandangID).
Where("(w_source.kandang_id IS NULL OR w_source.kandang_id <> w.kandang_id)").
Where("f.name IN ?", sapronakFlagsAll)
incomingLaying, err := scanAndGroupDetails(incomingLayingQuery)
if err != nil {
return nil, nil, err
}
for pid, rows := range incomingLaying {
incoming[pid] = append(incoming[pid], rows...)
}
outgoingQuery := r.withCtx(ctx).
Table("stock_allocations AS sa").
Select(`
std.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
st.transfer_date::timestamp AS date,
COALESCE(st.movement_number, '') AS reference,
0 AS qty_in,
COALESCE(SUM(sa.qty), 0) AS qty_out,
COALESCE(p.product_price, 0) AS price
`).
Joins("JOIN stock_transfer_details std ON std.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyStockTransferOut.String()).
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = std.dest_product_warehouse_id").
Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id").
Joins("JOIN products p ON p.id = std.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("w.kandang_id = ?", kandangID).
Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
Where("f.name IN ?", sapronakFlagsAll).
Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price")
outgoing, err := scanAndGroupDetails(outgoingQuery)
if err != nil {
return nil, nil, err
}
outgoingLayingQuery := r.withCtx(ctx).
Table("stock_allocations AS sa").
Select(`
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
lt.transfer_date::timestamp AS date,
COALESCE(lt.transfer_number, '') AS reference,
0 AS qty_in,
COALESCE(SUM(sa.qty), 0) AS qty_out,
COALESCE(p.product_price, 0) AS price
`).
Joins("JOIN laying_transfer_sources lts ON lts.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyTransferToLayingOut.String()).
Joins("JOIN laying_transfers lt ON lt.id = lts.laying_transfer_id").
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.laying_transfer_id = lt.id").
Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = ltt.product_warehouse_id").
Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id").
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("w.kandang_id = ?", kandangID).
Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
Where("f.name IN ?", sapronakFlagsAll).
Group("lts.id, pw.product_id, p.name, f.name, lt.transfer_date, lt.transfer_number, p.product_price")
outgoingLaying, err := scanAndGroupDetails(outgoingLayingQuery)
if err != nil {
return nil, nil, err
}
for pid, rows := range outgoingLaying {
outgoing[pid] = append(outgoing[pid], rows...)
}
return incoming, outgoing, nil
} }
func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) { // === CLOSING DATA PRODUKSI METHODS ===
query := r.withCtx(ctx).
Table("stock_allocations AS sa").
Select(`
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
COALESCE(mdp.delivery_date, mdp.created_at) AS date,
COALESCE(m.so_number, '') AS reference,
0 AS qty_in,
COALESCE(SUM(sa.qty), 0) AS qty_out,
COALESCE(mdp.unit_price, mp.unit_price, 0) AS price
`).
Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyMarketingDelivery.String()).
Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
Joins("JOIN marketings m ON m.id = mp.marketing_id").
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("w.kandang_id = ?", kandangID).
Where("f.name IN ?", sapronakFlagsAll).
Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price")
return scanAndGroupDetails(query) func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, 0, nil
}
var purchaseAgg struct {
TotalIn float64 `gorm:"column:total_in"`
}
err := r.DB().WithContext(ctx).
Table("purchase_items pi").
Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'").
Where("f.name = ?", "PAKAN").
Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(pi.total_qty), 0) AS total_in").
Scan(&purchaseAgg).Error
if err != nil {
return 0, 0, err
}
var usageAgg struct {
TotalUsed float64 `gorm:"column:total_used"`
}
err = r.DB().WithContext(ctx).
Table("recording_stocks rs").
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name = ?", "PAKAN").
Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used").
Scan(&usageAgg).Error
if err != nil {
return 0, 0, err
}
return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil
}
func (r *ClosingRepositoryImpl) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
var total float64
if err := r.DB().WithContext(ctx).
Model(&entity.ProjectChickin{}).
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(usage_qty), 0)").
Scan(&total).Error; err != nil {
return 0, err
}
return total, nil
}
func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
var agg struct {
Total float64 `gorm:"column:total_culling"`
}
err := r.DB().WithContext(ctx).
Table("recording_depletions rd").
Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name = ?", utils.FlagAyamCulling).
Select("COALESCE(SUM(rd.qty), 0) AS total_culling").
Scan(&agg).Error
if err != nil {
return 0, err
}
return agg.Total, nil
}
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, 0, 0, nil
}
var agg struct {
TotalWeight float64 `gorm:"column:total_weight"`
TotalQty float64 `gorm:"column:total_qty"`
TotalPrice float64 `gorm:"column:total_price"`
}
err := r.DB().WithContext(ctx).
Table("marketing_products mp").
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
Scan(&agg).Error
if err != nil {
return 0, 0, 0, err
}
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
}
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) {
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
return 0, 0, 0, nil
}
var agg struct {
TotalWeight float64 `gorm:"column:total_weight"`
TotalQty float64 `gorm:"column:total_qty"`
TotalPrice float64 `gorm:"column:total_price"`
}
err := r.DB().WithContext(ctx).
Table("marketing_products mp").
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name IN ?", flagNames).
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
Scan(&agg).Error
if err != nil {
return 0, 0, 0, err
}
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
}
func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) {
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
return 0, nil
}
var agg struct {
TotalQty float64 `gorm:"column:total_qty"`
}
err := r.DB().WithContext(ctx).
Table("recording_eggs re").
Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name IN ?", flagNames).
Select("COALESCE(SUM(re.qty), 0) AS total_qty").
Scan(&agg).Error
if err != nil {
return 0, err
}
return agg.TotalQty, nil
}
func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) {
if fcrID == 0 {
return []entity.FcrStandard{}, nil
}
var standards []entity.FcrStandard
if err := r.DB().WithContext(ctx).
Where("fcr_id = ?", fcrID).
Order("weight ASC").
Find(&standards).Error; err != nil {
return nil, err
}
return standards, nil
} }
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) { func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
File diff suppressed because it is too large Load Diff
+2 -4
View File
@@ -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, closingKeuanganSvc closing.ClosingKeuanganService) { func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService) {
ctrl := controller.NewClosingController(s, sapronakSvc, closingKeuanganSvc) ctrl := controller.NewClosingController(s, sapronakSvc)
route := v1.Group("/closings") route := v1.Group("/closings")
route.Use(m.Auth(u)) route.Use(m.Auth(u))
@@ -30,11 +30,9 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang) route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang)
route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject) route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject)
route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak) route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak)
route.Get("/:projectFlockId/sapronak/summary", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronakSummary)
route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP) route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP)
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,7 @@ 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)
GetClosingSapronakSummary(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, 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)
} }
@@ -48,6 +48,7 @@ type closingService struct {
Log *logrus.Logger Log *logrus.Logger
Validate *validator.Validate Validate *validator.Validate
Repository repository.ClosingRepository Repository repository.ClosingRepository
ClosingKeuanganRepo repository.ClosingKeuanganRepository
ProjectFlockRepo projectflockRepository.ProjectflockRepository ProjectFlockRepo projectflockRepository.ProjectflockRepository
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
MarketingRepo marketingRepository.MarketingRepository MarketingRepo marketingRepository.MarketingRepository
@@ -62,11 +63,12 @@ type closingService struct {
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
} }
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository, productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository, validate *validator.Validate) ClosingService { func NewClosingService(repo repository.ClosingRepository, closingKeuanganRepo repository.ClosingKeuanganRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository, productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository, validate *validator.Validate) ClosingService {
return &closingService{ return &closingService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
Repository: repo, Repository: repo,
ClosingKeuanganRepo: closingKeuanganRepo,
ProjectFlockRepo: projectFlockRepo, ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo, ProjectFlockKandangRepo: projectFlockKandangRepo,
MarketingRepo: marketingRepo, MarketingRepo: marketingRepo,
@@ -337,10 +339,8 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
} }
var projectFlockKandangIDs []uint var projectFlockKandangIDs []uint
if params.KandangID != nil && *params.KandangID > 0 { if params.Type == validation.SapronakTypeOutgoing {
projectFlockKandangIDs = []uint{*params.KandangID} projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID, 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")
@@ -354,7 +354,6 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
ProjectFlockKandangIDs: projectFlockKandangIDs, ProjectFlockKandangIDs: projectFlockKandangIDs,
Limit: params.Limit, Limit: params.Limit,
Offset: offset, Offset: offset,
Search: params.Search,
}) })
if err != nil { if err != nil {
s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err) s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err)
@@ -389,74 +388,6 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
return items, totalResults, nil return items, totalResults, nil
} }
func (s closingService) GetClosingSapronakSummary(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, error) {
if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
}
if params == nil {
params = &validation.ClosingSapronakQuery{}
}
if err := s.Validate.Struct(params); err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if params.Type != validation.SapronakTypeIncoming && params.Type != validation.SapronakTypeOutgoing {
return nil, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
}
if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
}
s.Log.Errorf("Failed get project flock %d for sapronak closing summary: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID)
if err != nil {
s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock")
}
var projectFlockKandangIDs []uint
if params.KandangID != nil && *params.KandangID > 0 {
projectFlockKandangIDs = []uint{*params.KandangID}
} else if params.Type == validation.SapronakTypeOutgoing {
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
}
}
rows, err := s.Repository.GetSapronakSummary(c.Context(), repository.SapronakQueryParams{
Type: params.Type,
WarehouseIDs: warehouseIDs,
ProjectFlockKandangIDs: projectFlockKandangIDs,
Search: params.Search,
})
if err != nil {
s.Log.Errorf("Failed to fetch sapronak %s summary for project flock %d: %+v", params.Type, projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sapronak summary data")
}
items := make([]dto.ClosingSapronakSummaryItemDTO, 0, len(rows))
for _, row := range rows {
items = append(items, dto.ClosingSapronakSummaryItemDTO{
Category: row.Category,
TotalQty: row.TotalQty,
Uom: dto.UomSummaryDTO{
ID: row.UomID,
Name: row.UomName,
},
})
}
return items, nil
}
func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) { func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) {
var kandangIDs []uint var kandangIDs []uint
db := s.Repository.DB().WithContext(ctx) db := s.Repository.DB().WithContext(ctx)
@@ -489,11 +420,14 @@ func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, proje
return ids, nil return ids, nil
} }
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) { func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint, kandangID *uint) ([]uint, error) {
var ids []uint 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
@@ -645,12 +579,107 @@ 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) {
s.Log.Infof("🔵 [CLOSING KEUANGAN] Starting fetch for ProjectFlockID: %d", projectFlockID)
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
); err != nil {
return nil, err
}
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
s.Log.Infof("✅ [CLOSING KEUANGAN] ProjectFlock fetched: ID=%d, Category=%s, FlockName=%s",
projectFlock.Id, projectFlock.Category, projectFlock.FlockName)
// Validasi: Closing Keuangan hanya untuk LAYING, bukan GROWING
if projectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
s.Log.Warnf("⚠️ [CLOSING KEUANGAN] ProjectFlock ID %d is GROWING category, closing keuangan not available", projectFlockID)
return nil, fiber.NewError(fiber.StatusNotFound, "Closing keuangan only available for LAYING category")
}
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
}
s.Log.Infof("💰 [CLOSING KEUANGAN] Budgets fetched: %d records", len(budgets))
actualUsageRows, err := s.ClosingKeuanganRepo.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost")
}
s.Log.Infof("📊 [CLOSING KEUANGAN] Actual Usage Costs fetched: %d records", len(actualUsageRows))
purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows)
s.Log.Infof("🛒 [CLOSING KEUANGAN] Converted to Purchase Items: %d items", len(purchaseItems))
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations")
}
s.Log.Infof("💸 [CLOSING KEUANGAN] Expense Realizations fetched: %d records", len(realizations))
deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
return db.Preload("MarketingProduct").
Preload("MarketingProduct.ProductWarehouse").
Preload("MarketingProduct.ProductWarehouse.Product")
})
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products")
}
s.Log.Infof("🚚 [CLOSING KEUANGAN] Marketing Delivery Products fetched: %d records", len(deliveryProducts))
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
}
s.Log.Infof("🐣 [CLOSING KEUANGAN] Chickins fetched: %d records", len(chickins))
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err)
}
s.Log.Infof("⚖️ [CLOSING KEUANGAN] Total Weight Produced: %.2f kg", totalWeightProduced)
totalEggWeightKg, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetTotalEggProductionWeightByProjectFlockID error: %v", err)
}
s.Log.Infof("🥚 [CLOSING KEUANGAN] Total Egg Weight: %.2f kg", totalEggWeightKg)
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
}
s.Log.Infof("📉 [CLOSING KEUANGAN] Total Depletion: %.2f", totalDepletion)
input := dto.ClosingKeuanganInput{
ProjectFlockCategory: projectFlock.Category,
PurchaseItems: purchaseItems,
Budgets: budgets,
Realizations: realizations,
DeliveryProducts: deliveryProducts,
Chickins: chickins,
TotalWeightProduced: totalWeightProduced,
TotalEggWeightKg: totalEggWeightKg,
TotalDepletion: totalDepletion,
}
report := dto.ToClosingKeuanganReport(input)
s.Log.Infof("✅ [CLOSING KEUANGAN] Report generated successfully for ProjectFlockID: %d", projectFlockID)
return &report, nil
}
func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) { 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")
} }
rows, err := s.Repository.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID) rows, err := s.ClosingKeuanganRepo.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID)
if err != nil { if err != nil {
s.Log.Errorf("Failed to get expedition HPP for project flock %d: %+v", projectFlockID, err) s.Log.Errorf("Failed to get expedition HPP for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch expedition HPP") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch expedition HPP")
@@ -682,16 +711,10 @@ 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")
} }
var projectFlockKandangIDs []uint projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID, kandangID)
if kandangID != nil && *kandangID > 0 { if err != nil {
projectFlockKandangIDs = []uint{*kandangID} s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err)
} else { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs")
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 {
@@ -930,19 +953,19 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
if !isGrowing { if !isGrowing {
if targetAverages.HenDayCount > 0 { if targetAverages.HenDayCount > 0 {
henDayAct := targetAverages.HenDayAvg henDayAct := targetAverages.HenDayAvg
performance.HenDayAct = henDayAct performance.HenDayAct = &henDayAct
} }
if targetAverages.HenHouseCount > 0 { if targetAverages.HenHouseCount > 0 {
henHouseAct := targetAverages.HenHouseAvg henHouseAct := targetAverages.HenHouseAvg
performance.HenHouseAct = henHouseAct performance.HenHouseAct = &henHouseAct
} }
if targetAverages.EggWeightCount > 0 { if targetAverages.EggWeightCount > 0 {
eggWeight := targetAverages.EggWeightAvg eggWeight := targetAverages.EggWeightAvg
performance.EggWeight = eggWeight performance.EggWeight = &eggWeight
} }
if targetAverages.EggMassCount > 0 { if targetAverages.EggMassCount > 0 {
eggMass := targetAverages.EggMassAvg eggMass := targetAverages.EggMassAvg
performance.EggMass = eggMass performance.EggMass = &eggMass
} }
} }
performance.DeffFcr = performance.FcrStd - performance.FcrAct performance.DeffFcr = performance.FcrStd - performance.FcrAct
@@ -952,16 +975,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
} }
} }
} }
@@ -1100,3 +1123,53 @@ 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.ClosingKeuanganRepo.GetProductsWithFlagsByIDs(ctx, productIDs)
if err != nil {
s.Log.Warnf("Failed to fetch products for actual usage: %v", err)
products = []entity.Product{}
}
// Create product map
productMap := make(map[uint]*entity.Product)
for i := range products {
productMap[products[i].Id] = &products[i]
}
// Convert to pseudo purchase items
purchaseItems := make([]entity.PurchaseItem, 0, len(actualUsageRows))
for _, row := range actualUsageRows {
product := productMap[row.ProductID]
// Skip if product not found
if product == nil {
s.Log.Warnf("Product ID %d not found for actual usage", row.ProductID)
continue
}
purchaseItem := entity.PurchaseItem{
Id: 0, // Pseudo item, no ID
ProductId: row.ProductID,
TotalQty: row.TotalQty,
TotalPrice: row.TotalPrice,
Price: row.AveragePrice,
Product: product,
}
purchaseItems = append(purchaseItems, purchaseItem)
}
return purchaseItems
}
@@ -1,640 +0,0 @@
package service
import (
"errors"
"strings"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
// ClosingKeuanganService handles closing keuangan business logic
type ClosingKeuanganService interface {
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganData, error)
GetClosingKeuanganByKandang(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error)
}
type closingKeuanganService struct {
Log *logrus.Logger
ClosingKeuanganRepo repository.ClosingKeuanganRepository
ProjectFlockRepo projectflockRepository.ProjectflockRepository
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
ChickinRepo chickinRepository.ProjectChickinRepository
RecordingRepo recordingRepository.RecordingRepository
}
func NewClosingKeuanganService(
closingKeuanganRepo repository.ClosingKeuanganRepository,
projectFlockRepo projectflockRepository.ProjectflockRepository,
projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository,
marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository,
expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository,
projectBudgetRepo projectflockRepository.ProjectBudgetRepository,
chickinRepo chickinRepository.ProjectChickinRepository,
recordingRepo recordingRepository.RecordingRepository,
) ClosingKeuanganService {
return &closingKeuanganService{
Log: utils.Log,
ClosingKeuanganRepo: closingKeuanganRepo,
ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
ExpenseRealizationRepo: expenseRealizationRepo,
ProjectBudgetRepo: projectBudgetRepo,
ChickinRepo: chickinRepo,
RecordingRepo: recordingRepo,
}
}
func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganData, error) {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
); err != nil {
return nil, err
}
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
}
// Preload Nonstock.Flags manually
var budgetIDs []uint
for _, b := range budgets {
budgetIDs = append(budgetIDs, b.Id)
}
if len(budgetIDs) > 0 {
err = s.ProjectBudgetRepo.DB().WithContext(c.Context()).
Preload("Nonstock.Flags").
Where("id IN ?", budgetIDs).
Find(&budgets).Error
}
// Get all kandang for this project flock
kandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs")
}
return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID)
}
func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error) {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
); err != nil {
return nil, err
}
// Validate and fetch project flock kandang
kandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), projectFlockKandangID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
}
if kandang.ProjectFlockId != projectFlockID {
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang does not belong to this project flock")
}
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
}
// Preload Nonstock.Flags manually
var budgetIDs []uint
for _, b := range budgets {
budgetIDs = append(budgetIDs, b.Id)
}
if len(budgetIDs) > 0 {
err = s.ProjectBudgetRepo.DB().WithContext(c.Context()).
Preload("Nonstock.Flags").
Where("id IN ?", budgetIDs).
Find(&budgets).Error
}
kandangs := []entity.ProjectFlockKandang{*kandang}
return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID)
}
func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFlock *entity.ProjectFlock, budgets []entity.ProjectBudget, kandangs []entity.ProjectFlockKandang, scopeID uint) (*dto.ClosingKeuanganData, error) {
// Define flag filters using constants
pakanFilters := []string{string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher)}
ovkFilters := []string{string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia)}
ayamFilters := []string{string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer)}
allFilters := append(pakanFilters, ovkFilters...)
allFilters = append(allFilters, ayamFilters...)
var allProductUsageRows []repository.ProductUsageRow
// Get ALL product usage
for _, kandang := range kandangs {
rows, err := s.ClosingKeuanganRepo.GetAllProductUsageByProjectFlockKandangID(c.Context(), kandang.Id, allFilters)
if err == nil {
allProductUsageRows = append(allProductUsageRows, rows...)
}
}
// Classify into categories based on flag priority
var pakanProductUsageRows []repository.ProductUsageRow
var ovkProductUsageRows []repository.ProductUsageRow
var ayamProductUsageRows []repository.ProductUsageRow
for _, row := range allProductUsageRows {
// Parse flag names from comma-separated string
flagNames := strings.Split(row.FlagNames, ",")
hasPakanFlag := false
hasOvkFlag := false
hasAyamFlag := false
for _, flag := range flagNames {
flag = strings.TrimSpace(flag)
if containsItem(pakanFilters, flag) {
hasPakanFlag = true
}
if containsItem(ovkFilters, flag) {
hasOvkFlag = true
}
if containsItem(ayamFilters, flag) {
hasAyamFlag = true
}
}
// Priority: PAKAN > OVK > AYAM
if hasPakanFlag {
pakanProductUsageRows = append(pakanProductUsageRows, row)
} else if hasOvkFlag {
ovkProductUsageRows = append(ovkProductUsageRows, row)
} else if hasAyamFlag {
ayamProductUsageRows = append(ayamProductUsageRows, row)
} else {
continue
}
}
// Calculate total price for each category
var totalPakanPrice, totalOvkPrice, totalAyamPrice float64
for _, row := range pakanProductUsageRows {
totalPakanPrice += row.TotalPengeluaran
}
for _, row := range ovkProductUsageRows {
totalOvkPrice += row.TotalPengeluaran
}
for _, row := range ayamProductUsageRows {
totalAyamPrice += row.TotalPengeluaran
}
// Determine if this is per-kandang or per-project-flock scope
isPerKandang := len(kandangs) == 1
var projectFlockKandangID *uint
if isPerKandang {
kandangID := kandangs[0].Id
projectFlockKandangID = &kandangID
}
var err error
// Fetch realizations
var realizations []entity.ExpenseRealization
if isPerKandang && projectFlockKandangID != nil {
realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, projectFlockKandangID)
} else {
realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, nil)
}
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations")
}
deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlock.Id, func(db *gorm.DB) *gorm.DB {
db = db.Preload("MarketingProduct").
Preload("MarketingProduct.ProductWarehouse").
Preload("MarketingProduct.ProductWarehouse.Product")
return db
})
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products")
}
// Filter by kandang if scope is per-kandang (manual filtering after fetch)
if isPerKandang && projectFlockKandangID != nil {
filteredProducts := make([]entity.MarketingDeliveryProduct, 0)
for _, dp := range deliveryProducts {
pfKandangID := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandangId
if pfKandangID != nil && *pfKandangID == *projectFlockKandangID {
filteredProducts = append(filteredProducts, dp)
}
}
deliveryProducts = filteredProducts
}
// Fetch chickins
var chickins []entity.ProjectChickin
if isPerKandang && projectFlockKandangID != nil {
chickins, err = s.ChickinRepo.GetByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
} else {
chickins, err = s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlock.Id)
}
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
}
// Get total depletion
var totalDepletion float64
if isPerKandang && projectFlockKandangID != nil {
totalDepletion, err = s.ClosingKeuanganRepo.GetTotalDepletionByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
} else {
totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlock.Id)
}
if err != nil {
totalDepletion = 0
}
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlock.Id)
if err != nil {
}
// Try to get actual weight from uniformity data
var totalWeightFromUniformity float64
if isPerKandang && projectFlockKandangID != nil {
totalWeightFromUniformity, err = s.ClosingKeuanganRepo.GetTotalWeightProducedFromUniformityByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
} else {
totalWeightFromUniformity, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockID(c.Context(), projectFlock.Id)
}
if err != nil {
} else if totalWeightFromUniformity > 0 {
totalWeightProduced = totalWeightFromUniformity
}
// Fetch egg data only for Laying category
var totalEggWeightKg float64
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
// TODO: Replace with actual method to get egg weight from RecordingRepo
// totalEggWeightKg, err = s.RecordingRepo.GetEggWeightByProjectFlockID(c.Context(), projectFlock.Id)
// For now, set to 0 as placeholder
totalEggWeightKg = 0
} else {
totalEggWeightKg = 0
}
// Build new DTO structure
// Calculate totals
var totalPopulation float64
for _, chickin := range chickins {
totalPopulation += chickin.UsageQty
}
// Calculate actual population (total population - depletion)
actualPopulation := totalPopulation - totalDepletion
// Calculate budget totals by category
calculateBudgetByFlag := func(flags []string) float64 {
var total float64
for _, budget := range budgets {
if budget.Nonstock != nil {
for _, nonstockFlag := range budget.Nonstock.Flags {
flagName := strings.ToUpper(nonstockFlag.Name)
for _, targetFlag := range flags {
if flagName == strings.ToUpper(targetFlag) {
total += budget.Price * budget.Qty
break
}
}
}
}
}
return total
}
// Budget per category
budgetPakan := calculateBudgetByFlag([]string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"})
budgetOvk := calculateBudgetByFlag([]string{"OVK", "OBAT", "VITAMIN", "KIMIA"})
budgetAyam := calculateBudgetByFlag([]string{"DOC", "PULLET", "LAYER"})
budgetEkspedisi := calculateBudgetByFlag([]string{"EKSPEDISI"})
// Operational budget = total budget - pakan - ovk - ayam - ekspedisi
totalBudgetAmount := 0.0
for _, budget := range budgets {
totalBudgetAmount += budget.Price * budget.Qty
}
budgetOperational := totalBudgetAmount - budgetPakan - budgetOvk - budgetAyam - budgetEkspedisi
// Calculate realization totals
var totalRealizationAmount float64
var totalEkspedisiRealization float64
for _, realization := range realizations {
amount := realization.Price * realization.Qty
totalRealizationAmount += amount
// Check if this is ekspedisi (need to check nonstock flags)
if realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Nonstock != nil {
for _, flag := range realization.ExpenseNonstock.Nonstock.Flags {
if flag.Name == "EKSPEDISI" {
totalEkspedisiRealization += amount
break
}
}
}
}
totalOperationalRealization := totalRealizationAmount - totalEkspedisiRealization
// Filter delivery products based on category
var filteredDeliveryProducts []entity.MarketingDeliveryProduct
for _, delivery := range deliveryProducts {
// Get product from delivery
if delivery.MarketingProduct.ProductWarehouse.Product.Id == 0 {
continue
}
product := delivery.MarketingProduct.ProductWarehouse.Product
isEggProduct := false
isChickenProduct := false
// Check product flags
for _, flag := range product.Flags {
flagName := strings.ToUpper(flag.Name)
// Egg product flags
if flagName == "TELUR" || flagName == "TELURUTUH" || flagName == "TELURPECAH" ||
flagName == "TELURPUTIH" || flagName == "TELURRETAK" {
isEggProduct = true
}
// Chicken product flags
if flagName == "AYAMAFKIR" || flagName == "AYAMCULLING" || flagName == "AYAMMATI" {
isChickenProduct = true
}
}
// Filter based on project flock category
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
// Laying: only egg products
if isEggProduct {
filteredDeliveryProducts = append(filteredDeliveryProducts, delivery)
}
} else {
// Growing/Contract Growing: only chicken products
if isChickenProduct || (!isEggProduct && !isChickenProduct) {
// Include if chicken product or if no specific flags (default to chicken)
filteredDeliveryProducts = append(filteredDeliveryProducts, delivery)
}
}
}
// Calculate total weight sold and sales amount from filtered products
var totalWeightSold float64
var totalSalesAmount float64
for _, delivery := range filteredDeliveryProducts {
totalWeightSold += delivery.TotalWeight
totalSalesAmount += delivery.TotalPrice
}
// Calculate metrics - always use kg ayam for rp_per_kg
calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
if actualPopulation > 0 {
rpPerBird = amount / actualPopulation // Use actual population
}
if totalWeightProduced > 0 {
rpPerKg = amount / totalWeightProduced
}
return
}
// Calculate metrics for profit loss (use total population and total weight produced)
calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
if totalPopulation > 0 {
rpPerBird = amount / totalPopulation
}
if totalWeightProduced > 0 {
rpPerKg = amount / totalWeightProduced
}
return
}
// Build HPP Items using constants
hppItems := []dto.HPPItem{}
// PAKAN item
pakanBudgetRpPerBird, pakanBudgetRpPerKg := calculateMetrics(budgetPakan)
pakanRealizationRpPerBird, pakanRealizationRpPerKg := calculateMetrics(totalPakanPrice)
hppItems = append(hppItems, dto.ToHPPItem(
1,
"purchase",
string(dto.HPPCodePakan),
"Pembelian Pakan",
dto.ToFinancialMetrics(pakanBudgetRpPerBird, pakanBudgetRpPerKg, budgetPakan),
dto.ToFinancialMetrics(pakanRealizationRpPerBird, pakanRealizationRpPerKg, totalPakanPrice),
))
// OVK item
ovkBudgetRpPerBird, ovkBudgetRpPerKg := calculateMetrics(budgetOvk)
ovkRealizationRpPerBird, ovkRealizationRpPerKg := calculateMetrics(totalOvkPrice)
hppItems = append(hppItems, dto.ToHPPItem(
2,
"purchase",
string(dto.HPPCodeOVK),
"Pembelian OVK",
dto.ToFinancialMetrics(ovkBudgetRpPerBird, ovkBudgetRpPerKg, budgetOvk),
dto.ToFinancialMetrics(ovkRealizationRpPerBird, ovkRealizationRpPerKg, totalOvkPrice),
))
// DOC/DEPRESIASI item
docCode := string(dto.HPPCodeDOC)
docLabel := "Pembelian DOC"
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
docCode = string(dto.HPPCodeDepresiasi)
docLabel = "Depresiasi"
}
docBudgetRpPerBird, docBudgetRpPerKg := calculateMetrics(budgetAyam)
docRealizationRpPerBird, docRealizationRpPerKg := calculateMetrics(totalAyamPrice)
hppItems = append(hppItems, dto.ToHPPItem(
3,
"purchase",
docCode,
docLabel,
dto.ToFinancialMetrics(docBudgetRpPerBird, docBudgetRpPerKg, budgetAyam),
dto.ToFinancialMetrics(docRealizationRpPerBird, docRealizationRpPerKg, totalAyamPrice),
))
// OVERHEAD item
overheadBudgetRpPerBird, overheadBudgetRpPerKg := calculateMetrics(budgetOperational)
overheadRealizationRpPerBird, overheadRealizationRpPerKg := calculateMetrics(totalOperationalRealization)
hppItems = append(hppItems, dto.ToHPPItem(
4,
"overhead",
string(dto.HPPCodeOverhead),
"Pengeluaran Overhead",
dto.ToFinancialMetrics(overheadBudgetRpPerBird, overheadBudgetRpPerKg, budgetOperational),
dto.ToFinancialMetrics(overheadRealizationRpPerBird, overheadRealizationRpPerKg, totalOperationalRealization),
))
// EKSPEDISI item
ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg := calculateMetrics(budgetEkspedisi)
ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg := calculateMetrics(totalEkspedisiRealization)
hppItems = append(hppItems, dto.ToHPPItem(
5,
"overhead",
string(dto.HPPCodeEkspedisi),
"Beban Ekspedisi",
dto.ToFinancialMetrics(ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg, budgetEkspedisi),
dto.ToFinancialMetrics(ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg, totalEkspedisiRealization),
))
// HPP Summary
totalBudgetHpp := budgetPakan + budgetOvk + budgetAyam + budgetOperational + budgetEkspedisi
totalRealizationHpp := totalPakanPrice + totalOvkPrice + totalAyamPrice + totalOperationalRealization + totalEkspedisiRealization
hppBudgetRpPerBird, hppBudgetRpPerKg := calculateMetrics(totalBudgetHpp)
hppRealizationRpPerBird, hppRealizationRpPerKg := calculateMetrics(totalRealizationHpp)
var eggBudgeting, eggRealization *dto.FinancialMetrics
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) && totalEggWeightKg > 0 {
eggBudgetRpPerKg := totalBudgetHpp / totalEggWeightKg
eggRealizationRpPerKg := totalRealizationHpp / totalEggWeightKg
eggBudgeting = &dto.FinancialMetrics{
RpPerBird: 0,
RpPerKg: eggBudgetRpPerKg,
Amount: totalBudgetHpp,
}
eggRealization = &dto.FinancialMetrics{
RpPerBird: 0,
RpPerKg: eggRealizationRpPerKg,
Amount: totalRealizationHpp,
}
}
hppSummary := dto.ToHPPSummary(
"HPP",
dto.ToFinancialMetrics(hppBudgetRpPerBird, hppBudgetRpPerKg, totalBudgetHpp),
dto.ToFinancialMetrics(hppRealizationRpPerBird, hppRealizationRpPerKg, totalRealizationHpp),
eggBudgeting,
eggRealization,
)
hppSection := dto.ToHPPSection(hppItems, hppSummary)
// Build Profit Loss Items using constants
plItems := []dto.ProfitLossItem{}
// SALES item
salesRpPerBird, salesRpPerKg := calculateProfitLossMetrics(totalSalesAmount)
salesLabel := "Penjualan Ayam"
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
salesLabel = "Penjualan Telur"
}
plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeSales),
salesLabel,
"income",
salesRpPerBird,
salesRpPerKg,
totalSalesAmount,
))
// SAPRONAK item - combines DOC/Depresiasi + PAKAN + OVK
totalSapronakAmount := totalAyamPrice + totalPakanPrice + totalOvkPrice
sapronakRpPerBird := docRealizationRpPerBird + pakanRealizationRpPerBird + ovkRealizationRpPerBird
sapronakRpPerKg := docRealizationRpPerKg + pakanRealizationRpPerKg + ovkRealizationRpPerKg
sapronakLabel := "Pengeluaran Sapronak"
plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeSapronak),
sapronakLabel,
"purchase",
sapronakRpPerBird,
sapronakRpPerKg,
totalSapronakAmount,
))
// OVERHEAD item
overheadRpPerBird, overheadRpPerKg := calculateMetrics(totalOperationalRealization)
plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeOverhead),
"Overhead",
"overhead",
overheadRpPerBird,
overheadRpPerKg,
totalOperationalRealization,
))
// EKSPEDISI item
plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeEkspedisi),
"Ekspedisi",
"overhead",
ekspedisiRealizationRpPerBird,
ekspedisiRealizationRpPerKg,
totalEkspedisiRealization,
))
// Profit Loss Summary
// Gross Profit = Sales - (DOC + PAKAN + OVK) only
// Gross Profit should NOT include overhead and ekspedisi
costOfGoodsSold := totalAyamPrice + totalPakanPrice + totalOvkPrice
costOfGoodsSoldRpPerBird := sapronakRpPerBird
grossProfit := totalSalesAmount - costOfGoodsSold
grossProfitRpPerBird := salesRpPerBird - costOfGoodsSoldRpPerBird
// Operating Expenses (Overhead + Ekspedisi)
totalOperatingExpenses := totalOperationalRealization + totalEkspedisiRealization
totalOperatingExpensesRpPerBird := overheadRpPerBird + ekspedisiRealizationRpPerBird
// Net Profit = Gross Profit - Operating Expenses
netProfit := grossProfit - totalOperatingExpenses
netProfitRpPerBird := grossProfitRpPerBird - totalOperatingExpensesRpPerBird
plSummary := dto.ToProfitLossSummary(
dto.ToFinancialMetrics(grossProfitRpPerBird, 0, grossProfit),
dto.ToFinancialMetrics(totalOperatingExpensesRpPerBird, 0, totalOperatingExpenses),
dto.ToFinancialMetrics(netProfitRpPerBird, 0, netProfit),
)
profitLossSection := dto.ToProfitLossSection(plItems, plSummary)
// Build complete response
data := dto.ToClosingKeuanganData(hppSection, profitLossSection)
return &data, nil
}
// containsItem checks if a string exists in a slice
func containsItem(slice []string, item string) bool {
for _, s := range slice {
if strings.EqualFold(s, item) {
return true
}
}
return false
}
@@ -2,8 +2,8 @@ package service
import ( import (
"context" "context"
"fmt"
"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 +112,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, params.Flag) items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, nil, nil, 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,6 +126,8 @@ 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,
@@ -263,7 +265,6 @@ type sapronakDetailMaps struct {
AdjOutgoing map[uint][]dto.SapronakDetailDTO AdjOutgoing map[uint][]dto.SapronakDetailDTO
TransferIn map[uint][]dto.SapronakDetailDTO TransferIn map[uint][]dto.SapronakDetailDTO
TransferOut map[uint][]dto.SapronakDetailDTO TransferOut map[uint][]dto.SapronakDetailDTO
SalesOut map[uint][]dto.SapronakDetailDTO
} }
func buildSapronakDetails( func buildSapronakDetails(
@@ -273,7 +274,6 @@ func buildSapronakDetails(
adjOutgoingRows map[uint][]repository.SapronakDetailRow, adjOutgoingRows map[uint][]repository.SapronakDetailRow,
transferInRows map[uint][]repository.SapronakDetailRow, transferInRows map[uint][]repository.SapronakDetailRow,
transferOutRows map[uint][]repository.SapronakDetailRow, transferOutRows map[uint][]repository.SapronakDetailRow,
salesOutRows map[uint][]repository.SapronakDetailRow,
) sapronakDetailMaps { ) sapronakDetailMaps {
result := sapronakDetailMaps{ result := sapronakDetailMaps{
Incoming: make(map[uint][]dto.SapronakDetailDTO), Incoming: make(map[uint][]dto.SapronakDetailDTO),
@@ -282,7 +282,6 @@ func buildSapronakDetails(
AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO), AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO),
TransferIn: make(map[uint][]dto.SapronakDetailDTO), TransferIn: make(map[uint][]dto.SapronakDetailDTO),
TransferOut: make(map[uint][]dto.SapronakDetailDTO), TransferOut: make(map[uint][]dto.SapronakDetailDTO),
SalesOut: make(map[uint][]dto.SapronakDetailDTO),
} }
addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) { addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) {
@@ -315,12 +314,11 @@ func buildSapronakDetails(
addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false) addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false)
addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true) addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false) addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
addRows(result.SalesOut, salesOutRows, "Penjualan", false)
return result return result
} }
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) { func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
// For sapronak closing report we intentionally ignore date range // 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)
@@ -355,10 +353,6 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
if err != nil { if err != nil {
return nil, nil, 0, 0, err return nil, nil, 0, 0, err
} }
salesOutRows, err := s.Repository.FetchSapronakSales(ctx, pfk.KandangId)
if err != nil {
return nil, nil, 0, 0, err
}
filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter)) filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter))
matchesFlag := func(f string) bool { matchesFlag := func(f string) bool {
@@ -371,34 +365,6 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
} }
return candidate == filterFlag return candidate == filterFlag
} }
dedupTransfers := func(src map[uint][]dto.SapronakDetailDTO) map[uint][]dto.SapronakDetailDTO {
result := make(map[uint][]dto.SapronakDetailDTO, len(src))
seen := make(map[string]struct{})
for pid, rows := range src {
for _, d := range rows {
dateKey := ""
if d.Tanggal != nil {
dateKey = d.Tanggal.Format("2006-01-02")
}
qtyKey := d.QtyMasuk
if qtyKey == 0 {
qtyKey = d.QtyKeluar
}
ref := strings.TrimSpace(d.NoReferensi)
key := fmt.Sprintf("%d|%s|%s|%.3f", pid, ref, dateKey, qtyKey)
if ref == "" {
key = fmt.Sprintf("%d|%s|%s|%.3f|%s", pid, ref, dateKey, qtyKey, strings.ToUpper(strings.TrimSpace(d.Flag)))
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
result[pid] = append(result[pid], d)
}
}
return result
}
// For project flocks with category GROWING, pullet usage from chickin // For project flocks with category GROWING, pullet usage from chickin
// should not be counted yet. Only when category is LAYING we allow // should not be counted yet. Only when category is LAYING we allow
@@ -437,17 +403,13 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...) usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...)
} }
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows, salesOutRows) detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows)
incomingDetails := detailMaps.Incoming incomingDetails := detailMaps.Incoming
usageDetails := detailMaps.Usage usageDetails := detailMaps.Usage
adjIncoming := detailMaps.AdjIncoming adjIncoming := detailMaps.AdjIncoming
adjOutgoing := detailMaps.AdjOutgoing adjOutgoing := detailMaps.AdjOutgoing
transIncoming := detailMaps.TransferIn transIncoming := detailMaps.TransferIn
transOutgoing := detailMaps.TransferOut transOutgoing := detailMaps.TransferOut
salesOutgoing := detailMaps.SalesOut
transIncoming = dedupTransfers(transIncoming)
transOutgoing = dedupTransfers(transOutgoing)
ensureGroup := func(flag string) *dto.SapronakGroupDTO { ensureGroup := func(flag string) *dto.SapronakGroupDTO {
if g, ok := groupMap[flag]; ok { if g, ok := groupMap[flag]; ok {
@@ -457,22 +419,6 @@ 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
@@ -608,18 +554,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
} }
for productID, details := range incomingDetails { for productID, details := range incomingDetails {
flag, name := resolveFlagName(productID, details) flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) { if !matchesFlag(flag) {
continue continue
} }
group := ensureGroup(flag) group := ensureGroup(flag)
for _, d := range details { for _, d := range details {
if d.Flag == "" { d.Flag = flag
d.Flag = flag d.ProductName = name
}
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
@@ -628,18 +575,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
} }
for productID, details := range adjIncoming { for productID, details := range adjIncoming {
flag, name := resolveFlagName(productID, details) flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) { if !matchesFlag(flag) {
continue continue
} }
group := ensureGroup(flag) group := ensureGroup(flag)
for _, d := range details { for _, d := range details {
if d.Flag == "" { d.Flag = flag
d.Flag = flag d.ProductName = name
}
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
@@ -648,18 +596,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
} }
for productID, details := range usageDetails { for productID, details := range usageDetails {
flag, name := resolveFlagName(productID, details) flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) { if !matchesFlag(flag) {
continue continue
} }
group := ensureGroup(flag) group := ensureGroup(flag)
for _, d := range details { for _, d := range details {
if d.Flag == "" { d.Flag = flag
d.Flag = flag d.ProductName = name
}
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
@@ -667,18 +616,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
} }
for productID, details := range adjOutgoing { for productID, details := range adjOutgoing {
flag, name := resolveFlagName(productID, details) flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) { if !matchesFlag(flag) {
continue continue
} }
group := ensureGroup(flag) group := ensureGroup(flag)
for _, d := range details { for _, d := range details {
if d.Flag == "" { d.Flag = flag
d.Flag = flag d.ProductName = name
}
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
@@ -686,18 +636,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
} }
for productID, details := range transIncoming { for productID, details := range transIncoming {
flag, name := resolveFlagName(productID, details) flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) { if !matchesFlag(flag) {
continue continue
} }
group := ensureGroup(flag) group := ensureGroup(flag)
for _, d := range details { for _, d := range details {
if d.Flag == "" { d.Flag = flag
d.Flag = flag d.ProductName = name
}
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
@@ -706,37 +657,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
} }
for productID, details := range transOutgoing { for productID, details := range transOutgoing {
flag, name := resolveFlagName(productID, details) flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) { if !matchesFlag(flag) {
continue continue
} }
group := ensureGroup(flag) group := ensureGroup(flag)
for _, d := range details { for _, d := range details {
if d.Flag == "" { d.Flag = flag
d.Flag = flag d.ProductName = name
}
if d.ProductName == "" {
d.ProductName = name
}
group.Items = append(group.Items, d)
group.TotalKeluar += d.QtyKeluar
group.SaldoAkhir -= d.QtyKeluar
}
}
for productID, details := range salesOutgoing {
flag, name := resolveFlagName(productID, details)
if !matchesFlag(flag) {
continue
}
group := ensureGroup(flag)
for _, d := range details {
if d.Flag == "" {
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
@@ -24,5 +24,4 @@ type ClosingSapronakQuery struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"` KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"`
Search string `query:"search" validate:"omitempty,max=100"`
} }
@@ -396,10 +396,6 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
updateBody["supplier_id"] = *req.SupplierID updateBody["supplier_id"] = *req.SupplierID
} }
if req.Notes != nil {
updateBody["notes"] = *req.Notes
}
if req.LocationID != nil { if req.LocationID != nil {
locationID := uint(*req.LocationID) locationID := uint(*req.LocationID)
updateBody["location_id"] = locationID updateBody["location_id"] = locationID
@@ -572,28 +568,20 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context") return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
} }
if *latestApproval.Action != entity.ApprovalActionUpdated {
if *latestApproval.Action != entity.ApprovalActionUpdated && latestApproval.StepNumber > uint16(utils.ExpenseStepPengajuan) {
approvalAction := entity.ApprovalActionUpdated approvalAction := entity.ApprovalActionUpdated
previousStep := approvalutils.ApprovalStep(latestApproval.StepNumber) - 1
if previousStep < utils.ExpenseStepPengajuan {
previousStep = utils.ExpenseStepPengajuan
}
if _, err := approvalSvcTx.CreateApproval( if _, err := approvalSvcTx.CreateApproval(
c.Context(), c.Context(),
utils.ApprovalWorkflowExpense, utils.ApprovalWorkflowExpense,
id, id,
previousStep, utils.ExpenseStepPengajuan,
&approvalAction, &approvalAction,
actorID, actorID,
nil); err != nil { nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step") return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step")
} }
} }
if s.DocumentSvc != nil && len(req.Documents) > 0 { if s.DocumentSvc != nil && len(req.Documents) > 0 {
@@ -31,7 +31,6 @@ type Update struct {
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"` Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"` SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
LocationID *uint64 `form:"location_id" json:"location_id" validate:"omitempty,gt=0"` LocationID *uint64 `form:"location_id" json:"location_id" validate:"omitempty,gt=0"`
Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"` Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"` ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
} }
@@ -4,17 +4,20 @@ import (
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
type TransferRelationDTO struct { type TransferRelationDTO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
MovementNumber string `json:"movement_number"` TransferReason string `json:"transfer_reason"`
TransferReason string `json:"transfer_reason"` TransferDate string `json:"transfer_date"`
TransferDate string `json:"transfer_date"` SourceWarehouse *WarehouseDetailDTO `json:"source_warehouse,omitempty"`
SourceWarehouse *warehouseDTO.WarehouseRelationDTO `json:"source_warehouse,omitempty"` DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"`
DestinationWarehouse *warehouseDTO.WarehouseRelationDTO `json:"destination_warehouse,omitempty"` }
type WarehouseSimpleDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
} }
type ProductSimpleDTO struct { type ProductSimpleDTO struct {
@@ -22,6 +25,16 @@ type ProductSimpleDTO struct {
Name string `json:"name"` Name string `json:"name"`
} }
type AreaDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type LocationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type SupplierSimpleDTO struct { type SupplierSimpleDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -35,6 +48,13 @@ type DocumentDTO struct {
Size float64 `json:"size"` Size float64 `json:"size"`
} }
type WarehouseDetailDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Location *LocationDTO `json:"location"`
Area *AreaDTO `json:"area"`
}
type TransferListDTO struct { type TransferListDTO struct {
TransferRelationDTO TransferRelationDTO
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
@@ -77,19 +97,16 @@ type TransferDeliveryItemDTO struct {
} }
func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO { func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
var sourceWarehouse *warehouseDTO.WarehouseRelationDTO var sourceWarehouse *WarehouseDetailDTO
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 { if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
mapped := warehouseDTO.ToWarehouseRelationDTO(*e.FromWarehouse) sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse)
sourceWarehouse = &mapped
} }
var destinationWarehouse *warehouseDTO.WarehouseRelationDTO var destinationWarehouse *WarehouseDetailDTO
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 { if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
mapped := warehouseDTO.ToWarehouseRelationDTO(*e.ToWarehouse) destinationWarehouse = toWarehouseDetailDTO(e.ToWarehouse)
destinationWarehouse = &mapped
} }
return TransferRelationDTO{ return TransferRelationDTO{
Id: e.Id, Id: e.Id,
MovementNumber: e.MovementNumber,
TransferReason: e.Reason, TransferReason: e.Reason,
TransferDate: e.CreatedAt.Format("2006-01-02"), TransferDate: e.CreatedAt.Format("2006-01-02"),
SourceWarehouse: sourceWarehouse, SourceWarehouse: sourceWarehouse,
@@ -97,6 +114,38 @@ func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
} }
} }
func toAreaDTO(a *entity.Area) *AreaDTO {
if a == nil {
return nil
}
return &AreaDTO{
Id: a.Id,
Name: a.Name,
}
}
func toLocationDTO(l *entity.Location) *LocationDTO {
if l == nil {
return nil
}
return &LocationDTO{
Id: l.Id,
Name: l.Name,
}
}
func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO {
if w == nil {
return nil
}
return &WarehouseDetailDTO{
Id: w.Id,
Name: w.Name,
Location: toLocationDTO(w.Location),
Area: toAreaDTO(&w.Area),
}
}
func ToTransferListDTO(e entity.StockTransfer) TransferListDTO { func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
var createdUser *userDTO.UserRelationDTO var createdUser *userDTO.UserRelationDTO
if e.CreatedUser != nil { if e.CreatedUser != nil {
@@ -40,6 +40,6 @@ func (r *StockTransferRepositoryImpl) GenerateMovementNumber(ctx context.Context
if err != nil { if err != nil {
return "", err return "", err
} }
movementNumber := fmt.Sprintf("PND-LTI-%05d", seq) movementNumber := fmt.Sprintf("ST-%05d", seq)
return movementNumber, nil return movementNumber, nil
} }
@@ -15,8 +15,8 @@ func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferServ
route := v1.Group("/transfers") route := v1.Group("/transfers")
route.Use(m.Auth(u)) route.Use(m.Auth(u))
route.Get("/", m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll) route.Get("/",m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll)
route.Post("/", m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne) route.Post("/",m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne)
route.Get("/:id", m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne) route.Get("/:id",m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne)
} }
@@ -99,11 +99,7 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
if params.Search != "" { if params.Search != "" {
searchTerm := "%" + strings.TrimSpace(params.Search) + "%" db = db.Where("movement_number ILIKE ?", "%"+strings.TrimSpace(params.Search)+"%")
db = db.Joins("LEFT JOIN warehouses AS from_warehouses ON from_warehouses.id = stock_transfers.from_warehouse_id").
Joins("LEFT JOIN warehouses AS to_warehouses ON to_warehouses.id = stock_transfers.to_warehouse_id").
Where("movement_number ILIKE ? OR from_warehouses.name ILIKE ? OR to_warehouses.name ILIKE ?",
searchTerm, searchTerm, searchTerm)
} }
return db.Order("created_at DESC").Order("updated_at DESC") return db.Order("created_at DESC").Order("updated_at DESC")
}) })
@@ -122,9 +118,9 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
}) })
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Transfer dengan ID %d tidak ditemukan", id)) return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
} }
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data transfer dengan ID %d", id)) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
} }
return transferPtr, nil return transferPtr, nil
@@ -140,12 +136,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
) )
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk dengan ID %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
} }
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengecek stok produk %d di gudang asal", product.ProductID)) return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek stok produk di gudang asal")
} }
if sourcePW.Quantity < product.ProductQty { if sourcePW.Quantity < product.ProductQty {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak mencukupi. Tersedia: %.2f, Diminta: %.2f", product.ProductID, sourcePW.Quantity, product.ProductQty)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
} }
pwIDs = append(pwIDs, sourcePW.Id) pwIDs = append(pwIDs, sourcePW.Id)
} }
@@ -165,10 +161,10 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID) projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock untuk gudang tujuan") return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
} }
if projectFlockKandang.ClosedAt != nil { if projectFlockKandang.ClosedAt != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Project flock untuk gudang tujuan sudah ditutup (closing) pada %s", projectFlockKandang.ClosedAt.Format("2006-01-02"))) return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing")
} }
actorID, err := m.ActorIDFromContext(c) actorID, err := m.ActorIDFromContext(c)
@@ -196,16 +192,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID))
} }
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data supplier dengan ID %d", delivery.SupplierID)) return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier")
} }
if supplier.Category != string(utils.SupplierCategoryBOP) { if supplier.Category != string(utils.SupplierCategoryBOP) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier '%s' (ID: %d) bukan kategori BOP. Kategori saat ini: %s", supplier.Name, delivery.SupplierID, supplier.Category)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID))
} }
} }
movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context()) movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context())
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat nomor movement transfer") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
} }
transferDate, _ := utils.ParseDateString(req.TransferDate) transferDate, _ := utils.ParseDateString(req.TransferDate)
@@ -243,16 +239,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
) )
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
} }
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data product warehouse untuk produk %d di gudang asal", product.ProductID)) return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse source")
} }
destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID( destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID), c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
) )
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data product warehouse untuk produk %d di gudang tujuan", product.ProductID)) return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse destination")
} }
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
ctx := c.Context() ctx := c.Context()
@@ -260,21 +256,14 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
if err != nil { if err != nil {
return err return err
} }
// Set ProjectFlockKandangId hanya jika ada kandang
var pfkID *uint
if projectFlockKandangID > 0 {
pfkID = &projectFlockKandangID
}
destPW = &entity.ProductWarehouse{ destPW = &entity.ProductWarehouse{
ProductId: uint(product.ProductID), ProductId: uint(product.ProductID),
WarehouseId: uint(req.DestinationWarehouseID), WarehouseId: uint(req.DestinationWarehouseID),
Quantity: 0, Quantity: 0,
ProjectFlockKandangId: pfkID, ProjectFlockKandangId: &projectFlockKandangID,
} }
if err := productWarehouseRepoTX.CreateOne(c.Context(), destPW, nil); err != nil { if err := productWarehouseRepoTX.CreateOne(c.Context(), destPW, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal membuat product warehouse untuk produk %d di gudang tujuan", product.ProductID)) return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat product warehouse destination")
} }
} }
@@ -320,7 +309,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
for _, prod := range item.Products { for _, prod := range item.Products {
detail, ok := detailMap[uint64(prod.ProductID)] detail, ok := detailMap[uint64(prod.ProductID)]
if !ok { if !ok {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan dalam daftar transfer untuk delivery #%d", prod.ProductID, i+1)) return fmt.Errorf("produk %d tidak ditemukan di detail", prod.ProductID)
} }
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{ deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
StockTransferDeliveryId: delivery.Id, StockTransferDeliveryId: delivery.Id,
@@ -383,7 +372,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Tx: tx, Tx: tx,
}) })
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak cukup di gudang asal untuk produk %d: %v", product.ProductID, err))
} }
if err := tx.Model(&entity.StockTransferDetail{}). if err := tx.Model(&entity.StockTransferDetail{}).
@@ -392,7 +381,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
"usage_qty": consumeResult.UsageQuantity, "usage_qty": consumeResult.UsageQuantity,
"pending_qty": consumeResult.PendingQuantity, "pending_qty": consumeResult.PendingQuantity,
}).Error; err != nil { }).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengupdate tracking usage untuk produk %d", product.ProductID)) return fmt.Errorf("gagal update usage tracking: %w", err)
} }
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber) note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
@@ -405,7 +394,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Tx: tx, Tx: tx,
}) })
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok untuk produk %d di gudang tujuan. Error: %v", product.ProductID, err)) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok di gudang tujuan untuk produk %d: %v", product.ProductID, err))
} }
if err := tx.Model(&entity.StockTransferDetail{}). if err := tx.Model(&entity.StockTransferDetail{}).
@@ -413,7 +402,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Updates(map[string]interface{}{ Updates(map[string]interface{}{
"total_qty": replenishResult.AddedQuantity, "total_qty": replenishResult.AddedQuantity,
}).Error; err != nil { }).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengupdate tracking total untuk produk %d", product.ProductID)) return fmt.Errorf("gagal update total tracking: %w", err)
} }
} }
@@ -447,7 +436,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
}) })
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal memproses transfer. Error: %v", err)) return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err))
} }
result, err := s.GetOne(c, uint(entityTransfer.Id)) result, err := s.GetOne(c, uint(entityTransfer.Id))
@@ -457,7 +446,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
if len(expensePayloads) > 0 { if len(expensePayloads) > 0 {
if err := s.notifyExpenseItemsDelivered(c, entityTransfer.Id, expensePayloads); err != nil { if err := s.notifyExpenseItemsDelivered(c, entityTransfer.Id, expensePayloads); err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal sinkronisasi data expense untuk transfer %s. Silakan cek manual di module expense", entityTransfer.MovementNumber)) s.Log.Errorf("Failed to sync expense for transfer %d: %+v", entityTransfer.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to sync expense: %v", err))
} }
} }
@@ -471,26 +461,32 @@ func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID u
return s.ExpenseBridge.OnItemsDelivered(c, transferID, payloads) return s.ExpenseBridge.OnItemsDelivered(c, transferID, payloads)
} }
func (s *transferService) notifyExpenseDetailsDeleted(ctx context.Context, transferID uint64, items []entity.StockTransferDetail) error {
if s.ExpenseBridge == nil || transferID == 0 || len(items) == 0 {
return nil
}
return s.ExpenseBridge.OnItemsDeleted(ctx, transferID, items)
}
func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) { func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil) warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID)) return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
} }
return 0, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data gudang dengan ID %d", warehouseID)) return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
} }
// Jika warehouse tidak punya kandang_id, return 0 tanpa error
if warehouse.KandangId == nil || *warehouse.KandangId == 0 { if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
return 0, nil return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID))
} }
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId)) projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak ada project flock aktif untuk kandang %d", *warehouse.KandangId)) return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
} }
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock kandang yang aktif") return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
} }
return uint(projectFlockKandang.Id), nil return uint(projectFlockKandang.Id), nil
@@ -140,21 +140,12 @@ func (b *transferExpenseBridge) markExpensesUpdated(ctx context.Context, expense
if actorID == 0 { if actorID == 0 {
actorID = 1 actorID = 1
} }
approvalRepo := commonRepo.NewApprovalRepository(b.db) svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
svc := commonSvc.NewApprovalService(approvalRepo) action := entity.ApprovalActionUpdated
action := entity.ApprovalActionCreated
for id := range expenseIDs { for id := range expenseIDs {
latestApproval, err := approvalRepo.LatestByTarget(ctx, string(utils.ApprovalWorkflowExpense), uint(id), nil) if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err return err
} }
if latestApproval == nil {
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
return err
}
}
} }
return nil return nil
} }
@@ -140,7 +140,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id"). Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id").
Where("marketing_delivery_products.delivery_date IS NOT NULL") Where("marketing_delivery_products.delivery_date IS NOT NULL")
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.AreaId > 0 || filters.LocationId > 0 || filters.Search != "" || filters.MarketingType != "" { if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" || filters.MarketingType != "" {
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id") db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id")
} }
@@ -148,30 +148,18 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id") db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id")
} }
if filters.WarehouseId > 0 || filters.Search != "" { if filters.WarehouseId > 0 {
db = db.Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id") db = db.Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id")
} }
if filters.Search != "" { if filters.Search != "" {
db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id") db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id")
db = db.Joins("LEFT JOIN users AS sales_users ON sales_users.id = marketings.sales_person_id") }
if filters.Search != "" {
searchPattern := "%" + filters.Search + "%" searchPattern := "%" + filters.Search + "%"
db = db.Where(`( db = db.Where("marketing_delivery_products.vehicle_number ILIKE ? OR marketings.so_number ILIKE ? OR customers.name ILIKE ? OR products.name ILIKE ?",
marketing_delivery_products.vehicle_number ILIKE ? OR searchPattern, searchPattern, searchPattern, searchPattern)
customers.name ILIKE ? OR
warehouses.name ILIKE ? OR
products.name ILIKE ? OR
sales_users.name ILIKE ? OR
CONCAT(
marketings.so_number,
'-',
COALESCE(TO_CHAR(marketing_delivery_products.delivery_date, 'YYYYMMDD'), ''),
'-',
COALESCE(product_warehouses.warehouse_id::text, '')
) ILIKE ?
)`,
searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern)
} }
if filters.CustomerId > 0 { if filters.CustomerId > 0 {
@@ -190,19 +178,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId) db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId)
} }
if filters.AreaId > 0 || filters.LocationId > 0 {
db = db.Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
Joins("LEFT JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id")
if filters.AreaId > 0 {
db = db.Where("project_flocks.area_id = ?", filters.AreaId)
}
if filters.LocationId > 0 {
db = db.Where("project_flocks.location_id = ?", filters.LocationId)
}
}
if filters.MarketingType != "" { if filters.MarketingType != "" {
db = db.Joins("LEFT JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'"). db = db.Joins("LEFT JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
Group("marketing_delivery_products.id") Group("marketing_delivery_products.id")
@@ -211,6 +186,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
case "ayam": case "ayam":
db = db.Where("flags.name IN (?)", []string{ db = db.Where("flags.name IN (?)", []string{
string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer), string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer),
string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagAyamMati),
}) })
case "telur": case "telur":
db = db.Where("flags.name IN (?)", []string{ db = db.Where("flags.name IN (?)", []string{
@@ -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 * totalWeight totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
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 * totalWeight totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
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 * totalWeight totalPrice := rp.UnitPrice * rp.Qty
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 * totalWeight totalPrice := rp.UnitPrice * rp.Qty
marketingProduct := &entity.MarketingProduct{ marketingProduct := &entity.MarketingProduct{
MarketingId: marketingId, MarketingId: marketingId,
@@ -14,7 +14,6 @@ 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"`
} }
@@ -53,8 +52,6 @@ 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,7 +28,6 @@ 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,10 +47,6 @@ 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
@@ -67,7 +63,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.withListRelations(db) db = s.withRelations(db)
if params.Search != "" { if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%") return db.Where("name ILIKE ?", "%"+params.Search+"%")
} }
@@ -76,23 +72,7 @@ func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
db = db.Where("category ILIKE ?", "%"+params.Category+"%") db = db.Where("category ILIKE ?", "%"+params.Category+"%")
} }
if params.Flag != "" { return db.Order("created_at DESC").Order("updated_at DESC")
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,8 +32,7 @@ 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"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
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,11 +110,7 @@ func (s *warehouseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
} }
typ := strings.ToUpper(req.Type) typ := strings.ToUpper(req.Type)
createValidationOpts := WarehouseTypeValidationOptions{ if err := validateWarehouseTypeRequirements(typ, &req.AreaId, req.LocationId, req.KandangId); err != nil {
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
} }
@@ -212,22 +208,9 @@ func (s warehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
finalKandangId = req.KandangId finalKandangId = req.KandangId
} }
originalLocationId := finalLocationId if err := validateWarehouseTypeRequirements(finalType, &finalAreaId, finalLocationId, finalKandangId); err != nil {
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)
@@ -255,65 +238,47 @@ func (s warehouseService) DeleteOne(c *fiber.Ctx, id uint) error {
return nil return nil
} }
type WarehouseTypeValidationOptions struct { func validateWarehouseTypeRequirements(typ string, areaID *uint, locationID *uint, kandangID *uint) error {
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 && *locationID != nil { if locationID != nil {
if opts.AutoClear && !opts.LocationProvided { return fiber.NewError(fiber.StatusBadRequest, "location_id must not be provided when type is AREA")
*locationID = nil
} else {
return fiber.NewError(fiber.StatusBadRequest, "location_id must not be provided when type is AREA")
}
} }
if kandangID != nil && *kandangID != nil { if kandangID != nil {
if opts.AutoClear && !opts.KandangProvided { return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is AREA")
*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 || *locationID == nil { if 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 && *kandangID != nil { if kandangID != nil {
if opts.AutoClear && !opts.KandangProvided { return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is LOCATION")
*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 || *locationID == nil { if 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 || *kandangID == nil { if 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
@@ -200,9 +200,9 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
newChikins = append(newChikins, newChickin) newChikins = append(newChikins, newChickin)
totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProductWarehouseID(c.Context(), chickinReq.ProductWarehouseId) totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get total population quantity for product warehouse %d", chickinReq.ProductWarehouseId)) return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get total population quantity for project_flock_kandang %d", req.ProjectFlockKandangId))
} }
availableQty := productWarehouse.Quantity - totalPopulationQty availableQty := productWarehouse.Quantity - totalPopulationQty
@@ -584,6 +584,7 @@ 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
@@ -1,7 +1,6 @@
package dto package dto
import ( import (
"strconv"
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -54,7 +53,6 @@ type ProjectFlockKandangListDTO struct {
ProjectFlockKandangRelationDTO ProjectFlockKandangRelationDTO
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"` ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
NameWithPeriod string `json:"name_with_period"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"` Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
@@ -106,7 +104,6 @@ func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e), ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
ProjectFlock: toProjectFlockDTO(projectFlockSummary), ProjectFlock: toProjectFlockDTO(projectFlockSummary),
Kandang: toKandangRelation(e.Kandang), Kandang: toKandangRelation(e.Kandang),
NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period),
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
CreatedUser: toCreatedUserDTO(e.ProjectFlock), CreatedUser: toCreatedUserDTO(e.ProjectFlock),
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }), Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
@@ -129,16 +126,6 @@ func toKandangRelation(kandang entity.Kandang) *kandangDTO.KandangRelationDTO {
return &mapped return &mapped
} }
func toNameWithPeriod(kandang entity.Kandang, period int) string {
if kandang.Name == "" {
return ""
}
if period == 0 {
return kandang.Name
}
return kandang.Name + " Period " + strconv.Itoa(period)
}
func toApprovalDTOSelector( func toApprovalDTOSelector(
e entity.ProjectFlockKandang, selector func(entity.ProjectFlockKandang) *entity.Approval) *approvalDTO.ApprovalRelationDTO { e entity.ProjectFlockKandang, selector func(entity.ProjectFlockKandang) *entity.Approval) *approvalDTO.ApprovalRelationDTO {
approval := selector(e) approval := selector(e)
@@ -160,7 +147,6 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e), ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
ProjectFlock: toProjectFlockDTO(projectFlockSummary), ProjectFlock: toProjectFlockDTO(projectFlockSummary),
Kandang: toKandangRelation(e.Kandang), Kandang: toKandangRelation(e.Kandang),
NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period),
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
CreatedUser: toCreatedUserDTO(e.ProjectFlock), CreatedUser: toCreatedUserDTO(e.ProjectFlock),
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }), Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
@@ -2,7 +2,6 @@ package repository
import ( import (
"context" "context"
"math"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -17,7 +16,6 @@ type ProjectFlockPopulationRepository interface {
GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error) GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error)
GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
GetTotalChickInByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (int64, error)
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
@@ -113,7 +111,7 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProductWarehouseID(c
err := r.DB().WithContext(ctx). err := r.DB().WithContext(ctx).
Model(&entity.ProjectFlockPopulation{}). Model(&entity.ProjectFlockPopulation{}).
Where("product_warehouse_id = ?", productWarehouseID). Where("product_warehouse_id = ?", productWarehouseID).
Select("COALESCE(SUM(total_qty - total_used_qty), 0)"). Select("COALESCE(SUM(total_qty), 0)").
Scan(&total).Error Scan(&total).Error
if err != nil { if err != nil {
return 0, err return 0, err
@@ -137,22 +135,3 @@ func (r *projectFlockPopulationRepositoryImpl) GetAvailableQtyByProjectFlockKand
} }
return total, nil return total, nil
} }
func (r *projectFlockPopulationRepositoryImpl) GetTotalChickInByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (int64, error) {
var total float64
err := r.DB().WithContext(ctx).
Table("project_flock_populations").
Select("COALESCE(SUM(project_flock_populations.total_qty - project_flock_populations.total_used_qty), 0) AS total_qty").
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
Scan(&total).Error
if err != nil {
return 0, err
}
if total < 0 {
total = 0
}
return int64(math.Round(total)), nil
}
@@ -26,9 +26,8 @@ func (u *RecordingController) GetAll(c *fiber.Ctx) error {
projectFlockID := c.QueryInt("project_flock_kandang_id", 0) projectFlockID := c.QueryInt("project_flock_kandang_id", 0)
query := &validation.Query{ query := &validation.Query{
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search"),
} }
if projectFlockID > 0 { if projectFlockID > 0 {
query.ProjectFlockKandangId = uint(projectFlockID) query.ProjectFlockKandangId = uint(projectFlockID)
@@ -15,13 +15,13 @@ import (
// === DTO Structs === // === DTO Structs ===
type RecordingProjectFlockDTO struct { type RecordingProjectFlockDTO struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id"` ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
FlockName string `json:"flock_name"` FlockName string `json:"flock_name"`
ProjectFlockCategory string `json:"project_flock_category"` ProjectFlockCategory string `json:"project_flock_category"`
Period int `json:"period"` Period int `json:"period"`
ProductionStandart *RecordingProductionStandardDTO `json:"production_standart,omitempty"` ProductionStandart *RecordingProductionStandardDTO `json:"production_standart,omitempty"`
Fcr *RecordingFcrDTO `json:"fcr,omitempty"` Fcr *RecordingFcrDTO `json:"fcr,omitempty"`
TotalChickQty float64 `json:"total_chick_qty"` TotalChickQty float64 `json:"total_chick_qty"`
} }
type RecordingProductionStandardDTO struct { type RecordingProductionStandardDTO struct {
@@ -53,13 +53,6 @@ type RecordingLocationDTO struct {
Address string `json:"address"` Address string `json:"address"`
} }
type RecordingKandangDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Capacity float64 `json:"capacity"`
}
type RecordingWarehouseDTO struct { type RecordingWarehouseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -89,14 +82,12 @@ type RecordingListDTO struct {
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
Kandang *RecordingKandangDTO `json:"kandang,omitempty"` Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"`
Location *RecordingLocationDTO `json:"location,omitempty"`
} }
type RecordingDetailDTO struct { type RecordingDetailDTO struct {
RecordingListDTO RecordingListDTO
ProductCategory string `json:"product_category"` ProductCategory string `json:"product_category"`
Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"`
Depletions []RecordingDepletionDTO `json:"depletions"` Depletions []RecordingDepletionDTO `json:"depletions"`
Stocks []RecordingStockDTO `json:"stocks"` Stocks []RecordingStockDTO `json:"stocks"`
Eggs []RecordingEggDTO `json:"eggs"` Eggs []RecordingEggDTO `json:"eggs"`
@@ -142,11 +133,10 @@ func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
return RecordingDetailDTO{ return RecordingDetailDTO{
RecordingListDTO: listDTO, RecordingListDTO: listDTO,
ProductCategory: recordingProductCategory(e), ProductCategory: recordingProductCategory(e),
Warehouse: recordingWarehouseDTO(e), Depletions: ToRecordingDepletionDTOs(e.Depletions),
Depletions: ToRecordingDepletionDTOs(e.Depletions), Stocks: ToRecordingStockDTOs(e.Stocks),
Stocks: ToRecordingStockDTOs(e.Stocks), Eggs: ToRecordingEggDTOs(e.Eggs),
Eggs: ToRecordingEggDTOs(e.Eggs),
} }
} }
@@ -212,8 +202,7 @@ func toRecordingListDTO(e entity.Recording) RecordingListDTO {
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt, UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser, CreatedUser: createdUser,
Kandang: recordingKandangDTO(e), Warehouse: recordingWarehouseDTO(e),
Location: recordingKandangLocationDTO(e),
} }
} }
@@ -225,20 +214,20 @@ func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
} }
return RecordingRelationDTO{ return RecordingRelationDTO{
Id: e.Id, Id: e.Id,
ProjectFlock: toRecordingProjectFlockDTO(e), ProjectFlock: toRecordingProjectFlockDTO(e),
RecordDatetime: e.RecordDatetime, RecordDatetime: e.RecordDatetime,
Day: intValue(e.Day), Day: intValue(e.Day),
TotalDepletionQty: floatValue(e.TotalDepletionQty), TotalDepletionQty: floatValue(e.TotalDepletionQty),
CumDepletionRate: floatValue(e.CumDepletionRate), CumDepletionRate: floatValue(e.CumDepletionRate),
CumIntake: intValue(e.CumIntake), CumIntake: intValue(e.CumIntake),
FcrValue: floatValue(e.FcrValue), FcrValue: floatValue(e.FcrValue),
HenDay: floatValue(e.HenDay), HenDay: floatValue(e.HenDay),
HenHouse: floatValue(e.HenHouse), HenHouse: floatValue(e.HenHouse),
FeedIntake: floatValue(e.FeedIntake), FeedIntake: floatValue(e.FeedIntake),
EggMass: floatValue(e.EggMass), EggMass: floatValue(e.EggMass),
EggWeight: floatValue(e.EggWeight), EggWeight: floatValue(e.EggWeight),
Approval: latestApproval, Approval: latestApproval,
} }
} }
@@ -332,34 +321,6 @@ func recordingWarehouseDTO(e entity.Recording) *RecordingWarehouseDTO {
return mapWarehouseDTO(&pw.Warehouse) return mapWarehouseDTO(&pw.Warehouse)
} }
func recordingKandangDTO(e entity.Recording) *RecordingKandangDTO {
if e.ProjectFlockKandang == nil || e.ProjectFlockKandang.Kandang.Id == 0 {
return nil
}
kandang := e.ProjectFlockKandang.Kandang
return &RecordingKandangDTO{
Id: kandang.Id,
Name: kandang.Name,
Status: kandang.Status,
Capacity: kandang.Capacity,
}
}
func recordingKandangLocationDTO(e entity.Recording) *RecordingLocationDTO {
if e.ProjectFlockKandang == nil || e.ProjectFlockKandang.Kandang.Id == 0 {
return nil
}
location := e.ProjectFlockKandang.Kandang.Location
if location.Id == 0 {
return nil
}
return &RecordingLocationDTO{
Id: location.Id,
Name: location.Name,
Address: location.Address,
}
}
func primaryProductWarehouse(e entity.Recording) *entity.ProductWarehouse { func primaryProductWarehouse(e entity.Recording) *entity.ProductWarehouse {
if len(e.Stocks) > 0 { if len(e.Stocks) > 0 {
pw := e.Stocks[0].ProductWarehouse pw := e.Stocks[0].ProductWarehouse
@@ -74,28 +74,6 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
panic(fmt.Sprintf("failed to register recording usable workflow: %v", err)) panic(fmt.Sprintf("failed to register recording usable workflow: %v", err))
} }
} }
if err := fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyRecordingDepletion,
Table: "recording_depletions",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "source_product_warehouse_id",
UsageQuantity: "qty",
PendingQuantity: "pending_qty",
CreatedAt: "id",
},
ExcludedStockables: []fifo.StockableKey{
fifo.StockableKeyTransferToLayingIn,
fifo.StockableKeyStockTransferIn,
fifo.StockableKeyAdjustmentIn,
fifo.StockableKeyPurchaseItems,
fifo.StockableKeyRecordingEgg,
},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register recording depletion usable workflow: %v", err))
}
}
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo) approvalService := commonSvc.NewApprovalService(approvalRepo)
@@ -17,7 +17,6 @@ type RecordingRepository interface {
repository.BaseRepository[entity.Recording] repository.BaseRepository[entity.Recording]
WithRelations(db *gorm.DB) *gorm.DB WithRelations(db *gorm.DB) *gorm.DB
ApplySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB
GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error)
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
@@ -25,7 +24,6 @@ type RecordingRepository interface {
DeleteStocks(tx *gorm.DB, recordingID uint) error DeleteStocks(tx *gorm.DB, recordingID uint) error
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error) ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error
UpdateDepletionPending(tx *gorm.DB, depletionID uint, pendingQty float64) error
CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error
DeleteDepletions(tx *gorm.DB, recordingID uint) error DeleteDepletions(tx *gorm.DB, recordingID uint) error
@@ -46,7 +44,6 @@ 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)
@@ -86,7 +83,6 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
Preload("CreatedUser"). Preload("CreatedUser").
Preload("ProjectFlockKandang"). Preload("ProjectFlockKandang").
Preload("ProjectFlockKandang.Kandang"). Preload("ProjectFlockKandang.Kandang").
Preload("ProjectFlockKandang.Kandang.Location").
Preload("ProjectFlockKandang.ProjectFlock"). Preload("ProjectFlockKandang.ProjectFlock").
Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard"). Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard").
Preload("ProjectFlockKandang.ProjectFlock.Fcr"). Preload("ProjectFlockKandang.ProjectFlock.Fcr").
@@ -110,42 +106,6 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
Preload("Eggs.ProductWarehouse.Warehouse.Location") Preload("Eggs.ProductWarehouse.Warehouse.Location")
} }
func (r *RecordingRepositoryImpl) ApplySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB {
normalized := strings.ToLower(strings.TrimSpace(rawSearch))
if normalized == "" {
return db
}
likeQuery := "%" + normalized + "%"
subQuery := db.Session(&gorm.Session{NewDB: true}).
Table("recordings").
Select("recordings.id").
Joins("LEFT JOIN project_flock_kandangs pfk ON pfk.id = recordings.project_flock_kandangs_id").
Joins("LEFT JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
Joins("LEFT JOIN kandangs k ON k.id = pfk.kandang_id").
Joins("LEFT JOIN locations l ON l.id = k.location_id").
Joins("LEFT JOIN recording_stocks rs ON rs.recording_id = recordings.id").
Joins("LEFT JOIN recording_depletions rd ON rd.recording_id = recordings.id").
Joins("LEFT JOIN recording_eggs re ON re.recording_id = recordings.id").
Joins("LEFT JOIN product_warehouses pws ON pws.id = rs.product_warehouse_id").
Joins("LEFT JOIN product_warehouses pwd ON pwd.id = rd.product_warehouse_id").
Joins("LEFT JOIN product_warehouses pwe ON pwe.id = re.product_warehouse_id").
Joins("LEFT JOIN warehouses ws ON ws.id = pws.warehouse_id").
Joins("LEFT JOIN warehouses wd ON wd.id = pwd.warehouse_id").
Joins("LEFT JOIN warehouses we ON we.id = pwe.warehouse_id").
Where(`
LOWER(pf.flock_name) LIKE ?
OR LOWER(k.name) LIKE ?
OR LOWER(l.name) LIKE ?
OR LOWER(l.address) LIKE ?
OR LOWER(ws.name) LIKE ?
OR LOWER(wd.name) LIKE ?
OR LOWER(we.name) LIKE ?`,
likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery,
)
return db.Where("recordings.id IN (?)", subQuery)
}
func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) { func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) {
if projectFlockKandangId == 0 { if projectFlockKandangId == 0 {
return nil, errors.New("project_flock_kandang_id is required") return nil, errors.New("project_flock_kandang_id is required")
@@ -206,12 +166,6 @@ func (r *RecordingRepositoryImpl) UpdateStockUsage(tx *gorm.DB, stockID uint, us
}).Error }).Error
} }
func (r *RecordingRepositoryImpl) UpdateDepletionPending(tx *gorm.DB, depletionID uint, pendingQty float64) error {
return tx.Model(&entity.RecordingDepletion{}).
Where("id = ?", depletionID).
Update("pending_qty", pendingQty).Error
}
func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error { func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error {
if len(depletions) == 0 { if len(depletions) == 0 {
return nil return nil
@@ -367,25 +321,38 @@ func (r *RecordingRepositoryImpl) GetTotalChickinByProjectFlockKandang(tx *gorm.
} }
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) { func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
var result struct { var rows []struct {
TotalQty float64 TotalQty float64
UomName string
} }
if err := tx. if err := tx.
Table("recording_stocks"). Table("recording_stocks").
Select("COALESCE(SUM(COALESCE(recording_stocks.usage_qty, 0) + COALESCE(recording_stocks.pending_qty, 0)), 0) AS total_qty"). Select("COALESCE(recording_stocks.usage_qty, 0) + COALESCE(recording_stocks.pending_qty, 0) AS total_qty, LOWER(uoms.name) AS uom_name").
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id"). Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
Joins("JOIN products ON products.id = product_warehouses.product_id"). Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN uoms ON uoms.id = products.uom_id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ? AND UPPER(flags.name) = ?", entity.FlagableTypeProduct, "PAKAN"). Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ? AND UPPER(flags.name) = ?", entity.FlagableTypeProduct, "PAKAN").
Where("recording_stocks.recording_id = ?", recordingID). Where("recording_stocks.recording_id = ?", recordingID).
Scan(&result).Error; err != nil { Scan(&rows).Error; err != nil {
return 0, err return 0, err
} }
if result.TotalQty <= 0 { var total float64
return 0, nil for _, row := range rows {
if row.TotalQty <= 0 {
continue
}
switch strings.TrimSpace(row.UomName) {
case "kilogram", "kg", "kilograms", "kilo":
total += row.TotalQty * 1000
case "gram", "g", "grams":
total += row.TotalQty
default:
total += row.TotalQty
}
} }
return result.TotalQty * 1000, nil return total, nil
} }
func (r *RecordingRepositoryImpl) GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error) { func (r *RecordingRepositoryImpl) GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error) {
@@ -581,30 +548,3 @@ 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
}
@@ -44,7 +44,6 @@ type RecordingFIFOIntegrationService interface {
} }
var recordingStockUsableKey = fifo.UsableKeyRecordingStock var recordingStockUsableKey = fifo.UsableKeyRecordingStock
var recordingDepletionUsableKey = fifo.UsableKeyRecordingDepletion
type recordingService struct { type recordingService struct {
Log *logrus.Logger Log *logrus.Logger
@@ -117,8 +116,7 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
if params.ProjectFlockKandangId != 0 { if params.ProjectFlockKandangId != 0 {
db = db.Where("project_flock_kandangs_id = ?", params.ProjectFlockKandangId) db = db.Where("project_flock_kandangs_id = ?", params.ProjectFlockKandangId)
} }
db = s.Repository.ApplySearchFilters(db, params.Search) return db.Order("record_datetime DESC").Order("created_at DESC")
return db.Order("recordings.record_datetime DESC").Order("recordings.created_at DESC")
}) })
if err != nil { if err != nil {
@@ -211,6 +209,9 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
if !isLaying && len(req.Eggs) > 0 { if !isLaying && len(req.Eggs) > 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks") return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
} }
if isLaying && len(req.Eggs) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details are required for laying project flocks")
}
if err := s.ensureProductWarehousesExist(c, req.Stocks, req.Depletions, req.Eggs); err != nil { if err := s.ensureProductWarehousesExist(c, req.Stocks, req.Depletions, req.Eggs); err != nil {
return nil, err return nil, err
@@ -279,24 +280,10 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
} }
mappedDepletions := recordingutil.MapDepletions(createdRecording.Id, req.Depletions) mappedDepletions := recordingutil.MapDepletions(createdRecording.Id, req.Depletions)
if s.FifoSvc != nil && len(mappedDepletions) > 0 {
sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, req.ProjectFlockKandangId)
if err != nil {
return err
}
for i := range mappedDepletions {
mappedDepletions[i].SourceProductWarehouseId = &sourceWarehouseID
}
}
if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil { if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil {
s.Log.Errorf("Failed to persist depletions: %+v", err) s.Log.Errorf("Failed to persist depletions: %+v", err)
return err return err
} }
if s.FifoSvc != nil {
if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions); err != nil {
return err
}
}
mappedEggs := recordingutil.MapEggs(createdRecording.Id, createdRecording.CreatedBy, req.Eggs) mappedEggs := recordingutil.MapEggs(createdRecording.Id, createdRecording.CreatedBy, req.Eggs)
if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil { if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil {
@@ -310,7 +297,11 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
} }
var warehouseDeltas map[uint]float64 var warehouseDeltas map[uint]float64
warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, mappedEggs) if s.FifoSvc != nil {
warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, nil)
} else {
warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, mappedEggs)
}
if err := s.adjustProductWarehouseQuantities(ctx, tx, warehouseDeltas); err != nil { if err := s.adjustProductWarehouseQuantities(ctx, tx, warehouseDeltas); err != nil {
s.Log.Errorf("Failed to adjust product warehouses: %+v", err) s.Log.Errorf("Failed to adjust product warehouses: %+v", err)
return err return err
@@ -416,6 +407,9 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
if !isLaying && len(req.Eggs) > 0 { if !isLaying && len(req.Eggs) > 0 {
return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks") return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
} }
if isLaying && len(req.Eggs) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Egg details are required for laying project flocks")
}
} }
if hasStockChanges { if hasStockChanges {
@@ -437,38 +431,17 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
} }
if hasDepletionChanges { if hasDepletionChanges {
if s.FifoSvc != nil {
if err := s.releaseRecordingDepletions(ctx, tx, existingDepletions); err != nil {
return err
}
}
if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil { if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil {
s.Log.Errorf("Failed to clear depletions: %+v", err) s.Log.Errorf("Failed to clear depletions: %+v", err)
return err return err
} }
mappedDepletions := recordingutil.MapDepletions(recordingEntity.Id, req.Depletions) mappedDepletions := recordingutil.MapDepletions(recordingEntity.Id, req.Depletions)
if s.FifoSvc != nil && len(mappedDepletions) > 0 {
sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, recordingEntity.ProjectFlockKandangId)
if err != nil {
return err
}
for i := range mappedDepletions {
mappedDepletions[i].SourceProductWarehouseId = &sourceWarehouseID
}
}
if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil { if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil {
s.Log.Errorf("Failed to update depletions: %+v", err) s.Log.Errorf("Failed to update depletions: %+v", err)
return err return err
} }
if s.FifoSvc != nil {
if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions); err != nil {
return err
}
}
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(existingDepletions, mappedDepletions, nil, nil)); err != nil { if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(existingDepletions, mappedDepletions, nil, nil)); err != nil {
s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", err) s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", err)
return err return err
@@ -674,11 +647,6 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
s.Log.Errorf("Failed to list depletions before delete: %+v", err) s.Log.Errorf("Failed to list depletions before delete: %+v", err)
return err return err
} }
if s.FifoSvc != nil {
if err := s.releaseRecordingDepletions(ctx, tx, oldDepletions); err != nil {
return err
}
}
oldEggs, err := s.Repository.ListEggs(tx, id) oldEggs, err := s.Repository.ListEggs(tx, id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -797,46 +765,6 @@ func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.
return nil return nil
} }
func (s *recordingService) consumeRecordingDepletions(ctx context.Context, tx *gorm.DB, depletions []entity.RecordingDepletion) error {
if len(depletions) == 0 || s.FifoSvc == nil {
return nil
}
for _, depletion := range depletions {
if depletion.Id == 0 {
continue
}
sourceWarehouseID := uint(0)
if depletion.SourceProductWarehouseId != nil {
sourceWarehouseID = *depletion.SourceProductWarehouseId
}
if sourceWarehouseID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion")
}
desired := depletion.Qty + depletion.PendingQty
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: recordingDepletionUsableKey,
UsableID: depletion.Id,
ProductWarehouseID: sourceWarehouseID,
Quantity: desired,
AllowPending: false,
Tx: tx,
})
if err != nil {
s.Log.Errorf("Failed to consume FIFO stock for recording depletion %d: %+v", depletion.Id, err)
return err
}
if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, result.PendingQuantity); err != nil {
return err
}
}
return nil
}
func (s *recordingService) ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error { func (s *recordingService) ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
return s.consumeRecordingStocks(ctx, tx, stocks) return s.consumeRecordingStocks(ctx, tx, stocks)
} }
@@ -868,67 +796,10 @@ func (s *recordingService) releaseRecordingStocks(ctx context.Context, tx *gorm.
return nil return nil
} }
func (s *recordingService) releaseRecordingDepletions(ctx context.Context, tx *gorm.DB, depletions []entity.RecordingDepletion) error {
if len(depletions) == 0 || s.FifoSvc == nil {
return nil
}
for _, depletion := range depletions {
if depletion.Id == 0 {
continue
}
sourceWarehouseID := uint(0)
if depletion.SourceProductWarehouseId != nil {
sourceWarehouseID = *depletion.SourceProductWarehouseId
}
if sourceWarehouseID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion")
}
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: recordingDepletionUsableKey,
UsableID: depletion.Id,
Tx: tx,
}); err != nil {
s.Log.Errorf("Failed to release FIFO stock for recording depletion %d: %+v", depletion.Id, err)
return err
}
if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, 0); err != nil {
return err
}
}
return nil
}
func (s *recordingService) ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error { func (s *recordingService) ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
return s.releaseRecordingStocks(ctx, tx, stocks) return s.releaseRecordingStocks(ctx, tx, stocks)
} }
func (s *recordingService) resolvePopulationWarehouseID(ctx context.Context, projectFlockKandangID uint) (uint, error) {
if projectFlockKandangID == 0 {
return 0, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
}
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
if err != nil {
s.Log.Errorf("Failed to fetch populations for project_flock_kandang_id=%d: %+v", projectFlockKandangID, err)
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data populasi")
}
for _, pop := range populations {
if pop.ProductWarehouseId > 0 && pop.TotalQty > 0 {
return pop.ProductWarehouseId, nil
}
}
for _, pop := range populations {
if pop.ProductWarehouseId > 0 {
return pop.ProductWarehouseId, nil
}
}
return 0, fiber.NewError(fiber.StatusBadRequest, "Source product warehouse populasi tidak ditemukan")
}
func buildWarehouseDeltas( func buildWarehouseDeltas(
oldDepletions, newDepletions []entity.RecordingDepletion, oldDepletions, newDepletions []entity.RecordingDepletion,
oldEggs, newEggs []entity.RecordingEgg, oldEggs, newEggs []entity.RecordingEgg,
@@ -1070,8 +941,10 @@ func (s *recordingService) syncRecordingStocks(
desired := item.Qty desired := item.Qty
stock.UsageQty = &desired stock.UsageQty = &desired
zero := 0.0 if item.PendingQty != nil {
stock.PendingQty = &zero pending := *item.PendingQty
stock.PendingQty = &pending
}
stocksToConsume = append(stocksToConsume, stock) stocksToConsume = append(stocksToConsume, stock)
} }
@@ -1117,20 +990,43 @@ func ensureRecordingEggsUnused(eggs []entity.RecordingEgg) error {
} }
func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool { func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool {
hasPending := false
for _, item := range incoming {
if item.PendingQty != nil {
hasPending = true
break
}
}
existingUsage := make(map[uint]float64) existingUsage := make(map[uint]float64)
existingTotal := make(map[uint]float64)
for _, stock := range existing { for _, stock := range existing {
var usage float64 var usage float64
var pending float64
if stock.UsageQty != nil { if stock.UsageQty != nil {
usage = *stock.UsageQty usage = *stock.UsageQty
} }
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
existingUsage[stock.ProductWarehouseId] += usage existingUsage[stock.ProductWarehouseId] += usage
existingTotal[stock.ProductWarehouseId] += usage + pending
} }
incomingUsage := make(map[uint]float64) incomingUsage := make(map[uint]float64)
incomingTotal := make(map[uint]float64)
for _, item := range incoming { for _, item := range incoming {
var pending float64
if item.PendingQty != nil {
pending = *item.PendingQty
}
incomingUsage[item.ProductWarehouseId] += item.Qty incomingUsage[item.ProductWarehouseId] += item.Qty
incomingTotal[item.ProductWarehouseId] += item.Qty + pending
} }
if hasPending {
return floatMapsMatch(existingTotal, incomingTotal)
}
return floatMapsMatch(existingUsage, incomingUsage) return floatMapsMatch(existingUsage, incomingUsage)
} }
@@ -1328,7 +1224,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
var eggMass float64 var eggMass float64
if remainingChick > 0 && totalEggWeightGrams > 0 { if remainingChick > 0 && totalEggWeightGrams > 0 {
eggMass = totalEggWeightGrams / remainingChick eggMass = (totalEggWeightGrams / remainingChick) * 1000
updates["egg_mass"] = eggMass updates["egg_mass"] = eggMass
recording.EggMass = &eggMass recording.EggMass = &eggMass
} else { } else {
@@ -1338,7 +1234,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
var eggWeight float64 var eggWeight float64
if totalEggQty > 0 && totalEggWeightGrams > 0 { if totalEggQty > 0 && totalEggWeightGrams > 0 {
eggWeight = totalEggWeightGrams / totalEggQty eggWeight = (totalEggWeightGrams / totalEggQty) * 1000
updates["egg_weight"] = eggWeight updates["egg_weight"] = eggWeight
recording.EggWeight = &eggWeight recording.EggWeight = &eggWeight
} else { } else {
@@ -2,8 +2,9 @@ package validation
type ( type (
Stock struct { Stock struct {
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"` ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
Qty float64 `json:"qty" validate:"required,gte=0"` Qty float64 `json:"qty" validate:"required,gte=0"`
PendingQty *float64 `json:"pending_qty,omitempty" validate:"omitempty,gte=0"`
} }
Depletion struct { Depletion struct {
@@ -19,24 +20,23 @@ type (
) )
type Create struct { type Create struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"` RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
Stocks []Stock `json:"stocks" validate:"dive"` Stocks []Stock `json:"stocks" validate:"dive"`
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"` Depletions []Depletion `json:"depletions" validate:"dive"`
Eggs []Egg `json:"eggs" validate:"omitempty,dive"` Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
} }
type Update struct { type Update struct {
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"` Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"` Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"` Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
} }
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,max=100"`
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"` ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
Search string `query:"search" validate:"omitempty,max=50"`
} }
type Approve struct { type Approve struct {
@@ -25,12 +25,8 @@ func NewTransferLayingController(transferLayingService service.TransferLayingSer
func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{ query := &validation.Query{
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
TransferDate: c.Query("transfer_date", ""),
FlockSource: uint(c.QueryInt("flock_source", 0)),
FlockDestination: uint(c.QueryInt("flock_destination", 0)),
} }
if query.Page < 1 || query.Limit < 1 { if query.Page < 1 || query.Limit < 1 {
@@ -183,6 +179,7 @@ func (u *TransferLayingController) Approval(c *fiber.Ctx) error {
}) })
} }
func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error { func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error {
projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32) projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32)
if err != nil { if err != nil {
@@ -162,19 +162,9 @@ func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouse
} }
func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO { func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO {
// Tampilkan requested qty sebelum approve, consumed qty setelah approve
var displayQty float64
if source.UsageQty > 0 {
// Sudah di-approve dan di-consume, tampilkan actual consumed quantity
displayQty = source.UsageQty
} else {
// Belum di-approve, tampilkan requested quantity
displayQty = source.RequestedQty
}
return LayingTransferSourceDTO{ return LayingTransferSourceDTO{
SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang), SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang),
Qty: displayQty, Qty: source.UsageQty, // Ambil dari UsageQty (FIFO consumed quantity)
ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse), ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse),
Note: source.Note, Note: source.Note,
} }
@@ -110,31 +110,8 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
// Apply search and filters
if params.Search != "" {
searchPattern := "%" + params.Search + "%"
db = db.Joins("LEFT JOIN project_flocks AS pf_from ON laying_transfers.from_project_flock_id = pf_from.id").
Joins("LEFT JOIN project_flocks AS pf_to ON laying_transfers.to_project_flock_id = pf_to.id").
Where("laying_transfers.transfer_number ILIKE ? OR laying_transfers.notes ILIKE ? OR pf_from.flock_name ILIKE ? OR pf_to.flock_name ILIKE ?",
searchPattern, searchPattern, searchPattern, searchPattern)
}
if params.TransferDate != "" {
db = db.Where("transfer_date::date = ?::date", params.TransferDate)
}
if params.FlockSource > 0 {
db = db.Where("from_project_flock_id = ?", params.FlockSource)
}
if params.FlockDestination > 0 {
db = db.Where("to_project_flock_id = ?", params.FlockDestination)
}
db = db.Order("created_at DESC")
db = s.withRelations(db) db = s.withRelations(db)
db = db.Order("created_at DESC")
return db return db
}) })
@@ -239,7 +216,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
for _, sourceDetail := range req.SourceKandangs { for _, sourceDetail := range req.SourceKandangs {
if sourceDetail.Quantity <= 0 { if sourceDetail.Quantity <= 0 {
continue return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang sumber harus lebih dari 0")
} }
totalSourceQty += sourceDetail.Quantity totalSourceQty += sourceDetail.Quantity
@@ -270,18 +247,11 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
for _, targetDetail := range req.TargetKandangs { for _, targetDetail := range req.TargetKandangs {
if targetDetail.Quantity <= 0 { if targetDetail.Quantity <= 0 {
continue return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang tujuan harus lebih dari 0")
} }
totalTargetQty += targetDetail.Quantity totalTargetQty += targetDetail.Quantity
} }
if totalSourceQty == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang sumber dengan jumlah lebih dari 0")
}
if totalTargetQty == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang tujuan dengan jumlah lebih dari 0")
}
if totalSourceQty != totalTargetQty { if totalSourceQty != totalTargetQty {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty))
} }
@@ -309,16 +279,11 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
} }
for _, sourceDetail := range req.SourceKandangs { for _, sourceDetail := range req.SourceKandangs {
if sourceDetail.Quantity == 0 {
continue
}
productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]
source := entity.LayingTransferSource{ source := entity.LayingTransferSource{
LayingTransferId: createBody.Id, LayingTransferId: createBody.Id,
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId, SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
RequestedQty: sourceDetail.Quantity, // Quantity yang diminta user
UsageQty: 0, UsageQty: 0,
PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval
ProductWarehouseId: &productWarehouseId, ProductWarehouseId: &productWarehouseId,
@@ -330,9 +295,6 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
} }
for _, targetDetail := range req.TargetKandangs { for _, targetDetail := range req.TargetKandangs {
if targetDetail.Quantity == 0 {
continue
}
targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId) targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
if err != nil { if err != nil {
@@ -501,9 +463,8 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
source := entity.LayingTransferSource{ source := entity.LayingTransferSource{
LayingTransferId: id, LayingTransferId: id,
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId, SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
RequestedQty: sourceDetail.Quantity, // Quantity yang diminta user
UsageQty: 0, UsageQty: 0,
PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval PendingUsageQty: sourceDetail.Quantity,
ProductWarehouseId: &productWarehouseId, ProductWarehouseId: &productWarehouseId,
} }
if err := sourceRepo.CreateOne(c.Context(), &source, nil); err != nil { if err := sourceRepo.CreateOne(c.Context(), &source, nil); err != nil {
@@ -739,7 +700,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID)) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID))
} }
note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber) note := fmt.Sprintf("Transfer to Laying #%s - Target Kandang", transfer.TransferNumber)
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyTransferToLayingIn, StockableKey: fifo.StockableKeyTransferToLayingIn,
StockableID: target.Id, StockableID: target.Id,
@@ -853,15 +814,15 @@ func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, project
kandangAvailableQty := make(map[uint]float64) kandangAvailableQty := make(map[uint]float64)
for _, kandang := range kandangs { for _, kandang := range kandangs {
// Gunakan fungsi repository yang sama dengan recording service
totalAvailable, err := s.ProjectFlockPopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), kandang.Id) totalQty, err := s.ProjectFlockPopulationRepo.GetTotalQtyByProjectFlockKandangID(ctx.Context(), kandang.Id)
if err != nil { if err != nil {
s.Log.Warnf("Failed to get available qty for kandang %d: %+v", kandang.Id, err) s.Log.Warnf("Failed to get total qty for kandang %d: %+v", kandang.Id, err)
kandangAvailableQty[kandang.Id] = 0 kandangAvailableQty[kandang.Id] = 0
continue continue
} }
kandangAvailableQty[kandang.Id] = totalAvailable kandangAvailableQty[kandang.Id] = totalQty
} }
return pf, kandangAvailableQty, nil return pf, kandangAvailableQty, nil
@@ -2,12 +2,12 @@ package validation
type SourceKandangDetail struct { type SourceKandangDetail struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"` ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
Quantity float64 `json:"quantity"` Quantity float64 `json:"quantity" validate:"required,gt=0"`
} }
type TargetKandangDetail struct { type TargetKandangDetail struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"` ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
Quantity float64 `json:"quantity"` Quantity float64 `json:"quantity" validate:"required,gt=0"`
} }
type Create struct { type Create struct {
@@ -29,12 +29,8 @@ type Update struct {
} }
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty"`
TransferDate string `query:"transfer_date" validate:"omitempty"`
FlockSource uint `query:"flock_source" validate:"omitempty,number"`
FlockDestination uint `query:"flock_destination" validate:"omitempty,number"`
} }
type Approve struct { type Approve struct {
@@ -345,52 +345,7 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
); err != nil { ); err != nil {
return nil, err return nil, err
} }
if err := s.ensureUniqueUniformity(c.Context(), 0, req.ProjectFlockKandangId, req.Week, &uniformDate); err != nil {
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
}
return nil, err
}
category := strings.TrimSpace(pfk.ProjectFlock.Category)
if s.ProductionStandardRepo != nil && pfk.ProjectFlock.ProductionStandardId != 0 {
if standard, err := s.ProductionStandardRepo.GetByID(c.Context(), pfk.ProjectFlock.ProductionStandardId, nil); err == nil {
if strings.TrimSpace(standard.ProjectCategory) != "" {
category = standard.ProjectCategory
}
}
}
weekBase := 1
if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) {
weekBase = 18
}
if req.Week < weekBase {
if weekBase == 18 {
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects")
}
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects")
}
var latestWeek int
if err := s.Repository.DB().WithContext(c.Context()).
Model(&entity.ProjectFlockKandangUniformity{}).
Where("project_flock_kandang_id = ? AND deleted_at IS NULL", req.ProjectFlockKandangId).
Select("COALESCE(MAX(week), 0)").
Scan(&latestWeek).Error; err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate uniformity week sequence")
}
if latestWeek == 0 && req.Week != weekBase {
if weekBase == 18 {
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects")
}
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects")
}
if latestWeek > 0 && req.Week > latestWeek+1 {
return nil, fiber.NewError(fiber.StatusBadRequest, "week must be sequential without skipping")
}
if err := s.ensureUniqueUniformity(c.Context(), 0, req.ProjectFlockKandangId, req.Week); err != nil {
return nil, err return nil, err
} }
@@ -532,35 +487,8 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
if req.ProjectFlockKandangId != nil { if req.ProjectFlockKandangId != nil {
targetPFKID = *req.ProjectFlockKandangId targetPFKID = *req.ProjectFlockKandangId
} }
if targetPFKID != 0 && targetWeek > 0 {
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetPFKID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
}
return nil, err
}
category := strings.TrimSpace(pfk.ProjectFlock.Category)
if s.ProductionStandardRepo != nil && pfk.ProjectFlock.ProductionStandardId != 0 {
if standard, err := s.ProductionStandardRepo.GetByID(c.Context(), pfk.ProjectFlock.ProductionStandardId, nil); err == nil {
if strings.TrimSpace(standard.ProjectCategory) != "" {
category = standard.ProjectCategory
}
}
}
weekBase := 1
if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) {
weekBase = 18
}
if targetWeek < weekBase {
if weekBase == 18 {
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects")
}
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects")
}
}
if targetDate != nil { if targetDate != nil {
if err := s.ensureUniqueUniformity(c.Context(), id, targetPFKID, targetWeek); err != nil { if err := s.ensureUniqueUniformity(c.Context(), id, targetPFKID, targetWeek, targetDate); err != nil {
return nil, err return nil, err
} }
} }
@@ -676,7 +604,7 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
return s.GetOne(c, id) return s.GetOne(c, id)
} }
func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int) error { func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int, uniformDate *time.Time) error {
if projectFlockKandangID == 0 || week == 0 { if projectFlockKandangID == 0 || week == 0 {
return nil return nil
} }
@@ -36,7 +36,6 @@ 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 {
@@ -167,21 +166,12 @@ func (b *expenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[
if actorID == 0 { if actorID == 0 {
actorID = 1 actorID = 1
} }
approvalRepo := commonRepo.NewApprovalRepository(b.db) svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
svc := commonSvc.NewApprovalService(approvalRepo) action := entity.ApprovalActionUpdated
action := entity.ApprovalActionCreated
for id := range expenseIDs { for id := range expenseIDs {
latestApproval, err := approvalRepo.LatestByTarget(ctx, string(utils.ApprovalWorkflowExpense), uint(id), nil) if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err return err
} }
if latestApproval == nil {
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
return err
}
}
} }
return nil return nil
} }
@@ -192,22 +182,6 @@ 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 {
@@ -231,9 +205,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(filtered) > 0 { if len(updates) > 0 {
ids := make([]uint, 0, len(filtered)) ids := make([]uint, 0, len(updates))
for _, upd := range filtered { for _, upd := range updates {
if upd.PurchaseItemID != 0 { if upd.PurchaseItemID != 0 {
ids = append(ids, upd.PurchaseItemID) ids = append(ids, upd.PurchaseItemID)
} }
@@ -278,7 +252,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
groups := make(map[string][]groupedItem) groups := make(map[string][]groupedItem)
for _, payload := range filtered { for _, payload := range updates {
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,7 +702,6 @@ 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
} }
@@ -757,7 +756,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
} }
visitedItems[payload.PurchaseItemID] = struct{}{} visitedItems[payload.PurchaseItemID] = struct{}{}
var supplierID uint supplierID := purchase.SupplierId
if payload.ExpeditionVendorID != nil && *payload.ExpeditionVendorID != 0 { if payload.ExpeditionVendorID != nil && *payload.ExpeditionVendorID != 0 {
supplierID = *payload.ExpeditionVendorID supplierID = *payload.ExpeditionVendorID
} }
@@ -771,15 +770,6 @@ 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,
@@ -787,7 +777,6 @@ 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,
}) })
@@ -975,7 +964,6 @@ 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)
} }
@@ -82,8 +82,6 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
ProductId: int64(ctx.QueryInt("product_id", 0)), ProductId: int64(ctx.QueryInt("product_id", 0)),
WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)), WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)),
SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)), SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)),
AreaId: int64(ctx.QueryInt("area_id", 0)),
LocationId: int64(ctx.QueryInt("location_id", 0)),
MarketingType: ctx.Query("marketing_type", ""), MarketingType: ctx.Query("marketing_type", ""),
FilterBy: ctx.Query("filter_by", ""), FilterBy: ctx.Query("filter_by", ""),
StartDate: ctx.Query("start_date", ""), StartDate: ctx.Query("start_date", ""),
@@ -244,65 +242,6 @@ 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 == "" {
@@ -1,120 +0,0 @@
package dto
import (
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/entities"
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories"
)
type CustomerPaymentReportRow struct {
TransactionType string `json:"transaction_type"`
TransactionID int64 `json:"transaction_id"`
TransDate time.Time `json:"trans_date"`
DeliveryDate *time.Time `json:"delivery_date"`
Reference string `json:"reference"`
Qty float64 `json:"qty"`
Weight float64 `json:"weight"`
AverageWeight float64 `json:"average_weight"`
UnitPrice float64 `json:"unit_price"`
FinalPrice float64 `json:"final_price"`
TotalPrice float64 `json:"total_price"`
PaymentAmount float64 `json:"payment_amount"`
AccountsReceivable float64 `json:"accounts_receivable"`
AgingDay *int `json:"aging_day"`
Status string `json:"status"`
VehicleNumbers []string `json:"vehicle_numbers"`
PickupInfo []string `json:"pickup_info"`
SalesPerson string `json:"sales_person"`
}
type CustomerPaymentReportSummary struct {
TotalQty float64 `json:"total_qty"`
TotalWeight float64 `json:"total_weight"`
TotalFinalAmount float64 `json:"total_final_amount"`
TotalGrandAmount float64 `json:"total_grand_amount"`
TotalPayment float64 `json:"total_payment"`
TotalAccountsReceivable float64 `json:"total_accounts_receivable"`
}
type CustomerPaymentReportItem struct {
Customer customerDTO.CustomerRelationDTO `json:"customer"`
InitialBalance float64 `json:"initial_balance"`
Rows []CustomerPaymentReportRow `json:"rows"`
Summary CustomerPaymentReportSummary `json:"summary"`
}
type CustomerPaymentReportResponse struct {
Data []CustomerPaymentReportItem `json:"data"`
}
func ToCustomerPaymentReportRow(tx repportRepo.CustomerPaymentTransaction) CustomerPaymentReportRow {
return CustomerPaymentReportRow{
TransactionType: tx.TransactionType,
TransactionID: tx.TransactionID,
TransDate: tx.TransDate,
DeliveryDate: tx.DeliveryDate,
Reference: tx.Reference,
Qty: tx.Qty,
Weight: tx.Weight,
AverageWeight: tx.AverageWeight,
UnitPrice: tx.Price,
FinalPrice: tx.FinalPrice,
TotalPrice: tx.TotalPrice,
PaymentAmount: tx.PaymentAmount,
VehicleNumbers: parseStringSlice(tx.VehicleNumbers),
PickupInfo: parseStringSlice(tx.PickupInfo),
SalesPerson: tx.SalesPerson,
}
}
func ToCustomerPaymentReportItem(customer entities.Customer, initialBalance float64, rows []CustomerPaymentReportRow, summary CustomerPaymentReportSummary) CustomerPaymentReportItem {
return CustomerPaymentReportItem{
Customer: customerDTO.ToCustomerRelationDTO(customer),
InitialBalance: initialBalance,
Rows: rows,
Summary: summary,
}
}
func ToCustomerPaymentReportSummary(rows []CustomerPaymentReportRow, initialBalance float64) CustomerPaymentReportSummary {
summary := CustomerPaymentReportSummary{}
for _, row := range rows {
summary.TotalQty += row.Qty
summary.TotalWeight += row.Weight
if row.TransactionType == "SALES" {
summary.TotalFinalAmount += row.FinalPrice
summary.TotalGrandAmount += row.TotalPrice
} else if row.TransactionType == "PAYMENT" {
summary.TotalPayment += row.PaymentAmount
}
}
// Total AR = Initial Balance - Total Sales + Total Payment
summary.TotalAccountsReceivable = initialBalance - summary.TotalGrandAmount + summary.TotalPayment
return summary
}
func parseStringSlice(str string) []string {
str = strings.TrimSpace(str)
if str == "" || str == "-" {
return []string{}
}
parts := strings.Split(str, ",")
result := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part != "" {
result = append(result, part)
}
}
return result
}
+35 -30
View File
@@ -25,15 +25,14 @@ type HppPerKandangResponseData struct {
} }
type HppPerKandangRowDTO struct { type HppPerKandangRowDTO struct {
ID int `json:"id"` ID int `json:"id"`
Kandang HppPerKandangRowKandangDTO `json:"kandang"` Kandang HppPerKandangRowKandangDTO `json:"kandang"`
NameWithPeriode string `json:"name_with_periode"` 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"`
// EggProductionTotalWeightKg float64 `json:"egg_production_total_weight_kg"` EggProductionKg float64 `json:"egg_production_kg"`
// EggProductionTotalPieces int64 `json:"egg_production_total_pieces"`
// 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"`
@@ -41,8 +40,8 @@ type HppPerKandangRowDTO struct {
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"` FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"` DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
AverageDocPriceRp int64 `json:"average_doc_price_rp"` AverageDocPriceRp int64 `json:"average_doc_price_rp"`
// HppRp float64 `json:"hpp_rp"` HppRp float64 `json:"hpp_rp"`
// RemainingValueRp int64 `json:"remaining_value_rp"` RemainingValueRp int64 `json:"remaining_value_rp"`
} }
type HppPerKandangRowKandangDTO struct { type HppPerKandangRowKandangDTO struct {
@@ -81,28 +80,34 @@ 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"`
AvgWeightKg float64 `json:"avg_weight_kg"` RemainingChickenBirds int64 `json:"remaining_chicken_birds"`
EggProductionPieces int64 `json:"egg_production_pieces"` RemainingChickenWeightKg float64 `json:"remaining_chicken_weight_kg"`
EggProductionKg float64 `json:"egg_production_kg"` AvgWeightKg float64 `json:"avg_weight_kg"`
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"` EggProductionPieces int64 `json:"egg_production_pieces"`
EggValueRp int64 `json:"egg_value_rp"` EggProductionKg float64 `json:"egg_production_kg"`
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"` EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"` EggValueRp int64 `json:"egg_value_rp"`
AverageDocPriceRp float64 `json:"average_doc_price_rp"` FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
HppRp float64 `json:"hpp_rp"` DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
RemainingValueRp int64 `json:"remaining_value_rp"` AverageDocPriceRp float64 `json:"average_doc_price_rp"`
HppRp float64 `json:"hpp_rp"`
RemainingValueRp int64 `json:"remaining_value_rp"`
} }
type HppPerKandangSummaryTotalDTO struct { type HppPerKandangSummaryTotalDTO struct {
AverageWeightKg float64 `json:"average_weight_kg"` TotalRemainingChickenBirds int64 `json:"total_remaining_chicken_birds"`
TotalEggProductionPieces int64 `json:"total_egg_production_pieces"` TotalRemainingChickenWeightKg float64 `json:"total_remaining_chicken_weight_kg"`
TotalEggProductionKg float64 `json:"total_egg_production_kg"` AverageWeightKg float64 `json:"average_weight_kg"`
AverageEggHppRpPerKg float64 `json:"average_egg_hpp_rp_per_kg"` TotalRemainingValueRp int64 `json:"total_remaining_value_rp"`
TotalEggValueRp int64 `json:"total_egg_value_rp"` TotalEggProductionPieces int64 `json:"total_egg_production_pieces"`
TotalAverageDocPriceRp float64 `json:"total_average_doc_price_rp"` TotalEggProductionKg float64 `json:"total_egg_production_kg"`
AverageEggHppRpPerKg float64 `json:"average_egg_hpp_rp_per_kg"`
TotalEggValueRp int64 `json:"total_egg_value_rp"`
TotalHppRp float64 `json:"total_hpp_rp"`
TotalAverageDocPriceRp float64 `json:"total_average_doc_price_rp"`
} }
func NewHppPerKandangFiltersDTO(area, location, kandang, weightMin, weightMax, period, showUnrecorded string) HppPerKandangFiltersDTO { func NewHppPerKandangFiltersDTO(area, location, kandang, weightMin, weightMax, period, showUnrecorded string) HppPerKandangFiltersDTO {
@@ -27,12 +27,12 @@ type PurchaseSupplierRowDTO struct {
} }
type PurchaseSupplierSummaryDTO struct { type PurchaseSupplierSummaryDTO struct {
TotalQty float64 `json:"total_qty"` TotalQty float64 `json:"total_qty"`
TotalPurchaseValue float64 `json:"total_purchase_value"` TotalPurchaseValue float64 `json:"total_purchase_value"`
TotalTransportValue float64 `json:"total_transport_value"` TotalTransportValue float64 `json:"total_transport_value"`
TotalAmount float64 `json:"total_amount"` TotalAmount float64 `json:"total_amount"`
TotalUnitPrice float64 `json:"total_unit_price"` TotalUnitPrice float64 `json:"total_unit_price"`
TotalTransportUnitPrice float64 `json:"total_transport_unit_price"` TotalTransportUnitPrice float64 `json:"total_transport_unit_price"`
} }
type PurchaseSupplierDTO struct { type PurchaseSupplierDTO struct {
@@ -122,6 +122,11 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem
rows := make([]PurchaseSupplierRowDTO, 0, len(items)) rows := make([]PurchaseSupplierRowDTO, 0, len(items))
summary := PurchaseSupplierSummaryDTO{} summary := PurchaseSupplierSummaryDTO{}
var unitPriceSum float64
var unitPriceCount int
var transportUnitPriceSum float64
var transportUnitPriceCount int
for i := range items { for i := range items {
row := ToPurchaseSupplierRowDTO(&items[i]) row := ToPurchaseSupplierRowDTO(&items[i])
rows = append(rows, row) rows = append(rows, row)
@@ -131,16 +136,19 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem
summary.TotalTransportValue += row.TransportValue summary.TotalTransportValue += row.TransportValue
summary.TotalAmount += row.TotalAmount summary.TotalAmount += row.TotalAmount
unitPriceSum += row.UnitPrice
unitPriceCount++
transportUnitPriceSum += row.TransportUnitPrice
transportUnitPriceCount++
} }
if summary.TotalQty > 0 { if unitPriceCount > 0 {
avg := summary.TotalPurchaseValue / summary.TotalQty summary.TotalUnitPrice = math.Round(unitPriceSum / float64(unitPriceCount))
summary.TotalUnitPrice = math.Round(avg)
} }
if summary.TotalQty > 0 { if transportUnitPriceCount > 0 {
avg := summary.TotalTransportValue / summary.TotalQty summary.TotalTransportUnitPrice = math.Round(transportUnitPriceSum / float64(transportUnitPriceCount))
summary.TotalTransportUnitPrice = math.Round(avg)
} }
return PurchaseSupplierDTO{ return PurchaseSupplierDTO{
+15 -4
View File
@@ -12,7 +12,6 @@ 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"
@@ -36,14 +35,26 @@ 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(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository, customerRepository, standardGrowthDetailRepository, productionStandardDetailRepository) repportService := sRepport.NewRepportService(
validate,
expenseRealizationRepository,
marketingDeliveryProductRepository,
purchaseRepository,
chickinRepository,
recordingRepository,
approvalSvc,
purchaseSupplierRepository,
debtSupplierRepository,
hppPerKandangRepository,
productionResultRepository,
standardGrowthDetailRepository,
productionStandardDetailRepository,
)
userService := sUser.NewUserService(userRepository, validate) userService := sUser.NewUserService(userRepository, validate)
RepportRoutes(router, userService, repportService) RepportRoutes(router, userService, repportService)
@@ -1,195 +0,0 @@
package repositories
import (
"context"
"time"
"gorm.io/gorm"
)
type CustomerPaymentTransaction struct {
TransactionType string `gorm:"column:transaction_type"`
TransactionID int64 `gorm:"column:transaction_id"`
CustomerID int64 `gorm:"column:customer_id"`
TransDate time.Time `gorm:"column:trans_date"`
DeliveryDate *time.Time `gorm:"column:delivery_date"`
Reference string `gorm:"column:reference"`
VehicleNumbers string `gorm:"column:vehicle_numbers"`
Qty float64 `gorm:"column:qty"`
Weight float64 `gorm:"column:weight"`
AverageWeight float64 `gorm:"column:average_weight"`
Price float64 `gorm:"column:price"`
FinalPrice float64 `gorm:"column:final_price"`
TotalPrice float64 `gorm:"column:total_price"`
PaymentAmount float64 `gorm:"column:payment_amount"`
PickupInfo string `gorm:"column:pickup_info"`
SalesPerson string `gorm:"column:sales_person"`
}
type CustomerPaymentRepository interface {
GetCustomerPaymentTransactions(ctx context.Context, customerID *uint) ([]CustomerPaymentTransaction, error)
GetInitialBalanceByCustomer(ctx context.Context, customerID uint) (float64, error)
GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int) ([]uint, int64, error)
}
type customerPaymentRepositoryImpl struct {
db *gorm.DB
}
func NewCustomerPaymentRepository(db *gorm.DB) CustomerPaymentRepository {
return &customerPaymentRepositoryImpl{db: db}
}
func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx context.Context, customerID *uint) ([]CustomerPaymentTransaction, error) {
salesQuery := r.db.WithContext(ctx).
Table("marketing_delivery_products mdp").
Select(`
'SALES' AS transaction_type,
mdp.id::BIGINT AS transaction_id,
c.id::BIGINT AS customer_id,
m.so_date::DATE AS trans_date,
mdp.delivery_date::DATE AS delivery_date,
m.so_number || '-' || TO_CHAR(mdp.delivery_date, 'YYYYMMDD') || '-' || CAST(pw.warehouse_id AS VARCHAR) AS reference,
COALESCE(mdp.vehicle_number, '') AS vehicle_numbers,
COALESCE(mdp.usage_qty, 0)::NUMERIC(15,3) AS qty,
COALESCE(mdp.total_weight, 0)::NUMERIC(15,3) AS weight,
COALESCE(mdp.avg_weight, 0)::NUMERIC(15,3) AS average_weight,
COALESCE(mdp.unit_price, 0)::NUMERIC(15,3) AS price,
COALESCE(mdp.total_price, 0)::NUMERIC(15,3) AS final_price,
COALESCE(mdp.total_price, 0)::NUMERIC(15,3) AS total_price,
0::NUMERIC(15,3) AS payment_amount,
w.name AS pickup_info,
u.name AS sales_person
`).
Joins("INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
Joins("INNER JOIN marketings m ON m.id = mp.marketing_id").
Joins("INNER JOIN customers c ON c.id = m.customer_id").
Joins("INNER JOIN product_warehouses pw ON pw.id = mdp.product_warehouse_id").
Joins("INNER JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("INNER JOIN users u ON u.id = m.sales_person_id").
Where("mdp.delivery_date IS NOT NULL").
Where("m.deleted_at IS NULL").
Where("c.deleted_at IS NULL")
if customerID != nil {
salesQuery = salesQuery.Where("c.id = ?", *customerID)
}
paymentQuery := r.db.WithContext(ctx).
Table("payments p").
Select(`
'PAYMENT' AS transaction_type,
p.id::BIGINT AS transaction_id,
c.id::BIGINT AS customer_id,
p.payment_date::DATE AS trans_date,
NULL AS delivery_date,
COALESCE(p.reference_number, p.payment_code) AS reference,
'-' AS vehicle_numbers,
0::NUMERIC(15,3) AS qty,
0::NUMERIC(15,3) AS weight,
0::NUMERIC(15,3) AS average_weight,
0::NUMERIC(15,3) AS price,
0::NUMERIC(15,3) AS final_price,
0::NUMERIC(15,3) AS total_price,
p.nominal::NUMERIC(15,3) AS payment_amount,
'-' AS pickup_info,
'-' AS sales_person
`).
Joins("INNER JOIN customers c ON c.id = p.party_id").
Where("p.party_type = ?", "CUSTOMER").
Where("p.direction = ?", "IN").
Where("p.transaction_type = ?", "PENJUALAN").
Where("p.deleted_at IS NULL").
Where("c.deleted_at IS NULL")
if customerID != nil {
paymentQuery = paymentQuery.Where("c.id = ?", *customerID)
}
var results []CustomerPaymentTransaction
err := r.db.WithContext(ctx).
Raw("? UNION ALL ? ORDER BY customer_id, trans_date, transaction_type DESC, transaction_id",
salesQuery,
paymentQuery,
).
Scan(&results).
Error
if err != nil {
return nil, err
}
return results, nil
}
func (r *customerPaymentRepositoryImpl) GetInitialBalanceByCustomer(ctx context.Context, customerID uint) (float64, error) {
var result struct {
Nominal float64
}
err := r.db.WithContext(ctx).
Table("payments").
Select("COALESCE(SUM(nominal), 0) as nominal").
Where("party_type = ?", "CUSTOMER").
Where("party_id = ?", customerID).
Where("transaction_type = ?", "SALDO_AWAL").
Where("deleted_at IS NULL").
Scan(&result).
Error
if err != nil {
return 0, err
}
return result.Nominal, nil
}
func (r *customerPaymentRepositoryImpl) GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int) ([]uint, int64, error) {
subQuery := r.db.WithContext(ctx).
Table("(" +
"SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp " +
"INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id " +
"INNER JOIN marketings m ON m.id = mp.marketing_id " +
"INNER JOIN customers c ON c.id = m.customer_id " +
"WHERE mdp.delivery_date IS NOT NULL AND m.deleted_at IS NULL AND c.deleted_at IS NULL " +
"UNION " +
"SELECT DISTINCT c.id as customer_id FROM payments p " +
"INNER JOIN customers c ON c.id = p.party_id " +
"WHERE p.party_type = 'CUSTOMER' AND p.direction = 'IN' " +
"AND p.transaction_type = 'PENJUALAN' AND p.deleted_at IS NULL AND c.deleted_at IS NULL" +
") as customer_ids")
var total int64
if err := subQuery.Count(&total).Error; err != nil {
return nil, 0, err
}
var customerIDs []uint
err := r.db.WithContext(ctx).
Table("("+
"SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp "+
"INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id "+
"INNER JOIN marketings m ON m.id = mp.marketing_id "+
"INNER JOIN customers c ON c.id = m.customer_id "+
"WHERE mdp.delivery_date IS NOT NULL AND m.deleted_at IS NULL AND c.deleted_at IS NULL "+
"UNION "+
"SELECT DISTINCT c.id as customer_id FROM payments p "+
"INNER JOIN customers c ON c.id = p.party_id "+
"WHERE p.party_type = 'CUSTOMER' AND p.direction = 'IN' "+
"AND p.transaction_type = 'PENJUALAN' AND p.deleted_at IS NULL AND c.deleted_at IS NULL"+
") as customer_ids").
Select("customer_id").
Order("customer_id ASC").
Limit(limit).
Offset(offset).
Pluck("customer_id", &customerIDs).
Error
if err != nil {
return nil, 0, err
}
return customerIDs, total, nil
}
@@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
@@ -18,8 +17,6 @@ type DebtSupplierRepository interface {
GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error) GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error)
GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error) GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error)
GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error) GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error)
GetPaymentSummariesByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]PaymentReferenceSummary, error)
GetInitialBalanceTotals(ctx context.Context, supplierIDs []uint) (map[uint]float64, error)
GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
} }
@@ -28,11 +25,6 @@ type debtSupplierRepositoryImpl struct {
db *gorm.DB db *gorm.DB
} }
type PaymentReferenceSummary struct {
Total float64
LatestPaymentDate time.Time
}
func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository { func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository {
return &debtSupplierRepositoryImpl{db: db} return &debtSupplierRepositoryImpl{db: db}
} }
@@ -175,8 +167,7 @@ func (r *debtSupplierRepositoryImpl) GetPaymentsBySuppliers(ctx context.Context,
Model(&entity.Payment{}). Model(&entity.Payment{}).
Where("party_type = ?", string(utils.PaymentPartySupplier)). Where("party_type = ?", string(utils.PaymentPartySupplier)).
Where("direction = ?", "OUT"). Where("direction = ?", "OUT").
Where("party_id IN ?", supplierIDs). Where("party_id IN ?", supplierIDs)
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal))
if strings.TrimSpace(filters.StartDate) != "" { if strings.TrimSpace(filters.StartDate) != "" {
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
@@ -247,7 +238,6 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co
Where("direction = ?", "OUT"). Where("direction = ?", "OUT").
Where("party_id IN ?", supplierIDs). Where("party_id IN ?", supplierIDs).
Where("reference_number IN ?", references). Where("reference_number IN ?", references).
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)).
Group("reference_number"). Group("reference_number").
Scan(&rows).Error; err != nil { Scan(&rows).Error; err != nil {
return nil, err return nil, err
@@ -264,75 +254,6 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co
return result, nil return result, nil
} }
func (r *debtSupplierRepositoryImpl) GetPaymentSummariesByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]PaymentReferenceSummary, error) {
if len(supplierIDs) == 0 || len(references) == 0 {
return map[string]PaymentReferenceSummary{}, nil
}
type paymentRow struct {
ReferenceNumber *string `gorm:"column:reference_number"`
Total float64 `gorm:"column:total"`
LatestPaymentDate time.Time `gorm:"column:latest_payment_date"`
}
rows := make([]paymentRow, 0)
if err := r.db.WithContext(ctx).
Model(&entity.Payment{}).
Select("reference_number, SUM(nominal) AS total, MAX(payment_date) AS latest_payment_date").
Where("party_type = ?", string(utils.PaymentPartySupplier)).
Where("direction = ?", "OUT").
Where("party_id IN ?", supplierIDs).
Where("reference_number IN ?", references).
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)).
Group("reference_number").
Scan(&rows).Error; err != nil {
return nil, err
}
result := make(map[string]PaymentReferenceSummary, len(rows))
for _, row := range rows {
if row.ReferenceNumber == nil || strings.TrimSpace(*row.ReferenceNumber) == "" {
continue
}
result[*row.ReferenceNumber] = PaymentReferenceSummary{
Total: row.Total,
LatestPaymentDate: row.LatestPaymentDate,
}
}
return result, nil
}
func (r *debtSupplierRepositoryImpl) GetInitialBalanceTotals(ctx context.Context, supplierIDs []uint) (map[uint]float64, error) {
if len(supplierIDs) == 0 {
return map[uint]float64{}, nil
}
type balanceRow struct {
SupplierID uint `gorm:"column:supplier_id"`
Total float64 `gorm:"column:total"`
}
rows := make([]balanceRow, 0)
if err := r.db.WithContext(ctx).
Model(&entity.Payment{}).
Select("party_id AS supplier_id, SUM(nominal) AS total").
Where("party_type = ?", string(utils.PaymentPartySupplier)).
Where("party_id IN ?", supplierIDs).
Where("transaction_type = ?", string(utils.TransactionTypeSaldoAwal)).
Group("party_id").
Scan(&rows).Error; err != nil {
return nil, err
}
result := make(map[uint]float64, len(rows))
for _, row := range rows {
result[row.SupplierID] = row.Total
}
return result, nil
}
func (r *debtSupplierRepositoryImpl) GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) { func (r *debtSupplierRepositoryImpl) GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) {
if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" { if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
return map[uint]float64{}, nil return map[uint]float64{}, nil
@@ -392,7 +313,6 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsBeforeDate(ctx context.Cont
Where("party_type = ?", string(utils.PaymentPartySupplier)). Where("party_type = ?", string(utils.PaymentPartySupplier)).
Where("direction = ?", "OUT"). Where("direction = ?", "OUT").
Where("party_id IN ?", supplierIDs). Where("party_id IN ?", supplierIDs).
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)).
Where("DATE(payment_date) < ?", dateFrom). Where("DATE(payment_date) < ?", dateFrom).
Group("party_id"). Group("party_id").
Scan(&rows).Error; err != nil { Scan(&rows).Error; err != nil {
@@ -11,46 +11,40 @@ import (
) )
type HppPerKandangRow struct { type HppPerKandangRow struct {
ProjectFlockKandangID uint KandangID uint
ProjectFlockPeriod int KandangName string
KandangID uint KandangStatus string
KandangName string LocationID uint
KandangStatus string LocationName string
LocationID uint PicID uint
LocationName string PicName string
PicID uint RemainingChickenBirds float64
PicName string RemainingChickenWeight float64
RecordingCount int64 EggProductionWeightKg float64
// RemainingChickenBirds float64 EggProductionPieces float64
// RemainingChickenWeight float64
EggProductionWeightKgRemaining float64
EggProductionPiecesRemaining float64
EggProductionTotalWeightKg float64
EggProductionTotalPieces float64
} }
type HppPerKandangCostRow struct { type HppPerKandangCostRow struct {
ProjectFlockKandangID uint KandangID uint
FeedCost float64 FeedCost float64
OvkCost float64 OvkCost float64
DocCost float64 DocCost float64
DocQty float64 DocQty float64
BudgetCost float64 BudgetCost float64
ExpenseCost float64 ExpenseCost float64
} }
type HppPerKandangSupplierRow struct { type HppPerKandangSupplierRow struct {
ProjectFlockKandangID uint KandangID uint
SupplierID uint SupplierID uint
SupplierName string SupplierName string
SupplierAlias string SupplierAlias string
Category string Category string
} }
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, projectFlockKandangIDs []uint) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error) GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error)
GetEggProductionByProjectFlockKandangIDs(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) (map[uint]HppPerKandangRow, error)
} }
type hppPerKandangRepository struct { type hppPerKandangRepository struct {
@@ -64,32 +58,9 @@ func NewHppPerKandangRepository(db *gorm.DB) HppPerKandangRepository {
func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error) { func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error) {
var rows []HppPerKandangRow var rows []HppPerKandangRow
latestApproval := r.db.WithContext(ctx).
Table("approvals AS a").
Select("a.approvable_id, a.action").
Joins(`
JOIN (
SELECT approvable_id, MAX(action_at) AS latest_action_at
FROM approvals
WHERE approvable_type = ?
GROUP BY approvable_id
) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`,
string(utils.ApprovalWorkflowRecording),
)
validRecordings := r.db.WithContext(ctx).
Table("recordings AS r").
Select("r.id, r.project_flock_kandangs_id, r.total_chick_qty").
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL").
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected))
query := r.db.WithContext(ctx). query := r.db.WithContext(ctx).
Table("project_flocks AS pf"). Table("recordings AS r").
Select(` Select(`
pfk.id AS project_flock_kandang_id,
pfk.period AS project_flock_period,
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,
@@ -97,31 +68,23 @@ 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(COUNT(vr.id), 0) AS recording_count, COALESCE(MAX(r.total_chick_qty), 0) AS remaining_chicken_birds,
COALESCE(MAX(vr.total_chick_qty), 0) AS remaining_chicken_birds, COALESCE(SUM(rbw.total_weight), 0) AS remaining_chicken_weight,
0 AS remaining_chicken_weight, COALESCE(SUM(re.weight), 0) AS egg_production_weight_kg,
0 AS egg_production_weight_kg, COALESCE(SUM(re.qty), 0) AS egg_production_pieces`).
0 AS egg_production_pieces, Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
0 AS egg_production_total_weight_kg,
0 AS egg_production_total_pieces`).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
Joins(`
LEFT JOIN (
SELECT project_flock_kandang_id, MIN(chick_in_date) AS chick_in_date
FROM project_chickins
GROUP BY project_flock_kandang_id
) AS pc ON pc.project_flock_kandang_id = pfk.id`).
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 (?) AS vr ON vr.project_flock_kandangs_id = pfk.id", validRecordings). Joins("LEFT JOIN recording_bws AS rbw ON rbw.recording_id = r.id").
Where("pf.category = ?", utils.ProjectFlockCategoryLaying). Joins("LEFT JOIN recording_eggs AS re ON re.recording_id = r.id").
Where("(pfk.closed_at IS NULL OR ? BETWEEN pc.chick_in_date AND pfk.closed_at)", start) 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("pfk.id, pfk.period, k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name"). query = query.Group("k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name").
Order("pfk.id ASC") Order("k.id ASC")
if err := query.Scan(&rows).Error; err != nil { if err := query.Scan(&rows).Error; err != nil {
return nil, err return nil, err
@@ -130,44 +93,41 @@ 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, projectFlockKandangIDs []uint) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error) { func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error) {
var rows []HppPerKandangCostRow 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(`
pfk.id AS project_flock_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.total_qty, 0) * COALESCE(tpi.price, 0) WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
ELSE 0 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.total_qty, 0) * COALESCE(tpi.price, 0) WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
ELSE 0 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("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval). Joins("JOIN locations AS loc ON loc.id = k.location_id").
Joins("LEFT JOIN recording_stocks AS rs ON rs.recording_id = r.id"). Joins("LEFT JOIN 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).
@@ -176,30 +136,31 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
Joins("LEFT JOIN purchase_items AS tpi ON tpi.product_id = std.product_id AND tpi.warehouse_id = st.from_warehouse_id"). Joins("LEFT JOIN 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 ?", projectFlockKandangIDs). Where("r.project_flock_kandangs_id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
Where("r.record_datetime < ?", 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 = query.Group("pfk.id").Order("pfk.id ASC") query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs)
query = query.Group("k.id").Order("k.id ASC")
if err := query.Scan(&rows).Error; err != nil { if err := query.Scan(&rows).Error; err != nil {
return nil, nil, err return nil, nil, err
} }
docRows := make([]struct { docRows := make([]struct {
ProjectFlockKandangID uint KandangID uint
DocCost float64 DocCost float64
DocQty float64 DocQty float64
SupplierID *uint SupplierID *uint
SupplierName *string SupplierName *string
SupplierAlias *string SupplierAlias *string
}, 0) }, 0)
docQuery := r.db.WithContext(ctx). docQuery := r.db.WithContext(ctx).
Table("project_chickins AS pc"). Table("project_chickins AS pc").
Select(` Select(`
pfk.id AS project_flock_kandang_id, pfk.kandang_id AS kandang_id,
COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS doc_cost, COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS doc_cost,
COALESCE(SUM(pc.usage_qty), 0) AS doc_qty, COALESCE(SUM(pc.usage_qty), 0) AS doc_qty,
s.id AS supplier_id, s.id AS supplier_id,
@@ -211,8 +172,9 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
Joins("LEFT JOIN purchase_items AS pi ON pi.product_warehouse_id = pc.product_warehouse_id"). Joins("LEFT JOIN 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 ?", projectFlockKandangIDs). Where("pc.project_flock_kandang_id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
Group("pfk.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
@@ -221,28 +183,28 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
costMap := make(map[uint]*HppPerKandangCostRow, len(rows)) costMap := make(map[uint]*HppPerKandangCostRow, len(rows))
for i := range rows { for i := range rows {
row := rows[i] row := rows[i]
costMap[row.ProjectFlockKandangID] = &rows[i] costMap[row.KandangID] = &rows[i]
} }
docSuppliers := make([]HppPerKandangSupplierRow, 0) docSuppliers := make([]HppPerKandangSupplierRow, 0)
docSeen := make(map[uint]map[uint]bool) docSeen := make(map[uint]map[uint]bool)
for _, doc := range docRows { for _, doc := range docRows {
entry, ok := costMap[doc.ProjectFlockKandangID] entry, ok := costMap[doc.KandangID]
if !ok { if !ok {
rows = append(rows, HppPerKandangCostRow{ rows = append(rows, HppPerKandangCostRow{
ProjectFlockKandangID: doc.ProjectFlockKandangID, KandangID: doc.KandangID,
}) })
entry = &rows[len(rows)-1] entry = &rows[len(rows)-1]
costMap[doc.ProjectFlockKandangID] = entry costMap[doc.KandangID] = entry
} }
entry.DocCost += doc.DocCost entry.DocCost += doc.DocCost
entry.DocQty += doc.DocQty entry.DocQty += doc.DocQty
if doc.SupplierID != nil { if doc.SupplierID != nil {
if docSeen[doc.ProjectFlockKandangID] == nil { if docSeen[doc.KandangID] == nil {
docSeen[doc.ProjectFlockKandangID] = make(map[uint]bool) docSeen[doc.KandangID] = make(map[uint]bool)
} }
if !docSeen[doc.ProjectFlockKandangID][*doc.SupplierID] { if !docSeen[doc.KandangID][*doc.SupplierID] {
docSeen[doc.ProjectFlockKandangID][*doc.SupplierID] = true docSeen[doc.KandangID][*doc.SupplierID] = true
supplierName := "" supplierName := ""
if doc.SupplierName != nil { if doc.SupplierName != nil {
supplierName = *doc.SupplierName supplierName = *doc.SupplierName
@@ -252,19 +214,19 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
supplierAlias = *doc.SupplierAlias supplierAlias = *doc.SupplierAlias
} }
docSuppliers = append(docSuppliers, HppPerKandangSupplierRow{ docSuppliers = append(docSuppliers, HppPerKandangSupplierRow{
ProjectFlockKandangID: doc.ProjectFlockKandangID, KandangID: doc.KandangID,
SupplierID: *doc.SupplierID, SupplierID: *doc.SupplierID,
SupplierName: supplierName, SupplierName: supplierName,
SupplierAlias: supplierAlias, SupplierAlias: supplierAlias,
Category: "DOC", Category: "DOC",
}) })
} }
} }
} }
budgetRows := make([]struct { budgetRows := make([]struct {
ProjectFlockKandangID uint KandangID uint
BudgetCost float64 BudgetCost float64
}, 0) }, 0)
pfkUsageSub := r.db. pfkUsageSub := r.db.
@@ -285,63 +247,63 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
budgetQuery := r.db.WithContext(ctx). budgetQuery := r.db.WithContext(ctx).
Table("project_flock_kandangs AS pfk"). Table("project_flock_kandangs AS pfk").
Select(` Select(`
pfk.id AS project_flock_kandang_id, k.id AS kandang_id,
COALESCE(SUM((pb.qty * pb.price) * COALESCE(k_usage.kandang_usage_qty, 0) / NULLIF(p_usage.project_usage_qty, 0)), 0) AS budget_cost`). COALESCE(SUM((pb.qty * pb.price) * COALESCE(k_usage.kandang_usage_qty, 0) / NULLIF(p_usage.project_usage_qty, 0)), 0) AS budget_cost`).
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). Joins("JOIN 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 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 (?)", projectFlockKandangIDs). Where("pfk.id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
Group("pfk.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
} }
for _, budget := range budgetRows { for _, budget := range budgetRows {
entry, ok := costMap[budget.ProjectFlockKandangID] entry, ok := costMap[budget.KandangID]
if !ok { if !ok {
rows = append(rows, HppPerKandangCostRow{ rows = append(rows, HppPerKandangCostRow{
ProjectFlockKandangID: budget.ProjectFlockKandangID, KandangID: budget.KandangID,
}) })
entry = &rows[len(rows)-1] entry = &rows[len(rows)-1]
costMap[budget.ProjectFlockKandangID] = entry costMap[budget.KandangID] = entry
} }
entry.BudgetCost += budget.BudgetCost entry.BudgetCost += budget.BudgetCost
} }
expenseRows := make([]struct { expenseRows := make([]struct {
ProjectFlockKandangID uint KandangID uint
ExpenseCost float64 ExpenseCost float64
}, 0) }, 0)
expenseQuery := r.db.WithContext(ctx). expenseQuery := r.db.WithContext(ctx).
Table("project_flock_kandangs AS pfk"). Table("project_flock_kandangs AS pfk").
Select(` Select(`
pfk.id AS project_flock_kandang_id, k.id AS kandang_id,
COALESCE(SUM(er.qty * er.price), 0) AS expense_cost`). COALESCE(SUM(er.qty * er.price), 0) AS expense_cost`).
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 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 (?)", projectFlockKandangIDs). Where("pfk.id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
Group("pfk.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
} }
for _, exp := range expenseRows { for _, exp := range expenseRows {
entry, ok := costMap[exp.ProjectFlockKandangID] entry, ok := costMap[exp.KandangID]
if !ok { if !ok {
rows = append(rows, HppPerKandangCostRow{ rows = append(rows, HppPerKandangCostRow{
ProjectFlockKandangID: exp.ProjectFlockKandangID, KandangID: exp.KandangID,
}) })
entry = &rows[len(rows)-1] entry = &rows[len(rows)-1]
costMap[exp.ProjectFlockKandangID] = entry costMap[exp.KandangID] = entry
} }
entry.ExpenseCost += exp.ExpenseCost entry.ExpenseCost += exp.ExpenseCost
} }
@@ -350,7 +312,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
feedQuery := r.db.WithContext(ctx). feedQuery := r.db.WithContext(ctx).
Table("recordings AS r"). Table("recordings AS r").
Select("DISTINCT pfk.id AS project_flock_kandang_id, s.id AS supplier_id, s.name AS supplier_name, s.alias AS supplier_alias"). Select("DISTINCT k.id AS kandang_id, s.id AS supplier_id, s.name AS supplier_name, s.alias AS supplier_alias").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id"). Joins("JOIN 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("JOIN locations AS loc ON loc.id = k.location_id").
@@ -361,21 +323,21 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id"). Joins("LEFT JOIN 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 (?)", projectFlockKandangIDs). Where("r.project_flock_kandangs_id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
Where("r.record_datetime < ?", 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
} }
for i := range feedSuppliers { for i := range feedSuppliers {
if _, exists := costMap[feedSuppliers[i].ProjectFlockKandangID]; !exists { if _, exists := costMap[feedSuppliers[i].KandangID]; !exists {
rows = append(rows, HppPerKandangCostRow{ rows = append(rows, HppPerKandangCostRow{
ProjectFlockKandangID: feedSuppliers[i].ProjectFlockKandangID, KandangID: feedSuppliers[i].KandangID,
}) })
costMap[feedSuppliers[i].ProjectFlockKandangID] = &rows[len(rows)-1] costMap[feedSuppliers[i].KandangID] = &rows[len(rows)-1]
} }
feedSuppliers[i].Category = "FEED" feedSuppliers[i].Category = "FEED"
} }
@@ -385,67 +347,6 @@ 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
EggProductionWeightKgRemaining float64
EggProductionPiecesRemaining float64
EggProductionTotalWeightKg float64
EggProductionTotalPieces float64
}
eggRows := make([]eggRow, 0)
query := r.db.WithContext(ctx).
Table("recordings AS r").
Select(`
r.project_flock_kandangs_id AS project_flock_kandang_id,
COALESCE(SUM((re.total_qty - re.total_used) * re.weight / 1000), 0) AS egg_production_weight_kg_remaining,
COALESCE(SUM(re.total_qty - re.total_used), 0) AS egg_production_pieces_remaining,
COALESCE(SUM(re.weight / 1000), 0) AS egg_production_total_weight_kg,
COALESCE(SUM(re.total_qty), 0) AS egg_production_total_pieces`).
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
Joins("LEFT JOIN recording_eggs AS re ON re.recording_id = r.id").
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
Where("r.record_datetime < ?", end).
Where("r.deleted_at IS NULL").
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
Group("r.project_flock_kandangs_id")
if err := query.Scan(&eggRows).Error; err != nil {
return nil, err
}
result := make(map[uint]HppPerKandangRow, len(eggRows))
for _, row := range eggRows {
result[row.ProjectFlockKandangID] = HppPerKandangRow{
ProjectFlockKandangID: row.ProjectFlockKandangID,
EggProductionWeightKgRemaining: row.EggProductionWeightKgRemaining,
EggProductionPiecesRemaining: row.EggProductionPiecesRemaining,
EggProductionTotalWeightKg: row.EggProductionTotalWeightKg,
EggProductionTotalPieces: row.EggProductionTotalPieces,
}
}
return result, nil
}
func applyLocationFilters(query *gorm.DB, areaIDs, locationIDs, kandangIDs []int64) *gorm.DB { 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)
@@ -454,7 +355,7 @@ func applyLocationFilters(query *gorm.DB, areaIDs, locationIDs, kandangIDs []int
query = query.Where("k.location_id IN ?", locationIDs) query = query.Where("k.location_id IN ?", locationIDs)
} }
if len(kandangIDs) > 0 { if len(kandangIDs) > 0 {
query = query.Where("pfk.id IN ?", kandangIDs) query = query.Where("k.id IN ?", kandangIDs)
} }
return query return query
} }
@@ -25,21 +25,6 @@ func NewPurchaseSupplierRepository(db *gorm.DB) PurchaseSupplierRepository {
return &purchaseSupplierRepositoryImpl{db: db} return &purchaseSupplierRepositoryImpl{db: db}
} }
func (r *purchaseSupplierRepositoryImpl) latestPurchaseApproval(ctx context.Context) *gorm.DB {
return r.db.WithContext(ctx).
Table("approvals AS a").
Select("a.approvable_id, a.step_number, 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.ApprovalWorkflowPurchase),
)
}
func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filters *validation.PurchaseSupplierQuery) *gorm.DB { func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filters *validation.PurchaseSupplierQuery) *gorm.DB {
dateColumn := "purchase_items.received_date" dateColumn := "purchase_items.received_date"
switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) { switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) {
@@ -49,16 +34,10 @@ func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context,
dateColumn = "purchase_items.received_date" dateColumn = "purchase_items.received_date"
} }
latestApproval := r.latestPurchaseApproval(ctx)
db := r.db.WithContext(ctx). db := r.db.WithContext(ctx).
Model(&entity.Supplier{}). Model(&entity.Supplier{}).
Joins("JOIN purchases ON purchases.supplier_id = suppliers.id"). Joins("JOIN purchases ON purchases.supplier_id = suppliers.id").
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id"). Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id")
Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", latestApproval).
Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)).
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
Where("purchase_items.received_date IS NOT NULL")
if filters.SupplierId > 0 { if filters.SupplierId > 0 {
db = db.Where("suppliers.id = ?", filters.SupplierId) db = db.Where("suppliers.id = ?", filters.SupplierId)
@@ -173,11 +152,7 @@ func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context
Preload("ExpenseNonstock.Expense"). Preload("ExpenseNonstock.Expense").
Preload("ExpenseNonstock.Expense.Supplier"). Preload("ExpenseNonstock.Expense.Supplier").
Joins("JOIN purchases ON purchases.id = purchase_items.purchase_id"). Joins("JOIN purchases ON purchases.id = purchase_items.purchase_id").
Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)). Where("purchases.supplier_id IN ?", supplierIDs)
Where("purchases.supplier_id IN ?", supplierIDs).
Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)).
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
Where("purchase_items.received_date IS NOT NULL")
if filters.ProductId > 0 { if filters.ProductId > 0 {
db = db.Where("purchase_items.product_id = ?", filters.ProductId) db = db.Where("purchase_items.product_id = ?", filters.ProductId)
+1 -1
View File
@@ -21,5 +21,5 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService
route.Get("/debt-supplier", m.RequirePermissions(m.P_ReportDebtSupplierGetAll), ctrl.GetDebtSupplier) route.Get("/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,7 +2,6 @@ package service
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"math" "math"
@@ -21,10 +20,9 @@ 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"
@@ -44,25 +42,21 @@ 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
DB *gorm.DB ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository
MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository PurchaseRepo purchaseRepo.PurchaseRepository
PurchaseRepo purchaseRepo.PurchaseRepository ChickinRepo chickinRepo.ProjectChickinRepository
ChickinRepo chickinRepo.ProjectChickinRepository RecordingRepo recordingRepo.RecordingRepository
RecordingRepo recordingRepo.RecordingRepository ApprovalSvc approvalService.ApprovalService
ApprovalSvc approvalService.ApprovalService PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository 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
} }
@@ -77,7 +71,6 @@ 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,
@@ -89,27 +82,22 @@ 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,
DB: db, ExpenseRealizationRepo: expenseRealizationRepo,
ExpenseRealizationRepo: expenseRealizationRepo, MarketingDeliveryRepo: marketingDeliveryRepo,
MarketingDeliveryRepo: marketingDeliveryRepo, PurchaseRepo: purchaseRepo,
PurchaseRepo: purchaseRepo, ChickinRepo: chickinRepo,
ChickinRepo: chickinRepo, RecordingRepo: recordingRepo,
RecordingRepo: recordingRepo, ApprovalSvc: approvalSvc,
ApprovalSvc: approvalSvc, PurchaseSupplierRepo: purchaseSupplierRepo,
PurchaseSupplierRepo: purchaseSupplierRepo, DebtSupplierRepo: debtSupplierRepo,
DebtSupplierRepo: debtSupplierRepo, HppPerKandangRepo: hppPerKandangRepo,
HppPerKandangRepo: hppPerKandangRepo, ProductionResultRepo: productionResultRepo,
ProductionResultRepo: productionResultRepo,
CustomerPaymentRepo: customerPaymentRepo,
CustomerRepo: customerRepo,
StandardGrowthDetailRepo: standardGrowthDetailRepo, StandardGrowthDetailRepo: standardGrowthDetailRepo,
ProductionStandardDetailRepo: productionStandardDetailRepo, ProductionStandardDetailRepo: productionStandardDetailRepo,
} }
@@ -320,15 +308,6 @@ 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 {
@@ -338,12 +317,6 @@ 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
@@ -417,261 +390,6 @@ 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
}
if len(item.Rows) > 0 {
result = append(result, item)
}
}
totalCustomers = int64(len(result))
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 paymentDate != nil {
days := int(paymentDate.Sub(tx.TransDate).Hours() / 24)
row.AgingDay = &days
} else {
days := 0
row.AgingDay = &days
}
} else {
days := int(time.Since(tx.TransDate).Hours() / 24)
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 {
// Cari payment yang digunakan untuk melunasi sales ini dengan FIFO
// Track payment allocations that are consumed by previous sales
type paymentAllocation struct {
date time.Time
amount float64
consumed float64
}
allocations := []paymentAllocation{}
runningBalance := 0.0
// Process all transactions before current sales to build allocation map
for i := 0; i < currentIndex; i++ {
if transactions[i].TransactionType == "PAYMENT" {
allocations = append(allocations, paymentAllocation{
date: transactions[i].TransDate,
amount: transactions[i].PaymentAmount,
consumed: 0,
})
runningBalance += transactions[i].PaymentAmount
} else if transactions[i].TransactionType == "SALES" {
salesAmount := transactions[i].TotalPrice
remainingToConsume := salesAmount
// Consume from oldest allocations first (FIFO)
for j := range allocations {
if remainingToConsume <= 0 {
break
}
available := allocations[j].amount - allocations[j].consumed
if available > 0 {
consume := available
if consume > remainingToConsume {
consume = remainingToConsume
}
allocations[j].consumed += consume
remainingToConsume -= consume
}
}
runningBalance -= salesAmount
}
}
// Now find which allocation covers the current sales
amountNeeded := currentSales.TotalPrice
for _, alloc := range allocations {
available := alloc.amount - alloc.consumed
if available > 0 {
if amountNeeded <= available {
// This allocation fully covers the sales
return "LUNAS", &alloc.date
} else {
// This allocation partially covers, continue to next
amountNeeded -= available
}
}
}
// If we get here, use the oldest allocation
if len(allocations) > 0 {
return "LUNAS", &allocations[0].date
}
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,
@@ -816,68 +534,6 @@ 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
@@ -1129,17 +785,6 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
return nil, 0, err return nil, 0, err
} }
initialBalanceTotals, err := s.DebtSupplierRepo.GetInitialBalanceTotals(c.Context(), supplierIDs)
if err != nil {
return nil, 0, err
}
references := collectDebtSupplierReferences(purchases)
paymentSummaries, err := s.DebtSupplierRepo.GetPaymentSummariesByReferences(c.Context(), supplierIDs, references)
if err != nil {
return nil, 0, err
}
location, err := time.LoadLocation("Asia/Jakarta") location, err := time.LoadLocation("Asia/Jakarta")
if err != nil { if err != nil {
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration") return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
@@ -1161,7 +806,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
continue continue
} }
initialBalance := initialBalanceTotals[supplierID] + (initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]) initialBalance := initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]
items := purchasesBySupplier[supplierID] items := purchasesBySupplier[supplierID]
paymentItems := paymentsBySupplier[supplierID] paymentItems := paymentsBySupplier[supplierID]
total := dto.DebtSupplierTotalDTO{} total := dto.DebtSupplierTotalDTO{}
@@ -1169,16 +814,6 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems)) combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
for _, purchase := range items { for _, purchase := range items {
row := buildDebtSupplierRow(purchase, now, location) row := buildDebtSupplierRow(purchase, now, location)
if reference := resolveDebtSupplierReference(purchase); reference != "" {
if summary, ok := paymentSummaries[reference]; ok {
if isDebtSupplierPaid(row.TotalPrice, summary.Total) {
row.Status = "Lunas"
if !summary.LatestPaymentDate.IsZero() {
row.Aging = calculateDebtSupplierAging(purchase, summary.LatestPaymentDate, location)
}
}
}
}
sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location) sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location)
combinedRows = append(combinedRows, debtSupplierRowItem{ combinedRows = append(combinedRows, debtSupplierRowItem{
Row: row, Row: row,
@@ -1395,55 +1030,6 @@ func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc
return purchase.CreatedAt.In(loc) return purchase.CreatedAt.In(loc)
} }
func collectDebtSupplierReferences(purchases []entity.Purchase) []string {
if len(purchases) == 0 {
return nil
}
seen := make(map[string]struct{}, len(purchases))
result := make([]string, 0, len(purchases))
for _, purchase := range purchases {
ref := resolveDebtSupplierReference(purchase)
if ref == "" {
continue
}
if _, ok := seen[ref]; ok {
continue
}
seen[ref] = struct{}{}
result = append(result, ref)
}
return result
}
func resolveDebtSupplierReference(purchase entity.Purchase) string {
if purchase.PoNumber != nil {
if ref := strings.TrimSpace(*purchase.PoNumber); ref != "" {
return ref
}
}
if ref := strings.TrimSpace(purchase.PrNumber); ref != "" {
return ref
}
return ""
}
func isDebtSupplierPaid(totalPrice, paymentTotal float64) bool {
if totalPrice <= 0 {
return true
}
return paymentTotal >= totalPrice-0.000001
}
func calculateDebtSupplierAging(purchase entity.Purchase, endDate time.Time, loc *time.Location) int {
prDate := purchase.CreatedAt.In(loc)
startDate := time.Date(prDate.Year(), prDate.Month(), prDate.Day(), 0, 0, 0, 0, loc)
stopDate := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, loc)
if stopDate.Before(startDate) {
return 0
}
return int(stopDate.Sub(startDate).Hours() / 24)
}
func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) { func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) {
params, filters, err := s.parseHppPerKandangQuery(ctx) params, filters, err := s.parseHppPerKandangQuery(ctx)
if err != nil { if err != nil {
@@ -1471,42 +1057,13 @@ 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)
validPfkIDs := make([]uint, 0, len(repoRows)) if err != nil {
pfkIndex := make(map[uint]int, len(repoRows)) return nil, nil, err
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].EggProductionWeightKgRemaining = egg.EggProductionWeightKgRemaining
repoRows[rowIdx].EggProductionPiecesRemaining = egg.EggProductionPiecesRemaining
repoRows[rowIdx].EggProductionTotalWeightKg = egg.EggProductionTotalWeightKg
repoRows[rowIdx].EggProductionTotalPieces = egg.EggProductionTotalPieces
}
}
}
costMap := make(map[uint]HppCostAggregate, len(costRows)) costMap := make(map[uint]HppCostAggregate, len(costRows))
for _, row := range costRows { for _, row := range costRows {
costMap[row.ProjectFlockKandangID] = HppCostAggregate{ costMap[row.KandangID] = HppCostAggregate{
FeedCost: row.FeedCost, FeedCost: row.FeedCost,
OvkCost: row.OvkCost, OvkCost: row.OvkCost,
DocCost: row.DocCost, DocCost: row.DocCost,
@@ -1535,15 +1092,15 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
category = "DOC" category = "DOC"
} }
if seen[sup.ProjectFlockKandangID] == nil { if seen[sup.KandangID] == nil {
seen[sup.ProjectFlockKandangID] = make(map[uint]bool) seen[sup.KandangID] = make(map[uint]bool)
} }
if seen[sup.ProjectFlockKandangID][sup.SupplierID] { if seen[sup.KandangID][sup.SupplierID] {
continue continue
} }
seen[sup.ProjectFlockKandangID][sup.SupplierID] = true seen[sup.KandangID][sup.SupplierID] = true
targetMap[sup.ProjectFlockKandangID] = append(targetMap[sup.ProjectFlockKandangID], dto.HppPerKandangSupplierDTO{ targetMap[sup.KandangID] = append(targetMap[sup.KandangID], dto.HppPerKandangSupplierDTO{
ID: int64(sup.SupplierID), ID: int64(sup.SupplierID),
Name: sup.SupplierName, Name: sup.SupplierName,
Alias: sup.SupplierAlias, Alias: sup.SupplierAlias,
@@ -1556,75 +1113,48 @@ 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
RemainingBirds int64 EggHppSum float64
RemainingWeightKg float64 EggHppCount int
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))
perRangeMap := make(map[weightRangeKey]*weightRangeAggregate) perRangeMap := make(map[weightRangeKey]*weightRangeAggregate)
var totalBirds int64 var totalBirds int64
// var totalWeight float64 var totalWeight float64
var totalEggPieces int64 var totalEggPieces int64
var totalEggKg float64 var totalEggKg float64
// var totalRemainingValueRp int64 var totalRemainingValueRp int64
var totalEggValueRp int64 var totalEggValueRp int64
// var totalHppSum float64 var totalHppSum float64
var totalHppCount int var totalHppCount int
var totalDocPriceSum float64 var totalDocPriceSum float64
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 { birdsFloat := row.RemainingChickenBirds
continue if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) {
birdsFloat = 0
} }
weightFloat := row.RemainingChickenWeight
// birdsFloat := row.RemainingChickenBirds if math.IsNaN(weightFloat) || math.IsInf(weightFloat, 0) {
// if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) { weightFloat = 0
// birdsFloat = 0
// }
// weightFloat := row.RemainingChickenWeight
// if math.IsNaN(weightFloat) || math.IsInf(weightFloat, 0) {
// weightFloat = 0
// }
eggPiecesFloatRemaining := row.EggProductionPiecesRemaining
if math.IsNaN(eggPiecesFloatRemaining) || math.IsInf(eggPiecesFloatRemaining, 0) {
eggPiecesFloatRemaining = 0
} }
eggTotalPiecesFloat := row.EggProductionTotalPieces eggPiecesFloat := row.EggProductionPieces
if math.IsNaN(eggTotalPiecesFloat) || math.IsInf(eggTotalPiecesFloat, 0) { if math.IsNaN(eggPiecesFloat) || math.IsInf(eggPiecesFloat, 0) {
eggTotalPiecesFloat = 0 eggPiecesFloat = 0
} }
eggRemainingWeightFloatRemaining := row.EggProductionWeightKgRemaining eggWeightFloat := row.EggProductionWeightKg
if math.IsNaN(eggRemainingWeightFloatRemaining) || math.IsInf(eggRemainingWeightFloatRemaining, 0) {
eggRemainingWeightFloatRemaining = 0
}
eggWeightFloat := row.EggProductionTotalWeightKg
if math.IsNaN(eggWeightFloat) || math.IsInf(eggWeightFloat, 0) { if math.IsNaN(eggWeightFloat) || math.IsInf(eggWeightFloat, 0) {
eggWeightFloat = 0 eggWeightFloat = 0
} }
avgWeight := 0.0 avgWeight := 0.0
if eggTotalPiecesFloat > 0 { if birdsFloat > 0 {
avgWeight = eggWeightFloat / eggTotalPiecesFloat avgWeight = weightFloat / birdsFloat
} }
if params.WeightMin != nil && avgWeight < *params.WeightMin {
continue
}
if params.WeightMax != nil && avgWeight > *params.WeightMax {
continue
}
weightMin := math.Floor(avgWeight*10) / 10 weightMin := math.Floor(avgWeight*10) / 10
if weightMin < 0 { if weightMin < 0 {
weightMin = 0 weightMin = 0
@@ -1632,30 +1162,28 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
weightMax := weightMin + 0.09 weightMax := weightMin + 0.09
rangeKey := weightRangeKey{Min: weightMin, Max: weightMax} rangeKey := weightRangeKey{Min: weightMin, Max: weightMax}
// rowBirds := int64(math.Round(birdsFloat)) rowBirds := int64(math.Round(birdsFloat))
costEntry := costMap[row.ProjectFlockKandangID] costEntry := costMap[row.KandangID]
totalCost := costEntry.FeedCost + costEntry.OvkCost + costEntry.DocCost + costEntry.BudgetCost + costEntry.ExpenseCost totalCost := costEntry.FeedCost + costEntry.OvkCost + costEntry.DocCost + costEntry.BudgetCost + costEntry.ExpenseCost
// hppRp := 0.0 hppRp := 0.0
// if weightFloat > 0 { if weightFloat > 0 {
// hppRp = totalCost / weightFloat hppRp = totalCost / weightFloat
// } }
eggHpp := 0.0 eggHpp := 0.0
if eggWeightFloat > 0 { if eggWeightFloat > 0 {
eggHpp = (totalCost / eggWeightFloat) / 1000 eggHpp = totalCost / eggWeightFloat
} }
rowEggPieces := int64(math.Round(eggPiecesFloatRemaining)) rowEggPieces := int64(math.Round(eggPiecesFloat))
rowEggValue := int64(eggHpp * eggRemainingWeightFloatRemaining) rowEggValue := int64(eggHpp * eggWeightFloat)
// rowRemainingValue := int64(hppRp * weightFloat) rowRemainingValue := int64(hppRp * weightFloat)
avgDocPrice := int64(0) avgDocPrice := int64(0)
if costEntry.DocQty > 0 { if costEntry.DocQty > 0 {
avgDocPrice = int64(math.Round(costEntry.DocCost / costEntry.DocQty)) avgDocPrice = int64(math.Round(costEntry.DocCost / costEntry.DocQty))
} }
nameWithPeriod := fmt.Sprintf("%s Period %d", row.KandangName, row.ProjectFlockPeriod)
dataRows = append(dataRows, dto.HppPerKandangRowDTO{ dataRows = append(dataRows, dto.HppPerKandangRowDTO{
ID: int(row.ProjectFlockKandangID), ID: int(row.KandangID),
Kandang: dto.HppPerKandangRowKandangDTO{ Kandang: dto.HppPerKandangRowKandangDTO{
ID: int64(row.KandangID), ID: int64(row.KandangID),
Name: row.KandangName, Name: row.KandangName,
@@ -1673,35 +1201,32 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
WeightMin: weightMin, WeightMin: weightMin,
WeightMax: weightMax, WeightMax: weightMax,
}, },
AvgWeightKg: avgWeight, RemainingChickenBirds: rowBirds,
NameWithPeriode: nameWithPeriod, RemainingChickenWeightKg: weightFloat,
AvgWeightKg: avgWeight,
// FeedCostRp: costEntry.FeedCost, // FeedCostRp: costEntry.FeedCost,
// OvkCostRp: costEntry.OvkCost, // OvkCostRp: costEntry.OvkCost,
DocSuppliers: docSupplierMap[row.ProjectFlockKandangID], DocSuppliers: docSupplierMap[row.KandangID],
FeedSuppliers: feedSupplierMap[row.ProjectFlockKandangID], FeedSuppliers: feedSupplierMap[row.KandangID],
EggProductionPieces: int64(math.Round(eggPiecesFloatRemaining)), EggProductionPieces: rowEggPieces,
EggProductionKg: eggRemainingWeightFloatRemaining, EggProductionKg: eggWeightFloat,
// EggProductionTotalWeightKg: eggWeightFloat, AverageDocPriceRp: avgDocPrice,
// EggProductionTotalPieces: int64(math.Round(eggTotalPiecesFloat)), HppRp: hppRp,
AverageDocPriceRp: avgDocPrice, EggHppRpPerKg: eggHpp,
// HppRp: hppRp, RemainingValueRp: rowRemainingValue,
EggHppRpPerKg: eggHpp, EggValueRp: rowEggValue,
// RemainingValueRp: rowRemainingValue,
EggValueRp: rowEggValue,
}) })
// totalBirds += rowBirds totalBirds += rowBirds
// totalWeight += weightFloat totalWeight += weightFloat
totalEggPieces += rowEggPieces totalEggPieces += rowEggPieces
totalEggKg += eggRemainingWeightFloatRemaining totalEggKg += eggWeightFloat
// totalRemainingValueRp += rowRemainingValue totalRemainingValueRp += rowRemainingValue
totalEggValueRp += rowEggValue totalEggValueRp += rowEggValue
totalAvgWeightSum += avgWeight if weightFloat > 0 {
totalAvgWeightCount++ totalHppSum += hppRp
// if weightFloat > 0 { totalHppCount++
// totalHppSum += hppRp }
// totalHppCount++
// }
if avgDocPrice > 0 { if avgDocPrice > 0 {
totalDocPriceSum += float64(avgDocPrice) totalDocPriceSum += float64(avgDocPrice)
totalDocPriceCount++ totalDocPriceCount++
@@ -1721,30 +1246,16 @@ 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
// rangeAgg.RemainingBirds += rowBirds rangeSummary.RemainingChickenBirds += rowBirds
// rangeAgg.RemainingWeightKg += row.RemainingChickenWeight rangeSummary.RemainingChickenWeightKg += row.RemainingChickenWeight
rangeAgg.AvgWeightSum += avgWeight
rangeAgg.AvgWeightCount++
for _, supplier := range feedSupplierMap[row.ProjectFlockKandangID] {
if _, ok := rangeAgg.FeedSuppliers[supplier.ID]; !ok {
rangeAgg.FeedSuppliers[supplier.ID] = supplier
}
}
for _, supplier := range docSupplierMap[row.ProjectFlockKandangID] {
if _, ok := rangeAgg.DocSuppliers[supplier.ID]; !ok {
rangeAgg.DocSuppliers[supplier.ID] = supplier
}
}
rangeSummary.EggProductionPieces += rowEggPieces rangeSummary.EggProductionPieces += rowEggPieces
rangeSummary.EggProductionKg += eggRemainingWeightFloatRemaining rangeSummary.EggProductionKg += eggWeightFloat
// rangeSummary.RemainingValueRp += rowRemainingValue rangeSummary.RemainingValueRp += rowRemainingValue
rangeSummary.EggValueRp += rowEggValue rangeSummary.EggValueRp += rowEggValue
if eggWeightFloat > 0 { if eggWeightFloat > 0 {
rangeAgg.EggHppSum += eggHpp rangeAgg.EggHppSum += eggHpp
@@ -1768,37 +1279,31 @@ 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 agg.AvgWeightCount > 0 { if entry.RemainingChickenBirds > 0 {
entry.AvgWeightKg = agg.AvgWeightSum / float64(agg.AvgWeightCount) entry.AvgWeightKg = entry.RemainingChickenWeightKg / float64(entry.RemainingChickenBirds)
} }
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{
TotalEggProductionPieces: totalEggPieces, TotalRemainingChickenBirds: totalBirds,
TotalEggProductionKg: totalEggKg, TotalRemainingChickenWeightKg: totalWeight,
TotalEggValueRp: totalEggValueRp, TotalEggProductionPieces: totalEggPieces,
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)
@@ -1891,9 +1396,6 @@ func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.Hp
if err != nil { if err != nil {
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
} }
if weightMin != nil && weightMax != nil && *weightMin > *weightMax {
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, "weight_min must be less than or equal to weight_max")
}
params := &validation.HppPerKandangQuery{ params := &validation.HppPerKandangQuery{
Page: page, Page: page,
@@ -17,14 +17,12 @@ type ExpenseQuery struct {
type MarketingQuery struct { type MarketingQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"` Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=100"` Search string `query:"search" validate:"omitempty,max=100"`
CustomerId int64 `query:"customer_id" validate:"omitempty"` CustomerId int64 `query:"customer_id" validate:"omitempty"`
ProductId int64 `query:"product_id" validate:"omitempty"` ProductId int64 `query:"product_id" validate:"omitempty"`
WarehouseId int64 `query:"warehouse_id" validate:"omitempty"` WarehouseId int64 `query:"warehouse_id" validate:"omitempty"`
SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"`
AreaId int64 `query:"area_id" validate:"omitempty"`
LocationId int64 `query:"location_id" validate:"omitempty"`
MarketingType string `query:"marketing_type" validate:"omitempty,oneof=ayam telur trading"` MarketingType string `query:"marketing_type" validate:"omitempty,oneof=ayam telur trading"`
FilterBy string `query:"filter_by" validate:"omitempty,oneof=so_date realization_date"` FilterBy string `query:"filter_by" validate:"omitempty,oneof=so_date realization_date"`
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
@@ -58,7 +56,7 @@ type DebtSupplierQuery struct {
type HppPerKandangQuery struct { type HppPerKandangQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"` Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,max=1000,gt=0"` Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
Period string `query:"period" validate:"required"` Period string `query:"period" validate:"required"`
ShowUnrecorded bool `query:"show_unrecorded"` ShowUnrecorded bool `query:"show_unrecorded"`
AreaIDs []int64 `query:"-"` AreaIDs []int64 `query:"-"`
@@ -70,14 +68,6 @@ type HppPerKandangQuery struct {
type ProductionResultQuery struct { type ProductionResultQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"` Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,max=1000,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"`
}
+11 -12
View File
@@ -2,19 +2,18 @@ package fifo
const ( const (
// Usable Keys // Usable Keys
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK" UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
UsableKeyRecordingDepletion UsableKey = "RECORDING_DEPLETION" UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN" UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY" UsableKeyTransferToLayingOut UsableKey = "TRANSFERTOLAYING_OUT"
UsableKeyTransferToLayingOut UsableKey = "TRANSFERTOLAYING_OUT" UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT"
UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT" UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT"
UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT"
// Stockable Keys // Stockable Keys
StockableKeyTransferToLayingIn StockableKey = "TRANSFERTOLAYING_IN" StockableKeyTransferToLayingIn StockableKey = "TRANSFERTOLAYING_IN"
StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN" StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN"
StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN" StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN"
StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS" StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS"
StockableKeyProjectFlockPopulation StockableKey = "PROJECT_FLOCK_POPULATION" StockableKeyProjectFlockPopulation StockableKey = "PROJECT_FLOCK_POPULATION"
StockableKeyRecordingEgg StockableKey = "RECORDING_EGG" StockableKeyRecordingEgg StockableKey = "RECORDING_EGG"
) )
@@ -14,10 +14,15 @@ func MapStocks(recordingID uint, items []validation.Stock) []entity.RecordingSto
for _, item := range items { for _, item := range items {
usagePtr := new(float64) usagePtr := new(float64)
*usagePtr = item.Qty *usagePtr = item.Qty
pending := item.PendingQty
if pending == nil {
pending = new(float64)
}
result = append(result, entity.RecordingStock{ result = append(result, entity.RecordingStock{
RecordingId: recordingID, RecordingId: recordingID,
ProductWarehouseId: item.ProductWarehouseId, ProductWarehouseId: item.ProductWarehouseId,
UsageQty: usagePtr, UsageQty: usagePtr,
PendingQty: pending,
}) })
} }
return result return result