resolve conflict to sprint 7

This commit is contained in:
MacBook Air M1
2025-12-22 15:22:12 +07:00
25 changed files with 1419 additions and 292 deletions
+15
View File
@@ -588,6 +588,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Uom: "Ekor", Uom: "Ekor",
Category: "Day Old Chick", Category: "Day Old Chick",
Price: 1, Price: 1,
Flags: []utils.FlagType{utils.FlagAyamAfkir},
}, },
{ {
Name: "Ayam Mati", Name: "Ayam Mati",
@@ -596,6 +597,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Uom: "Ekor", Uom: "Ekor",
Category: "Day Old Chick", Category: "Day Old Chick",
Price: 1, Price: 1,
Flags: []utils.FlagType{utils.FlagAyamMati},
}, },
{ {
Name: "Ayam Culling", Name: "Ayam Culling",
@@ -604,6 +606,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Uom: "Ekor", Uom: "Ekor",
Category: "Day Old Chick", Category: "Day Old Chick",
Price: 1, Price: 1,
Flags: []utils.FlagType{utils.FlagAyamCulling},
}, },
{ {
Name: "Telur Konsumsi Baik", Name: "Telur Konsumsi Baik",
@@ -612,6 +615,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Uom: "Unit", Uom: "Unit",
Category: "Telur", Category: "Telur",
Price: 1, Price: 1,
Flags: []utils.FlagType{utils.FlagTelurUtuh},
}, },
{ {
Name: "Telur Pecah", Name: "Telur Pecah",
@@ -620,6 +624,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Uom: "Unit", Uom: "Unit",
Category: "Telur", Category: "Telur",
Price: 1, Price: 1,
Flags: []utils.FlagType{utils.FlagTelurPecah},
}, },
{ {
Name: "281 SPECIAL STARTER", Name: "281 SPECIAL STARTER",
@@ -632,6 +637,16 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter}, Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter},
}, },
{
Name: "Ayam Layer",
Brand: "-",
Sku: "LYR0001",
Uom: "Ekor",
Category: "Pullet",
Price: 20000,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagLayer},
},
} }
for _, seed := range seeds { for _, seed := range seeds {
+21 -13
View File
@@ -40,12 +40,11 @@ const (
P_ApprovalGetAll = "lti.approval.list" P_ApprovalGetAll = "lti.approval.list"
) )
const ( const (
P_ReportExpenseGetAll = "lti.repport.expense.list" P_ReportExpenseGetAll = "lti.repport.expense.list"
P_ReportDeliveryGetAll = "lti.repport.delivery.list" P_ReportDeliveryGetAll = "lti.repport.delivery.list"
P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list" P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list"
) )
const ( const (
P_ProductStockGetAll = "lti.inventory.product_stock.list" P_ProductStockGetAll = "lti.inventory.product_stock.list"
P_ProductStockGetOne = "lti.inventory.product_stock.detail" P_ProductStockGetOne = "lti.inventory.product_stock.detail"
@@ -53,18 +52,18 @@ const (
P_ProductWarehouseGetOne = "lti.inventory.product_warehouses.detail" P_ProductWarehouseGetOne = "lti.inventory.product_warehouses.detail"
) )
const ( const (
P_ClosingGetAll = "lti.closing.list" P_ClosingGetAll = "lti.closing.list"
P_ClosingPenjualan = "lti.closing.penjualan" P_ClosingPenjualan = "lti.closing.penjualan"
P_ClosingGetSummary = "lti.closing.getsummary" P_ClosingGetSummary = "lti.closing.getsummary"
P_ClosingGetOverhead = "lti.closing.getoverhead" P_ClosingGetOverhead = "lti.closing.getoverhead"
P_ClosingCountSapronakKandang = "lti.closing.getsapronakcount.kandang" P_ClosingCountSapronakKandang = "lti.closing.getsapronakcount.kandang"
P_ClosingCountSapronak = "lti.closing.getsapronakcount" P_ClosingCountSapronak = "lti.closing.getsapronakcount"
P_ClosingSapronak = "lti.closing.getsapronak" P_ClosingSapronak = "lti.closing.getsapronak"
P_ClosingExpeditionHpp = "lti.closing.expedition" P_ClosingExpeditionHpp = "lti.closing.expedition"
P_ClosingExpeditionHppByKandang = "lti.closing.expedition.kandang" P_ClosingExpeditionHppByKandang = "lti.closing.expedition.kandang"
P_ClosingDataProduction = "lti.closing.production.data" P_ClosingDataProduction = "lti.closing.production.data"
P_ClosingKeuangan = "lti.closing.keuangan"
) )
const ( const (
@@ -73,10 +72,19 @@ const (
P_TransferCreateOne = "lti.inventory.transfer.create" P_TransferCreateOne = "lti.inventory.transfer.create"
) )
const (
P_TransferToLaying_GetAll = "lti.production.transfer_to_laying.list"
P_TransferToLaying_GetOne = "lti.production.transfer_to_laying.detail"
P_TransferToLaying_CreateOne = "lti.production.transfer_to_laying.create"
P_TransferToLaying_UpdateOne = "lti.production.transfer_to_laying.update"
P_TransferToLaying_DeleteOne = "lti.production.transfer_to_laying.delete"
P_TransferToLaying_Approval = "lti.production.transfer_to_laying.approve"
P_TransferToLaying_GetAvailableQty = "lti.production.transfer_to_laying.getavailableqty"
)
const ( const (
P_DeliveryGetAll = "lti.marketing.delivery_order.list" P_DeliveryGetAll = "lti.marketing.delivery_order.list"
P_DeliveryGetOne = "lti.marketing.delivery_order.detail" P_DeliveryGetOne = "lti.marketing.delivery_order.detail"
P_DeliveryCreateOne = "lti.marketing.delivery_order.create"
P_DeliveryUpdateOne = "lti.marketing.delivery_order.update" P_DeliveryUpdateOne = "lti.marketing.delivery_order.update"
P_SalesOrderDelete = "lti.marketing.sales_order.delete" P_SalesOrderDelete = "lti.marketing.sales_order.delete"
P_SalesOrderApproval = "lti.marketing.sales_order.approve" P_SalesOrderApproval = "lti.marketing.sales_order.approve"
@@ -246,6 +246,28 @@ func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error {
}) })
} }
func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
param := c.Params("project_flock_id")
projectFlockID, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
}
result, err := u.ClosingService.GetClosingKeuangan(c, uint(projectFlockID))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing keuangan 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")
@@ -0,0 +1,568 @@
package dto
import (
"slices"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
// === CONSTANTS ===
const (
HPPGroupPengeluaran = "HPP dan Pengeluaran"
HPPGroupBahanBaku = "HPP dan Bahan Baku"
HPPLabelOverhead = "Pengeluaran Overhead"
HPPLabelEkspedisi = "Beban Ekspedisi"
HPPSummaryLabel = "HPP"
PLSalesTypeChicken = "Penjualan Ayam Besar"
PLSalesTypeEgg = "Penjualan Telur"
PLItemTypeSapronak = "Pembelian Sapronak"
PLItemTypeOverhead = "Pengeluaran Overhead"
PLItemTypeEkspedisi = "Beban Ekspedisi"
PLSummaryLabelGrossProfit = "LABA RUGI BRUTTO"
PLSummaryLabelSubTotal = "SUB TOTAL"
PLSummaryLabelNetProfit = "LABA RUGI NETTO"
PurchaseLabelPrefix = "Pembelian "
)
// === CONTEXT STRUCTS ===
type CalculationContext struct {
TotalPopulation float64
TotalWeightProduced 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
TotalDepletion float64
}
// === BASE METRICS ===
type FinancialMetrics struct {
RpPerBird float64 `json:"rp_per_bird"`
RpPerKg float64 `json:"rp_per_kg"`
Amount float64 `json:"amount"`
}
type Comparison struct {
Budgeting FinancialMetrics `json:"budgeting"`
Realization FinancialMetrics `json:"realization"`
}
// === HPP PURCHASES PACKAGE ===
type HppItem struct {
Type string `json:"type"`
Comparison
}
type HppGroup struct {
GroupName string `json:"group_name"`
Data []HppItem `json:"data"`
}
type SummaryHpp struct {
Label string `json:"label"`
Comparison
}
type HppPurchasesSection struct {
Hpp []HppGroup `json:"hpp"`
SummaryHpp SummaryHpp `json:"summary_hpp"`
}
// === PROFIT LOSS PACKAGE ===
type PLItem struct {
Type string `json:"type"`
FinancialMetrics
}
type PLSummaryItem struct {
Label string `json:"label"`
FinancialMetrics
}
type PLSummaryGroup struct {
GrossProfit PLSummaryItem `json:"gross_profit"`
SubTotal PLSummaryItem `json:"sub_total"`
NetProfit PLSummaryItem `json:"net_profit"`
}
type ProfitLossData struct {
Penjualan []PLItem `json:"penjualan"`
Pembelian []PLItem `json:"pembelian"`
Overhead PLItem `json:"overhead"`
Ekspedisi PLItem `json:"ekspedisi"`
Summary PLSummaryGroup `json:"summary"`
}
type ProfitLossSection struct {
Data ProfitLossData `json:"data"`
}
// === RESPONSE DTO (ROOT) ===
type ReportResponse struct {
HppPurchases HppPurchasesSection `json:"hpp_purchases"`
ProfitLoss ProfitLossSection `json:"profit_loss"`
}
// === MAPPER FUNCTIONS ===
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
return FinancialMetrics{
RpPerBird: rpPerBird,
RpPerKg: rpPerKg,
Amount: amount,
}
}
func ToComparison(budgeting, realization FinancialMetrics) Comparison {
return Comparison{
Budgeting: budgeting,
Realization: realization,
}
}
// === HPP PENGELUARAN (from Purchase Items) ===
func getFlagLabel(flagType utils.FlagType) string {
return PurchaseLabelPrefix + string(flagType)
}
func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, ctx CalculationContext) []HppItem {
flags := []utils.FlagType{
utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan,
utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher,
utils.FlagOVK, utils.FlagObat, utils.FlagVitamin, utils.FlagKimia,
}
items := []HppItem{}
seenFlags := make(map[utils.FlagType]bool)
for _, item := range purchaseItems {
if item.Product == nil || len(item.Product.Flags) == 0 {
continue
}
for _, flag := range item.Product.Flags {
flagType := utils.FlagType(flag.Name)
if slices.Contains(flags, flagType) && !seenFlags[flagType] {
amount := sumPurchasesByFlag(purchaseItems, flagType)
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.TotalPopulation, ctx.TotalWeightProduced)
items = append(items, HppItem{
Type: getFlagLabel(flagType),
Comparison: ToComparison(
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
),
})
seenFlags[flagType] = true
}
}
}
return items
}
// === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) ===
func createHppOverheadItem(budgetAmount, realizationAmount float64, ctx CalculationContext) HppItem {
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
return HppItem{
Type: HPPLabelOverhead,
Comparison: ToComparison(
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount),
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
),
}
}
func createHppEkspedisiItem(ekspedisiAmount float64, ctx CalculationContext) HppItem {
ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
return HppItem{
Type: HPPLabelEkspedisi,
Comparison: ToComparison(
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
),
}
}
func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppGroup {
items := []HppItem{}
budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
realizationAmount := getOperationalExpenses(realizations)
if budgetAmount > 0 || realizationAmount > 0 {
items = append(items, createHppOverheadItem(budgetAmount, realizationAmount, ctx))
}
ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
items = append(items, createHppEkspedisiItem(ekspedisiAmount, ctx))
return HppGroup{
GroupName: HPPGroupBahanBaku,
Data: items,
}
}
// === HPP SUMMARY ===
func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) SummaryHpp {
purchaseTotal := sumPurchaseTotal(purchaseItems)
budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
totalBudget := purchaseTotal + budgetTotal
totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true })
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, ctx.TotalPopulation, ctx.TotalWeightProduced)
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced)
return SummaryHpp{
Label: label,
Comparison: ToComparison(
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
),
}
}
func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppPurchasesSection {
hppGroups := []HppGroup{
{
GroupName: HPPGroupPengeluaran,
Data: buildHppItemsByPurchaseFlags(purchaseItems, ctx),
},
ToHppBahanBakuGroup(budgets, realizations, ctx),
}
summaryHpp := ToSummaryHpp(HPPSummaryLabel, purchaseItems, budgets, realizations, ctx)
return HppPurchasesSection{
Hpp: hppGroups,
SummaryHpp: summaryHpp,
}
}
// === PROFIT & LOSS ===
func ToPLItem(itemType string, metrics FinancialMetrics) PLItem {
return PLItem{
Type: itemType,
FinancialMetrics: metrics,
}
}
func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem {
return PLSummaryItem{
Label: label,
FinancialMetrics: metrics,
}
}
func 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)
bopAmount := getOperationalExpenses(realizations)
totalCost := purchaseAmount + bopAmount
return []PLItem{
createPLItemWithMetrics(PLItemTypeSapronak, totalCost, 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{
Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems),
}
}
func aggregatePLItems(items []PLItem, label string) PLItem {
totalAmount, totalPerBird := sumPLItems(items)
return ToPLItem(label, ToFinancialMetrics(totalPerBird, 0, totalAmount))
}
func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse {
return ReportResponse{
HppPurchases: hppPurchases,
ProfitLoss: profitLoss,
}
}
func ToClosingKeuanganReport(input ClosingKeuanganInput) ReportResponse {
var totalPopulation float64
var totalWeightSold float64
for _, chickin := range input.Chickins {
totalPopulation += chickin.UsageQty
}
for _, delivery := range input.DeliveryProducts {
totalWeightSold += delivery.TotalWeight
}
ctx := CalculationContext{
TotalPopulation: totalPopulation,
TotalWeightProduced: input.TotalWeightProduced,
TotalDepletion: input.TotalDepletion,
TotalWeightSold: totalWeightSold,
ActualPopulation: totalPopulation - input.TotalDepletion,
}
hppSection := ToHppPurchasesSection(input.PurchaseItems, input.Budgets, input.Realizations, 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
}
@@ -35,8 +35,7 @@ type PenjualanRealisasiResponseDTO struct {
func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO { func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
// todo: usia ayam masih dummy age := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate)
age := 0
var product *productDTO.ProductRelationDTO var product *productDTO.ProductRelationDTO
if e.MarketingProduct.ProductWarehouse.Product.Id != 0 { if e.MarketingProduct.ProductWarehouse.Product.Id != 0 {
@@ -101,3 +100,20 @@ func extractPeriodFromRealisasi(realisasi []entity.MarketingDeliveryProduct) int
} }
return 0 return 0
} }
func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int {
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 {
return 0
}
earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate
for _, chickin := range projectFlockKandang.Chickins {
if chickin.ChickInDate.Before(earliestChickinDate) {
earliestChickinDate = chickin.ChickInDate
}
}
ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24)
ageInWeeks := ageInDays / 7
return ageInWeeks
}
@@ -69,7 +69,7 @@ func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseReal
return dto return dto
} }
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty float64) OverheadListDTO { func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64) OverheadListDTO {
overheadsByNonstockID := make(map[uint]*OverheadDTO) overheadsByNonstockID := make(map[uint]*OverheadDTO)
latestDateByNonstockID := make(map[uint]string) latestDateByNonstockID := make(map[uint]string)
@@ -119,7 +119,8 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
for nonstockID, overhead := range overheadsByNonstockID { for nonstockID, overhead := range overheadsByNonstockID {
overhead.ActualDate = latestDateByNonstockID[nonstockID] overhead.ActualDate = latestDateByNonstockID[nonstockID]
overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalChickinQty)
overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalActualPopulation)
if overhead.ActualQuantity > 0 { if overhead.ActualQuantity > 0 {
overhead.ActualUnitPrice = overhead.ActualTotalAmount / overhead.ActualQuantity overhead.ActualUnitPrice = overhead.ActualTotalAmount / overhead.ActualQuantity
@@ -139,7 +140,7 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
BudgetTotalAmount: totalBudgetAmount, BudgetTotalAmount: totalBudgetAmount,
ActualQuantity: totalActualQuantity, ActualQuantity: totalActualQuantity,
ActualTotalAmount: totalActualAmount, ActualTotalAmount: totalActualAmount,
CostPerBird: calculateCostPerBird(totalActualAmount, totalChickinQty), CostPerBird: calculateCostPerBird(totalActualAmount, totalActualPopulation),
}, },
Overheads: overheadItems, Overheads: overheadItems,
} }
@@ -158,9 +159,9 @@ func calculateTotal(qty, price float64) float64 {
return qty * price return qty * price
} }
func calculateCostPerBird(totalPrice, totalChickinQty float64) float64 { func calculateCostPerBird(totalPrice, totalActualPopulation float64) float64 {
if totalChickinQty > 0 { if totalActualPopulation > 0 {
return totalPrice / totalChickinQty return totalPrice / totalActualPopulation
} }
return 0 return 0
} }
+5 -1
View File
@@ -13,6 +13,8 @@ import (
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -30,10 +32,12 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db) marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db) expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
chickinRepo := rChickin.NewChickinRepository(db) chickinRepo := rChickin.NewChickinRepository(db)
recordingRepo := rRecording.NewRecordingRepository(db)
purchaseRepo := rPurchase.NewPurchaseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo) approvalService := commonSvc.NewApprovalService(approvalRepo)
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, validate) closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate)
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate) sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
+1
View File
@@ -31,4 +31,5 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingExpeditionHpp), ctrl.GetExpeditionHPP) route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingExpeditionHpp), ctrl.GetExpeditionHPP)
route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingExpeditionHppByKandang), ctrl.GetExpeditionHPPByKandang) route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingExpeditionHppByKandang), ctrl.GetExpeditionHPPByKandang)
route.Get("/:projectFlockId/production-data", m.RequirePermissions(m.P_ClosingDataProduction), ctrl.GetClosingDataProduksi) route.Get("/:projectFlockId/production-data", m.RequirePermissions(m.P_ClosingDataProduction), ctrl.GetClosingDataProduksi)
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingKeuangan), ctrl.GetClosingKeuangan)
} }
@@ -17,6 +17,8 @@ import (
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
@@ -34,6 +36,7 @@ type ClosingService interface {
GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error)
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error)
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error)
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
} }
@@ -48,9 +51,11 @@ type closingService struct {
ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
ChickinRepo chickinRepository.ProjectChickinRepository ChickinRepo chickinRepository.ProjectChickinRepository
PurchaseRepo purchaseRepository.PurchaseRepository
RecordingRepo recordingRepository.RecordingRepository
} }
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, validate *validator.Validate) ClosingService { func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, validate *validator.Validate) ClosingService {
return &closingService{ return &closingService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
@@ -62,6 +67,8 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje
ExpenseRealizationRepo: expenseRealizationRepo, ExpenseRealizationRepo: expenseRealizationRepo,
ProjectBudgetRepo: projectBudgetRepo, ProjectBudgetRepo: projectBudgetRepo,
ChickinRepo: chickinRepo, ChickinRepo: chickinRepo,
PurchaseRepo: purchaseRepo,
RecordingRepo: recordingRepo,
} }
} }
@@ -134,6 +141,7 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entit
Preload("MarketingProduct.ProductWarehouse.Warehouse"). Preload("MarketingProduct.ProductWarehouse.Warehouse").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang"). Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang"). Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
Preload("MarketingProduct.Marketing"). Preload("MarketingProduct.Marketing").
Preload("MarketingProduct.Marketing.Customer"). Preload("MarketingProduct.Marketing.Customer").
Order("marketing_delivery_products.delivery_date DESC") Order("marketing_delivery_products.delivery_date DESC")
@@ -379,11 +387,95 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.Ove
totalChickinQty += chickin.UsageQty totalChickinQty += chickin.UsageQty
} }
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty) totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
}
totalActualPopulation := totalChickinQty - totalDepletion
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation)
return &result, nil return &result, nil
} }
func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) {
if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
}
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: func(ctx context.Context, id uint) (bool, error) {
_, err := s.ProjectFlockRepo.GetByID(ctx, id, nil)
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return err == nil, err
}},
); err != nil {
return nil, err
}
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
if err != nil {
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")
}
purchaseItems, err := s.PurchaseRepo.GetItemsByProjectFlockID(c.Context(), projectFlockID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch purchase items")
}
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations")
}
deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
return db.Preload("MarketingProduct").
Preload("MarketingProduct.ProductWarehouse").
Preload("MarketingProduct.ProductWarehouse.Product")
})
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products")
}
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
}
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err)
}
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
}
input := dto.ClosingKeuanganInput{
ProjectFlockCategory: projectFlock.Category,
PurchaseItems: purchaseItems,
Budgets: budgets,
Realizations: realizations,
DeliveryProducts: deliveryProducts,
Chickins: chickins,
TotalWeightProduced: totalWeightProduced,
TotalDepletion: totalDepletion,
}
report := dto.ToClosingKeuanganReport(input)
return &report, nil
}
// GetExpeditionHPP menghitung HPP ekspedisi per vendor untuk sebuah project flock. // GetExpeditionHPP menghitung HPP ekspedisi per vendor untuk sebuah project flock.
// Jika projectFlockKandangID tidak nil, maka hanya data untuk kandang tersebut yang dihitung. // Jika projectFlockKandangID tidak nil, maka hanya data untuk kandang tersebut yang dihitung.
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) {
@@ -686,4 +778,5 @@ func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (fl
} }
return closest.Mortality, closest.FcrNumber return closest.Mortality, closest.FcrNumber
} }
@@ -44,12 +44,13 @@ func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Conte
Preload("ExpenseNonstock"). Preload("ExpenseNonstock").
Preload("ExpenseNonstock.Nonstock"). Preload("ExpenseNonstock.Nonstock").
Preload("ExpenseNonstock.Nonstock.Uom"). Preload("ExpenseNonstock.Nonstock.Uom").
Preload("ExpenseNonstock.Nonstock.Flags").
Preload("ExpenseNonstock.Expense"). Preload("ExpenseNonstock.Expense").
Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id"). Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id").
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id"). Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id").
Where("expenses.category = ?", "BOP"). Joins("LEFT JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id").
Where("project_flock_kandangs.project_flock_id = ? OR kandangs.id IN (SELECT kandang_id FROM project_flock_kandangs WHERE project_flock_id = ?)", projectFlockID, projectFlockID).
Find(&realizations).Error Find(&realizations).Error
return realizations, err return realizations, err
} }
@@ -66,7 +67,8 @@ func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context
Preload("Expense.Supplier"). Preload("Expense.Supplier").
Preload("Kandang"). Preload("Kandang").
Preload("Kandang.Location"). Preload("Kandang.Location").
Preload("Nonstock") Preload("Nonstock").
Preload("Nonstock.Flags")
}). }).
Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id"). Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id"). Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id").
@@ -213,7 +213,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
var projectFlockKandangId *uint64 var projectFlockKandangId *uint64
if req.Category == "BOP" { if req.Category == string(utils.ExpenseCategoryBOP) {
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID)) projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID))
if err != nil { if err != nil {
@@ -230,10 +230,10 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
nonstockId := costItem.NonstockID nonstockId := costItem.NonstockID
var kandangId *uint64 var kandangId *uint64
if req.Category == "NON-BOP" { if req.Category == string(utils.ExpenseCategoryNonBOP) {
id := uint64(expenseNonstock.KandangID) id := uint64(expenseNonstock.KandangID)
kandangId = &id kandangId = &id
} else if req.Category == "BOP" { } else if req.Category == string(utils.ExpenseCategoryBOP) {
if projectFlockKandangId != nil { if projectFlockKandangId != nil {
kandangId = &expenseNonstock.KandangID kandangId = &expenseNonstock.KandangID
} }
@@ -385,7 +385,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
} }
if categoryChanged { if categoryChanged {
if currentExpense.Category == "BOP" && newCategory == "NON-BOP" { if currentExpense.Category == string(utils.ExpenseCategoryBOP) && newCategory == string(utils.ExpenseCategoryNonBOP) {
var existingExpenseNonstocks []entity.ExpenseNonstock var existingExpenseNonstocks []entity.ExpenseNonstock
if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil { if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil {
@@ -400,7 +400,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock kandang id to null") return fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock kandang id to null")
} }
} }
} else if currentExpense.Category == "NON-BOP" && newCategory == "BOP" { } else if currentExpense.Category == string(utils.ExpenseCategoryNonBOP) && newCategory == string(utils.ExpenseCategoryBOP) {
var existingExpenseNonstocks []entity.ExpenseNonstock var existingExpenseNonstocks []entity.ExpenseNonstock
if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil { if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil {
@@ -457,7 +457,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
for _, expenseNonstock := range *req.ExpenseNonstocks { for _, expenseNonstock := range *req.ExpenseNonstocks {
var projectFlockKandangId *uint64 var projectFlockKandangId *uint64
if updatedExpense.Category == "BOP" { if updatedExpense.Category == string(utils.ExpenseCategoryBOP) {
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx) projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx)
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID)) projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID))
if err != nil { if err != nil {
@@ -480,10 +480,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
} }
var kandangId *uint64 var kandangId *uint64
if updatedExpense.Category == "NON-BOP" { if updatedExpense.Category == string(utils.ExpenseCategoryNonBOP) {
id := uint64(expenseNonstock.KandangID) id := uint64(expenseNonstock.KandangID)
kandangId = &id kandangId = &id
} else if updatedExpense.Category == "BOP" { } else if updatedExpense.Category == string(utils.ExpenseCategoryBOP) {
if projectFlockKandangId != nil { if projectFlockKandangId != nil {
kandangId = &expenseNonstock.KandangID kandangId = &expenseNonstock.KandangID
} }
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"strings"
"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"
@@ -31,8 +32,6 @@ func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProduct
func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error) { func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error) {
var deliveryProducts []entity.MarketingDeliveryProduct var deliveryProducts []entity.MarketingDeliveryProduct
// JOIN digunakan untuk filter WHERE clause ke ProjectFlockID yang berada 3 level relasi atas
// Entity relations digunakan di Preload (callback) untuk load data, bukan untuk filter
db := r.DB().WithContext(ctx). db := r.DB().WithContext(ctx).
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id"). Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id"). Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
@@ -91,16 +90,19 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
Preload("Marketing.SalesPerson"). Preload("Marketing.SalesPerson").
Preload("ProductWarehouse"). Preload("ProductWarehouse").
Preload("ProductWarehouse.Product"). Preload("ProductWarehouse.Product").
Preload("ProductWarehouse.Warehouse") Preload("ProductWarehouse.Product.Flags").
Preload("ProductWarehouse.Warehouse").
Preload("ProductWarehouse.ProjectFlockKandang").
Preload("ProductWarehouse.ProjectFlockKandang.ProjectFlock")
}). }).
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id"). Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id") Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id")
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.ProjectFlockKandangId > 0 { if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" {
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")
} }
if filters.ProductId > 0 { if filters.ProductId > 0 || filters.Search != "" {
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")
} }
@@ -109,8 +111,13 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
} }
if filters.Search != "" { if filters.Search != "" {
db = db.Where("marketing_delivery_products.vehicle_number ILIKE ?", db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id")
"%"+filters.Search+"%") }
if filters.Search != "" {
searchPattern := "%" + filters.Search + "%"
db = db.Where("marketing_delivery_products.vehicle_number ILIKE ? OR marketings.so_number ILIKE ? OR customers.name ILIKE ? OR products.name ILIKE ?",
searchPattern, searchPattern, searchPattern, searchPattern)
} }
if filters.CustomerId > 0 { if filters.CustomerId > 0 {
@@ -121,10 +128,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
db = db.Where("marketings.sales_person_id = ?", filters.SalesPersonId) db = db.Where("marketings.sales_person_id = ?", filters.SalesPersonId)
} }
if filters.MarketingId > 0 {
db = db.Where("marketings.id = ?", filters.MarketingId)
}
if filters.ProductId > 0 { if filters.ProductId > 0 {
db = db.Where("product_warehouses.product_id = ?", filters.ProductId) db = db.Where("product_warehouses.product_id = ?", filters.ProductId)
} }
@@ -133,17 +136,92 @@ 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.ProjectFlockKandangId > 0 { if filters.FilterBy != "" && (filters.StartDate != "" || filters.EndDate != "") {
db = db.Where("product_warehouses.project_flock_kandang_id = ?", filters.ProjectFlockKandangId) if filters.FilterBy == "so_date" {
} if filters.StartDate != "" {
if startDate, err := utils.ParseDateString(filters.StartDate); err == nil {
if filters.DeliveryDate != "" { db = db.Where("marketings.so_date >= ?", startDate)
if deliveryDate, err := utils.ParseDateString(filters.DeliveryDate); err == nil { }
nextDate := deliveryDate.AddDate(0, 0, 1) }
db = db.Where("marketing_delivery_products.delivery_date >= ? AND marketing_delivery_products.delivery_date < ?", deliveryDate, nextDate) if filters.EndDate != "" {
if endDate, err := utils.ParseDateString(filters.EndDate); err == nil {
nextDate := endDate.AddDate(0, 0, 1)
db = db.Where("marketings.so_date < ?", nextDate)
}
}
} else if filters.FilterBy == "realization_date" {
if filters.StartDate != "" {
if startDate, err := utils.ParseDateString(filters.StartDate); err == nil {
db = db.Where("marketing_delivery_products.delivery_date >= ?", startDate)
}
}
if filters.EndDate != "" {
if endDate, err := utils.ParseDateString(filters.EndDate); err == nil {
nextDate := endDate.AddDate(0, 0, 1)
db = db.Where("marketing_delivery_products.delivery_date < ?", nextDate)
}
}
} }
} }
sortColumn := "marketing_delivery_products.id"
sortOrder := "DESC"
if filters.SortBy != "" {
switch filters.SortBy {
case "so_date":
sortColumn = "marketings.so_date"
case "realization_date":
sortColumn = "marketing_delivery_products.delivery_date"
case "customer":
sortColumn = "customers.name"
if !containsJoin(db, "customers") {
db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id")
}
case "warehouse":
sortColumn = "warehouses.name"
if !containsJoin(db, "warehouses") {
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id")
}
case "product":
sortColumn = "products.name"
if !containsJoin(db, "products") {
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
Joins("LEFT JOIN products ON products.id = product_warehouses.product_id")
}
case "sales_person":
sortColumn = "sales_users.name"
if !containsJoin(db, "sales_users") {
db = db.Joins("LEFT JOIN users AS sales_users ON sales_users.id = marketings.sales_person_id")
}
case "vehicle_number":
sortColumn = "marketing_delivery_products.vehicle_number"
case "sales_amount":
sortColumn = "marketing_delivery_products.total_price"
case "hpp_amount":
sortColumn = "marketing_delivery_products.total_price"
case "qty":
sortColumn = "marketing_delivery_products.qty"
case "average_weight":
sortColumn = "marketing_delivery_products.avg_weight"
case "total_weight":
sortColumn = "marketing_delivery_products.total_weight"
case "sales_price":
sortColumn = "marketing_delivery_products.unit_price"
case "hpp_price":
sortColumn = "marketing_delivery_products.unit_price"
case "aging_days":
sortColumn = "marketing_delivery_products.delivery_date"
}
}
if filters.SortOrder != "" && (filters.SortOrder == "asc" || filters.SortOrder == "desc") {
sortOrder = strings.ToUpper(filters.SortOrder)
}
db = db.Order(sortColumn + " " + sortOrder)
if err := db.Count(&total).Error; err != nil { if err := db.Count(&total).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -151,10 +229,15 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
if err := db. if err := db.
Offset(offset). Offset(offset).
Limit(limit). Limit(limit).
Order("marketing_delivery_products.id DESC").
Find(&deliveryProducts).Error; err != nil { Find(&deliveryProducts).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
return deliveryProducts, total, nil return deliveryProducts, total, nil
} }
func containsJoin(db *gorm.DB, tableName string) bool {
statement := db.Statement
joinSQL := statement.SQL.String()
return strings.Contains(joinSQL, "JOIN "+tableName)
}
+6 -10
View File
@@ -16,16 +16,12 @@ func RegisterRoutes(router fiber.Router, userService user.UserService, salesOrde
route := router.Group("/marketing") route := router.Group("/marketing")
route.Use(m.Auth(userService)) route.Use(m.Auth(userService))
route.Get("/", deliveryOrdersCtrl.GetAll) route.Get("/",m.RequirePermissions(m.P_DeliveryGetAll), deliveryOrdersCtrl.GetAll)
route.Get("/:id", deliveryOrdersCtrl.GetOne) route.Get("/:id",m.RequirePermissions(m.P_DeliveryGetOne), deliveryOrdersCtrl.GetOne)
route.Delete("/:id", salesOrdersCtrl.DeleteOne) route.Delete("/:id",m.RequirePermissions(m.P_SalesOrderDelete), salesOrdersCtrl.DeleteOne)
route.Post("/sales-orders", salesOrdersCtrl.CreateOne) route.Post("/sales-orders",m.RequirePermissions(m.P_SalesOrderCreateOne), salesOrdersCtrl.CreateOne)
route.Patch("/sales-orders/:id", salesOrdersCtrl.UpdateOne) route.Patch("/sales-orders/:id",m.RequirePermissions(m.P_SalesOrderUpdateOne), salesOrdersCtrl.UpdateOne)
route.Post("/sales-orders/approvals", salesOrdersCtrl.Approval) route.Post("/sales-orders/approvals",m.RequirePermissions(m.P_SalesOrderApproval), salesOrdersCtrl.Approval)
route.Get("/delivery-orders", deliveryOrdersCtrl.GetAll)
route.Get("/delivery-orders/:id", deliveryOrdersCtrl.GetOne)
route.Post("/delivery-orders", deliveryOrdersCtrl.CreateOne)
route.Patch("/delivery-orders/:id", deliveryOrdersCtrl.UpdateOne)
} }
@@ -15,6 +15,7 @@ type ProjectChickinRepository interface {
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error)
} }
type ChickinRepositoryImpl struct { type ChickinRepositoryImpl struct {
@@ -90,3 +91,14 @@ func (r *ChickinRepositoryImpl) GetTotalPendingUsageQtyByProjectFlockKandangID(c
} }
return total, nil return total, nil
} }
func (r *ChickinRepositoryImpl) GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
var result float64
err := r.db.WithContext(ctx).
Table("project_chickins").
Select("COALESCE(SUM(project_chickins.usage_qty), 0)").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = project_chickins.project_flock_kandang_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Scan(&result).Error
return result, err
}
@@ -143,6 +143,10 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d is not bound to kandang's warehouse", chickinReq.ProductWarehouseId)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d is not bound to kandang's warehouse", chickinReq.ProductWarehouseId))
} }
if productWarehouse.ProjectFlockKandangId == nil || *productWarehouse.ProjectFlockKandangId != req.ProjectFlockKandangId {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d is not attached to project_flock_kandang %d. Only product warehouses with matching project_flock_kandang_id can be chickin-ed", chickinReq.ProductWarehouseId, req.ProjectFlockKandangId))
}
chickinDate, err := utils.ParseDateString(chickinReq.ChickInDate) chickinDate, err := utils.ParseDateString(chickinReq.ChickInDate)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid ChickInDate format for product warehouse %d", chickinReq.ProductWarehouseId)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid ChickInDate format for product warehouse %d", chickinReq.ProductWarehouseId))
@@ -450,7 +454,8 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
} }
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID) pfkID := approvableID
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID, &pfkID)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
} }
@@ -466,7 +471,8 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
} }
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID) pfkID := approvableID
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID, &pfkID)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
} }
@@ -538,11 +544,19 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
return updated, nil return updated, nil
} }
func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) { func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint, projectFlockKandangId *uint) (*entity.ProductWarehouse, error) {
products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId) products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId)
if err == nil && len(products) > 0 { if err == nil && len(products) > 0 {
return &products[0], nil existingPW := &products[0]
// Update project_flock_kandang_id if not already set
if existingPW.ProjectFlockKandangId == nil && projectFlockKandangId != nil {
existingPW.ProjectFlockKandangId = projectFlockKandangId
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).UpdateOne(ctx.Context(), existingPW.Id, existingPW, nil); err != nil {
return nil, fmt.Errorf("failed to update %s product warehouse with project_flock_kandang_id: %w", categoryCode, err)
}
}
return existingPW, nil
} }
product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode) product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode)
@@ -554,9 +568,10 @@ func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId
} }
newPW := &entity.ProductWarehouse{ newPW := &entity.ProductWarehouse{
ProductId: product.Id, ProductId: product.Id,
WarehouseId: warehouseId, WarehouseId: warehouseId,
Quantity: 0, ProjectFlockKandangId: projectFlockKandangId,
Quantity: 0,
// CreatedBy: actorID, // CreatedBy: actorID,
} }
@@ -190,13 +190,16 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project
result := make(map[uint]float64) result := make(map[uint]float64)
for _, pw := range products { for _, pw := range products {
availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw)
if err != nil {
s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err)
}
if availableQty > 0 { if pw.ProjectFlockKandangId != nil && *pw.ProjectFlockKandangId == projectFlockKandang.Id {
result[pw.Id] = availableQty availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw)
if err != nil {
s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err)
}
if availableQty > 0 {
result[pw.Id] = availableQty
}
} }
} }
@@ -45,6 +45,10 @@ type RecordingRepository interface {
GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error)
GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error) GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error)
GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error)
GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error)
GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error)
GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error)
GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeightKg float64, err error)
} }
type RecordingRepositoryImpl struct { type RecordingRepositoryImpl struct {
@@ -363,6 +367,85 @@ func (r *RecordingRepositoryImpl) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint
return weight, true, nil return weight, true, nil
} }
func (r *RecordingRepositoryImpl) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) {
if projectFlockID == 0 {
return 0, 0, nil
}
totalChickinQty, err := r.getTotalChickinQtyByProjectFlockID(ctx, projectFlockID)
if err != nil {
return 0, 0, err
}
totalDepletion, err := r.GetTotalDepletionByProjectFlockID(ctx, projectFlockID)
if err != nil {
return 0, 0, err
}
actualQty := totalChickinQty - totalDepletion
avgWeight, err := r.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID)
if err != nil {
return 0, 0, err
}
totalWeight = actualQty * avgWeight
return totalWeight, actualQty, nil
}
func (r *RecordingRepositoryImpl) getTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
var result float64
err := r.DB().WithContext(ctx).
Table("project_chickins").
Select("COALESCE(SUM(project_chickins.usage_qty), 0)").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = project_chickins.project_flock_kandang_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Scan(&result).Error
return result, err
}
func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
var result float64
err := r.DB().WithContext(ctx).
Table("recording_depletions").
Select("COALESCE(SUM(recording_depletions.qty), 0)").
Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Scan(&result).Error
return result, err
}
func (r *RecordingRepositoryImpl) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
var result float64
err := r.DB().WithContext(ctx).
Table("recording_bws").
Select("COALESCE(AVG(recording_bws.avg_weight), 0)").
Joins("JOIN recordings ON recordings.id = recording_bws.recording_id").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Where("recordings.record_datetime = (SELECT MAX(record_datetime) FROM recordings r2 WHERE r2.project_flock_kandangs_id IN (SELECT id FROM project_flock_kandangs WHERE project_flock_id = ?))", projectFlockID).
Scan(&result).Error
return result, err
}
func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
if projectFlockID == 0 {
return 0, nil
}
var result float64
err := r.DB().WithContext(ctx).
Table("recording_eggs").
Select("COALESCE(SUM(recording_eggs.qty * recording_eggs.weight), 0) / 1000").
Joins("JOIN recordings ON recordings.id = recording_eggs.recording_id").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Scan(&result).Error
return result, err
}
func nextRecordingDay(days []int) int { func nextRecordingDay(days []int) int {
if len(days) == 0 { if len(days) == 0 {
return 1 return 1
@@ -21,11 +21,11 @@ func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying.
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
// route.Post("/approval", m.Auth(u), ctrl.Approval) // route.Post("/approval", m.Auth(u), ctrl.Approval)
route.Get("/", ctrl.GetAll) route.Get("/",m.RequirePermissions(m.P_TransferToLaying_GetAll), ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/",m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne) route.Get("/:id",m.RequirePermissions(m.P_TransferToLaying_GetOne), ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne) route.Patch("/:id",m.RequirePermissions(m.P_TransferToLaying_UpdateOne), ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne) route.Delete("/:id",m.RequirePermissions(m.P_TransferToLaying_DeleteOne), ctrl.DeleteOne)
route.Post("/approvals", ctrl.Approval) route.Post("/approvals",m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Approval)
route.Get("/project-flocks/:project_flock_id/available-qty", ctrl.GetAvailableQtyPerKandang) route.Get("/project-flocks/:project_flock_id/available-qty",m.RequirePermissions(m.P_TransferToLaying_GetAvailableQty), ctrl.GetAvailableQtyPerKandang)
} }
@@ -25,6 +25,8 @@ type PurchaseRepository interface {
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error)
BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error
GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error)
GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error)
} }
type PurchaseRepositoryImpl struct { type PurchaseRepositoryImpl struct {
@@ -289,6 +291,38 @@ func (r *PurchaseRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB,
return count > 0, nil return count > 0, nil
} }
func (r *PurchaseRepositoryImpl) GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) {
return r.GetItemsByWarehouseKandang(ctx, projectFlockID)
}
func (r *PurchaseRepositoryImpl) GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) {
var items []entity.PurchaseItem
var kandangIDs []uint
err := r.DB().WithContext(ctx).
Table("project_flock_kandangs").
Where("project_flock_id = ?", projectFlockID).
Pluck("kandang_id", &kandangIDs).Error
if err != nil {
return nil, err
}
if len(kandangIDs) == 0 {
return []entity.PurchaseItem{}, nil
}
err = r.DB().WithContext(ctx).
Preload("Product").
Preload("Product.Flags").
Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id").
Where("warehouses.kandang_id IN ?", kandangIDs).
Find(&items).Error
return items, err
}
func parseNumericSuffix(value, prefix string) (int, bool) { func parseNumericSuffix(value, prefix string) (int, bool) {
if !strings.HasPrefix(value, prefix) { if !strings.HasPrefix(value, prefix) {
return 0, false return 0, false
@@ -11,6 +11,17 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
// === Marketing Report Response ===
type MarketingReportResponse struct {
Code int `json:"code"`
Status string `json:"status"`
Message string `json:"message"`
Meta response.Meta `json:"meta"`
Data []dto.RepportMarketingItemDTO `json:"data"`
Total *dto.Summary `json:"total,omitempty"`
}
type RepportController struct { type RepportController struct {
RepportService service.RepportService RepportService service.RepportService
} }
@@ -62,16 +73,18 @@ func (c *RepportController) GetExpense(ctx *fiber.Ctx) error {
func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
query := &validation.MarketingQuery{ query := &validation.MarketingQuery{
Page: ctx.QueryInt("page", 1), Page: ctx.QueryInt("page", 1),
Limit: ctx.QueryInt("limit", 10), Limit: ctx.QueryInt("limit", 10),
Search: ctx.Query("search", ""), Search: ctx.Query("search", ""),
CustomerId: int64(ctx.QueryInt("customer_id", 0)), CustomerId: int64(ctx.QueryInt("customer_id", 0)),
ProjectFlockKandangId: int64(ctx.QueryInt("project_flock_kandang_id", 0)), ProductId: int64(ctx.QueryInt("product_id", 0)),
DeliveryDate: ctx.Query("delivery_date", ""), WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)),
ProductId: int64(ctx.QueryInt("product_id", 0)), SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)),
WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)), FilterBy: ctx.Query("filter_by", ""),
SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)), StartDate: ctx.Query("start_date", ""),
MarketingId: int64(ctx.QueryInt("marketing_id", 0)), EndDate: ctx.Query("end_date", ""),
SortBy: ctx.Query("sort_by", ""),
SortOrder: ctx.Query("sort_order", ""),
} }
if query.Page < 1 || query.Limit < 1 { if query.Page < 1 || query.Limit < 1 {
@@ -83,8 +96,11 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
return err return err
} }
total := dto.ToSummaryFromDTOItems(result)
return ctx.Status(fiber.StatusOK). return ctx.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.RepportMarketingListDTO]{ JSON(MarketingReportResponse{
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Get marketing report successfully", Message: "Get marketing report successfully",
@@ -94,7 +110,8 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults, TotalResults: totalResults,
}, },
Data: result, Data: result,
Total: total,
}) })
} }
@@ -4,216 +4,258 @@ import (
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
marketingDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto" marketingDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
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"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
) )
// === DTO Structs === type RepportMarketingItemDTO struct {
ID int `json:"id"`
type RepportMarketingBaseDTO struct { SoDate time.Time `json:"so_date"`
Id uint `json:"id"` RealizationDate time.Time `json:"realization_date"`
SoNumber string `json:"so_number"` AgingDays int `json:"aging_days"`
SoDate time.Time `json:"so_date"` Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"` Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
SalesPerson *userDTO.UserRelationDTO `json:"sales_person,omitempty"` DoNumber string `json:"do_number"`
Notes string `json:"notes"` Sales *userDTO.UserRelationDTO `json:"sales,omitempty"`
CreatedAt time.Time `json:"created_at"` VehicleNumber string `json:"vehicle_number"`
UpdatedAt time.Time `json:"updated_at"` Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
MarketingType string `json:"marketing_type"`
Qty float64 `json:"qty"`
AverageWeightKg float64 `json:"average_weight_kg"`
TotalWeightKg float64 `json:"total_weight_kg"`
SalesPricePerKg float64 `json:"sales_price_per_kg"`
HppPricePerKg float64 `json:"hpp_price_per_kg"`
SalesAmount float64 `json:"sales_amount"`
HppAmount float64 `json:"hpp_amount"`
} }
type RepportMarketingProductDTO struct { type Summary struct {
Id uint `json:"id"` TotalQty int `json:"total_qty"`
MarketingProductId uint `json:"marketing_product_id"` TotalWeightKg float64 `json:"total_weight_kg"`
Qty float64 `json:"qty"` TotalSalesAmount int64 `json:"total_sales_amount"`
UnitPrice float64 `json:"unit_price"` TotalHppAmount int64 `json:"total_hpp_amount"`
AvgWeight float64 `json:"avg_weight"` TotalHppPricePerKg float64 `json:"total_hpp_price_per_kg"`
TotalWeight float64 `json:"total_weight"`
TotalPrice float64 `json:"total_price"`
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
CreatedAt time.Time `json:"created_at"`
} }
type RepportMarketingDeliveryDTO struct { type RepportMarketingResponseDTO struct {
Id uint `json:"id"` Items []RepportMarketingItemDTO `json:"items"`
MarketingProductId uint `json:"marketing_product_id"` Total *Summary `json:"total,omitempty"`
Qty float64 `json:"qty"`
UnitPrice float64 `json:"unit_price"`
TotalWeight float64 `json:"total_weight"`
AvgWeight float64 `json:"avg_weight"`
TotalPrice float64 `json:"total_price"`
DeliveryDate *time.Time `json:"delivery_date,omitempty"`
VehicleNumber string `json:"vehicle_number"`
DoNumber string `json:"do_number"`
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
CreatedAt time.Time `json:"created_at"`
} }
type RepportMarketingListDTO struct { func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) RepportMarketingItemDTO {
RepportMarketingBaseDTO soDate := time.Time{}
MarketingProduct RepportMarketingProductDTO `json:"marketing_product"` agingDays := 0
MarketingDelivery RepportMarketingDeliveryDTO `json:"marketing_delivery"` if mdp.MarketingProduct.Marketing.SoDate.Year() > 1 {
TotalMarketingProduct float64 `json:"total_marketing_product"` soDate = mdp.MarketingProduct.Marketing.SoDate
TotalMarketingDelivery float64 `json:"total_marketing_delivery"` agingDays = int(time.Since(soDate).Hours() / 24)
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"`
}
// === MAPPERS ===
func ToRepportMarketingBaseDTO(m *entity.Marketing) RepportMarketingBaseDTO {
if m == nil {
return RepportMarketingBaseDTO{}
} }
var customer *customerDTO.CustomerRelationDTO realizationDate := time.Time{}
if m.Customer.Id != 0 { if mdp.DeliveryDate != nil {
mapped := customerDTO.ToCustomerRelationDTO(m.Customer) realizationDate = *mdp.DeliveryDate
customer = &mapped
} }
var salesPerson *userDTO.UserRelationDTO doNumber := marketingDTO.GenerateDeliveryOrderNumber(mdp.MarketingProduct.Marketing.SoNumber, mdp.DeliveryDate, mdp.MarketingProduct.ProductWarehouse.WarehouseId)
if m.SalesPerson.Id != 0 {
mapped := userDTO.ToUserRelationDTO(m.SalesPerson) totalWeightKg := mdp.Qty * mdp.AvgWeight
salesPerson = &mapped salesAmount := totalWeightKg * mdp.UnitPrice
var hpp float64
var hppAmount float64
if isProductEligibleForHpp(mdp, category) {
hpp = hppPricePerKg
hppAmount = totalWeightKg * hppPricePerKg
} }
return RepportMarketingBaseDTO{ item := RepportMarketingItemDTO{
Id: m.Id, ID: int(mdp.Id),
SoNumber: m.SoNumber, SoDate: soDate,
SoDate: m.SoDate, RealizationDate: realizationDate,
Customer: customer, AgingDays: agingDays,
SalesPerson: salesPerson, DoNumber: doNumber,
Notes: m.Notes, MarketingType: getMarketingType(mdp),
CreatedAt: m.CreatedAt, Qty: mdp.Qty,
UpdatedAt: m.UpdatedAt, AverageWeightKg: mdp.AvgWeight,
} TotalWeightKg: totalWeightKg,
} SalesPricePerKg: mdp.UnitPrice,
HppPricePerKg: hpp,
func ToRepportMarketingProductDTO(mp *entity.MarketingProduct) RepportMarketingProductDTO { SalesAmount: salesAmount,
if mp == nil { HppAmount: hppAmount,
return RepportMarketingProductDTO{}
} }
var product *productDTO.ProductRelationDTO if mdp.MarketingProduct.ProductWarehouse.WarehouseId != 0 {
if mp.ProductWarehouse.Product.Id != 0 { mapped := warehouseDTO.ToWarehouseRelationDTO(mdp.MarketingProduct.ProductWarehouse.Warehouse)
mapped := productDTO.ToProductRelationDTO(mp.ProductWarehouse.Product) item.Warehouse = &mapped
product = &mapped
} }
return RepportMarketingProductDTO{ if mdp.MarketingProduct.Marketing.CustomerId != 0 {
Id: mp.Id, mapped := customerDTO.ToCustomerRelationDTO(mdp.MarketingProduct.Marketing.Customer)
MarketingProductId: mp.Id, item.Customer = &mapped
Qty: mp.Qty,
UnitPrice: mp.UnitPrice,
AvgWeight: mp.AvgWeight,
TotalWeight: mp.TotalWeight,
TotalPrice: mp.TotalPrice,
Product: product,
CreatedAt: time.Now(),
}
}
func ToRepportMarketingDeliveryDTO(mdp *entity.MarketingDeliveryProduct, soNumber string) RepportMarketingDeliveryDTO {
if mdp == nil {
return RepportMarketingDeliveryDTO{}
} }
var product *productDTO.ProductRelationDTO if mdp.MarketingProduct.Marketing.SalesPersonId != 0 {
if mdp.MarketingProduct.ProductWarehouse.Product.Id != 0 { mapped := userDTO.ToUserRelationDTO(mdp.MarketingProduct.Marketing.SalesPerson)
item.Sales = &mapped
}
item.VehicleNumber = mdp.VehicleNumber
if mdp.MarketingProduct.ProductWarehouse.ProductId != 0 {
mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product) mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product)
product = &mapped item.Product = &mapped
} }
warehouseId := uint(0) return item
if mdp.MarketingProduct.ProductWarehouse.Id != 0 { }
warehouseId = mdp.MarketingProduct.ProductWarehouse.WarehouseId
func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) []RepportMarketingItemDTO {
items := make([]RepportMarketingItemDTO, 0, len(mdps))
for _, mdp := range mdps {
items = append(items, ToRepportMarketingItemDTO(mdp, hppPricePerKg, category))
}
return items
}
func ToRepportMarketingItemDTOsWithHppMap(mdps []entity.MarketingDeliveryProduct, hppMap map[uint]float64) []RepportMarketingItemDTO {
items := make([]RepportMarketingItemDTO, 0, len(mdps))
for _, mdp := range mdps {
hppPerKg := float64(0)
category := ""
if projectFlockKandang := mdp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil {
if hpp, exists := hppMap[projectFlockKandang.ProjectFlockId]; exists {
hppPerKg = hpp
}
category = projectFlockKandang.ProjectFlock.Category
}
item := ToRepportMarketingItemDTO(mdp, hppPerKg, category)
items = append(items, item)
}
return items
}
func getMarketingType(mdp entity.MarketingDeliveryProduct) string {
hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
if hasAyam {
return "ayam"
}
if hasTelur {
return "telur"
}
return "trading"
}
func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur bool) {
if len(flags) == 0 {
return false, false
} }
doNumber := marketingDTO.GenerateDeliveryOrderNumber(soNumber, mdp.DeliveryDate, warehouseId) for _, flag := range flags {
ft := utils.FlagType(flag.Name)
return RepportMarketingDeliveryDTO{ if ft == utils.FlagAyamAfkir || ft == utils.FlagAyamCulling || ft == utils.FlagAyamMati ||
Id: mdp.Id, ft == utils.FlagDOC || ft == utils.FlagPullet || ft == utils.FlagLayer {
MarketingProductId: mdp.MarketingProductId, hasAyam = true
Qty: mdp.Qty, }
UnitPrice: mdp.UnitPrice,
TotalWeight: mdp.TotalWeight, if ft == utils.FlagTelur || ft == utils.FlagTelurUtuh || ft == utils.FlagTelurPecah ||
AvgWeight: mdp.AvgWeight, ft == utils.FlagTelurPutih || ft == utils.FlagTelurRetak {
TotalPrice: mdp.TotalPrice, hasTelur = true
DeliveryDate: mdp.DeliveryDate, }
VehicleNumber: mdp.VehicleNumber, }
DoNumber: doNumber,
Product: product, return hasAyam, hasTelur
CreatedAt: time.Now(), }
func isProductEligibleForHpp(mdp entity.MarketingDeliveryProduct, category string) bool {
hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing {
return hasAyam
}
return hasAyam || hasTelur
}
func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) *Summary {
if len(mdps) == 0 {
return nil
}
totalQty := 0
totalWeightKg := 0.0
totalEligibleWeightKg := 0.0
totalSalesAmount := int64(0)
totalHppAmount := int64(0)
for _, mdp := range mdps {
calculatedTotalWeight := mdp.Qty * mdp.AvgWeight
totalQty += int(mdp.Qty)
totalWeightKg += calculatedTotalWeight
totalSalesAmount += int64(calculatedTotalWeight * mdp.UnitPrice)
if isProductEligibleForHpp(mdp, category) {
totalEligibleWeightKg += calculatedTotalWeight
totalHppAmount += int64(calculatedTotalWeight * hppPricePerKg)
}
}
totalHppPricePerKg := float64(0)
if totalEligibleWeightKg > 0 {
totalHppPricePerKg = float64(totalHppAmount) / totalEligibleWeightKg
}
return &Summary{
TotalQty: totalQty,
TotalWeightKg: totalWeightKg,
TotalSalesAmount: totalSalesAmount,
TotalHppAmount: totalHppAmount,
TotalHppPricePerKg: totalHppPricePerKg,
} }
} }
func ToRepportMarketingListDTO(baseDTO RepportMarketingBaseDTO, mp *entity.MarketingProduct, mdp *entity.MarketingDeliveryProduct, latestApproval *approvalDTO.ApprovalRelationDTO) RepportMarketingListDTO { func ToSummaryFromDTOItems(items []RepportMarketingItemDTO) *Summary {
var marketingProduct RepportMarketingProductDTO if len(items) == 0 {
var marketingDelivery RepportMarketingDeliveryDTO return nil
if mp != nil {
marketingProduct = ToRepportMarketingProductDTO(mp)
} }
if mdp != nil { totalQty := 0
marketingDelivery = ToRepportMarketingDeliveryDTO(mdp, baseDTO.SoNumber) totalWeightKg := 0.0
totalSalesAmount := int64(0)
totalHppAmount := int64(0)
for _, item := range items {
totalQty += int(item.Qty)
totalWeightKg += item.TotalWeightKg
totalSalesAmount += int64(item.SalesAmount)
totalHppAmount += int64(item.HppAmount)
} }
totalMarketingProduct := float64(0) totalHppPricePerKg := float64(0)
totalMarketingDelivery := float64(0) if totalWeightKg > 0 {
totalHppPricePerKg = float64(totalHppAmount) / totalWeightKg
if mp != nil {
totalMarketingProduct = mp.Qty * mp.UnitPrice
} }
if mdp != nil { return &Summary{
totalMarketingDelivery = mdp.Qty * mdp.UnitPrice TotalQty: totalQty,
} TotalWeightKg: totalWeightKg,
TotalSalesAmount: totalSalesAmount,
return RepportMarketingListDTO{ TotalHppAmount: totalHppAmount,
RepportMarketingBaseDTO: baseDTO, TotalHppPricePerKg: totalHppPricePerKg,
MarketingProduct: marketingProduct,
MarketingDelivery: marketingDelivery,
TotalMarketingProduct: totalMarketingProduct,
TotalMarketingDelivery: totalMarketingDelivery,
LatestApproval: latestApproval,
} }
} }
func ToRepportMarketingListDTOs(deliveryProducts []entity.MarketingDeliveryProduct) []RepportMarketingListDTO { func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) RepportMarketingResponseDTO {
result := make([]RepportMarketingListDTO, 0, len(deliveryProducts)) items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg, category)
total := ToSummary(mdps, hppPricePerKg, category)
marketingMap := make(map[uint]entity.MarketingDeliveryProduct) return RepportMarketingResponseDTO{
for _, dp := range deliveryProducts { Items: items,
if dp.MarketingProduct.Marketing.Id == 0 { Total: total,
continue
}
marketingID := dp.MarketingProduct.Marketing.Id
if _, exists := marketingMap[marketingID]; !exists {
marketingMap[marketingID] = dp
}
} }
for _, deliveryProduct := range marketingMap {
if deliveryProduct.MarketingProduct.Marketing.Id == 0 {
continue
}
marketing := &deliveryProduct.MarketingProduct.Marketing
baseDTO := ToRepportMarketingBaseDTO(marketing)
var latestApproval *approvalDTO.ApprovalRelationDTO
if marketing.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval)
latestApproval = &mapped
}
mdp := &deliveryProduct
dto := ToRepportMarketingListDTO(baseDTO, &deliveryProduct.MarketingProduct, mdp, latestApproval)
result = append(result, dto)
}
return result
} }
+7 -1
View File
@@ -12,6 +12,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"
chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -23,12 +26,15 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
expenseRealizationRepository := expenseRepo.NewExpenseRealizationRepository(db) expenseRealizationRepository := expenseRepo.NewExpenseRealizationRepository(db)
marketingDeliveryProductRepository := marketingRepo.NewMarketingDeliveryProductRepository(db) marketingDeliveryProductRepository := marketingRepo.NewMarketingDeliveryProductRepository(db)
purchaseRepository := purchaseRepo.NewPurchaseRepository(db)
chickinRepository := chickinRepo.NewChickinRepository(db)
recordingRepository := recordingRepo.NewRecordingRepository(db)
approvalRepository := commonRepo.NewApprovalRepository(db) approvalRepository := commonRepo.NewApprovalRepository(db)
purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db) purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db)
userRepository := rUser.NewUserRepository(db) userRepository := rUser.NewUserRepository(db)
approvalSvc := approvalService.NewApprovalService(approvalRepository) approvalSvc := approvalService.NewApprovalService(approvalRepository)
repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, approvalSvc, purchaseSupplierRepository) repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository)
userService := sUser.NewUserService(userRepository, validate) userService := sUser.NewUserService(userRepository, validate)
RepportRoutes(router, userService, repportService) RepportRoutes(router, userService, repportService)
@@ -1,6 +1,8 @@
package service package service
import ( import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories" repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
@@ -10,6 +12,9 @@ import (
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -21,7 +26,7 @@ import (
type RepportService interface { type RepportService interface {
GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error)
GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error) GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error)
GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error)
} }
@@ -30,6 +35,9 @@ type repportService struct {
Validate *validator.Validate Validate *validator.Validate
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository
PurchaseRepo purchaseRepo.PurchaseRepository
ChickinRepo chickinRepo.ProjectChickinRepository
RecordingRepo recordingRepo.RecordingRepository
ApprovalSvc approvalService.ApprovalService ApprovalSvc approvalService.ApprovalService
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
} }
@@ -38,6 +46,9 @@ func NewRepportService(
validate *validator.Validate, validate *validator.Validate,
expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository,
marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository,
purchaseRepo purchaseRepo.PurchaseRepository,
chickinRepo chickinRepo.ProjectChickinRepository,
recordingRepo recordingRepo.RecordingRepository,
approvalSvc approvalService.ApprovalService, approvalSvc approvalService.ApprovalService,
purchaseSupplierRepo repportRepo.PurchaseSupplierRepository, purchaseSupplierRepo repportRepo.PurchaseSupplierRepository,
) RepportService { ) RepportService {
@@ -46,6 +57,9 @@ func NewRepportService(
Validate: validate, Validate: validate,
ExpenseRealizationRepo: expenseRealizationRepo, ExpenseRealizationRepo: expenseRealizationRepo,
MarketingDeliveryRepo: marketingDeliveryRepo, MarketingDeliveryRepo: marketingDeliveryRepo,
PurchaseRepo: purchaseRepo,
ChickinRepo: chickinRepo,
RecordingRepo: recordingRepo,
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
PurchaseSupplierRepo: purchaseSupplierRepo, PurchaseSupplierRepo: purchaseSupplierRepo,
} }
@@ -89,7 +103,7 @@ func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuer
return result, total, nil return result, total, nil
} }
func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error) { func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, 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
} }
@@ -101,29 +115,100 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing
return nil, 0, err return nil, 0, err
} }
marketingIDMap := make(map[uint]bool) projectFlockIDMap := make(map[uint]bool)
marketingIDs := make([]uint, 0) hppMap := make(map[uint]float64)
for _, dp := range deliveryProducts { for _, dp := range deliveryProducts {
if marketingID := dp.MarketingProduct.Marketing.Id; marketingID > 0 && !marketingIDMap[marketingID] { if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil {
marketingIDs = append(marketingIDs, marketingID) projectFlockID := projectFlockKandang.ProjectFlockId
marketingIDMap[marketingID] = true if projectFlockID > 0 && !projectFlockIDMap[projectFlockID] {
projectFlockIDMap[projectFlockID] = true
category := projectFlockKandang.ProjectFlock.Category
hppPerKg := s.calculateHppPricePerKg(c.Context(), projectFlockID, category)
hppMap[projectFlockID] = hppPerKg
}
} }
} }
approvals, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowMarketing, marketingIDs, func(db *gorm.DB) *gorm.DB { items := dto.ToRepportMarketingItemDTOsWithHppMap(deliveryProducts, hppMap)
return db.Preload("ActionUser") return items, total, nil
}) }
func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, category string) float64 {
totalCost := s.getTotalProjectCost(ctx, projectFlockID)
if totalCost == 0 {
s.Log.Warnf("HPP calculation: No cost found for project flock ID %d. Check if purchase items are linked to project_flock_kandang_id", projectFlockID)
return 0
}
chickinQty, err := s.ChickinRepo.GetTotalChickinQtyByProjectFlockID(ctx, projectFlockID)
if err != nil { if err != nil {
s.Log.Warnf("LatestByTargets error: %v", err) s.Log.Warnf("HPP calculation: Failed to get chickin qty for project flock ID %d: %v", projectFlockID, err)
} }
for i := range deliveryProducts { depletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(ctx, projectFlockID)
if approval, exists := approvals[deliveryProducts[i].MarketingProduct.Marketing.Id]; exists && approval != nil { if err != nil {
deliveryProducts[i].MarketingProduct.Marketing.LatestApproval = approval s.Log.Warnf("HPP calculation: Failed to get depletion for project flock ID %d: %v", projectFlockID, err)
}
avgWeight, err := s.RecordingRepo.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID)
if err != nil {
s.Log.Warnf("HPP calculation: Failed to get avg weight for project flock ID %d: %v", projectFlockID, err)
}
var totalWeight float64
if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing {
totalWeight = (chickinQty - depletion) * avgWeight
} else {
eggWeight, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(ctx, projectFlockID)
if err != nil {
s.Log.Warnf("HPP calculation: Failed to get egg weight for project flock ID %d: %v", projectFlockID, err)
}
totalWeight = (chickinQty-depletion)*avgWeight + eggWeight
}
if totalWeight == 0 {
return 0
}
hppPricePerKg := totalCost / totalWeight
return hppPricePerKg
}
func (s *repportService) getTotalProjectCost(ctx context.Context, projectFlockID uint) float64 {
if projectFlockID == 0 {
return 0
}
purchases, err := s.PurchaseRepo.GetItemsByProjectFlockID(ctx, projectFlockID)
if err != nil {
s.Log.Errorf("getTotalProjectCost: GetItemsByProjectFlockID error for project flock ID %d: %v", projectFlockID, err)
return 0
}
cost := float64(0)
purchaseCost := float64(0)
for _, p := range purchases {
purchaseCost += p.TotalPrice
}
cost += purchaseCost
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID)
if err != nil {
s.Log.Warnf("getTotalProjectCost: GetByProjectFlockID error for project flock ID %d: %v", projectFlockID, err)
}
bopCost := float64(0)
for _, r := range realizations {
if r.ExpenseNonstock != nil && r.ExpenseNonstock.Expense != nil &&
r.ExpenseNonstock.Expense.Category == string(utils.ExpenseCategoryBOP) {
bopCost += r.Price * r.Qty
} }
} }
cost += bopCost
return dto.ToRepportMarketingListDTOs(deliveryProducts), total, nil return cost
} }
func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) { func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) {
@@ -16,16 +16,18 @@ 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,max=100,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"`
ProjectFlockKandangId int64 `query:"project_flock_kandang_id" validate:"omitempty"` ProductId int64 `query:"product_id" validate:"omitempty"`
DeliveryDate string `query:"delivery_date" validate:"omitempty"` WarehouseId int64 `query:"warehouse_id" validate:"omitempty"`
ProductId int64 `query:"product_id" validate:"omitempty"` SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"`
WarehouseId int64 `query:"warehouse_id" validate:"omitempty"` FilterBy string `query:"filter_by" validate:"omitempty,oneof=so_date realization_date"`
SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
MarketingId int64 `query:"marketing_id" validate:"omitempty"` EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
SortBy string `query:"sort_by" validate:"omitempty,oneof=so_date realization_date customer warehouse product sales_person vehicle_number sales_amount hpp_amount qty average_weight total_weight sales_price hpp_price aging_days"`
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
} }
type PurchaseSupplierQuery struct { type PurchaseSupplierQuery struct {
+19
View File
@@ -135,6 +135,17 @@ const (
SupplierCategorySapronak SupplierCategory = "SAPRONAK" SupplierCategorySapronak SupplierCategory = "SAPRONAK"
) )
// -------------------------------------------------------------------
// ExpenseCategory
// -------------------------------------------------------------------
type ExpenseCategory string
const (
ExpenseCategoryBOP ExpenseCategory = "BOP"
ExpenseCategoryNonBOP ExpenseCategory = "NON-BOP"
)
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Kandang Status // Kandang Status
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -429,6 +440,14 @@ func IsValidSupplierCategory(v string) bool {
return false return false
} }
func IsValidExpenseCategory(v string) bool {
switch ExpenseCategory(v) {
case ExpenseCategoryBOP, ExpenseCategoryNonBOP:
return true
}
return false
}
// example use // example use
// Recording helper // Recording helper