~inferiormartin/shishutsu

bb92e25328095f72ac7640ad8958c2362749a055 — Maarten Vos a month ago 1c71d66
add partial support for shi-import
A cmd/shi-import/.gitignore => cmd/shi-import/.gitignore +1 -0
@@ 0,0 1,1 @@
shi-import

M cmd/shi-import/main.go => cmd/shi-import/main.go +62 -4
@@ 1,14 1,59 @@
package main

import (
	"database/sql"
	"encoding/csv"
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"
	"time"

	"git.sr.ht/~inferiormartin/shishutsu/config"
	"git.sr.ht/~inferiormartin/shishutsu/database"
)

func main() {
	//TODO: this program is too specific with what it imports, needs to improve
	cfg, err := config.Load()
	if err != nil {
		log.Fatal(err)
	}
	conn, ok := cfg.Get("shishutsu", "connection-string")
	if !ok {
		log.Fatal("shi-import: unable to get connection-string. shishutsu connection-string not defined?")
	}
	db, err := sql.Open("postgres", conn)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()
	transactions := read()
	if err = database.InsertTransactions(transactions, db); err != nil {
		log.Fatal(err)
	}
	log.Printf("%d transactions imported.", len(transactions))
}

func parseDate(date string) time.Time {
	r, err := time.Parse("02/01/2006", date)
	if err != nil {
		log.Fatal(err)
	}
	return r
}

func parseFloat(value string) float64 {
	value = strings.Replace(value, ".", "", -1)
	value = strings.Replace(value, ",", ".", -1)
	result, err := strconv.ParseFloat(value, 64)
	if err != nil {
		log.Fatal(err)
	}
	return result
}

func read() []*database.Transaction {
	args := os.Args[1:]
	data, err := os.ReadFile(args[0])
	if err != nil {


@@ 16,10 61,23 @@ func main() {
	}
	r := csv.NewReader(strings.NewReader(string(data)))
	r.Comma = ';'
	rows, err := r.ReadAll()
	results, err := r.ReadAll()
	if err != nil {
		log.Fatal(err)
	}
	//TODO: connect and insert rows into database
	fmt.Print(rows[len(rows)-1])
	results = results[1:]
	var transactions []*database.Transaction
	for _, result := range results {
		item := &database.Transaction{
			-1,
			"dc5dadda-063c-45a4-b6a8-61357b962a37", //TODO: allow import for any user
			"TODO",                                 //TODO: my bank's specific csv does not contain an IBAN because the statement exports are specific for an account. allow manual override using flag?
			int64(parseFloat(result[6]) * 100),
			parseDate(result[4]),
			parseDate(result[5]),
			result[8],
		}
		transactions = append(transactions, item)
	}
	return transactions
}

M cmd/shi-web/main.go => cmd/shi-web/main.go +31 -4
@@ 1,11 1,14 @@
package main

import (
	"database/sql"
	"html/template"
	"log"
	"net/http"

	"git.sr.ht/~inferiormartin/shishutsu/config"
	"git.sr.ht/~inferiormartin/shishutsu/database"
	"git.sr.ht/~inferiormartin/shishutsu/web"
)

func main() {


@@ 20,10 23,34 @@ func main() {
		log.Fatal(err)
	}

	conn, ok := cfg.Get("shishutsu", "connection-string")
	if !ok {
		log.Fatal("shi-web: unable to get connection-string. shishutsu connection-string not defined?")
	}
	//TODO: better connection management
	db, err := sql.Open("postgres", conn)
	defer db.Close()

	fs := http.FileServer(http.Dir("./static"))
	http.Handle("/static/", http.StripPrefix("/static/", fs))
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		err = tmpl.ExecuteTemplate(w, "index.html", web.getDashboard())
		rows, err := db.Query("SELECT id, account_id, iban, amount, transaction_date, interest_date, description FROM transaction")
		if err != nil {
			log.Fatal(err)
		}
		defer rows.Close()
		var transactions []database.Transaction
		for rows.Next() {
			var t database.Transaction
			if err = rows.Scan(&t.ID, &t.AccountID, &t.IBAN, &t.Amount, &t.TransactionDate, &t.InterestDate, &t.Description); err != nil {
				log.Fatal(err)
			}
			transactions = append(transactions, t)
		}
		if err = rows.Err(); err != nil {
			log.Fatal(err)
		}
		err = tmpl.ExecuteTemplate(w, "index.html", &web.DashboardPage{transactions})
	})

	http.HandleFunc("/sign-in", func(w http.ResponseWriter, r *http.Request) {


@@ 31,10 58,10 @@ func main() {
		case http.MethodGet:
			err = tmpl.ExecuteTemplate(w, "sign-in.html", nil)
		case http.MethodPost:
			_ := r.FormValue("email")
			_ := r.FormValue("password")
			//_ := r.FormValue("email")
			//_ := r.FormValue("password")

			err = tmpl.ExecuteTemplate(w, "index.html", web.getDashboard())
			//err = tmpl.ExecuteTemplate(w, "index.html", web.getDashboard())
		}
	})


