fix: CSV reports now include claim currency column, claim totals, and total claim header

This commit is contained in:
Claus Lohmar 2026-07-14 14:33:45 +00:00
parent f1b029949e
commit 22d2507d52
2 changed files with 76 additions and 73 deletions

View file

@ -568,85 +568,64 @@ func generateCSV(eventName string, expenses []database.Expense, userName, userDe
var buf bytes.Buffer var buf bytes.Buffer
writer := csv.NewWriter(&buf) writer := csv.NewWriter(&buf)
// Write user metadata row (prepared by). // Write user metadata row.
if userName != "" { if userName != "" {
metaLine := fmt.Sprintf("Prepared by: %s", userName) metaLine := fmt.Sprintf("Prepared by: %s", userName)
if userDept != "" && userDept != "-" { if userDept != "" && userDept != "-" {
metaLine += fmt.Sprintf(" | Department: %s", userDept) metaLine += fmt.Sprintf(" | Department: %s", userDept)
} }
writer.Write([]string{metaLine}) writer.Write([]string{metaLine})
writer.Write([]string{""}) // blank separator writer.Write([]string{""})
} }
// Determine if we need conversion columns. // Total claim summary.
hasConversion := false var totalClaim float64
claimCur := ""
for _, exp := range expenses { for _, exp := range expenses {
if exp.ConvertedAmount > 0 && exp.BaseCurrency != "" && exp.BaseCurrency != exp.Currency { cAmt := exp.ConvertedAmount
hasConversion = true if cAmt <= 0 {
break cAmt = exp.Amount
}
totalClaim += cAmt
if claimCur == "" && exp.BaseCurrency != "" {
claimCur = exp.BaseCurrency
} }
} }
if claimCur == "" && len(expenses) > 0 {
// Write header row. claimCur = expenses[0].Currency
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)
} }
writer.Write([]string{fmt.Sprintf("Total Claim: %.2f %s", totalClaim, claimCur)})
writer.Write([]string{""})
// Write one data row per expense with item number. // Write header row with both local and claim columns.
totalOrig := 0.0 header := []string{"#", "Date", "Merchant", "Local Amt", "Currency", "Claim Amt", "Claim Curr", "Category", "Description"}
totalConv := 0.0 writer.Write(header)
// Write data rows.
var tableLocal, tableClaim float64
for i, exp := range expenses { for i, exp := range expenses {
itemNum := i + 1 claimAmt := exp.ConvertedAmount
var row []string claimCur := exp.BaseCurrency
if hasConversion { if claimAmt <= 0 {
row = []string{ claimAmt = exp.Amount
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,
}
} }
if err := writer.Write(row); err != nil { if claimCur == "" {
return nil, fmt.Errorf("write CSV row: %w", err) claimCur = exp.Currency
} }
totalOrig += exp.Amount row := []string{
totalConv += exp.ConvertedAmount 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. // Write totals row.
if hasConversion { writer.Write([]string{"TOTAL", "", "", fmt.Sprintf("%.2f", tableLocal), "", fmt.Sprintf("%.2f", tableClaim), "", "", ""})
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.Flush() writer.Flush()
if err := writer.Error(); err != nil { if err := writer.Error(); err != nil {

View file

@ -581,6 +581,25 @@ func generateMonthlyCSV(monthName string, events []database.Event, expenses []da
} }
buf.WriteString("\r\n") 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. // Group expenses by event.
expensesByEvent := make(map[string][]database.Expense) expensesByEvent := make(map[string][]database.Expense)
eventNames := make(map[string]string) eventNames := make(map[string]string)
@ -592,7 +611,7 @@ func generateMonthlyCSV(monthName string, events []database.Event, expenses []da
} }
itemNum := 1 itemNum := 1
var grandTotalOrig, grandTotalConv float64 var grandLocal, grandClaim float64
for _, evt := range events { for _, evt := range events {
evtExpenses := expensesByEvent[evt.ID] 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(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 { for _, exp := range evtExpenses {
buf.WriteString(fmt.Sprintf("%d,%s,%s,%.2f,%s,%s,%s\r\n", claimAmt := exp.ConvertedAmount
itemNum, exp.Date, exp.Merchant, exp.Amount, exp.Currency, exp.Category, exp.Description)) claimCur := exp.BaseCurrency
evtTotal += exp.Amount if claimAmt <= 0 {
if exp.ConvertedAmount > 0 { claimAmt = exp.Amount
grandTotalConv += exp.ConvertedAmount
} else {
grandTotalConv += 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++ itemNum++
} }
buf.WriteString(fmt.Sprintf("Event Total,,,,%.2f,,\r\n", evtTotal)) buf.WriteString(fmt.Sprintf("Event Total,,,%.2f,,%.2f,,\r\n", evtLocal, evtClaim))
grandTotalOrig += evtTotal 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)) filename := fmt.Sprintf("monthly-%s-report.csv", sanitiseFilename(monthName))
return &email.Attachment{ return &email.Attachment{