Compare commits
No commits in common. "d321481e29a3c98e148021d81ac0d735bf93fe07" and "f721e6a910bd40301dc06d7c43a8b1e57942bd1b" have entirely different histories.
d321481e29
...
f721e6a910
@ -1,43 +0,0 @@
|
|||||||
"""Category rule date format
|
|
||||||
|
|
||||||
Revision ID: 7adf89ec8d14
|
|
||||||
Revises: 83603bb7ef9c
|
|
||||||
Create Date: 2022-12-10 00:08:47.535765+00:00
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "7adf89ec8d14"
|
|
||||||
down_revision = "83603bb7ef9c"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.alter_column(
|
|
||||||
"categories_rules",
|
|
||||||
"date",
|
|
||||||
existing_type=sa.VARCHAR(),
|
|
||||||
type_=sa.Date(),
|
|
||||||
existing_nullable=True,
|
|
||||||
schema="transactions",
|
|
||||||
postgresql_using="date::date"
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.alter_column(
|
|
||||||
"categories_rules",
|
|
||||||
"date",
|
|
||||||
existing_type=sa.Date(),
|
|
||||||
type_=sa.VARCHAR(),
|
|
||||||
existing_nullable=True,
|
|
||||||
schema="transactions",
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
"""Amount of transaction per period
|
|
||||||
|
|
||||||
Revision ID: 83603bb7ef9c
|
|
||||||
Revises: 8b5d5fbc8211
|
|
||||||
Create Date: 2022-12-09 23:12:15.644758+00:00
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "83603bb7ef9c"
|
|
||||||
down_revision = "8b5d5fbc8211"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.add_column(
|
|
||||||
"categories_schedules",
|
|
||||||
sa.Column("amount", sa.Integer(), nullable=True),
|
|
||||||
schema="transactions",
|
|
||||||
)
|
|
||||||
op.drop_column("categories_schedules", "recurring", schema="transactions")
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.add_column(
|
|
||||||
"categories_schedules",
|
|
||||||
sa.Column("recurring", sa.BOOLEAN(), autoincrement=False, nullable=False),
|
|
||||||
schema="transactions",
|
|
||||||
)
|
|
||||||
op.drop_column("categories_schedules", "amount", schema="transactions")
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import argparse
|
import argparse
|
||||||
import datetime as dt
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from pfbudget.common.types import Operation
|
from pfbudget.common.types import Operation
|
||||||
@ -110,12 +109,13 @@ def argparser() -> argparse.ArgumentParser:
|
|||||||
"""
|
"""
|
||||||
Categorizing
|
Categorizing
|
||||||
"""
|
"""
|
||||||
categorize = subparsers.add_parser(
|
p_categorize = subparsers.add_parser(
|
||||||
"categorize",
|
"categorize",
|
||||||
description="Categorizes the transactions in the selected database",
|
description="Categorizes the transactions in the selected database",
|
||||||
parents=[universal],
|
parents=[universal],
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
)
|
)
|
||||||
categorize.set_defaults(op=Operation.Categorize)
|
p_categorize.set_defaults(command=Operation.Categorize)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Graph
|
Graph
|
||||||
@ -343,7 +343,7 @@ def category(parser: argparse.ArgumentParser, universal: argparse.ArgumentParser
|
|||||||
rule = commands.add_parser("rule", parents=[universal])
|
rule = commands.add_parser("rule", parents=[universal])
|
||||||
rule.set_defaults(op=Operation.CategoryRule)
|
rule.set_defaults(op=Operation.CategoryRule)
|
||||||
rule.add_argument("category", nargs="+", type=str)
|
rule.add_argument("category", nargs="+", type=str)
|
||||||
rule.add_argument("--date", nargs=1, type=dt.date.fromisoformat)
|
rule.add_argument("--date", nargs=1, type=str)
|
||||||
rule.add_argument("--description", nargs=1, type=str)
|
rule.add_argument("--description", nargs=1, type=str)
|
||||||
rule.add_argument("--bank", nargs=1, type=str)
|
rule.add_argument("--bank", nargs=1, type=str)
|
||||||
rule.add_argument("--min", nargs=1, type=float)
|
rule.add_argument("--min", nargs=1, type=float)
|
||||||
|
|||||||
@ -1,10 +1,4 @@
|
|||||||
from pfbudget.db.model import (
|
from pfbudget.db.model import Transaction, TransactionCategory
|
||||||
Category,
|
|
||||||
CategorySelector,
|
|
||||||
Selector,
|
|
||||||
Transaction,
|
|
||||||
TransactionCategory,
|
|
||||||
)
|
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
@ -15,7 +9,7 @@ class Categorizer:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.options["null_days"] = 4
|
self.options["null_days"] = 4
|
||||||
|
|
||||||
def categorize(self, transactions: list[Transaction], categories: list[Category]):
|
def categorize(self, transactions: list[Transaction]):
|
||||||
"""Overarching categorization tool
|
"""Overarching categorization tool
|
||||||
|
|
||||||
Receives a list of transactions (by ref) and updates their category
|
Receives a list of transactions (by ref) and updates their category
|
||||||
@ -25,7 +19,6 @@ class Categorizer:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
self._nullify(transactions)
|
self._nullify(transactions)
|
||||||
self._rules(transactions, categories)
|
|
||||||
|
|
||||||
def _nullify(self, transactions: list[Transaction]):
|
def _nullify(self, transactions: list[Transaction]):
|
||||||
count = 0
|
count = 0
|
||||||
@ -45,39 +38,10 @@ class Categorizer:
|
|||||||
and cancel.amount == -transaction.amount
|
and cancel.amount == -transaction.amount
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
transaction.category = TransactionCategory(
|
transaction.category = TransactionCategory(name="null")
|
||||||
name="null", selector=CategorySelector(Selector.nullifier)
|
cancel.category = TransactionCategory(name="null")
|
||||||
)
|
|
||||||
cancel.category = TransactionCategory(
|
|
||||||
name="null", selector=CategorySelector(Selector.nullifier)
|
|
||||||
)
|
|
||||||
matching.extend([transaction, cancel])
|
matching.extend([transaction, cancel])
|
||||||
count += 2
|
count += 2
|
||||||
break
|
break
|
||||||
|
|
||||||
print(f"Nullified {count} transactions")
|
print(f"Nullified {count} transactions")
|
||||||
|
|
||||||
def _rules(self, transactions: list[Transaction], categories: list[Category]):
|
|
||||||
for category in [c for c in categories if c.rules]:
|
|
||||||
for rule in category.rules:
|
|
||||||
for transaction in [t for t in transactions if not t.category]:
|
|
||||||
if rule.date:
|
|
||||||
if rule.date < transaction.date:
|
|
||||||
continue
|
|
||||||
if rule.description:
|
|
||||||
if rule.description not in transaction.description:
|
|
||||||
continue
|
|
||||||
if rule.bank:
|
|
||||||
if rule.bank != transaction.bank:
|
|
||||||
continue
|
|
||||||
if rule.min_amount:
|
|
||||||
if rule.min_amount > transaction.amount:
|
|
||||||
continue
|
|
||||||
if rule.max_amount:
|
|
||||||
if rule.max_amount <= transaction.amount:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# passed all conditions, assign category
|
|
||||||
transaction.category = TransactionCategory(
|
|
||||||
category.name, CategorySelector(Selector.rules)
|
|
||||||
)
|
|
||||||
|
|||||||
@ -28,10 +28,7 @@ class Manager:
|
|||||||
# TODO this is a monstrosity, remove when possible
|
# TODO this is a monstrosity, remove when possible
|
||||||
download(self, self.args)
|
download(self, self.args)
|
||||||
case Operation.Categorize:
|
case Operation.Categorize:
|
||||||
with self.db.session() as session:
|
self.categorize()
|
||||||
uncategorized = session.uncategorized()
|
|
||||||
categories = session.categories()
|
|
||||||
Categorizer().categorize(uncategorized, categories)
|
|
||||||
|
|
||||||
case Operation.Register:
|
case Operation.Register:
|
||||||
# self._db = DbClient(args["database"])
|
# self._db = DbClient(args["database"])
|
||||||
@ -109,6 +106,11 @@ class Manager:
|
|||||||
with self.db.session() as session:
|
with self.db.session() as session:
|
||||||
session.add(transactions)
|
session.add(transactions)
|
||||||
|
|
||||||
|
def categorize(self):
|
||||||
|
with self.db.session() as session:
|
||||||
|
uncategorized = session.uncategorized()
|
||||||
|
Categorizer().categorize(uncategorized)
|
||||||
|
|
||||||
# def get_bank_by(self, key: str, value: str) -> Bank:
|
# def get_bank_by(self, key: str, value: str) -> Bank:
|
||||||
# client = DatabaseClient(self.__db)
|
# client = DatabaseClient(self.__db)
|
||||||
# bank = client.get_bank(key, value)
|
# bank = client.get_bank(key, value)
|
||||||
|
|||||||
@ -129,9 +129,5 @@ class DbClient:
|
|||||||
stmt = select(Transaction).where(~Transaction.category.has())
|
stmt = select(Transaction).where(~Transaction.category.has())
|
||||||
return self.__session.scalars(stmt).all()
|
return self.__session.scalars(stmt).all()
|
||||||
|
|
||||||
def categories(self) -> list[Category]:
|
|
||||||
stmt = select(Category)
|
|
||||||
return self.__session.scalars(stmt).all()
|
|
||||||
|
|
||||||
def session(self) -> ClientSession:
|
def session(self) -> ClientSession:
|
||||||
return self.ClientSession(self.engine)
|
return self.ClientSession(self.engine)
|
||||||
|
|||||||
@ -81,7 +81,9 @@ class Transaction(Base):
|
|||||||
bank: Mapped[bankfk]
|
bank: Mapped[bankfk]
|
||||||
amount: Mapped[money]
|
amount: Mapped[money]
|
||||||
|
|
||||||
category: Mapped[Optional[TransactionCategory]] = relationship()
|
category: Mapped[Optional[TransactionCategory]] = relationship(
|
||||||
|
back_populates="original", lazy="joined", default=None
|
||||||
|
)
|
||||||
note: Mapped[Optional[Note]] = relationship(back_populates="original", default=None)
|
note: Mapped[Optional[Note]] = relationship(back_populates="original", default=None)
|
||||||
tags: Mapped[Optional[set[Tag]]] = relationship(
|
tags: Mapped[Optional[set[Tag]]] = relationship(
|
||||||
back_populates="original",
|
back_populates="original",
|
||||||
@ -136,7 +138,8 @@ class TransactionCategory(Base):
|
|||||||
id: Mapped[idfk] = mapped_column(primary_key=True, init=False)
|
id: Mapped[idfk] = mapped_column(primary_key=True, init=False)
|
||||||
name: Mapped[str] = mapped_column(ForeignKey(Category.name))
|
name: Mapped[str] = mapped_column(ForeignKey(Category.name))
|
||||||
|
|
||||||
selector: Mapped[CategorySelector] = relationship()
|
original: Mapped[Transaction] = relationship(back_populates="category")
|
||||||
|
selector: Mapped[CategorySelector] = relationship(back_populates="category")
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Category({self.name})"
|
return f"Category({self.name})"
|
||||||
@ -179,15 +182,12 @@ class CategoryRule(Base):
|
|||||||
|
|
||||||
id: Mapped[idpk] = mapped_column(autoincrement=True, init=False)
|
id: Mapped[idpk] = mapped_column(autoincrement=True, init=False)
|
||||||
name: Mapped[catfk] = mapped_column()
|
name: Mapped[catfk] = mapped_column()
|
||||||
date: Mapped[Optional[dt.date]] = mapped_column()
|
date: Mapped[Optional[str]] = mapped_column()
|
||||||
description: Mapped[Optional[str]] = mapped_column()
|
description: Mapped[Optional[str]] = mapped_column()
|
||||||
bank: Mapped[Optional[str]] = mapped_column()
|
bank: Mapped[Optional[str]] = mapped_column()
|
||||||
min_amount: Mapped[Optional[float]] = mapped_column()
|
min_amount: Mapped[Optional[float]] = mapped_column()
|
||||||
max_amount: Mapped[Optional[float]] = mapped_column()
|
max_amount: Mapped[Optional[float]] = mapped_column()
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash(self.id)
|
|
||||||
|
|
||||||
|
|
||||||
class Selector(enum.Enum):
|
class Selector(enum.Enum):
|
||||||
unknown = enum.auto()
|
unknown = enum.auto()
|
||||||
@ -211,10 +211,11 @@ class CategorySelector(Base):
|
|||||||
BigInteger,
|
BigInteger,
|
||||||
ForeignKey(TransactionCategory.id, ondelete="CASCADE"),
|
ForeignKey(TransactionCategory.id, ondelete="CASCADE"),
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
init=False,
|
|
||||||
)
|
)
|
||||||
selector: Mapped[categoryselector]
|
selector: Mapped[categoryselector]
|
||||||
|
|
||||||
|
category: Mapped[TransactionCategory] = relationship(back_populates="selector")
|
||||||
|
|
||||||
|
|
||||||
class Period(enum.Enum):
|
class Period(enum.Enum):
|
||||||
daily = "daily"
|
daily = "daily"
|
||||||
@ -230,11 +231,15 @@ class CategorySchedule(Base):
|
|||||||
__tablename__ = "categories_schedules"
|
__tablename__ = "categories_schedules"
|
||||||
|
|
||||||
name: Mapped[catfk] = mapped_column(primary_key=True)
|
name: Mapped[catfk] = mapped_column(primary_key=True)
|
||||||
|
recurring: Mapped[bool]
|
||||||
period: Mapped[Optional[scheduleperiod]]
|
period: Mapped[Optional[scheduleperiod]]
|
||||||
period_multiplier: Mapped[Optional[int]]
|
period_multiplier: Mapped[Optional[int]]
|
||||||
amount: Mapped[Optional[int]]
|
|
||||||
|
|
||||||
category: Mapped[Category] = relationship(back_populates="schedule")
|
category: Mapped[Category] = relationship(back_populates="schedule")
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"{self.name} schedule=Schedule(period={self.period}, multiplier={self.period_multiplier}, amount={self.amount})"
|
return (
|
||||||
|
f"{self.name} schedule=Schedule(period={self.period}, multiplier={self.period_multiplier})"
|
||||||
|
if self.recurring
|
||||||
|
else f"{self.name} has no Schedule"
|
||||||
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user