Compare commits
7 Commits
fd24ac3318
...
e670d3ddee
| Author | SHA1 | Date | |
|---|---|---|---|
| e670d3ddee | |||
| 2263175348 | |||
| d289473271 | |||
| f943374ade | |||
| abff76ad4e | |||
| e0fc310ef6 | |||
| 13709b7a04 |
@ -1,3 +1,6 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
from pfbudget.cli.runnable import argparser
|
from pfbudget.cli.runnable import argparser
|
||||||
from pfbudget.common.types import Operation
|
from pfbudget.common.types import Operation
|
||||||
from pfbudget.core.manager import Manager
|
from pfbudget.core.manager import Manager
|
||||||
@ -7,10 +10,13 @@ from pfbudget.utils.utils import parse_args_period
|
|||||||
|
|
||||||
def interactive(manager: Manager):
|
def interactive(manager: Manager):
|
||||||
with manager.db.session() as session:
|
with manager.db.session() as session:
|
||||||
|
|
||||||
categories = session.get(type.Category)
|
categories = session.get(type.Category)
|
||||||
print(f"Available categories: {categories}")
|
print(f"Available categories: {[c.name for c in categories]}")
|
||||||
print(f"Available tags: {session.get(type.Tag)}")
|
tags = session.get(type.Tag)
|
||||||
transactions = session.get(type.Transaction, ~type.Transaction.category.has())
|
print(f"Available tags: {[t.name for t in tags]}")
|
||||||
|
|
||||||
|
transactions = session.uncategorized()
|
||||||
print(f"{len(transactions)} transactions left to categorize")
|
print(f"{len(transactions)} transactions left to categorize")
|
||||||
|
|
||||||
for transaction in sorted(transactions):
|
for transaction in sorted(transactions):
|
||||||
@ -18,34 +24,91 @@ def interactive(manager: Manager):
|
|||||||
quit = False
|
quit = False
|
||||||
next = True
|
next = True
|
||||||
while next:
|
while next:
|
||||||
match (input("(<category>/split/tag/note/quit): ")):
|
match (input("(<category>(:tag)/split/note/skip/quit): ")):
|
||||||
|
case "skip":
|
||||||
|
next = False
|
||||||
|
continue
|
||||||
|
|
||||||
case "quit" | "exit":
|
case "quit" | "exit":
|
||||||
next = False
|
next = False
|
||||||
quit = True
|
quit = True
|
||||||
|
|
||||||
case "tag":
|
case "split":
|
||||||
tag = input("tag: ")
|
manager.action(Operation.Split, split(transaction, categories, tags))
|
||||||
transaction.tags.add(type.TransactionTag(tag))
|
next = False
|
||||||
|
|
||||||
case "note":
|
case "note":
|
||||||
note = input("note: ")
|
note = input("note: ")
|
||||||
transaction.note = type.Note(note)
|
transaction.note = type.Note(note)
|
||||||
|
|
||||||
case other:
|
case other:
|
||||||
if other not in [c.name for c in categories]:
|
if len(li := other.split(":")) > 1:
|
||||||
print(f"{other} is not a valid category")
|
_category = li[0]
|
||||||
|
_tags = li[1:]
|
||||||
|
else:
|
||||||
|
_category = other
|
||||||
|
_tags = []
|
||||||
|
|
||||||
|
if _category not in [c.name for c in categories]:
|
||||||
|
print(f"{other} doesn't have a valid category")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
transaction.category = type.TransactionCategory(
|
transaction.category = type.TransactionCategory(
|
||||||
other,
|
_category,
|
||||||
type.CategorySelector(type.Selector_T.manual),
|
type.CategorySelector(type.Selector_T.manual),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for tag in _tags:
|
||||||
|
if tag not in [t.name for t in tags]:
|
||||||
|
session.add([type.Tag(tag)])
|
||||||
|
tags = session.get(type.Tag)
|
||||||
|
|
||||||
|
transaction.tags.add(type.TransactionTag(tag))
|
||||||
|
|
||||||
next = False
|
next = False
|
||||||
|
|
||||||
|
session.commit()
|
||||||
if quit:
|
if quit:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def split(
|
||||||
|
original: type.Transaction,
|
||||||
|
categories: Sequence[type.Category],
|
||||||
|
tags: Sequence[type.Tag],
|
||||||
|
) -> list[type.Transaction]:
|
||||||
|
|
||||||
|
total = original.amount
|
||||||
|
splitted: list[type.Transaction] = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if abs(sum(t.amount for t in splitted)) > abs(total):
|
||||||
|
print(
|
||||||
|
"The total amount from the splitted transactions exceeds the original transaction amount, please try again..."
|
||||||
|
)
|
||||||
|
splitted.clear()
|
||||||
|
|
||||||
|
if sum(t.amount for t in splitted) == total:
|
||||||
|
break
|
||||||
|
|
||||||
|
while (category := input("New transaction category: ")) not in [
|
||||||
|
c.name for c in categories
|
||||||
|
]:
|
||||||
|
print(f"{category} is not a valid category")
|
||||||
|
|
||||||
|
amount = input("amount: ")
|
||||||
|
|
||||||
|
split = type.Transaction(original.date, original.description, Decimal(amount))
|
||||||
|
split.category = type.TransactionCategory(
|
||||||
|
category, type.CategorySelector(type.Selector_T.manual)
|
||||||
|
)
|
||||||
|
|
||||||
|
splitted.append(split)
|
||||||
|
|
||||||
|
splitted.insert(0, original)
|
||||||
|
return splitted
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
argparser = argparser()
|
argparser = argparser()
|
||||||
args = vars(argparser.parse_args())
|
args = vars(argparser.parse_args())
|
||||||
|
|||||||
@ -35,6 +35,7 @@ class Categorizer:
|
|||||||
|
|
||||||
@Timer(name="nullify")
|
@Timer(name="nullify")
|
||||||
def _nullify(self, transactions: Sequence[t.BankTransaction]):
|
def _nullify(self, transactions: Sequence[t.BankTransaction]):
|
||||||
|
print(f"Nullifying {len(transactions)} transactions")
|
||||||
count = 0
|
count = 0
|
||||||
matching = []
|
matching = []
|
||||||
for transaction in transactions:
|
for transaction in transactions:
|
||||||
@ -72,6 +73,7 @@ class Categorizer:
|
|||||||
transactions: Sequence[t.BankTransaction],
|
transactions: Sequence[t.BankTransaction],
|
||||||
categories: Sequence[t.Category],
|
categories: Sequence[t.Category],
|
||||||
):
|
):
|
||||||
|
print(f"Categorizing {len(transactions)} transactions")
|
||||||
d = {}
|
d = {}
|
||||||
for category in [c for c in categories if c.rules]:
|
for category in [c for c in categories if c.rules]:
|
||||||
for rule in category.rules:
|
for rule in category.rules:
|
||||||
@ -112,6 +114,7 @@ class Categorizer:
|
|||||||
def _rule_based_tags(
|
def _rule_based_tags(
|
||||||
self, transactions: Sequence[t.BankTransaction], tags: Sequence[t.Tag]
|
self, transactions: Sequence[t.BankTransaction], tags: Sequence[t.Tag]
|
||||||
):
|
):
|
||||||
|
print(f"Tagging {len(transactions)} transactions")
|
||||||
d = {}
|
d = {}
|
||||||
for tag in [t for t in tags if len(t.rules) > 0]:
|
for tag in [t for t in tags if len(t.rules) > 0]:
|
||||||
for rule in tag.rules:
|
for rule in tag.rules:
|
||||||
|
|||||||
@ -116,7 +116,14 @@ class Manager:
|
|||||||
banks = NordigenInput().country_banks(params[0])
|
banks = NordigenInput().country_banks(params[0])
|
||||||
print(banks)
|
print(banks)
|
||||||
|
|
||||||
case Operation.BankAdd | Operation.CategoryAdd | Operation.NordigenAdd | Operation.RuleAdd | Operation.TagAdd | Operation.TagRuleAdd:
|
case (
|
||||||
|
Operation.BankAdd
|
||||||
|
| Operation.CategoryAdd
|
||||||
|
| Operation.NordigenAdd
|
||||||
|
| Operation.RuleAdd
|
||||||
|
| Operation.TagAdd
|
||||||
|
| Operation.TagRuleAdd
|
||||||
|
):
|
||||||
with self.db.session() as session:
|
with self.db.session() as session:
|
||||||
session.add(params)
|
session.add(params)
|
||||||
|
|
||||||
@ -190,12 +197,20 @@ class Manager:
|
|||||||
assert len(originals) == 1, ">1 transactions matched {original.id}!"
|
assert len(originals) == 1, ">1 transactions matched {original.id}!"
|
||||||
|
|
||||||
originals[0].split = True
|
originals[0].split = True
|
||||||
transactions = [
|
transactions = []
|
||||||
SplitTransaction(
|
for t in params[1:]:
|
||||||
originals[0].date, t.description, t.amount, originals[0].id
|
if originals[0].date != t.date:
|
||||||
|
t.date = originals[0].date
|
||||||
|
print(
|
||||||
|
f"{t.date} is different from original date {originals[0].date}, using original"
|
||||||
|
)
|
||||||
|
|
||||||
|
splitted = SplitTransaction(
|
||||||
|
t.date, t.description, t.amount, originals[0].id
|
||||||
)
|
)
|
||||||
for t in params[1:]
|
splitted.category = t.category
|
||||||
]
|
transactions.append(splitted)
|
||||||
|
|
||||||
session.add(transactions)
|
session.add(transactions)
|
||||||
|
|
||||||
case Operation.Export:
|
case Operation.Export:
|
||||||
|
|||||||
@ -2,6 +2,7 @@ from dataclasses import asdict
|
|||||||
from sqlalchemy import create_engine, delete, select, update
|
from sqlalchemy import create_engine, delete, select, update
|
||||||
from sqlalchemy.dialects.postgresql import insert
|
from sqlalchemy.dialects.postgresql import insert
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy.sql.expression import false
|
||||||
from typing import Sequence, Type, TypeVar
|
from typing import Sequence, Type, TypeVar
|
||||||
|
|
||||||
from pfbudget.db.model import (
|
from pfbudget.db.model import (
|
||||||
@ -9,6 +10,7 @@ from pfbudget.db.model import (
|
|||||||
CategoryGroup,
|
CategoryGroup,
|
||||||
CategorySchedule,
|
CategorySchedule,
|
||||||
Link,
|
Link,
|
||||||
|
Transaction,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -57,6 +59,22 @@ class DbClient:
|
|||||||
|
|
||||||
return self.__session.scalars(stmt).all()
|
return self.__session.scalars(stmt).all()
|
||||||
|
|
||||||
|
def uncategorized(self) -> Sequence[Transaction]:
|
||||||
|
"""Selects all valid uncategorized transactions
|
||||||
|
At this moment that includes:
|
||||||
|
- Categories w/o category
|
||||||
|
- AND non-split categories
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Sequence[Transaction]: transactions left uncategorized
|
||||||
|
"""
|
||||||
|
stmt = (
|
||||||
|
select(Transaction)
|
||||||
|
.where(~Transaction.category.has())
|
||||||
|
.where(Transaction.split == false())
|
||||||
|
)
|
||||||
|
return self.__session.scalars(stmt).all()
|
||||||
|
|
||||||
def add(self, rows: list):
|
def add(self, rows: list):
|
||||||
self.__session.add_all(rows)
|
self.__session.add_all(rows)
|
||||||
|
|
||||||
|
|||||||
@ -91,7 +91,7 @@ class Transaction(Base, Export):
|
|||||||
description: Mapped[Optional[str]]
|
description: Mapped[Optional[str]]
|
||||||
amount: Mapped[money]
|
amount: Mapped[money]
|
||||||
|
|
||||||
split: Mapped[bool] = mapped_column(init=False)
|
split: Mapped[bool] = mapped_column(init=False, default=False)
|
||||||
|
|
||||||
type: Mapped[str] = mapped_column(init=False)
|
type: Mapped[str] = mapped_column(init=False)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user