Implement transfer creation #19

Merged
jacob1123 merged 5 commits from enable-transfers into master 2022-02-24 00:13:20 +01:00
8 changed files with 110 additions and 63 deletions

View File

@ -130,7 +130,7 @@ func (q *Queries) GetAccountsWithBalance(ctx context.Context, budgetID uuid.UUID
} }
const searchAccounts = `-- name: SearchAccounts :many const searchAccounts = `-- name: SearchAccounts :many
SELECT accounts.id, accounts.budget_id, accounts.name FROM accounts SELECT accounts.id, accounts.budget_id, accounts.name, true as is_account FROM accounts
WHERE accounts.budget_id = $1 WHERE accounts.budget_id = $1
AND accounts.name LIKE $2 AND accounts.name LIKE $2
ORDER BY accounts.name ORDER BY accounts.name
@ -142,9 +142,10 @@ type SearchAccountsParams struct {
} }
type SearchAccountsRow struct { type SearchAccountsRow struct {
ID uuid.UUID ID uuid.UUID
BudgetID uuid.UUID BudgetID uuid.UUID
Name string Name string
IsAccount bool
} }
func (q *Queries) SearchAccounts(ctx context.Context, arg SearchAccountsParams) ([]SearchAccountsRow, error) { func (q *Queries) SearchAccounts(ctx context.Context, arg SearchAccountsParams) ([]SearchAccountsRow, error) {
@ -156,7 +157,12 @@ func (q *Queries) SearchAccounts(ctx context.Context, arg SearchAccountsParams)
var items []SearchAccountsRow var items []SearchAccountsRow
for rows.Next() { for rows.Next() {
var i SearchAccountsRow var i SearchAccountsRow
if err := rows.Scan(&i.ID, &i.BudgetID, &i.Name); err != nil { if err := rows.Scan(
&i.ID,
&i.BudgetID,
&i.Name,
&i.IsAccount,
); err != nil {
return nil, err return nil, err
} }
items = append(items, i) items = append(items, i)

View File

@ -83,6 +83,10 @@ func (n Numeric) Sub(other Numeric) Numeric {
panic("Cannot subtract with different exponents") panic("Cannot subtract with different exponents")
} }
func (n Numeric) Neg() Numeric {
return Numeric{pgtype.Numeric{Exp: n.Exp, Int: big.NewInt(-1 * n.Int.Int64()), Status: n.Status}}
}
func (n Numeric) Add(other Numeric) Numeric { func (n Numeric) Add(other Numeric) Numeric {
left := n left := n
right := other right := other

View File

@ -22,7 +22,7 @@ GROUP BY accounts.id, accounts.name
ORDER BY accounts.name; ORDER BY accounts.name;
-- name: SearchAccounts :many -- name: SearchAccounts :many
SELECT accounts.id, accounts.budget_id, accounts.name FROM accounts SELECT accounts.id, accounts.budget_id, accounts.name, true as is_account FROM accounts
WHERE accounts.budget_id = @budget_id WHERE accounts.budget_id = @budget_id
AND accounts.name LIKE @search AND accounts.name LIKE @search
ORDER BY accounts.name; ORDER BY accounts.name;

View File

@ -1,6 +1,7 @@
package server package server
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"time" "time"
@ -14,8 +15,9 @@ import (
type NewTransactionPayload struct { type NewTransactionPayload struct {
Date JSONDate `json:"date"` Date JSONDate `json:"date"`
Payee struct { Payee struct {
ID uuid.NullUUID ID uuid.NullUUID
Name string Name string
IsAccount bool
} `json:"payee"` } `json:"payee"`
Category struct { Category struct {
ID uuid.NullUUID ID uuid.NullUUID
@ -36,39 +38,42 @@ func (h *Handler) newTransaction(c *gin.Context) {
return return
} }
amount := numeric.Numeric{} amount, err := numeric.Parse(payload.Amount)
err = amount.Set(payload.Amount)
if err != nil { if err != nil {
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("amount: %w", err)) c.AbortWithError(http.StatusBadRequest, fmt.Errorf("amount: %w", err))
return return
} }
payeeID := payload.Payee.ID newTransaction := postgres.CreateTransactionParams{
if !payeeID.Valid && payload.Payee.Name != "" { Memo: payload.Memo,
newPayee := postgres.CreatePayeeParams{ Date: time.Time(payload.Date),
Name: payload.Payee.Name, Amount: amount,
BudgetID: payload.BudgetID, Status: postgres.TransactionStatus(payload.State),
}
if payload.Payee.IsAccount {
newTransaction.GroupID = uuid.NullUUID{UUID: uuid.New(), Valid: true}
newTransaction.Amount = amount.Neg()
newTransaction.AccountID = payload.Payee.ID.UUID
newTransaction.CategoryID = uuid.NullUUID{}
_, err = h.Service.CreateTransaction(c.Request.Context(), newTransaction)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create transfer transaction: %w", err))
return
} }
payee, err := h.Service.CreatePayee(c.Request.Context(), newPayee)
newTransaction.Amount = amount
} else {
payeeID, err := GetPayeeID(c.Request.Context(), payload, h)
if err != nil { if err != nil {
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create payee: %w", err)) c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create payee: %w", err))
} }
newTransaction.PayeeID = payeeID
payeeID = uuid.NullUUID{
UUID: payee.ID,
Valid: true,
}
} }
newTransaction := postgres.CreateTransactionParams{ newTransaction.CategoryID = payload.Category.ID
Memo: payload.Memo, newTransaction.AccountID = payload.AccountID
Date: time.Time(payload.Date),
Amount: amount,
AccountID: payload.AccountID,
PayeeID: payeeID,
CategoryID: payload.Category.ID,
Status: postgres.TransactionStatus(payload.State),
}
transaction, err := h.Service.CreateTransaction(c.Request.Context(), newTransaction) transaction, err := h.Service.CreateTransaction(c.Request.Context(), newTransaction)
if err != nil { if err != nil {
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create transaction: %w", err)) c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create transaction: %w", err))
@ -77,3 +82,25 @@ func (h *Handler) newTransaction(c *gin.Context) {
c.JSON(http.StatusOK, transaction) c.JSON(http.StatusOK, transaction)
} }
func GetPayeeID(context context.Context, payload NewTransactionPayload, h *Handler) (uuid.NullUUID, error) {
payeeID := payload.Payee.ID
if payeeID.Valid {
return payeeID, nil
}
if payload.Payee.Name == "" {
return uuid.NullUUID{}, nil
}
newPayee := postgres.CreatePayeeParams{
Name: payload.Payee.Name,
BudgetID: payload.BudgetID,
}
payee, err := h.Service.CreatePayee(context, newPayee)
if err != nil {
return uuid.NullUUID{}, fmt.Errorf("create payee: %w", err)
}
return uuid.NullUUID{UUID: payee.ID, Valid: true}, nil
}

View File

@ -0,0 +1,8 @@
<script lang="ts" setup>
</script>
<template>
<button class="px-4 py-2 text-base font-medium rounded-md shadow-sm focus:outline-none focus:ring-2">
<slot></slot>
</button>
</template>

View File

@ -5,11 +5,6 @@ import { useBudgetsStore } from "../stores/budget"
import { useAccountStore } from "../stores/budget-account" import { useAccountStore } from "../stores/budget-account"
import { useSettingsStore } from "../stores/settings" import { useSettingsStore } from "../stores/settings"
const props = defineProps<{
budgetid: string,
accountid: string,
}>();
const ExpandMenu = computed(() => useSettingsStore().Menu.Expand); const ExpandMenu = computed(() => useSettingsStore().Menu.Expand);
const budgetStore = useBudgetsStore(); const budgetStore = useBudgetsStore();
@ -30,7 +25,7 @@ const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBa
{{CurrentBudgetName}} {{CurrentBudgetName}}
</span> </span>
<span class="bg-orange-200 rounded-lg m-1 p-1 px-3 flex flex-col"> <span class="bg-orange-200 rounded-lg m-1 p-1 px-3 flex flex-col">
<router-link :to="'/budget/'+budgetid+'/budgeting'">Budget</router-link><br /> <router-link :to="'/budget/'+CurrentBudgetID+'/budgeting'">Budget</router-link><br />
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/reports'">Reports</router-link>--> <!--<router-link :to="'/budget/'+CurrentBudgetID+'/reports'">Reports</router-link>-->
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/all-accounts'">All Accounts</router-link>--> <!--<router-link :to="'/budget/'+CurrentBudgetID+'/all-accounts'">All Accounts</router-link>-->
</span> </span>
@ -40,7 +35,7 @@ const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBa
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="OnBudgetAccountsBalance" /> <Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="OnBudgetAccountsBalance" />
</div> </div>
<div v-for="account in OnBudgetAccounts" class="flex flex-row justify-between"> <div v-for="account in OnBudgetAccounts" class="flex flex-row justify-between">
<router-link :to="'/budget/'+budgetid+'/account/'+account.ID">{{account.Name}}</router-link> <router-link :to="'/budget/'+CurrentBudgetID+'/account/'+account.ID">{{account.Name}}</router-link>
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="account.Balance" /> <Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="account.Balance" />
</div> </div>
</li> </li>
@ -50,7 +45,7 @@ const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBa
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="OffBudgetAccountsBalance" /> <Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="OffBudgetAccountsBalance" />
</div> </div>
<div v-for="account in OffBudgetAccounts" class="flex flex-row justify-between"> <div v-for="account in OffBudgetAccounts" class="flex flex-row justify-between">
<router-link :to="'/budget/'+budgetid+'/account/'+account.ID">{{account.Name}}</router-link> <router-link :to="'/budget/'+CurrentBudgetID+'/account/'+account.ID">{{account.Name}}</router-link>
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="account.Balance" /> <Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="account.Balance" />
</div> </div>
</li> </li>

View File

@ -5,6 +5,7 @@ import { DELETE, POST } from "../api";
import { useBudgetsStore } from "../stores/budget"; import { useBudgetsStore } from "../stores/budget";
import { useSessionStore } from "../stores/session"; import { useSessionStore } from "../stores/session";
import Card from "../components/Card.vue"; import Card from "../components/Card.vue";
import Button from "../components/Button.vue";
const transactionsFile = ref<File | undefined>(undefined); const transactionsFile = ref<File | undefined>(undefined);
const assignmentsFile = ref<File | undefined>(undefined); const assignmentsFile = ref<File | undefined>(undefined);
@ -57,37 +58,41 @@ function ynabImport() {
<template> <template>
<div> <div>
<h1>Danger Zone</h1> <h1>Danger Zone</h1>
<div class="grid md:grid-cols-2 gap-6"> <div class="grid md:grid-cols-2 gap-6">
<Card> <Card class="flex-col p-3">
<h2>Clear Budget</h2> <h2 class="text-lg font-bold">Clear Budget</h2>
<p>This removes transactions and assignments to start from scratch. Accounts and categories are kept. Not undoable!</p> <p>This removes transactions and assignments to start from scratch. Accounts and categories are kept. Not undoable!</p>
<button @click="clearBudget">Clear budget</button> <Button class="bg-red-500" @click="clearBudget">Clear budget</Button>
</Card> </Card>
<Card> <Card class="flex-col p-3">
<h2>Delete Budget</h2> <h2 class="text-lg font-bold">Delete Budget</h2>
<p>This deletes the whole bugdet including all transactions, assignments, accounts and categories. Not undoable!</p> <p>This deletes the whole bugdet including all transactions, assignments, accounts and categories. Not undoable!</p>
<button @click="deleteBudget">Delete budget</button> <Button class="bg-red-500" @click="deleteBudget">Delete budget</button>
</Card> </Card>
<Card> <Card class="flex-col p-3">
<h2>Fix all historic negative category-balances</h2> <h2 class="text-lg font-bold">Fix all historic negative category-balances</h2>
<p>This restores YNABs functionality, that would substract any overspent categories' balances from next months inflows.</p> <p>This restores YNABs functionality, that would substract any overspent categories' balances from next months inflows.</p>
<button @click="cleanNegative">Fix negative</button> <Button class="bg-orange-500" @click="cleanNegative">Fix negative</button>
</Card> </Card>
<Card> <Card class="flex-col p-3">
<h2>Import YNAB Budget</h2> <h2 class="text-lg font-bold">Import YNAB Budget</h2>
<div class="flex flex-row">
<div>
<label for="transactions_file">
Transaktionen:
<input type="file" @change="gotTransactions" accept="text/*" />
</label>
<br />
<label for="assignments_file">
Budget:
<input type="file" @change="gotAssignments" accept="text/*" />
</label>
</div>
<label for="transactions_file"> <Button class="bg-blue-500" :disabled="filesIncomplete" @click="ynabImport">Importieren</Button>
Transaktionen: </div>
<input type="file" @change="gotTransactions" accept="text/*" />
</label>
<br />
<label for="assignments_file">
Budget:
<input type="file" @change="gotAssignments" accept="text/*" />
</label>
<button :disabled="filesIncomplete" @click="ynabImport">Importieren</button>
</Card> </Card>
</div> </div>
</div> </div>

View File

@ -36,8 +36,10 @@ export const useSessionStore = defineStore('session', {
this.Session = { this.Session = {
User: x.User, User: x.User,
Token: x.Token, Token: x.Token,
}, }
this.Budgets = x.Budgets; for (const budget of x.Budgets) {
this.Budgets.set(budget.ID, budget);
}
}, },
async login(login: any) { async login(login: any) {
const response = await POST("/user/login", JSON.stringify(login)); const response = await POST("/user/login", JSON.stringify(login));