Implement assignment import and clear of budget
This commit is contained in:
parent
6f4bff929e
commit
0b0b20c5ec
@ -2,6 +2,7 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@ -75,3 +76,28 @@ func (h *Handler) budgeting(c *gin.Context) {
|
|||||||
|
|
||||||
c.HTML(http.StatusOK, "budgeting.html", d)
|
c.HTML(http.StatusOK, "budgeting.html", d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) clearBudget(c *gin.Context) {
|
||||||
|
budgetID := c.Param("budgetid")
|
||||||
|
budgetUUID, err := uuid.Parse(budgetID)
|
||||||
|
if err != nil {
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, "/login")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := h.Service.DB.DeleteAllAssignments(c.Request.Context(), budgetUUID)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Deleted %d assignments\n", rows)
|
||||||
|
|
||||||
|
rows, err = h.Service.DB.DeleteAllTransactions(c.Request.Context(), budgetUUID)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Deleted %d transactions\n", rows)
|
||||||
|
}
|
||||||
|
48
http/http.go
48
http/http.go
@ -1,6 +1,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
@ -57,6 +58,7 @@ func (h *Handler) Serve() {
|
|||||||
withBudget.Use(h.verifyLoginWithRedirect)
|
withBudget.Use(h.verifyLoginWithRedirect)
|
||||||
withBudget.Use(h.getImportantData)
|
withBudget.Use(h.getImportantData)
|
||||||
withBudget.GET("/budget/:budgetid", h.budgeting)
|
withBudget.GET("/budget/:budgetid", h.budgeting)
|
||||||
|
withBudget.GET("/budget/:budgetid/clear", h.clearBudget)
|
||||||
withBudget.GET("/budget/:budgetid/:year/:month", h.budgeting)
|
withBudget.GET("/budget/:budgetid/:year/:month", h.budgeting)
|
||||||
withBudget.GET("/budget/:budgetid/all-accounts", h.budget)
|
withBudget.GET("/budget/:budgetid/all-accounts", h.budget)
|
||||||
withBudget.GET("/budget/:budgetid/accounts", h.accounts)
|
withBudget.GET("/budget/:budgetid/accounts", h.accounts)
|
||||||
@ -86,21 +88,9 @@ func (h *Handler) Serve() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) importYNAB(c *gin.Context) {
|
func (h *Handler) importYNAB(c *gin.Context) {
|
||||||
transactionsFile, err := c.FormFile("transactions")
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
transactions, err := transactionsFile.Open()
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
budgetID, succ := c.GetPostForm("budget_id")
|
budgetID, succ := c.GetPostForm("budget_id")
|
||||||
if !succ {
|
if !succ {
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("no budget_id specified"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,7 +106,37 @@ func (h *Handler) importYNAB(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ynab.Import(transactions)
|
transactionsFile, err := c.FormFile("transactions")
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transactions, err := transactionsFile.Open()
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ynab.ImportTransactions(transactions)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assignmentsFile, err := c.FormFile("assignments")
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assignments, err := assignmentsFile.Open()
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ynab.ImportAssignments(assignments)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
|
@ -56,7 +56,61 @@ func NewYNABImport(q *postgres.Queries, budgetID uuid.UUID) (*YNABImport, error)
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ynab *YNABImport) Import(r io.Reader) error {
|
func (ynab *YNABImport) ImportAssignments(r io.Reader) error {
|
||||||
|
csv := csv.NewReader(r)
|
||||||
|
csv.Comma = '\t'
|
||||||
|
csv.LazyQuotes = true
|
||||||
|
|
||||||
|
csvData, err := csv.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not read from tsv: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for _, record := range csvData[1:] {
|
||||||
|
//"Month" "Category Group/Category" "Category Group" "Category" "Budgeted" "Activity" "Available"
|
||||||
|
//"Apr 2019" "Income: Next Month" "Income" "Next Month" 0,00€ 0,00€ 0,00€
|
||||||
|
dateString := record[0]
|
||||||
|
date, err := time.Parse("Jan 2006", dateString)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not parse date %s: %w", dateString, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryGroup, categoryName := record[2], record[3] //also in 1 joined by :
|
||||||
|
category, err := ynab.GetCategory(categoryGroup, categoryName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not get category %s/%s: %w", categoryGroup, categoryName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
amountString := record[4]
|
||||||
|
amount, err := GetAmount(amountString, "0,00€")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not parse amount %s: %w", amountString, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if amount.Int.Int64() == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
assignment := postgres.CreateAssignmentParams{
|
||||||
|
Date: date,
|
||||||
|
CategoryID: category.UUID,
|
||||||
|
Amount: amount,
|
||||||
|
}
|
||||||
|
_, err = ynab.queries.CreateAssignment(ynab.Context, assignment)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not save assignment %v: %w", assignment, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Imported %d assignments\n", count)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ynab *YNABImport) ImportTransactions(r io.Reader) error {
|
||||||
csv := csv.NewReader(r)
|
csv := csv.NewReader(r)
|
||||||
csv.Comma = '\t'
|
csv.Comma = '\t'
|
||||||
csv.LazyQuotes = true
|
csv.LazyQuotes = true
|
||||||
@ -88,10 +142,10 @@ func (ynab *YNABImport) Import(r io.Reader) error {
|
|||||||
return fmt.Errorf("could not get payee %s: %w", payeeName, err)
|
return fmt.Errorf("could not get payee %s: %w", payeeName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
categoryGroup, categoryName := record[5], record[6] //also in 5 + 6 split by group/category
|
categoryGroup, categoryName := record[5], record[6] //also in 4 joined by :
|
||||||
category, err := ynab.GetCategory(categoryGroup, categoryName)
|
category, err := ynab.GetCategory(categoryGroup, categoryName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not get category %s: %w", payeeName, err)
|
return fmt.Errorf("could not get category %s/%s: %w", categoryGroup, categoryName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
memo := record[7]
|
memo := record[7]
|
||||||
|
54
postgres/assignments.sql.go
Normal file
54
postgres/assignments.sql.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// source: assignments.sql
|
||||||
|
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createAssignment = `-- name: CreateAssignment :one
|
||||||
|
INSERT INTO assignments (
|
||||||
|
date, amount, category_id
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3
|
||||||
|
)
|
||||||
|
RETURNING id, category_id, date, memo, amount
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateAssignmentParams struct {
|
||||||
|
Date time.Time
|
||||||
|
Amount Numeric
|
||||||
|
CategoryID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateAssignment(ctx context.Context, arg CreateAssignmentParams) (Assignment, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createAssignment, arg.Date, arg.Amount, arg.CategoryID)
|
||||||
|
var i Assignment
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CategoryID,
|
||||||
|
&i.Date,
|
||||||
|
&i.Memo,
|
||||||
|
&i.Amount,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAllAssignments = `-- name: DeleteAllAssignments :execrows
|
||||||
|
DELETE FROM assignments
|
||||||
|
USING categories
|
||||||
|
INNER JOIN category_groups ON categories.category_group_id = category_groups.id
|
||||||
|
WHERE categories.id = assignments.category_id AND category_groups.budget_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteAllAssignments(ctx context.Context, budgetID uuid.UUID) (int64, error) {
|
||||||
|
result, err := q.db.ExecContext(ctx, deleteAllAssignments, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result.RowsAffected()
|
||||||
|
}
|
@ -91,6 +91,14 @@ func (q *Queries) GetCategories(ctx context.Context, budgetID uuid.UUID) ([]GetC
|
|||||||
|
|
||||||
const getCategoriesWithBalance = `-- name: GetCategoriesWithBalance :many
|
const getCategoriesWithBalance = `-- name: GetCategoriesWithBalance :many
|
||||||
SELECT categories.id, categories.name, category_groups.name as group,
|
SELECT categories.id, categories.name, category_groups.name as group,
|
||||||
|
COALESCE(
|
||||||
|
(
|
||||||
|
SELECT SUM(a_hist.amount)
|
||||||
|
FROM assignments a_hist
|
||||||
|
WHERE categories.id = a_hist.category_id
|
||||||
|
AND a_hist.date < $1
|
||||||
|
)
|
||||||
|
, 0)::decimal(12,2) as balance_assignments,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
(
|
(
|
||||||
SELECT SUM(t_hist.amount)
|
SELECT SUM(t_hist.amount)
|
||||||
@ -124,6 +132,7 @@ type GetCategoriesWithBalanceRow struct {
|
|||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
Name string
|
Name string
|
||||||
Group string
|
Group string
|
||||||
|
BalanceAssignments Numeric
|
||||||
Balance Numeric
|
Balance Numeric
|
||||||
Activity Numeric
|
Activity Numeric
|
||||||
}
|
}
|
||||||
@ -141,6 +150,7 @@ func (q *Queries) GetCategoriesWithBalance(ctx context.Context, arg GetCategorie
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Group,
|
&i.Group,
|
||||||
|
&i.BalanceAssignments,
|
||||||
&i.Balance,
|
&i.Balance,
|
||||||
&i.Activity,
|
&i.Activity,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
@ -15,6 +15,14 @@ type Account struct {
|
|||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Assignment struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
CategoryID uuid.UUID
|
||||||
|
Date time.Time
|
||||||
|
Memo sql.NullString
|
||||||
|
Amount Numeric
|
||||||
|
}
|
||||||
|
|
||||||
type Budget struct {
|
type Budget struct {
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
Name string
|
Name string
|
||||||
|
13
postgres/queries/assignments.sql
Normal file
13
postgres/queries/assignments.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
-- name: CreateAssignment :one
|
||||||
|
INSERT INTO assignments (
|
||||||
|
date, amount, category_id
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: DeleteAllAssignments :execrows
|
||||||
|
DELETE FROM assignments
|
||||||
|
USING categories
|
||||||
|
INNER JOIN category_groups ON categories.category_group_id = category_groups.id
|
||||||
|
WHERE categories.id = assignments.category_id AND category_groups.budget_id = @budget_id;
|
@ -21,6 +21,14 @@ WHERE category_groups.budget_id = $1;
|
|||||||
|
|
||||||
-- name: GetCategoriesWithBalance :many
|
-- name: GetCategoriesWithBalance :many
|
||||||
SELECT categories.id, categories.name, category_groups.name as group,
|
SELECT categories.id, categories.name, category_groups.name as group,
|
||||||
|
COALESCE(
|
||||||
|
(
|
||||||
|
SELECT SUM(a_hist.amount)
|
||||||
|
FROM assignments a_hist
|
||||||
|
WHERE categories.id = a_hist.category_id
|
||||||
|
AND a_hist.date < @from_date
|
||||||
|
)
|
||||||
|
, 0)::decimal(12,2) as balance_assignments,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
(
|
(
|
||||||
SELECT SUM(t_hist.amount)
|
SELECT SUM(t_hist.amount)
|
||||||
|
@ -27,3 +27,9 @@ LEFT JOIN category_groups ON category_groups.id = categories.category_group_id
|
|||||||
WHERE transactions.account_id = $1
|
WHERE transactions.account_id = $1
|
||||||
ORDER BY transactions.date DESC
|
ORDER BY transactions.date DESC
|
||||||
LIMIT 200;
|
LIMIT 200;
|
||||||
|
|
||||||
|
-- name: DeleteAllTransactions :execrows
|
||||||
|
DELETE FROM transactions
|
||||||
|
USING accounts
|
||||||
|
WHERE accounts.budget_id = @budget_id
|
||||||
|
AND accounts.id = transactions.account_id;
|
11
postgres/schema/202112071547_assignments.sql
Normal file
11
postgres/schema/202112071547_assignments.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE assignments (
|
||||||
|
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||||
|
category_id uuid NOT NULL REFERENCES categories (id),
|
||||||
|
date date NOT NULL,
|
||||||
|
memo text,
|
||||||
|
amount decimal(12,2) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE assignments;
|
@ -48,6 +48,21 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteAllTransactions = `-- name: DeleteAllTransactions :execrows
|
||||||
|
DELETE FROM transactions
|
||||||
|
USING accounts
|
||||||
|
WHERE accounts.budget_id = $1
|
||||||
|
AND accounts.id = transactions.account_id
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteAllTransactions(ctx context.Context, budgetID uuid.UUID) (int64, error) {
|
||||||
|
result, err := q.db.ExecContext(ctx, deleteAllTransactions, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
const getTransactionsForAccount = `-- name: GetTransactionsForAccount :many
|
const getTransactionsForAccount = `-- name: GetTransactionsForAccount :many
|
||||||
SELECT transactions.id, transactions.date, transactions.memo, transactions.amount,
|
SELECT transactions.id, transactions.date, transactions.memo, transactions.amount,
|
||||||
accounts.name as account, COALESCE(payees.name, '') as payee, COALESCE(category_groups.name, '') as category_group, COALESCE(categories.name, '') as category
|
accounts.name as account, COALESCE(payees.name, '') as payee, COALESCE(category_groups.name, '') as category_group, COALESCE(categories.name, '') as category
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
<td>
|
<td>
|
||||||
</td>
|
</td>
|
||||||
{{template "amount-cell" .Balance}}
|
{{template "amount-cell" .Balance}}
|
||||||
|
{{template "amount-cell" .BalanceAssignments}}
|
||||||
{{template "amount-cell" .Activity}}
|
{{template "amount-cell" .Activity}}
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user