Present values rounded to cents #19

Merged
natercio merged 3 commits from rounding-to-cents into main 2025-11-26 10:08:34 +00:00
2 changed files with 77 additions and 10 deletions

View File

@@ -3,16 +3,15 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/nmoniz/any2anexoj)](https://goreportcard.com/report/github.com/nmoniz/any2anexoj)
[![Coverage Status](https://coveralls.io/repos/github/nmoniz/any2anexoj/badge.svg?branch=coveralls-badge)](https://coveralls.io/github/nmoniz/any2anexoj?branch=coveralls-badge)
![Screenshot](https://i.ibb.co/TDDypq8X/Screenshot-2025-11-18-at-16-17-16.png)
<p align="center">
<img src="https://i.ibb.co/0yRtwq2C/0-FBA40-FD-D97-A-4-AFB-8618-49582-DB98-F3-C.png" alt="Screenshot" border="0">
</p>
This tool converts the statements from known brokers and exchanges into a format compatible with section 9 from the Portuguese IRS form: [Mod_3_anexo_j](https://info.portaldasfinancas.gov.pt/pt/apoio_contribuinte/modelos_formularios/irs/Documents/Mod_3_anexo_J.pdf)
> [!WARNING]
> Although I made significant efforts to ensure the correctness of the calculations you should verify any outputs produced by this tool on your own or with a certified accountant.
> [!NOTE]
> This tool is in early stages of development. Use at your own risk!
## Install
```bash
@@ -24,3 +23,9 @@ go install github.com/nmoniz/any2anexoj/cmd/any2anexoj-cli@latest
```bash
cat statement.csv | any2anexoj-cli --platform=tranding212
```
## Rounding
All Euro values are rounded to cents (2 decimal places) but internal calculations use the statement values with full precision.
There are no explicit rules or details about how to round Euro values in Anexo J.
This application rounds according to `Portaria n.º 1180/2001, art. 2.º, alínea c) e d)` (Ministerial Order / Government Order) examples, which imply we should round to the 2nd decimal place by rounding up (ceiling) or down (floor) depending on whether the third decimal place is ≥ 5 or < 5, respectively.

View File

@@ -2,8 +2,10 @@ package internal
import (
"context"
"fmt"
"io"
"github.com/biter777/countries"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/shopspring/decimal"
@@ -26,6 +28,21 @@ func NewTableWriter(w io.Writer) *TableWriter {
t.SetOutputMirror(w)
t.SetAutoIndex(true)
t.SetStyle(table.StyleLight)
t.SetColumnConfigs([]table.ColumnConfig{
colCountry(1),
colOther(2),
colOther(3),
colOther(4),
colOther(5),
colEuros(6),
colOther(7),
colOther(8),
colOther(9),
colEuros(10),
colEuros(11),
colEuros(12),
colCountry(13),
})
t.AppendHeader(table.Row{"", "", "Realisation", "Realisation", "Realisation", "Realisation", "Acquisition", "Acquisition", "Acquisition", "Acquisition", "", "", ""}, table.RowConfig{AutoMerge: true})
t.AppendHeader(table.Row{"Source Country", "Code", "Year", "Month", "Day", "Value", "Year", "Month", "Day", "Value", "Expenses", "Paid Taxes", "Counter Country"})
@@ -37,17 +54,62 @@ func NewTableWriter(w io.Writer) *TableWriter {
}
func (tw *TableWriter) Write(_ context.Context, ri ReportItem) error {
tw.totalEarned = tw.totalEarned.Add(ri.SellValue)
tw.totalSpent = tw.totalSpent.Add(ri.BuyValue)
tw.totalFees = tw.totalFees.Add(ri.Fees)
tw.totalTaxes = tw.totalTaxes.Add(ri.Taxes)
tw.totalEarned = tw.totalEarned.Add(ri.SellValue.Round(2))
tw.totalSpent = tw.totalSpent.Add(ri.BuyValue.Round(2))
tw.totalFees = tw.totalFees.Add(ri.Fees.Round(2))
tw.totalTaxes = tw.totalTaxes.Add(ri.Taxes.Round(2))
tw.table.AppendRow(table.Row{ri.AssetCountry, ri.Nature, ri.SellTimestamp.Year(), int(ri.SellTimestamp.Month()), ri.SellTimestamp.Day(), ri.SellValue, ri.BuyTimestamp.Year(), ri.BuyTimestamp.Month(), ri.BuyTimestamp.Day(), ri.BuyValue, ri.Fees, ri.Taxes, ri.BrokerCountry})
tw.table.AppendRow(table.Row{
ri.AssetCountry, ri.Nature,
ri.SellTimestamp.Year(), int(ri.SellTimestamp.Month()), ri.SellTimestamp.Day(), ri.SellValue.StringFixed(2),
ri.BuyTimestamp.Year(), int(ri.BuyTimestamp.Month()), ri.BuyTimestamp.Day(), ri.BuyValue.StringFixed(2),
ri.Fees.StringFixed(2), ri.Taxes.StringFixed(2), ri.BrokerCountry,
})
return nil
}
func (tw *TableWriter) Render() {
tw.table.AppendFooter(table.Row{"SUM", "SUM", "SUM", "SUM", "SUM", tw.totalEarned, "", "", "", tw.totalSpent, tw.totalFees, tw.totalTaxes}, table.RowConfig{AutoMerge: true, AutoMergeAlign: text.AlignRight})
tw.table.AppendFooter(table.Row{"SUM", "SUM", "SUM", "SUM", "SUM", tw.totalEarned.StringFixed(2), "", "", "", tw.totalSpent.StringFixed(2), tw.totalFees.StringFixed(2), tw.totalTaxes.StringFixed(2)}, table.RowConfig{AutoMerge: true, AutoMergeAlign: text.AlignRight})
tw.table.Render()
}
func colEuros(n int) table.ColumnConfig {
return table.ColumnConfig{
Number: n,
Align: text.AlignRight,
AlignFooter: text.AlignRight,
AlignHeader: text.AlignRight,
WidthMin: 12,
Transformer: func(val any) string {
return fmt.Sprintf("%v €", val)
},
TransformerFooter: func(val any) string {
return fmt.Sprintf("%v €", val)
},
}
}
func colOther(n int) table.ColumnConfig {
return table.ColumnConfig{
Number: n,
Align: text.AlignLeft,
AlignFooter: text.AlignLeft,
AlignHeader: text.AlignLeft,
}
}
func colCountry(n int) table.ColumnConfig {
return table.ColumnConfig{
Number: n,
Align: text.AlignLeft,
AlignFooter: text.AlignLeft,
AlignHeader: text.AlignLeft,
WidthMax: 24,
WidthMaxEnforcer: text.Trim,
Transformer: func(val any) string {
countryCode := val.(int64)
return fmt.Sprintf("%v - %s", val, countries.ByNumeric(int(countryCode)).Info().Name)
},
}
}