A database/transaction.go => database/transaction.go +53 -0
@@ 0,0 1,53 @@
package database

import (
	"database/sql"
	"fmt"
	"time"

	"github.com/lib/pq"
)

type Transaction struct {
	ID              int32
	AccountID       string
	IBAN            string
	Amount          int64
	TransactionDate time.Time
	InterestDate    time.Time
	Description     string
}

func (transaction *Transaction) ToMoney() string {
	money := float64(transaction.Amount) / 100
	return fmt.Sprintf("%.2f", money)
}

//TODO: there probably is a better way to do this
//TODO: do i have to close trans?
func InsertTransactions(transactions []*Transaction, db *sql.DB) error {
	txn, err := db.Begin()
	if err != nil {
		return err
	}
	stmt, err := txn.Prepare(pq.CopyIn("transaction", "account_id", "iban", "amount", "transaction_date", "interest_date", "description"))
	if err != nil {
		return err
	}
	for _, transaction := range transactions {
		_, err := stmt.Exec(transaction.AccountID, transaction.IBAN, transaction.Amount, transaction.TransactionDate, transaction.InterestDate, transaction.Description)
		if err != nil {
			return err
		}
	}
	if _, err = stmt.Exec(); err != nil {
		return err
	}
	if err = stmt.Close(); err != nil {
		return err
	}
	if err = txn.Commit(); err != nil {
		return err
	}
	return nil
}

M templates/transactions.html => templates/transactions.html +6 -20
@@ 1,33 1,19 @@
<table class="styled-table">
    <thead>                                   
        <tr>
            <th>Account</th>
            <!--<th>Owner</th>-->  
            <!--<th>Opponent</th>-->
            <!--<th>Sales Id</th>-->
            <th>Date</th>   
            <!--<th>Currency Date</th>-->
            <th>IBAN</th>   
            <th>Amount</th>
            <!--<th>Currency</th>-->
            <th>Date</th>
            <th>Description</th>
            <!--<th>Sales Detail</th>--> 
            <!--<th>Message</th>-->
        </tr>
    </thead>                        
    <tbody>
        {{ range .Items }}
        {{ range .Transactions }}
        <tr> 
            <!--<td>{{ .AccountId }}</td>-->
            <td>{{ .AccountName }}</td>
            <!--<td>{{ .AccountOpponent }}</td>-->
            <!--<td>{{ .SalesId }}</td>-->
            <td>{{ .Date.Format "02/01/2006" }}</td>
            <!--<td>{{ .CurrencyDate }}</td>-->
            <td>€ {{ .ToEuro }}</td>
            <!--<td>{{ .Currency }}</td>-->
            <td>{{ .IBAN }}</td>
            <td>€ {{ .ToMoney }}</td>
            <td>{{ .TransactionDate.Format "02/01/2006" }}</td>
            <td>{{ .Description }}</td>
            <!--<td>{{ .SalesDetail }}</td>--> 
            <!--<td>{{ .Message }}</td>-->
        </tr>
        {{ end }}                          
    </tbody>

M web/dashboard.go => web/dashboard.go +2 -89
@@ 1,96 1,9 @@
package web

import (
	"encoding/csv"
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"
	"time"
	"git.sr.ht/~inferiormartin/shishutsu/database"
)

type DashboardPage struct {
	Items []DashboardItem
}

type DashboardItem struct {
	AccountId       string
	AccountName     string
	AccountOpponent string
	SalesId         string
	Date            time.Time
	CurrencyDate    time.Time
	Amount          int64
	Currency        string
	Description     string
	SalesDetail     string
	Message         string
}

//TODO: rename: also works for dollars
func toEuro(amount int64) string {
	euro := float64(amount) / 100
	return fmt.Sprintf("%.2f", euro)
}

func (e *DashboardItem) ToEuro() string {
	return toEuro(e.Amount)
}

func parseDate(date string) time.Time {
	r, err := time.Parse("02/01/2006", date)
	if err != nil {
		log.Fatal(err)
	}
	return r
}

func parseFloat(value string) float64 {
	value = strings.Replace(value, ".", "", -1)
	value = strings.Replace(value, ",", ".", -1)
	result, err := strconv.ParseFloat(value, 64)

	if err != nil {
		log.Fatal(err)
	}

	return result
}

//TODO: use cmd/shi-import to import csv
func GetDashboard() *DashboardPage {
	args := os.Args[1:]
	data, err := os.ReadFile(args[0])
	if err != nil {
		log.Fatal(err)
	}
	r := csv.NewReader(strings.NewReader(string(data)))
	r.Comma = ';'
	results, err := r.ReadAll()
	if err != nil {
		log.Fatal(err)
	}
	results = results[1:]

	page := &DashboardPage{make([]DashboardItem, 0)}

	for _, result := range results {
		item := DashboardItem{
			result[0],
			result[1],
			result[2],
			result[3],
			parseDate(result[4]),
			parseDate(result[5]),
			int64(parseFloat(result[6]) * 100),
			result[7],
			result[8],
			result[9],
			result[10],
		}
		page.Items = append(page.Items, item)
	}

	return page
	Transactions []database.Transaction
}