336 lines
11 KiB
Python
336 lines
11 KiB
Python
# German: Soll -> Abgang vom Konto
|
|
# Haben -> Zugang aufs Konto
|
|
# English: Debit -> from account
|
|
# Credit -> to account
|
|
from enum import IntEnum
|
|
from sqlalchemy import func, case, and_, or_
|
|
from sqlalchemy.ext.hybrid import hybrid_property
|
|
from datetime import datetime
|
|
from werkzeug.exceptions import BadRequest, NotFound, Conflict
|
|
|
|
from flaschengeist.database import db
|
|
from flaschengeist.models.user import User, _UserAttribute
|
|
from flaschengeist.app import logger
|
|
|
|
from .models import Transaction
|
|
from . import permissions, BalancePlugin
|
|
|
|
__attribute_limit = "balance_limit"
|
|
|
|
|
|
class NotifyType(IntEnum):
|
|
SEND_TO = 0x01
|
|
SEND_FROM = 0x02
|
|
ADD_FROM = 0x03
|
|
SUB_FROM = 0x04
|
|
|
|
|
|
def set_limit(user: User, limit: float, override=True):
|
|
if override or not user.has_attribute(__attribute_limit):
|
|
user.set_attribute(__attribute_limit, limit)
|
|
db.session.commit()
|
|
|
|
|
|
def get_limit(user: User) -> float:
|
|
return user.get_attribute(__attribute_limit, default=None)
|
|
|
|
|
|
def get_balance(user, start: datetime = None, end: datetime = None):
|
|
query = db.session.query(func.sum(Transaction.amount))
|
|
if start:
|
|
query = query.filter(start <= Transaction.time)
|
|
if end:
|
|
query = query.filter(Transaction.time <= end)
|
|
|
|
credit = query.filter(Transaction.receiver_ == user).scalar() or 0
|
|
debit = query.filter(Transaction.sender_ == user).scalar() or 0
|
|
return credit, debit, credit - debit
|
|
|
|
|
|
def get_balances(
|
|
start: datetime = None,
|
|
end: datetime = None,
|
|
limit=None,
|
|
offset=None,
|
|
descending=None,
|
|
sortBy=None,
|
|
_filter=None,
|
|
):
|
|
logger.debug(
|
|
f"get_balances(start={start}, end={end}, limit={limit}, offset={offset}, descending={descending}, sortBy={sortBy}, _filter={_filter})"
|
|
)
|
|
|
|
class _User(User):
|
|
_debit = db.relationship(Transaction, back_populates="sender_", foreign_keys=[Transaction._sender_id])
|
|
_credit = db.relationship(
|
|
Transaction,
|
|
back_populates="receiver_",
|
|
foreign_keys=[Transaction._receiver_id],
|
|
)
|
|
|
|
@hybrid_property
|
|
def debit(self):
|
|
return sum([cred.amount for cred in self._debit])
|
|
|
|
@debit.expression
|
|
def debit(cls):
|
|
a = (
|
|
db.select(func.sum(Transaction.amount))
|
|
.where(cls.id_ == Transaction._sender_id, Transaction.amount)
|
|
.scalar_subquery()
|
|
)
|
|
return case([(a, a)], else_=0)
|
|
|
|
@hybrid_property
|
|
def credit(self):
|
|
return sum([cred.amount for cred in self._credit])
|
|
|
|
@credit.expression
|
|
def credit(cls):
|
|
b = (
|
|
db.select(func.sum(Transaction.amount))
|
|
.where(cls.id_ == Transaction._receiver_id, Transaction.amount)
|
|
.scalar_subquery()
|
|
)
|
|
return case([(b, b)], else_=0)
|
|
|
|
@hybrid_property
|
|
def limit(self):
|
|
return self.get_attribute("balance_limit", None)
|
|
|
|
@limit.expression
|
|
def limit(cls):
|
|
return (
|
|
db.select(_UserAttribute.value)
|
|
.where(
|
|
and_(
|
|
cls.id_ == _UserAttribute.user,
|
|
_UserAttribute.name == "balance_limit",
|
|
)
|
|
)
|
|
.scalar_subquery()
|
|
)
|
|
|
|
def get_debit(self, start: datetime = None, end: datetime = None):
|
|
if start and end:
|
|
return sum([deb.amount for deb in self._debit if start <= deb.time and deb.time <= end])
|
|
if start:
|
|
return sum([deb.amount for deb in self._dedit if start <= deb.time])
|
|
if end:
|
|
return sum([deb.amount for deb in self._dedit if deb.time <= end])
|
|
return self.debit
|
|
|
|
def get_credit(self, start: datetime = None, end: datetime = None):
|
|
if start and end:
|
|
return sum([cred.amount for cred in self._credit if start <= cred.time and cred.time <= end])
|
|
if start:
|
|
return sum([cred.amount for cred in self._credit if start <= cred.time])
|
|
if end:
|
|
return sum([cred.amount for cred in self._credit if cred.time <= end])
|
|
return self.credit
|
|
|
|
query = _User.query
|
|
|
|
if start:
|
|
q1 = query.join(_User._credit).filter(start <= Transaction.time)
|
|
q2 = query.join(_User._debit).filter(start <= Transaction.time)
|
|
query = q1.union(q2)
|
|
if end:
|
|
q1 = query.join(_User._credit).filter(Transaction.time <= end)
|
|
q2 = query.join(_User._debit).filter(Transaction.time <= end)
|
|
query = q1.union(q2)
|
|
|
|
if _filter:
|
|
query = query.filter(
|
|
or_(
|
|
_User.firstname.ilike(f"%{_filter.lower()}%"),
|
|
_User.lastname.ilike(f"%{_filter.lower()}%"),
|
|
)
|
|
)
|
|
|
|
if sortBy == "balance":
|
|
if descending:
|
|
query = query.order_by(
|
|
(_User.credit - _User.debit).desc(),
|
|
_User.lastname.asc(),
|
|
_User.firstname.asc(),
|
|
)
|
|
else:
|
|
query = query.order_by(
|
|
(_User.credit - _User.debit).asc(),
|
|
_User.lastname.asc(),
|
|
_User.firstname.asc(),
|
|
)
|
|
elif sortBy == "limit":
|
|
if descending:
|
|
query = query.order_by(_User.limit.desc(), User.lastname.asc(), User.firstname.asc())
|
|
else:
|
|
query = query.order_by(_User.limit.asc(), User.lastname.asc(), User.firstname.asc())
|
|
elif sortBy == "firstname":
|
|
if descending:
|
|
query = query.order_by(User.firstname.desc(), User.lastname.desc())
|
|
else:
|
|
query = query.order_by(User.firstname.asc(), User.lastname.asc())
|
|
elif sortBy == "lastname":
|
|
if descending:
|
|
query = query.order_by(User.lastname.desc(), User.firstname.desc())
|
|
else:
|
|
query = query.order_by(User.lastname.asc(), User.firstname.asc())
|
|
|
|
count = None
|
|
if limit:
|
|
count = query.count()
|
|
query = query.limit(limit)
|
|
if offset:
|
|
query = query.offset(offset)
|
|
users = query
|
|
all = {}
|
|
|
|
for user in users:
|
|
all[user.userid] = [user.get_credit(start, end), 0]
|
|
all[user.userid][1] = user.get_debit(start, end)
|
|
|
|
return all, count
|
|
|
|
|
|
def send(sender: User, receiver, amount: float, author: User):
|
|
"""Send credit from one user to an other
|
|
|
|
Args:
|
|
sender: User who sends the amount
|
|
receiver: User who receives the amount
|
|
amount: Amount to send
|
|
author: User authoring this transaction
|
|
Returns:
|
|
Transaction that was created
|
|
Raises:
|
|
BadRequest if amount <= 0
|
|
"""
|
|
logger.debug(f"send(sender={sender}, receiver={receiver}, amount={amount}, author={author})")
|
|
if amount <= 0:
|
|
raise BadRequest
|
|
|
|
if sender and sender.has_attribute(__attribute_limit):
|
|
if (get_balance(sender)[2] - amount) < sender.get_attribute(__attribute_limit) and not author.has_permission(
|
|
permissions.EXCEED_LIMIT
|
|
):
|
|
raise Conflict("Limit exceeded")
|
|
|
|
transaction = Transaction(sender_=sender, receiver_=receiver, amount=amount, author_=author)
|
|
db.session.add(transaction)
|
|
db.session.commit()
|
|
if sender is not None and sender.id_ != author.id_:
|
|
if receiver is not None:
|
|
BalancePlugin.getPlugin().notify(
|
|
sender,
|
|
"Neue Transaktion",
|
|
{
|
|
"type": NotifyType.SEND_FROM,
|
|
"receiver_id": receiver.userid,
|
|
"author_id": author.userid,
|
|
"amount": amount,
|
|
},
|
|
)
|
|
else:
|
|
BalancePlugin.getPlugin().notify(
|
|
sender,
|
|
"Neue Transaktion",
|
|
{
|
|
"type": NotifyType.SUB_FROM,
|
|
"author_id": author.userid,
|
|
"amount": amount,
|
|
},
|
|
)
|
|
if receiver is not None and receiver.id_ != author.id_:
|
|
if sender is not None:
|
|
BalancePlugin.getPlugin().notify(
|
|
receiver,
|
|
"Neue Transaktion",
|
|
{
|
|
"type": NotifyType.SEND_TO,
|
|
"sender_id": sender.userid,
|
|
"amount": amount,
|
|
},
|
|
)
|
|
else:
|
|
BalancePlugin.getPlugin().notify(
|
|
receiver,
|
|
"Neue Transaktion",
|
|
{
|
|
"type": NotifyType.ADD_FROM,
|
|
"author_id": author.userid,
|
|
"amount": amount,
|
|
},
|
|
)
|
|
return transaction
|
|
|
|
|
|
def change_balance(user, amount: float, author):
|
|
"""Change balance of user
|
|
|
|
Args:
|
|
user: User to change balance
|
|
amount: Amount to change balance
|
|
author: User authoring this transaction
|
|
"""
|
|
sender = user if amount < 0 else None
|
|
receiver = user if amount > 0 else None
|
|
return send(sender, receiver, abs(amount), author)
|
|
|
|
|
|
def get_transaction(transaction_id) -> Transaction:
|
|
transaction = Transaction.query.get(transaction_id)
|
|
if not transaction:
|
|
raise NotFound
|
|
return transaction
|
|
|
|
|
|
def get_transactions(
|
|
user,
|
|
start=None,
|
|
end=None,
|
|
limit=None,
|
|
offset=None,
|
|
show_reversal=False,
|
|
show_cancelled=True,
|
|
descending=False,
|
|
):
|
|
count = None
|
|
query = Transaction.query.filter((Transaction.sender_ == user) | (Transaction.receiver_ == user))
|
|
if start:
|
|
query = query.filter(start <= Transaction.time)
|
|
if end:
|
|
query = query.filter(Transaction.time <= end)
|
|
# Do not show reversals if disabled or cancelled ones are hidden
|
|
if not show_reversal or not show_cancelled:
|
|
query = query.filter(Transaction.original_ == None)
|
|
if not show_cancelled:
|
|
query = query.filter(Transaction.reversal_id.is_(None))
|
|
if descending:
|
|
query = query.order_by(Transaction.time.desc())
|
|
else:
|
|
query = query.order_by(Transaction.time)
|
|
if limit is not None:
|
|
count = query.count()
|
|
query = query.limit(limit)
|
|
if offset is not None:
|
|
query = query.offset(offset)
|
|
|
|
return query.all(), count
|
|
|
|
|
|
def reverse_transaction(transaction: Transaction, author: User):
|
|
"""Reverse a transaction
|
|
|
|
Args:
|
|
transaction: Transaction to reverse
|
|
author: User that wants the transaction to be reverted
|
|
"""
|
|
if transaction.reversal_:
|
|
raise Conflict
|
|
reversal = send(transaction.receiver_, transaction.sender_, transaction.amount, author)
|
|
reversal.original_ = transaction
|
|
transaction.reversal = reversal
|
|
db.session.commit()
|
|
return reversal
|