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"`
|
HasHeader bool `json:"has_header"`
|
||||||
DecimalSeparator string `json:"decimal_separator"` // "." or ","
|
DecimalSeparator string `json:"decimal_separator"` // "." or ","
|
||||||
FieldSeparator string `json:"field_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])
|
amtStr := strings.TrimSpace(record[m.AmountColumn])
|
||||||
desc := cleanDescription(record[m.DescriptionColumn])
|
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)
|
t, err := time.Parse(m.DateFormat, dateStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, fmt.Sprintf("linha %d: data inválida %q", lineNum, dateStr))
|
errs = append(errs, fmt.Sprintf("linha %d: data inválida %q", lineNum, dateStr))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}, amtStr)
|
||||||
|
cleaned = strings.TrimSpace(cleaned)
|
||||||
|
|
||||||
if m.DecimalSeparator == "," {
|
if m.DecimalSeparator == "," {
|
||||||
amtStr = strings.ReplaceAll(amtStr, ".", "")
|
cleaned = strings.ReplaceAll(cleaned, ".", "")
|
||||||
amtStr = strings.ReplaceAll(amtStr, ",", ".")
|
cleaned = strings.ReplaceAll(cleaned, ",", ".")
|
||||||
}
|
}
|
||||||
amt, err := strconv.ParseFloat(amtStr, 64)
|
amt, err := strconv.ParseFloat(cleaned, 64)
|
||||||
if err != nil {
|
if err != nil || cleaned == "" {
|
||||||
errs = append(errs, fmt.Sprintf("linha %d: valor inválido %q", lineNum, amtStr))
|
errs = append(errs, fmt.Sprintf("linha %d: valor inválido %q", lineNum, amtStr))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.SkipNegative && amt < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
txType := "expense"
|
txType := "expense"
|
||||||
if amt > 0 {
|
if !m.AllExpenses && amt > 0 {
|
||||||
txType = "income"
|
txType = "income"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export interface CSVMapping {
|
|||||||
has_header: boolean
|
has_header: boolean
|
||||||
decimal_separator: string
|
decimal_separator: string
|
||||||
field_separator: string
|
field_separator: string
|
||||||
|
skip_negative: boolean
|
||||||
|
all_expenses: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useImportStore = defineStore('import', () => {
|
export const useImportStore = defineStore('import', () => {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const csvMapping = ref<CSVMapping>({
|
|||||||
has_header: true,
|
has_header: true,
|
||||||
decimal_separator: ',',
|
decimal_separator: ',',
|
||||||
field_separator: ';',
|
field_separator: ';',
|
||||||
|
skip_negative: false,
|
||||||
|
all_expenses: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
function onFileChange(e: Event) {
|
function onFileChange(e: Event) {
|
||||||
@@ -130,6 +132,14 @@ function fmt(amount: number) {
|
|||||||
<input type="checkbox" v-model="csvMapping.has_header" />
|
<input type="checkbox" v-model="csvMapping.has_header" />
|
||||||
Arquivo tem cabeçalho
|
Arquivo tem cabeçalho
|
||||||
</label>
|
</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>
|
</div>
|
||||||
</NeonPanel>
|
</NeonPanel>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user