diff --git a/internal/modules/repports/repositories/debt_supplier.repository.go b/internal/modules/repports/repositories/debt_supplier.repository.go index 977db610..74039ebf 100644 --- a/internal/modules/repports/repositories/debt_supplier.repository.go +++ b/internal/modules/repports/repositories/debt_supplier.repository.go @@ -37,6 +37,21 @@ func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository { return &debtSupplierRepositoryImpl{db: db} } +func (r *debtSupplierRepositoryImpl) latestPurchaseApproval(ctx context.Context) *gorm.DB { + return r.db.WithContext(ctx). + Table("approvals AS a"). + Select("a.approvable_id, a.step_number, a.action"). + Joins(` + JOIN ( + SELECT approvable_id, MAX(action_at) AS latest_action_at + FROM approvals + WHERE approvable_type = ? + GROUP BY approvable_id + ) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`, + string(utils.ApprovalWorkflowPurchase), + ) +} + func resolveDebtSupplierDateColumn(filterBy string) string { switch strings.ToLower(strings.TrimSpace(filterBy)) { case "po_date": @@ -54,7 +69,11 @@ func (r *debtSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filt db := r.db.WithContext(ctx). Model(&entity.Supplier{}). Joins("JOIN purchases ON purchases.supplier_id = suppliers.id"). - Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id") + Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id"). + Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)). + Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)). + Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)). + Where("purchase_items.received_date IS NOT NULL") if len(filters.SupplierIDs) > 0 { db = db.Where("suppliers.id IN ?", filters.SupplierIDs) @@ -207,7 +226,11 @@ func (r *debtSupplierRepositoryImpl) getPurchaseIDs(ctx context.Context, supplie Table("purchases"). Select("DISTINCT purchases.id"). Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id"). - Where("purchases.supplier_id IN ?", supplierIDs) + Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)). + Where("purchases.supplier_id IN ?", supplierIDs). + Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)). + Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)). + Where("purchase_items.received_date IS NOT NULL") if filters.StartDate != "" { if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { @@ -355,7 +378,11 @@ func (r *debtSupplierRepositoryImpl) GetPurchaseTotalsBeforeDate(ctx context.Con Table("purchases"). Select("purchases.supplier_id AS supplier_id, SUM(purchase_items.total_price) AS total"). Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id"). + Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)). Where("purchases.supplier_id IN ?", supplierIDs). + Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)). + Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)). + Where("purchase_items.received_date IS NOT NULL"). Where(fmt.Sprintf("DATE(%s) < ?", dateColumn), dateFrom). Group("purchases.supplier_id"). Scan(&rows).Error; err != nil { diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index effec76a..bbe1d111 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -1156,12 +1156,6 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu return nil, 0, err } - references := collectDebtSupplierReferences(purchases) - paymentSummaries, err := s.DebtSupplierRepo.GetPaymentSummariesByReferences(c.Context(), supplierIDs, references) - if err != nil { - return nil, 0, err - } - location, err := time.LoadLocation("Asia/Jakarta") if err != nil { return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration") @@ -1176,6 +1170,16 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu DeltaBalance float64 CountTotals bool } + type debtSupplierAllocation struct { + RowIndex int + SortTime time.Time + Amount float64 + Purchase entity.Purchase + } + type paymentAllocation struct { + Date time.Time + Amount float64 + } for _, supplierID := range supplierIDs { supplier, exists := supplierMap[supplierID] @@ -1189,19 +1193,11 @@ 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)) for _, purchase := range items { row := buildDebtSupplierRow(purchase, now, location) - if reference := resolveDebtSupplierReference(purchase); reference != "" { - if summary, ok := paymentSummaries[reference]; ok { - if isDebtSupplierPaid(row.TotalPrice, summary.Total) { - row.Status = "Lunas" - if !summary.LatestPaymentDate.IsZero() { - row.Aging = calculateDebtSupplierAging(purchase, summary.LatestPaymentDate, location) - } - } - } - } sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location) + rowIndex := len(combinedRows) combinedRows = append(combinedRows, debtSupplierRowItem{ Row: row, SortTime: sortTime, @@ -1209,6 +1205,24 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu DeltaBalance: -row.TotalPrice, CountTotals: true, }) + purchaseAllocations = append(purchaseAllocations, debtSupplierAllocation{ + RowIndex: rowIndex, + SortTime: sortTime, + Amount: row.TotalPrice, + Purchase: purchase, + }) + } + + 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 } for _, payment := range paymentItems { @@ -1221,6 +1235,53 @@ 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 + } + 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 = calculateDebtSupplierAging(allocation.Purchase, pay.Date, location) + purchaseIndex++ + } + } + if purchaseIndex >= len(remaining) { + break + } + } } sort.SliceStable(combinedRows, func(i, j int) bool {