mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 21:41:55 +00:00
FEAT[BE[: enhance marketing report items with aging days calculation
This commit is contained in:
@@ -53,11 +53,10 @@ type ProductRelationDTOFixed struct {
|
|||||||
SellingPrice *float64 `json:"selling_price,omitempty"`
|
SellingPrice *float64 `json:"selling_price,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToMarketingReportItems(mdps []entity.MarketingDeliveryProduct, hppMap map[uint]float64) []RepportMarketingItemDTO {
|
func ToMarketingReportItems(mdps []entity.MarketingDeliveryProduct, hppMap map[uint]float64, agingMap map[int]int) []RepportMarketingItemDTO {
|
||||||
items := make([]RepportMarketingItemDTO, 0, len(mdps))
|
items := make([]RepportMarketingItemDTO, 0, len(mdps))
|
||||||
|
|
||||||
for _, mdp := range mdps {
|
for _, mdp := range mdps {
|
||||||
// Get HPP and category from map
|
|
||||||
hppPerKg := float64(0)
|
hppPerKg := float64(0)
|
||||||
category := ""
|
category := ""
|
||||||
if projectFlockKandang := mdp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil {
|
if projectFlockKandang := mdp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil {
|
||||||
@@ -67,13 +66,16 @@ func ToMarketingReportItems(mdps []entity.MarketingDeliveryProduct, hppMap map[u
|
|||||||
category = projectFlockKandang.ProjectFlock.Category
|
category = projectFlockKandang.ProjectFlock.Category
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate dates
|
|
||||||
soDate := time.Time{}
|
soDate := time.Time{}
|
||||||
agingDays := 0
|
agingDays := 0
|
||||||
if mdp.MarketingProduct.Marketing.SoDate.Year() > 1 {
|
if mdp.MarketingProduct.Marketing.SoDate.Year() > 1 {
|
||||||
soDate = mdp.MarketingProduct.Marketing.SoDate
|
soDate = mdp.MarketingProduct.Marketing.SoDate
|
||||||
|
if ag, exists := agingMap[int(mdp.Id)]; exists {
|
||||||
|
agingDays = ag
|
||||||
|
} else {
|
||||||
agingDays = int(time.Since(soDate).Hours() / 24)
|
agingDays = int(time.Since(soDate).Hours() / 24)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
realizationDate := time.Time{}
|
realizationDate := time.Time{}
|
||||||
if mdp.DeliveryDate != nil {
|
if mdp.DeliveryDate != nil {
|
||||||
@@ -106,7 +108,6 @@ func ToMarketingReportItems(mdps []entity.MarketingDeliveryProduct, hppMap map[u
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine marketing type
|
|
||||||
marketingType := "trading"
|
marketingType := "trading"
|
||||||
if hasTrading {
|
if hasTrading {
|
||||||
marketingType = "trading"
|
marketingType = "trading"
|
||||||
@@ -196,13 +197,9 @@ func ToSummaryFromDTOItems(items []RepportMarketingItemDTO) *Summary {
|
|||||||
totalHppPricePerKg = float64(totalHppAmount) / totalWeightKg
|
totalHppPricePerKg = float64(totalHppAmount) / totalWeightKg
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(items) > 0 {
|
|
||||||
avgSalesAmount = float64(totalSalesAmount) / float64(len(items))
|
|
||||||
}
|
|
||||||
|
|
||||||
if totalQty > 0 {
|
if totalQty > 0 {
|
||||||
avgWeightKg = totalWeightKg / float64(totalQty)
|
avgWeightKg = totalWeightKg / float64(totalQty)
|
||||||
avgSalesAmount = float64(totalSalesAmount) / float64(totalQty) // ← TAMBAHAN INI
|
avgSalesAmount = float64(totalSalesAmount) / float64(totalQty)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Summary{
|
return &Summary{
|
||||||
|
|||||||
@@ -165,6 +165,47 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customerGroups := make(map[uint][]entity.MarketingDeliveryProduct)
|
||||||
|
for _, dp := range deliveryProducts {
|
||||||
|
customerID := dp.MarketingProduct.Marketing.CustomerId
|
||||||
|
customerGroups[customerID] = append(customerGroups[customerID], dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
agingMap := make(map[int]int)
|
||||||
|
for customerID := range customerGroups {
|
||||||
|
transactions, err := s.CustomerPaymentRepo.GetCustomerPaymentTransactions(c.Context(), &customerID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Warnf("Failed to get transactions for customer %d: %v", customerID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
initialBalance, err := s.CustomerPaymentRepo.GetInitialBalanceByCustomer(c.Context(), customerID)
|
||||||
|
if err != nil {
|
||||||
|
initialBalance = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
runningBalance := initialBalance
|
||||||
|
for i, tx := range transactions {
|
||||||
|
if tx.TransactionType == "SALES" {
|
||||||
|
previousBalance := runningBalance
|
||||||
|
runningBalance -= tx.TotalPrice
|
||||||
|
currentBalance := runningBalance
|
||||||
|
|
||||||
|
_, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, currentBalance)
|
||||||
|
|
||||||
|
if paymentDate != nil {
|
||||||
|
agingDays := int(paymentDate.Sub(tx.TransDate).Hours() / 24)
|
||||||
|
agingMap[int(tx.TransactionID)] = agingDays
|
||||||
|
} else {
|
||||||
|
agingDays := int(time.Since(tx.TransDate).Hours() / 24)
|
||||||
|
agingMap[int(tx.TransactionID)] = agingDays
|
||||||
|
}
|
||||||
|
} else if tx.TransactionType == "PAYMENT" {
|
||||||
|
runningBalance += tx.PaymentAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
projectFlockIDMap := make(map[uint]bool)
|
projectFlockIDMap := make(map[uint]bool)
|
||||||
hppMap := make(map[uint]float64)
|
hppMap := make(map[uint]float64)
|
||||||
|
|
||||||
@@ -181,7 +222,7 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items := dto.ToMarketingReportItems(deliveryProducts, hppMap)
|
items := dto.ToMarketingReportItems(deliveryProducts, hppMap, agingMap)
|
||||||
return items, total, nil
|
return items, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,12 +463,10 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine customer IDs to process
|
|
||||||
var customerIDs []uint
|
var customerIDs []uint
|
||||||
var totalCustomers int64
|
var totalCustomers int64
|
||||||
|
|
||||||
if len(params.CustomerIDs) > 0 {
|
if len(params.CustomerIDs) > 0 {
|
||||||
// Specific customer IDs mode (no pagination)
|
|
||||||
customerIDs = params.CustomerIDs
|
customerIDs = params.CustomerIDs
|
||||||
totalCustomers = int64(len(customerIDs))
|
totalCustomers = int64(len(customerIDs))
|
||||||
|
|
||||||
@@ -435,7 +474,6 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C
|
|||||||
return []dto.CustomerPaymentReportItem{}, 0, nil
|
return []dto.CustomerPaymentReportItem{}, 0, nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Multiple customers mode with pagination
|
|
||||||
page := params.Page
|
page := params.Page
|
||||||
limit := params.Limit
|
limit := params.Limit
|
||||||
if page < 1 {
|
if page < 1 {
|
||||||
@@ -574,15 +612,7 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
|
|||||||
func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) {
|
func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) {
|
||||||
currentSales := transactions[currentIndex]
|
currentSales := transactions[currentIndex]
|
||||||
|
|
||||||
// Status Logic:
|
|
||||||
// 1. LUNAS: previousBalance >= salesAmount (paid from deposit)
|
|
||||||
// 2. LUNAS: future payments make AR >= 0 (eventually paid)
|
|
||||||
// 3. DIBAYAR SEBAGIAN: has payment but not enough
|
|
||||||
// 4. BELUM LUNAS: no payment at all
|
|
||||||
|
|
||||||
if previousBalance >= currentSales.TotalPrice {
|
if previousBalance >= currentSales.TotalPrice {
|
||||||
// Cari payment yang digunakan untuk melunasi sales ini dengan FIFO
|
|
||||||
// Track payment allocations that are consumed by previous sales
|
|
||||||
type paymentAllocation struct {
|
type paymentAllocation struct {
|
||||||
date time.Time
|
date time.Time
|
||||||
amount float64
|
amount float64
|
||||||
@@ -591,7 +621,6 @@ func (s *repportService) determineSalesStatusAndPaymentDate(transactions []reppo
|
|||||||
allocations := []paymentAllocation{}
|
allocations := []paymentAllocation{}
|
||||||
runningBalance := 0.0
|
runningBalance := 0.0
|
||||||
|
|
||||||
// Process all transactions before current sales to build allocation map
|
|
||||||
for i := 0; i < currentIndex; i++ {
|
for i := 0; i < currentIndex; i++ {
|
||||||
if transactions[i].TransactionType == "PAYMENT" {
|
if transactions[i].TransactionType == "PAYMENT" {
|
||||||
allocations = append(allocations, paymentAllocation{
|
allocations = append(allocations, paymentAllocation{
|
||||||
@@ -604,7 +633,6 @@ func (s *repportService) determineSalesStatusAndPaymentDate(transactions []reppo
|
|||||||
salesAmount := transactions[i].TotalPrice
|
salesAmount := transactions[i].TotalPrice
|
||||||
remainingToConsume := salesAmount
|
remainingToConsume := salesAmount
|
||||||
|
|
||||||
// Consume from oldest allocations first (FIFO)
|
|
||||||
for j := range allocations {
|
for j := range allocations {
|
||||||
if remainingToConsume <= 0 {
|
if remainingToConsume <= 0 {
|
||||||
break
|
break
|
||||||
@@ -623,22 +651,18 @@ func (s *repportService) determineSalesStatusAndPaymentDate(transactions []reppo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now find which allocation covers the current sales
|
|
||||||
amountNeeded := currentSales.TotalPrice
|
amountNeeded := currentSales.TotalPrice
|
||||||
for _, alloc := range allocations {
|
for _, alloc := range allocations {
|
||||||
available := alloc.amount - alloc.consumed
|
available := alloc.amount - alloc.consumed
|
||||||
if available > 0 {
|
if available > 0 {
|
||||||
if amountNeeded <= available {
|
if amountNeeded <= available {
|
||||||
// This allocation fully covers the sales
|
|
||||||
return "LUNAS", &alloc.date
|
return "LUNAS", &alloc.date
|
||||||
} else {
|
} else {
|
||||||
// This allocation partially covers, continue to next
|
|
||||||
amountNeeded -= available
|
amountNeeded -= available
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get here, use the oldest allocation
|
|
||||||
if len(allocations) > 0 {
|
if len(allocations) > 0 {
|
||||||
return "LUNAS", &allocations[0].date
|
return "LUNAS", &allocations[0].date
|
||||||
}
|
}
|
||||||
@@ -690,7 +714,6 @@ func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionRe
|
|||||||
if record.Day != nil {
|
if record.Day != nil {
|
||||||
result.Woa = float64(*record.Day)
|
result.Woa = float64(*record.Day)
|
||||||
}
|
}
|
||||||
// avgWeight := calculateAverageBodyWeight(record.BodyWeights)
|
|
||||||
avgWeight := 1.0
|
avgWeight := 1.0
|
||||||
if avgWeight > 0 {
|
if avgWeight > 0 {
|
||||||
result.Bw = avgWeight
|
result.Bw = avgWeight
|
||||||
@@ -1570,12 +1593,9 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows))
|
dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows))
|
||||||
perRangeMap := make(map[weightRangeKey]*weightRangeAggregate)
|
perRangeMap := make(map[weightRangeKey]*weightRangeAggregate)
|
||||||
var totalBirds int64
|
var totalBirds int64
|
||||||
// var totalWeight float64
|
|
||||||
var totalEggPieces int64
|
var totalEggPieces int64
|
||||||
var totalEggKg float64
|
var totalEggKg float64
|
||||||
// var totalRemainingValueRp int64
|
|
||||||
var totalEggValueRp int64
|
var totalEggValueRp int64
|
||||||
// var totalHppSum float64
|
|
||||||
var totalHppCount int
|
var totalHppCount int
|
||||||
var totalDocPriceSum float64
|
var totalDocPriceSum float64
|
||||||
var totalDocPriceCount int
|
var totalDocPriceCount int
|
||||||
@@ -1589,14 +1609,6 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// birdsFloat := row.RemainingChickenBirds
|
|
||||||
// if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) {
|
|
||||||
// birdsFloat = 0
|
|
||||||
// }
|
|
||||||
// weightFloat := row.RemainingChickenWeight
|
|
||||||
// if math.IsNaN(weightFloat) || math.IsInf(weightFloat, 0) {
|
|
||||||
// weightFloat = 0
|
|
||||||
// }
|
|
||||||
eggPiecesFloatRemaining := row.EggProductionPiecesRemaining
|
eggPiecesFloatRemaining := row.EggProductionPiecesRemaining
|
||||||
if math.IsNaN(eggPiecesFloatRemaining) || math.IsInf(eggPiecesFloatRemaining, 0) {
|
if math.IsNaN(eggPiecesFloatRemaining) || math.IsInf(eggPiecesFloatRemaining, 0) {
|
||||||
eggPiecesFloatRemaining = 0
|
eggPiecesFloatRemaining = 0
|
||||||
@@ -1632,13 +1644,8 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
weightMax := weightMin + 0.09
|
weightMax := weightMin + 0.09
|
||||||
rangeKey := weightRangeKey{Min: weightMin, Max: weightMax}
|
rangeKey := weightRangeKey{Min: weightMin, Max: weightMax}
|
||||||
|
|
||||||
// rowBirds := int64(math.Round(birdsFloat))
|
|
||||||
costEntry := costMap[row.ProjectFlockKandangID]
|
costEntry := costMap[row.ProjectFlockKandangID]
|
||||||
totalCost := costEntry.FeedCost + costEntry.OvkCost + costEntry.DocCost + costEntry.BudgetCost + costEntry.ExpenseCost
|
totalCost := costEntry.FeedCost + costEntry.OvkCost + costEntry.DocCost + costEntry.BudgetCost + costEntry.ExpenseCost
|
||||||
// hppRp := 0.0
|
|
||||||
// if weightFloat > 0 {
|
|
||||||
// hppRp = totalCost / weightFloat
|
|
||||||
// }
|
|
||||||
eggHpp := 0.0
|
eggHpp := 0.0
|
||||||
if eggWeightFloat > 0 {
|
if eggWeightFloat > 0 {
|
||||||
eggHpp = (totalCost / eggWeightFloat) / 1000
|
eggHpp = (totalCost / eggWeightFloat) / 1000
|
||||||
@@ -1646,7 +1653,6 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
|
|
||||||
rowEggPieces := int64(math.Round(eggPiecesFloatRemaining))
|
rowEggPieces := int64(math.Round(eggPiecesFloatRemaining))
|
||||||
rowEggValue := int64(eggHpp * eggRemainingWeightFloatRemaining)
|
rowEggValue := int64(eggHpp * eggRemainingWeightFloatRemaining)
|
||||||
// rowRemainingValue := int64(hppRp * weightFloat)
|
|
||||||
avgDocPrice := int64(0)
|
avgDocPrice := int64(0)
|
||||||
if costEntry.DocQty > 0 {
|
if costEntry.DocQty > 0 {
|
||||||
avgDocPrice = int64(math.Round(costEntry.DocCost / costEntry.DocQty))
|
avgDocPrice = int64(math.Round(costEntry.DocCost / costEntry.DocQty))
|
||||||
@@ -1675,33 +1681,20 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
},
|
},
|
||||||
AvgWeightKg: avgWeight,
|
AvgWeightKg: avgWeight,
|
||||||
NameWithPeriode: nameWithPeriod,
|
NameWithPeriode: nameWithPeriod,
|
||||||
// FeedCostRp: costEntry.FeedCost,
|
|
||||||
// OvkCostRp: costEntry.OvkCost,
|
|
||||||
DocSuppliers: docSupplierMap[row.ProjectFlockKandangID],
|
DocSuppliers: docSupplierMap[row.ProjectFlockKandangID],
|
||||||
FeedSuppliers: feedSupplierMap[row.ProjectFlockKandangID],
|
FeedSuppliers: feedSupplierMap[row.ProjectFlockKandangID],
|
||||||
EggProductionPieces: int64(math.Round(eggPiecesFloatRemaining)),
|
EggProductionPieces: int64(math.Round(eggPiecesFloatRemaining)),
|
||||||
EggProductionKg: eggRemainingWeightFloatRemaining,
|
EggProductionKg: eggRemainingWeightFloatRemaining,
|
||||||
// EggProductionTotalWeightKg: eggWeightFloat,
|
|
||||||
// EggProductionTotalPieces: int64(math.Round(eggTotalPiecesFloat)),
|
|
||||||
AverageDocPriceRp: avgDocPrice,
|
AverageDocPriceRp: avgDocPrice,
|
||||||
// HppRp: hppRp,
|
|
||||||
EggHppRpPerKg: eggHpp,
|
EggHppRpPerKg: eggHpp,
|
||||||
// RemainingValueRp: rowRemainingValue,
|
|
||||||
EggValueRp: rowEggValue,
|
EggValueRp: rowEggValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
// totalBirds += rowBirds
|
|
||||||
// totalWeight += weightFloat
|
|
||||||
totalEggPieces += rowEggPieces
|
totalEggPieces += rowEggPieces
|
||||||
totalEggKg += eggRemainingWeightFloatRemaining
|
totalEggKg += eggRemainingWeightFloatRemaining
|
||||||
// totalRemainingValueRp += rowRemainingValue
|
|
||||||
totalEggValueRp += rowEggValue
|
totalEggValueRp += rowEggValue
|
||||||
totalAvgWeightSum += avgWeight
|
totalAvgWeightSum += avgWeight
|
||||||
totalAvgWeightCount++
|
totalAvgWeightCount++
|
||||||
// if weightFloat > 0 {
|
|
||||||
// totalHppSum += hppRp
|
|
||||||
// totalHppCount++
|
|
||||||
// }
|
|
||||||
if avgDocPrice > 0 {
|
if avgDocPrice > 0 {
|
||||||
totalDocPriceSum += float64(avgDocPrice)
|
totalDocPriceSum += float64(avgDocPrice)
|
||||||
totalDocPriceCount++
|
totalDocPriceCount++
|
||||||
@@ -1728,8 +1721,6 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
}
|
}
|
||||||
|
|
||||||
rangeSummary := rangeAgg.Summary
|
rangeSummary := rangeAgg.Summary
|
||||||
// rangeAgg.RemainingBirds += rowBirds
|
|
||||||
// rangeAgg.RemainingWeightKg += row.RemainingChickenWeight
|
|
||||||
rangeAgg.AvgWeightSum += avgWeight
|
rangeAgg.AvgWeightSum += avgWeight
|
||||||
rangeAgg.AvgWeightCount++
|
rangeAgg.AvgWeightCount++
|
||||||
for _, supplier := range feedSupplierMap[row.ProjectFlockKandangID] {
|
for _, supplier := range feedSupplierMap[row.ProjectFlockKandangID] {
|
||||||
@@ -1744,7 +1735,6 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
}
|
}
|
||||||
rangeSummary.EggProductionPieces += rowEggPieces
|
rangeSummary.EggProductionPieces += rowEggPieces
|
||||||
rangeSummary.EggProductionKg += eggRemainingWeightFloatRemaining
|
rangeSummary.EggProductionKg += eggRemainingWeightFloatRemaining
|
||||||
// rangeSummary.RemainingValueRp += rowRemainingValue
|
|
||||||
rangeSummary.EggValueRp += rowEggValue
|
rangeSummary.EggValueRp += rowEggValue
|
||||||
if eggWeightFloat > 0 {
|
if eggWeightFloat > 0 {
|
||||||
rangeAgg.EggHppSum += eggHpp
|
rangeAgg.EggHppSum += eggHpp
|
||||||
|
|||||||
Reference in New Issue
Block a user