feat: importação de fatura de cartão — strip R$, skip negativos, all_expenses
Parser limpa prefixo R$ e símbolos de moeda antes de parsear o valor. Flags SkipNegative e AllExpenses para CSV de fatura de cartão. UI: checkboxes "Fatura de cartão" e "Ignorar valores negativos". Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
@@ -41,4 +41,6 @@ type CSVMapping struct {
|
||||
HasHeader bool `json:"has_header"`
|
||||
DecimalSeparator string `json:"decimal_separator"` // "." or ","
|
||||
FieldSeparator string `json:"field_separator"` // "," or ";"
|
||||
SkipNegative bool `json:"skip_negative"` // skip rows with negative amount (e.g. credit card bill payments)
|
||||
AllExpenses bool `json:"all_expenses"` // treat all rows as expense (credit card statements)
|
||||
}
|
||||
|
||||
@@ -57,24 +57,42 @@ func ParseCSV(r io.Reader, m model.CSVMapping) ([]model.ImportRow, []string) {
|
||||
amtStr := strings.TrimSpace(record[m.AmountColumn])
|
||||
desc := cleanDescription(record[m.DescriptionColumn])
|
||||
|
||||
// Skip blank rows (e.g. empty line after header in some bank exports)
|
||||
if dateStr == "" && amtStr == "" && desc == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
t, err := time.Parse(m.DateFormat, dateStr)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("linha %d: data inválida %q", lineNum, dateStr))
|
||||
continue
|
||||
}
|
||||
|
||||
if m.DecimalSeparator == "," {
|
||||
amtStr = strings.ReplaceAll(amtStr, ".", "")
|
||||
amtStr = strings.ReplaceAll(amtStr, ",", ".")
|
||||
// Strip currency symbols (R$, $, €, etc.) keeping digits, comma, dot, minus
|
||||
cleaned := strings.Map(func(r rune) rune {
|
||||
if r >= '0' && r <= '9' || r == ',' || r == '.' || r == '-' {
|
||||
return r
|
||||
}
|
||||
amt, err := strconv.ParseFloat(amtStr, 64)
|
||||
if err != nil {
|
||||
return -1
|
||||
}, amtStr)
|
||||
cleaned = strings.TrimSpace(cleaned)
|
||||
|
||||
if m.DecimalSeparator == "," {
|
||||
cleaned = strings.ReplaceAll(cleaned, ".", "")
|
||||
cleaned = strings.ReplaceAll(cleaned, ",", ".")
|
||||
}
|
||||
amt, err := strconv.ParseFloat(cleaned, 64)
|
||||
if err != nil || cleaned == "" {
|
||||
errs = append(errs, fmt.Sprintf("linha %d: valor inválido %q", lineNum, amtStr))
|
||||
continue
|
||||
}
|
||||
|
||||
if m.SkipNegative && amt < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
txType := "expense"
|
||||
if amt > 0 {
|
||||
if !m.AllExpenses && amt > 0 {
|
||||
txType = "income"
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ export interface CSVMapping {
|
||||
has_header: boolean
|
||||
decimal_separator: string
|
||||
field_separator: string
|
||||
skip_negative: boolean
|
||||
all_expenses: boolean
|
||||
}
|
||||
|
||||
export const useImportStore = defineStore('import', () => {
|
||||
|
||||
@@ -17,6 +17,8 @@ const csvMapping = ref<CSVMapping>({
|
||||
has_header: true,
|
||||
decimal_separator: ',',
|
||||
field_separator: ';',
|
||||
skip_negative: false,
|
||||
all_expenses: false,
|
||||
})
|
||||
|
||||
function onFileChange(e: Event) {
|
||||
@@ -130,6 +132,14 @@ function fmt(amount: number) {
|
||||
<input type="checkbox" v-model="csvMapping.has_header" />
|
||||
Arquivo tem cabeçalho
|
||||
</label>
|
||||
<label class="fc-label fc-import-cfg__checkbox">
|
||||
<input type="checkbox" v-model="csvMapping.all_expenses" />
|
||||
Fatura de cartão (todos são despesas)
|
||||
</label>
|
||||
<label class="fc-label fc-import-cfg__checkbox">
|
||||
<input type="checkbox" v-model="csvMapping.skip_negative" />
|
||||
Ignorar valores negativos
|
||||
</label>
|
||||
</div>
|
||||
</NeonPanel>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user