From 22d2507d52f4c4c66507152c967f08a150737c9d Mon Sep 17 00:00:00 2001 From: cclohmar Date: Tue, 14 Jul 2026 14:33:45 +0000 Subject: [PATCH] fix: CSV reports now include claim currency column, claim totals, and total claim header --- internal/handlers/file.go | 99 +++++++++++++++---------------------- internal/handlers/months.go | 50 ++++++++++++++----- 2 files changed, 76 insertions(+), 73 deletions(-) diff --git a/internal/handlers/file.go b/internal/handlers/file.go index c6df847..b827895 100644 --- a/internal/handlers/file.go +++ b/internal/handlers/file.go @@ -568,85 +568,64 @@ func generateCSV(eventName string, expenses []database.Expense, userName, userDe var buf bytes.Buffer writer := csv.NewWriter(&buf) - // Write user metadata row (prepared by). + // Write user metadata row. if userName != "" { metaLine := fmt.Sprintf("Prepared by: %s", userName) if userDept != "" && userDept != "-" { metaLine += fmt.Sprintf(" | Department: %s", userDept) } writer.Write([]string{metaLine}) - writer.Write([]string{""}) // blank separator + writer.Write([]string{""}) } - // Determine if we need conversion columns. - hasConversion := false + // Total claim summary. + var totalClaim float64 + claimCur := "" for _, exp := range expenses { - if exp.ConvertedAmount > 0 && exp.BaseCurrency != "" && exp.BaseCurrency != exp.Currency { - hasConversion = true - break + cAmt := exp.ConvertedAmount + if cAmt <= 0 { + cAmt = exp.Amount + } + totalClaim += cAmt + if claimCur == "" && exp.BaseCurrency != "" { + claimCur = exp.BaseCurrency } } - - // Write header row. - var header []string - if hasConversion { - header = []string{"#", "Date", "Merchant", "Amount", "Currency", "Converted", "Claim Currency", "Category", "Description"} - } else { - header = []string{"#", "Date", "Merchant", "Amount", "Currency", "Category", "Description"} - } - if err := writer.Write(header); err != nil { - return nil, fmt.Errorf("write CSV header: %w", err) + if claimCur == "" && len(expenses) > 0 { + claimCur = expenses[0].Currency } + writer.Write([]string{fmt.Sprintf("Total Claim: %.2f %s", totalClaim, claimCur)}) + writer.Write([]string{""}) - // Write one data row per expense with item number. - totalOrig := 0.0 - totalConv := 0.0 + // Write header row with both local and claim columns. + header := []string{"#", "Date", "Merchant", "Local Amt", "Currency", "Claim Amt", "Claim Curr", "Category", "Description"} + writer.Write(header) + + // Write data rows. + var tableLocal, tableClaim float64 for i, exp := range expenses { - itemNum := i + 1 - var row []string - if hasConversion { - row = []string{ - fmt.Sprintf("%d", itemNum), - exp.Date, - exp.Merchant, - fmt.Sprintf("%.2f", exp.Amount), - exp.Currency, - fmt.Sprintf("%.2f", exp.ConvertedAmount), - exp.BaseCurrency, - exp.Category, - exp.Description, - } - } else { - row = []string{ - fmt.Sprintf("%d", itemNum), - exp.Date, - exp.Merchant, - fmt.Sprintf("%.2f", exp.Amount), - exp.Currency, - exp.Category, - exp.Description, - } + claimAmt := exp.ConvertedAmount + claimCur := exp.BaseCurrency + if claimAmt <= 0 { + claimAmt = exp.Amount } - if err := writer.Write(row); err != nil { - return nil, fmt.Errorf("write CSV row: %w", err) + if claimCur == "" { + claimCur = exp.Currency } - totalOrig += exp.Amount - totalConv += exp.ConvertedAmount + row := []string{ + fmt.Sprintf("%d", i+1), + exp.Date, exp.Merchant, + fmt.Sprintf("%.2f", exp.Amount), exp.Currency, + fmt.Sprintf("%.2f", claimAmt), claimCur, + exp.Category, exp.Description, + } + writer.Write(row) + tableLocal += exp.Amount + tableClaim += claimAmt } // Write totals row. - if hasConversion { - baseCur := "" - for _, exp := range expenses { - if exp.BaseCurrency != "" { - baseCur = exp.BaseCurrency - break - } - } - writer.Write([]string{"TOTAL", "", "", fmt.Sprintf("%.2f", totalOrig), "", fmt.Sprintf("%.2f", totalConv), baseCur, "", ""}) - } else { - writer.Write([]string{"TOTAL", "", "", fmt.Sprintf("%.2f", totalOrig), "", "", "", ""}) - } + writer.Write([]string{"TOTAL", "", "", fmt.Sprintf("%.2f", tableLocal), "", fmt.Sprintf("%.2f", tableClaim), "", "", ""}) writer.Flush() if err := writer.Error(); err != nil { diff --git a/internal/handlers/months.go b/internal/handlers/months.go index edba1b3..04abdc9 100644 --- a/internal/handlers/months.go +++ b/internal/handlers/months.go @@ -581,6 +581,25 @@ func generateMonthlyCSV(monthName string, events []database.Event, expenses []da } buf.WriteString("\r\n") + // Total claim summary. + var totalClaim float64 + claimCur := "" + for _, exp := range expenses { + cAmt := exp.ConvertedAmount + if cAmt <= 0 { + cAmt = exp.Amount + } + totalClaim += cAmt + if claimCur == "" && exp.BaseCurrency != "" { + claimCur = exp.BaseCurrency + } + } + if claimCur == "" && len(expenses) > 0 { + claimCur = expenses[0].Currency + } + buf.WriteString(fmt.Sprintf("Total Claim: %.2f %s\r\n", totalClaim, claimCur)) + buf.WriteString("\r\n") + // Group expenses by event. expensesByEvent := make(map[string][]database.Expense) eventNames := make(map[string]string) @@ -592,7 +611,7 @@ func generateMonthlyCSV(monthName string, events []database.Event, expenses []da } itemNum := 1 - var grandTotalOrig, grandTotalConv float64 + var grandLocal, grandClaim float64 for _, evt := range events { evtExpenses := expensesByEvent[evt.ID] @@ -601,24 +620,29 @@ func generateMonthlyCSV(monthName string, events []database.Event, expenses []da } buf.WriteString(fmt.Sprintf("\r\n--- %s ---\r\n", eventNames[evt.ID])) - buf.WriteString("#,Date,Merchant,Amount,Currency,Category,Description\r\n") + buf.WriteString("#,Date,Merchant,Local Amt,Currency,Claim Amt,Claim Curr,Category,Description\r\n") - var evtTotal float64 + var evtLocal, evtClaim float64 for _, exp := range evtExpenses { - buf.WriteString(fmt.Sprintf("%d,%s,%s,%.2f,%s,%s,%s\r\n", - itemNum, exp.Date, exp.Merchant, exp.Amount, exp.Currency, exp.Category, exp.Description)) - evtTotal += exp.Amount - if exp.ConvertedAmount > 0 { - grandTotalConv += exp.ConvertedAmount - } else { - grandTotalConv += exp.Amount + claimAmt := exp.ConvertedAmount + claimCur := exp.BaseCurrency + if claimAmt <= 0 { + claimAmt = exp.Amount } + if claimCur == "" { + claimCur = exp.Currency + } + buf.WriteString(fmt.Sprintf("%d,%s,%s,%.2f,%s,%.2f,%s,%s,%s\r\n", + itemNum, exp.Date, exp.Merchant, exp.Amount, exp.Currency, claimAmt, claimCur, exp.Category, exp.Description)) + evtLocal += exp.Amount + evtClaim += claimAmt itemNum++ } - buf.WriteString(fmt.Sprintf("Event Total,,,,%.2f,,\r\n", evtTotal)) - grandTotalOrig += evtTotal + buf.WriteString(fmt.Sprintf("Event Total,,,%.2f,,%.2f,,\r\n", evtLocal, evtClaim)) + grandLocal += evtLocal + grandClaim += evtClaim } - buf.WriteString(fmt.Sprintf("\r\nGrand Total,,,,%.2f,,\r\n", grandTotalOrig)) + buf.WriteString(fmt.Sprintf("\r\nGrand Total,,,%.2f,,%.2f,,\r\n", grandLocal, grandClaim)) filename := fmt.Sprintf("monthly-%s-report.csv", sanitiseFilename(monthName)) return &email.Attachment{