mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-06-09 15:07:49 +00:00
Merge branch 'production' into feat/transfer-laying
This commit is contained in:
@@ -831,37 +831,28 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing
|
||||
customerGroups[customerID] = append(customerGroups[customerID], dp)
|
||||
}
|
||||
|
||||
// Aging untuk setiap MDP berdasarkan payment_allocations: LUNAS pakai last_payment_date,
|
||||
// else pakai today.
|
||||
agingMap := make(map[int]int)
|
||||
for customerID := range customerGroups {
|
||||
transactions, err := s.CustomerPaymentRepo.GetCustomerPaymentTransactions(c.Context(), &customerID)
|
||||
if err != nil {
|
||||
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
|
||||
allMdpIDsForAging := make([]uint, 0)
|
||||
for _, dp := range deliveryProducts {
|
||||
allMdpIDsForAging = append(allMdpIDsForAging, dp.Id)
|
||||
}
|
||||
mdpAllocSummaryForMarketing, err := s.fetchMdpAllocationSummary(c.Context(), allMdpIDsForAging)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
for _, dp := range deliveryProducts {
|
||||
summary := mdpAllocSummaryForMarketing[dp.Id]
|
||||
soDate := dp.MarketingProduct.Marketing.SoDate
|
||||
if customerPaymentStatusFromAllocation(dp.TotalPrice, summary.PaidAmount) == "LUNAS" && !summary.LastPaymentDate.IsZero() {
|
||||
days := int(summary.LastPaymentDate.Sub(soDate).Hours() / 24)
|
||||
if days < 0 {
|
||||
days = 0
|
||||
}
|
||||
agingMap[int(dp.Id)] = days
|
||||
} else {
|
||||
agingMap[int(dp.Id)] = int(time.Since(soDate).Hours() / 24)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1250,28 +1241,39 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
|
||||
return dto.CustomerPaymentReportItem{}, err
|
||||
}
|
||||
|
||||
// Batch fetch payment allocation summaries untuk semua SALES rows (per MDP).
|
||||
mdpIDs := make([]uint, 0)
|
||||
for _, tx := range transactions {
|
||||
if tx.TransactionType == "SALES" && tx.TransactionID > 0 {
|
||||
mdpIDs = append(mdpIDs, uint(tx.TransactionID))
|
||||
}
|
||||
}
|
||||
mdpAllocSummary, err := s.fetchMdpAllocationSummary(ctx, mdpIDs)
|
||||
if err != nil {
|
||||
return dto.CustomerPaymentReportItem{}, err
|
||||
}
|
||||
|
||||
rows := make([]dto.CustomerPaymentReportRow, 0, len(transactions))
|
||||
runningBalance := initialBalance
|
||||
|
||||
for i, tx := range transactions {
|
||||
|
||||
previousBalance := runningBalance
|
||||
for _, tx := range transactions {
|
||||
|
||||
row := dto.ToCustomerPaymentReportRow(tx)
|
||||
|
||||
if tx.TransactionType == "SALES" {
|
||||
runningBalance -= tx.TotalPrice
|
||||
status, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, runningBalance)
|
||||
row.Status = status
|
||||
summary := mdpAllocSummary[uint(tx.TransactionID)]
|
||||
row.Status = customerPaymentStatusFromAllocation(tx.TotalPrice, summary.PaidAmount)
|
||||
|
||||
if status == "LUNAS" {
|
||||
if paymentDate != nil {
|
||||
days := int(paymentDate.Sub(tx.TransDate).Hours() / 24)
|
||||
row.AgingDay = &days
|
||||
} else {
|
||||
days := 0
|
||||
row.AgingDay = &days
|
||||
if row.Status == "LUNAS" && !summary.LastPaymentDate.IsZero() {
|
||||
days := int(summary.LastPaymentDate.Sub(tx.TransDate).Hours() / 24)
|
||||
if days < 0 {
|
||||
days = 0
|
||||
}
|
||||
row.AgingDay = &days
|
||||
} else if row.Status == "LUNAS" {
|
||||
zero := 0
|
||||
row.AgingDay = &zero
|
||||
} else {
|
||||
days := int(time.Since(tx.TransDate).Hours() / 24)
|
||||
row.AgingDay = &days
|
||||
@@ -1343,91 +1345,19 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
|
||||
return dto.ToCustomerPaymentReportItem(*customer, initialBalance, rows, summary), nil
|
||||
}
|
||||
|
||||
func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) {
|
||||
currentSales := transactions[currentIndex]
|
||||
|
||||
if previousBalance >= currentSales.TotalPrice {
|
||||
type paymentAllocation struct {
|
||||
date time.Time
|
||||
amount float64
|
||||
consumed float64
|
||||
}
|
||||
allocations := []paymentAllocation{}
|
||||
runningBalance := 0.0
|
||||
|
||||
for i := 0; i < currentIndex; i++ {
|
||||
if transactions[i].TransactionType == "PAYMENT" {
|
||||
allocations = append(allocations, paymentAllocation{
|
||||
date: transactions[i].TransDate,
|
||||
amount: transactions[i].PaymentAmount,
|
||||
consumed: 0,
|
||||
})
|
||||
runningBalance += transactions[i].PaymentAmount
|
||||
} else if transactions[i].TransactionType == "SALES" {
|
||||
salesAmount := transactions[i].TotalPrice
|
||||
remainingToConsume := salesAmount
|
||||
|
||||
for j := range allocations {
|
||||
if remainingToConsume <= 0 {
|
||||
break
|
||||
}
|
||||
available := allocations[j].amount - allocations[j].consumed
|
||||
if available > 0 {
|
||||
consume := available
|
||||
if consume > remainingToConsume {
|
||||
consume = remainingToConsume
|
||||
}
|
||||
allocations[j].consumed += consume
|
||||
remainingToConsume -= consume
|
||||
}
|
||||
}
|
||||
runningBalance -= salesAmount
|
||||
}
|
||||
}
|
||||
|
||||
amountNeeded := currentSales.TotalPrice
|
||||
for _, alloc := range allocations {
|
||||
available := alloc.amount - alloc.consumed
|
||||
if available > 0 {
|
||||
if amountNeeded <= available {
|
||||
return "LUNAS", &alloc.date
|
||||
} else {
|
||||
amountNeeded -= available
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(allocations) > 0 {
|
||||
return "LUNAS", &allocations[0].date
|
||||
}
|
||||
return "LUNAS", nil
|
||||
// customerPaymentStatusFromAllocation menentukan status per-MDP berdasarkan
|
||||
// SUM(payment_allocations.amount) vs MDP total_price.
|
||||
func customerPaymentStatusFromAllocation(totalPrice, paidAmount float64) string {
|
||||
if totalPrice <= fifoAllocationEpsilon {
|
||||
return "LUNAS"
|
||||
}
|
||||
|
||||
hasPartialPaymentFromBalance := previousBalance > 0 && previousBalance < currentSales.TotalPrice
|
||||
|
||||
futureBalance := currentBalance
|
||||
hasPayment := false
|
||||
var paymentDateThatMadeItLunas *time.Time
|
||||
|
||||
for i := currentIndex + 1; i < len(transactions); i++ {
|
||||
if transactions[i].TransactionType == "PAYMENT" {
|
||||
futureBalance += transactions[i].PaymentAmount
|
||||
hasPayment = true
|
||||
|
||||
if futureBalance >= 0 {
|
||||
paymentDateThatMadeItLunas = &transactions[i].TransDate
|
||||
return "LUNAS", paymentDateThatMadeItLunas
|
||||
}
|
||||
} else if transactions[i].TransactionType == "SALES" {
|
||||
futureBalance -= transactions[i].TotalPrice
|
||||
}
|
||||
if paidAmount+fifoAllocationEpsilon >= totalPrice {
|
||||
return "LUNAS"
|
||||
}
|
||||
|
||||
if hasPayment || hasPartialPaymentFromBalance {
|
||||
return "DIBAYAR SEBAGIAN", nil
|
||||
if paidAmount > fifoAllocationEpsilon {
|
||||
return "DIBAYAR SEBAGIAN"
|
||||
}
|
||||
|
||||
return "BELUM LUNAS", nil
|
||||
return "BELUM LUNAS"
|
||||
}
|
||||
|
||||
func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionResultDTO {
|
||||
@@ -1951,15 +1881,34 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
DeltaBalance float64
|
||||
CountTotals bool
|
||||
}
|
||||
type debtSupplierAllocation struct {
|
||||
RowIndex int
|
||||
SortTime time.Time
|
||||
Amount float64
|
||||
CalcAging func(endDate time.Time) int
|
||||
|
||||
// Batch fetch payment allocation summaries (per purchase + per expense) untuk semua supplier.
|
||||
// FIFO matching dilakukan saat payment di-create/update; report tinggal baca dari DB.
|
||||
allPurchaseIDs := make([]uint, 0)
|
||||
allExpenseIDs := make([]uint64, 0)
|
||||
for _, sid := range supplierIDs {
|
||||
for _, p := range purchasesBySupplier[sid] {
|
||||
allPurchaseIDs = append(allPurchaseIDs, p.Id)
|
||||
}
|
||||
for _, e := range expensesBySupplier[sid] {
|
||||
allExpenseIDs = append(allExpenseIDs, e.Id)
|
||||
}
|
||||
}
|
||||
type paymentAllocation struct {
|
||||
Date time.Time
|
||||
Amount float64
|
||||
purchaseAllocSummary, err := s.fetchPurchaseAllocationSummary(c.Context(), allPurchaseIDs)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
expenseAllocSummary, err := s.fetchExpenseAllocationSummary(c.Context(), allExpenseIDs)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// rowRef tracks which combinedRows index belongs to which purchase/expense untuk update status di-akhir.
|
||||
type rowRef struct {
|
||||
Index int
|
||||
Kind string // "PURCHASE" / "EXPENSE"
|
||||
Purchase entity.Purchase
|
||||
Expense entity.Expense
|
||||
}
|
||||
|
||||
for _, supplierID := range supplierIDs {
|
||||
@@ -1974,7 +1923,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
total := dto.DebtSupplierTotalDTO{}
|
||||
|
||||
combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
|
||||
purchaseAllocations := make([]debtSupplierAllocation, 0, len(items))
|
||||
rowRefs := make([]rowRef, 0, len(items)+len(expensesBySupplier[supplierID]))
|
||||
for _, purchase := range items {
|
||||
row := buildDebtSupplierRow(purchase, now, location)
|
||||
sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location)
|
||||
@@ -1986,13 +1935,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
DeltaBalance: -row.TotalPrice,
|
||||
CountTotals: true,
|
||||
})
|
||||
capturedPurchase := purchase
|
||||
purchaseAllocations = append(purchaseAllocations, debtSupplierAllocation{
|
||||
RowIndex: rowIndex,
|
||||
SortTime: sortTime,
|
||||
Amount: row.TotalPrice,
|
||||
CalcAging: func(endDate time.Time) int { return calculateDebtSupplierAging(capturedPurchase, endDate, location) },
|
||||
})
|
||||
rowRefs = append(rowRefs, rowRef{Index: rowIndex, Kind: "PURCHASE", Purchase: purchase})
|
||||
}
|
||||
|
||||
for _, exp := range expensesBySupplier[supplierID] {
|
||||
@@ -2006,25 +1949,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
DeltaBalance: -row.TotalPrice,
|
||||
CountTotals: true,
|
||||
})
|
||||
capturedExp := exp
|
||||
purchaseAllocations = append(purchaseAllocations, debtSupplierAllocation{
|
||||
RowIndex: rowIndex,
|
||||
SortTime: sortTime,
|
||||
Amount: row.TotalPrice,
|
||||
CalcAging: func(endDate time.Time) int { return calculateExpenseAging(capturedExp, endDate, location) },
|
||||
})
|
||||
}
|
||||
|
||||
paymentAllocations := make([]paymentAllocation, 0, len(paymentItems)+1)
|
||||
initialAllocation := initialBalanceTotals[supplierID] + initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]
|
||||
paymentCarry := 0.0
|
||||
if initialAllocation > 0 && len(purchaseAllocations) > 0 {
|
||||
paymentAllocations = append(paymentAllocations, paymentAllocation{
|
||||
Date: purchaseAllocations[0].SortTime,
|
||||
Amount: initialAllocation,
|
||||
})
|
||||
} else if initialAllocation < 0 {
|
||||
paymentCarry = -initialAllocation
|
||||
rowRefs = append(rowRefs, rowRef{Index: rowIndex, Kind: "EXPENSE", Expense: exp})
|
||||
}
|
||||
|
||||
for _, payment := range paymentItems {
|
||||
@@ -2037,51 +1962,29 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
DeltaBalance: payment.Nominal,
|
||||
CountTotals: false,
|
||||
})
|
||||
paymentAllocations = append(paymentAllocations, paymentAllocation{
|
||||
Date: sortTime,
|
||||
Amount: payment.Nominal,
|
||||
})
|
||||
}
|
||||
|
||||
if len(purchaseAllocations) > 0 && len(paymentAllocations) > 0 {
|
||||
sort.SliceStable(purchaseAllocations, func(i, j int) bool {
|
||||
return purchaseAllocations[i].SortTime.Before(purchaseAllocations[j].SortTime)
|
||||
})
|
||||
sort.SliceStable(paymentAllocations, func(i, j int) bool {
|
||||
return paymentAllocations[i].Date.Before(paymentAllocations[j].Date)
|
||||
})
|
||||
remaining := make([]float64, len(purchaseAllocations))
|
||||
for i := range purchaseAllocations {
|
||||
remaining[i] = purchaseAllocations[i].Amount
|
||||
// Determine Status & Aging dari payment_allocations DB.
|
||||
for _, ref := range rowRefs {
|
||||
rowTotal := combinedRows[ref.Index].Row.TotalPrice
|
||||
if rowTotal <= fifoAllocationEpsilon {
|
||||
continue
|
||||
}
|
||||
purchaseIndex := 0
|
||||
for _, pay := range paymentAllocations {
|
||||
amount := pay.Amount
|
||||
if amount <= 0 {
|
||||
continue
|
||||
}
|
||||
if paymentCarry > 0 {
|
||||
used := math.Min(amount, paymentCarry)
|
||||
paymentCarry -= used
|
||||
amount -= used
|
||||
}
|
||||
for amount > 0 && purchaseIndex < len(remaining) {
|
||||
if remaining[purchaseIndex] <= 0 {
|
||||
purchaseIndex++
|
||||
continue
|
||||
}
|
||||
used := math.Min(amount, remaining[purchaseIndex])
|
||||
remaining[purchaseIndex] -= used
|
||||
amount -= used
|
||||
if remaining[purchaseIndex] <= 0.000001 {
|
||||
allocation := purchaseAllocations[purchaseIndex]
|
||||
combinedRows[allocation.RowIndex].Row.Status = "Lunas"
|
||||
combinedRows[allocation.RowIndex].Row.Aging = allocation.CalcAging(pay.Date)
|
||||
purchaseIndex++
|
||||
}
|
||||
}
|
||||
if purchaseIndex >= len(remaining) {
|
||||
break
|
||||
var summary paymentAllocationSummary
|
||||
if ref.Kind == "PURCHASE" {
|
||||
summary = purchaseAllocSummary[ref.Purchase.Id]
|
||||
} else {
|
||||
summary = expenseAllocSummary[ref.Expense.Id]
|
||||
}
|
||||
if summary.PaidAmount+fifoAllocationEpsilon < rowTotal {
|
||||
continue
|
||||
}
|
||||
combinedRows[ref.Index].Row.Status = "Lunas"
|
||||
if !summary.LastPaymentDate.IsZero() {
|
||||
if ref.Kind == "PURCHASE" {
|
||||
combinedRows[ref.Index].Row.Aging = calculateDebtSupplierAging(ref.Purchase, summary.LastPaymentDate.In(location), location)
|
||||
} else {
|
||||
combinedRows[ref.Index].Row.Aging = calculateExpenseAging(ref.Expense, summary.LastPaymentDate.In(location), location)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2257,6 +2160,115 @@ func buildDebtSupplierPaymentRow(payment entity.Payment, loc *time.Location) dto
|
||||
}
|
||||
}
|
||||
|
||||
// fifoAllocationEpsilon untuk float comparison saat membandingkan paid vs total.
|
||||
const fifoAllocationEpsilon = 0.001
|
||||
|
||||
// paymentAllocationSummary aggregates per-document paid amount + latest payment date
|
||||
// from payment_allocations table, sebagai pengganti FIFO greedy in-memory.
|
||||
type paymentAllocationSummary struct {
|
||||
PaidAmount float64
|
||||
LastPaymentDate time.Time
|
||||
}
|
||||
|
||||
// fetchPurchaseAllocationSummary returns map[purchase_id]{paid_amount, last_payment_date}.
|
||||
// paid_amount = SUM(payment_allocations.amount) untuk semua items dalam purchase.
|
||||
// last_payment_date = MAX(payments.payment_date) untuk allocation tersebut.
|
||||
func (s *repportService) fetchPurchaseAllocationSummary(ctx context.Context, purchaseIDs []uint) (map[uint]paymentAllocationSummary, error) {
|
||||
out := make(map[uint]paymentAllocationSummary)
|
||||
if len(purchaseIDs) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
type row struct {
|
||||
PurchaseID uint
|
||||
Total float64
|
||||
LastPayment *time.Time
|
||||
}
|
||||
var rows []row
|
||||
if err := s.db.WithContext(ctx).
|
||||
Table("payment_allocations pa").
|
||||
Joins("JOIN purchase_items pi ON pi.id = pa.purchase_item_id").
|
||||
Joins("JOIN payments p ON p.id = pa.payment_id").
|
||||
Select("pi.purchase_id AS purchase_id, SUM(pa.amount) AS total, MAX(p.payment_date) AS last_payment").
|
||||
Where("pi.purchase_id IN ?", purchaseIDs).
|
||||
Group("pi.purchase_id").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range rows {
|
||||
summary := paymentAllocationSummary{PaidAmount: r.Total}
|
||||
if r.LastPayment != nil {
|
||||
summary.LastPaymentDate = *r.LastPayment
|
||||
}
|
||||
out[r.PurchaseID] = summary
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// fetchExpenseAllocationSummary returns map[expense_id]{paid_amount, last_payment_date}.
|
||||
// Allocation di expense_realization_id → JOIN expense_nonstocks → expenses.id.
|
||||
func (s *repportService) fetchExpenseAllocationSummary(ctx context.Context, expenseIDs []uint64) (map[uint64]paymentAllocationSummary, error) {
|
||||
out := make(map[uint64]paymentAllocationSummary)
|
||||
if len(expenseIDs) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
type row struct {
|
||||
ExpenseID uint64
|
||||
Total float64
|
||||
LastPayment *time.Time
|
||||
}
|
||||
var rows []row
|
||||
if err := s.db.WithContext(ctx).
|
||||
Table("payment_allocations pa").
|
||||
Joins("JOIN expense_realizations er ON er.id = pa.expense_realization_id").
|
||||
Joins("JOIN expense_nonstocks en ON en.id = er.expense_nonstock_id").
|
||||
Joins("JOIN payments p ON p.id = pa.payment_id").
|
||||
Select("en.expense_id AS expense_id, SUM(pa.amount) AS total, MAX(p.payment_date) AS last_payment").
|
||||
Where("en.expense_id IN ?", expenseIDs).
|
||||
Group("en.expense_id").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range rows {
|
||||
summary := paymentAllocationSummary{PaidAmount: r.Total}
|
||||
if r.LastPayment != nil {
|
||||
summary.LastPaymentDate = *r.LastPayment
|
||||
}
|
||||
out[r.ExpenseID] = summary
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// fetchMdpAllocationSummary returns map[mdp_id]{paid_amount, last_payment_date}.
|
||||
func (s *repportService) fetchMdpAllocationSummary(ctx context.Context, mdpIDs []uint) (map[uint]paymentAllocationSummary, error) {
|
||||
out := make(map[uint]paymentAllocationSummary)
|
||||
if len(mdpIDs) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
type row struct {
|
||||
MdpID uint
|
||||
Total float64
|
||||
LastPayment *time.Time
|
||||
}
|
||||
var rows []row
|
||||
if err := s.db.WithContext(ctx).
|
||||
Table("payment_allocations pa").
|
||||
Joins("JOIN payments p ON p.id = pa.payment_id").
|
||||
Select("pa.marketing_delivery_product_id AS mdp_id, SUM(pa.amount) AS total, MAX(p.payment_date) AS last_payment").
|
||||
Where("pa.marketing_delivery_product_id IN ?", mdpIDs).
|
||||
Group("pa.marketing_delivery_product_id").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range rows {
|
||||
summary := paymentAllocationSummary{PaidAmount: r.Total}
|
||||
if r.LastPayment != nil {
|
||||
summary.LastPaymentDate = *r.LastPayment
|
||||
}
|
||||
out[r.MdpID] = summary
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc *time.Location) time.Time {
|
||||
if strings.EqualFold(strings.TrimSpace(filterBy), "po_date") {
|
||||
if purchase.PoDate != nil && !purchase.PoDate.IsZero() {
|
||||
@@ -2360,10 +2372,10 @@ func buildDebtSupplierExpenseRow(exp entity.Expense, warehouses []entity.Warehou
|
||||
aging = int(endDay.Sub(startDay).Hours() / 24)
|
||||
}
|
||||
|
||||
totalPrice := 0.0
|
||||
for _, ns := range exp.Nonstocks {
|
||||
totalPrice += ns.Qty * ns.Price
|
||||
}
|
||||
// TotalPrice pakai expense.GrandTotal (= SUM realisasi) supaya konsisten dengan
|
||||
// FIFO allocation yang juga pakai realisasi. Hindari pakai SUM nonstock pengajuan
|
||||
// karena bisa beda nilai dari realisasi → mismatch dengan paid_amount → status salah.
|
||||
totalPrice := exp.GrandTotal
|
||||
|
||||
var area *areaDTO.AreaRelationDTO
|
||||
if exp.Location != nil && exp.Location.Area.Id != 0 {
|
||||
|
||||
Reference in New Issue
Block a user