示例#1
0
class ReceiptItem(Base):
    __tablename__ = 'receiptitem'

    id = db.Column(db.Integer, primary_key=True)

    receipt_id = db.Column(db.Integer, db.ForeignKey('receipt.id'))

    name = db.Column(db.String(100), nullable=False)
    amount = db.Column(db.Float(asdecimal=True), nullable=False)
示例#2
0
class Base(db.Model):
    # @declared_attr
    # def __tablename__(cls):
    #     return cls.__name__.lower()
    __abstract__ = True

    created_on = db.Column(db.DateTime(), default=db.func.now())
    updated_on = db.Column(db.DateTime(), default=db.func.now(),
                           onupdate=db.func.now())

    @declared_attr
    def __mapper_args__(cls):
        return {
            "order_by": cls.created_on.desc()
        }
示例#3
0
class Balance(OwnedMixin, Base):
    __tablename__ = 'balance'

    id = db.Column(db.Integer, primary_key=True)

    # to_user
    # from_user

    amount = db.Column(db.Float(asdecimal=True),
                       nullable=False)
    paid = db.Column(db.Boolean, nullable=False, default=balance_paid_default)

    receipt_id = db.Column(db.Integer, db.ForeignKey('receipt.id'))

    # receipt_name = column_property(
    #     select(
    #         [Receipt.name]
    #     ).select_from(
    #         Receipt
    #     ).where(
    #         Receipt.id == receipt_id
    #     ).correlate_except(Receipt)
    # )

    @property
    def receipt_name(self):
        from .receipt_model import Receipt

        receipt_name = select(
                [Receipt.name]
            ).select_from(
                Receipt
            ).where(
                Receipt.id == self.receipt_id
            ).correlate_except(Receipt)

        return db.session.scalar(receipt_name)

    @property
    def is_to_and_from_owner(self):
        return self.to_user_id == self.from_user_id
示例#4
0
class Payment(RequestMixin, Base):
    __tablename__ = 'payment'

    id = db.Column(db.Integer, primary_key=True)

    # TODO
    date = db.Column(db.DateTime(), default=func.now(), nullable=False)

    # accepted
    # archived

    message = db.Column(db.String(MAX_MESSAGE_LENGTH))

    # to_user_id
    # from_user_id
    # to_user
    # from_user

    amount = db.Column(db.Float(asdecimal=True), nullable=False)

    # methods return True if modified, else False
    def accept(self):
        def callback():
            app.logger.debug("PAYMENT ACCEPT")
            s = Settlement.get(self.from_user_id, self.to_user_id)
            app.logger.debug("add payment settlement %s", s)
            s.add_payment(self)

        return super().accept(callback=callback)

    def reject(self):
        def callback():
            s = Settlement.get(self.from_user_id, self.to_user_id)
            app.logger.debug("remove payment settlement %s", s)
            s.remove_payment(self)

        return super().reject(callback=callback)
示例#5
0
class Friend(RequestMixin, Base):
    __tablename__ = 'friend'

    id = db.Column(db.Integer, primary_key=True)

    # to_user_id
    # from_user_id

    def accept(self):
        def callback():
            # add settlement objects for each user in the friendrequest on
            # accept
            if Settlement.get(self.from_user_id, self.to_user_id) is None:
                db.session.add(
                    Settlement(
                        left_user_id=self.from_user_id,
                        right_user_id=self.to_user_id,
                        paid_amount=0.0,
                        owed_amount=0.0,
                    ))

        return super().accept(callback=callback)
示例#6
0
class Receipt(Base):
    __tablename__ = 'receipt'
    # user

    id = db.Column(db.Integer, primary_key=True)

    name = db.Column(db.String(100), nullable=False, default="New Receipt")
    amount = db.Column(db.Float(asdecimal=True), nullable=False, default=0)
    date = db.Column(db.Date, default=func.now(), nullable=False)

    tax = db.Column(db.Float(asdecimal=True), nullable=False, default=0)

    # resolved = db.Column(db.Boolean)

    # user
    # users

    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))

    balances = relationship("Balance",
                            backref="receipt",
                            cascade="all,delete-orphan")

    receipt_items = relationship("ReceiptItem",
                                 backref="receipt",
                                 foreign_keys=[ReceiptItem.receipt_id],
                                 cascade="all,delete-orphan")

    resolved = column_property(
        and_(
            exists(  # check that balance exists, else not resolved
                select([Balance.id]).select_from(Balance).where(
                    Balance.receipt_id == id, ).correlate_except(Balance)),
            ~exists(  # if bal exist, check that no unpaid balances exist
                select([Balance.id]).select_from(Balance).where(
                    and_(Balance.receipt_id == id,
                         Balance.paid.is_(False))).correlate_except(Balance))))
示例#7
0
class User(Base):
    __tablename__ = 'user'
    # friend_of
    id = db.Column(db.Integer, primary_key=True)

    username = db.Column(db.String(100), unique=True, nullable=False)
    password = db.Column(db.String(100), nullable=False)
    fullname = db.Column(db.String(100), nullable=False)

    receipts_owned = relationship("Receipt",
                                  backref="user",
                                  cascade="merge,delete")

    receipts_in = relationship("Receipt",
                               secondary=receipt_association_table,
                               backref="users")

    balances_to_user = relationship("Balance",
                                    foreign_keys=[Balance.to_user_id],
                                    backref="to_user")
    balances_from_user = relationship("Balance",
                                      foreign_keys=[Balance.from_user_id],
                                      backref="from_user")

    payments_to_user = relationship("Payment",
                                    foreign_keys=[Payment.to_user_id],
                                    backref="to_user")
    payments_from_user = relationship("Payment",
                                      foreign_keys=[Payment.from_user_id],
                                      backref="from_user")

    settlements_right_user = relationship(
        "Settlement",
        foreign_keys=[Settlement.right_user_id],
        backref="to_user")

    settlements_left_user = relationship(
        "Settlement", foreign_keys=[Settlement.left_user_id], backref="user")

    receipt_items = relationship("ReceiptItem",
                                 secondary=receiptitem_association_table,
                                 backref="users")

    friend_to_user = relationship("Friend",
                                  foreign_keys=[Friend.to_user_id],
                                  backref="to_user")

    friend_from_user = relationship("Friend",
                                    foreign_keys=[Friend.from_user_id],
                                    backref="from_user")

    @property
    def settlements(self):
        settle_from = db.session.query(
            Settlement.left_user_id.label("left_user_id"),
            Settlement.right_user_id.label("right_user_id")).filter_by(
                left_user_id=self.id)

        settle_to = db.session.query(
            Settlement.left_user_id.label("left_user_id"),
            Settlement.right_user_id.label("right_user_id")).filter_by(
                right_user_id=self.id)

        result_ids = settle_from.union(settle_to).subquery()

        result = db.session.query(Settlement).select_from(result_ids).join(
            Settlement,
            and_(Settlement.left_user_id == result_ids.c.left_user_id,
                 Settlement.right_user_id == result_ids.c.right_user_id))

        return result

    @property
    def friends(self):
        friends_from = db.session.query(
            Friend.to_user_id.label("id")).select_from(Friend).filter_by(
                from_user_id=self.id, accepted=True)

        friends_to = db.session.query(
            Friend.from_user_id.label("id")).select_from(Friend).filter_by(
                to_user_id=self.id, accepted=True)

        all_friends_ids = friends_from.union(friends_to).subquery()

        all_friends = db.session.query(User).select_from(all_friends_ids).join(
            User, User.id == all_friends_ids.c.id)

        return all_friends

    @property
    def receipts_owned_unresolved(self):
        result = Receipt.query.filter(Receipt.user_id == self.id,
                                      Receipt.resolved.is_(False))
        return result

    @property
    def receipts_owned_resolved(self):
        result = Receipt.query.filter(Receipt.user_id == self.id,
                                      Receipt.resolved.is_(True))
        return result

    @property
    def receipts_owed_unresolved(self):
        result = Receipt.query.join(receipt_association_table, ).filter(
            receipt_association_table.c.left_id == self.id,
            Receipt.user_id != self.id, Receipt.resolved.is_(False))

        # app.logger.debug("receipts_owed expression running - %s", result)

        return result

    @property
    def receipts_owed_resolved(self):
        result = Receipt.query.join(receipt_association_table, ).filter(
            receipt_association_table.c.left_id == self.id,
            Receipt.user_id != self.id, Receipt.resolved.is_(True))

        # app.logger.debug("receipts_owed expression running - %s", result)

        return result

    @property
    def receipts_owed(self):
        result = Receipt.query.join(receipt_association_table, ).filter(
            receipt_association_table.c.left_id == self.id,
            Receipt.user_id != self.id)

        # app.logger.debug("receipts_owed expression running - %s", result)

        return result

    @property
    def receipts_resolved(self):
        # TODO
        result = Receipt.query.join(receipt_association_table, ).filter(
            or_(
                receipt_association_table.c.left_id == self.id,
                receipt_association_table.c.right_id == self.id,
            ),
            # Receipt.user_id != self.id,
            Receipt.resolved.is_(True))

        return result
示例#8
0
class RequestMixin(OwnedMixin, object):
    accepted = db.Column(db.Boolean)
    archived_from = db.Column(db.Boolean, default=False, nullable=False)
    archived_to = db.Column(db.Boolean, default=False, nullable=False)

    @classmethod
    def get_received(cls, user):
        return cls.query.filter_by(
            # accepted=None,
            to_user_id=user.id,
            archived_to=False
        )

    @classmethod
    def get_sent(cls, user):
        return cls.query.filter_by(
            from_user_id=user.id,
            archived_from=False
        )

    @classmethod
    def archive_sent(cls, user):
        # archive payments not archived yet which are not in "pending" state
        # (null)
        cls.query.filter(
            cls.from_user_id == user.id,
            cls.archived_from.is_(False),
            cls.accepted.isnot(None)
        ).update({"archived_from": True})

        cls.query.filter(
            cls.to_user_id == user.id,
            cls.archived_to.is_(False),
            cls.accepted.isnot(None)
        ).update({"archived_to": True})

        db.session.commit()

    def accept(self, callback=lambda: None):
        app.logger.debug("accepted for %s is %s", self.id, self.accepted)
        if self.accepted is False or self.accepted is None:
            # False means payment was previously rejected
            # None means payment has not been accepted or rejected yet
            # rejected or not added yet

            # in this case, we add the payment to the settlement
            # from user -> to user

            # TODO exception id settlement none
            callback()

            # unarchive
            self.archived_from = False
            self.archived_to = False
            self.accepted = True

            return True

        return False

    def reject(self, callback=lambda: None):
        if self.accepted is True:
            # True means payment was previously accepted
            # if payment was previously accepted, we have to remove payment
            # value

            # in this case, we remove the previously added payment to the
            # settlement from user -> to user
            # s = self.from_user.get_settlement_to(self.to_user)

            # TODO exception id settlement none
            callback()

            # unarchive
            self.archived_from = False
            self.archived_to = False
            self.accepted = False

            return True

        if self.accepted is None:
            # None means payment is new
            self.accepted = False
            return True

        return False
示例#9
0
 def to_user_id(cls):
     return db.Column(db.Integer, db.ForeignKey('user.id'))
示例#10
0
# from flask import current_app as app
from sqlalchemy.orm import relationship, column_property
from sqlalchemy.sql import func, select, exists, and_

from receipt_split.meta import db
from . import Base, Balance

receiptitem_association_table = db.Table(
    'user_receiptitem_association', db.metadata,
    db.Column('left_id', db.Integer, db.ForeignKey('user.id')),
    db.Column('right_id', db.Integer, db.ForeignKey('receiptitem.id')))

receipt_association_table = db.Table(
    'user_receipt_association', db.metadata,
    db.Column('left_id', db.Integer, db.ForeignKey('user.id')),
    db.Column('right_id', db.Integer, db.ForeignKey('receipt.id')))


class ReceiptItem(Base):
    __tablename__ = 'receiptitem'

    id = db.Column(db.Integer, primary_key=True)

    receipt_id = db.Column(db.Integer, db.ForeignKey('receipt.id'))

    name = db.Column(db.String(100), nullable=False)
    amount = db.Column(db.Float(asdecimal=True), nullable=False)


class Receipt(Base):
    __tablename__ = 'receipt'
class Settlement(Base):
    __tablename__ = 'settlement'
    __table_args__ = (db.CheckConstraint('left_user_id <> right_user_id'), )

    left_user_id = db.Column(db.Integer,
                             db.ForeignKey('user.id'),
                             primary_key=True,
                             nullable=False)
    right_user_id = db.Column(db.Integer,
                              db.ForeignKey('user.id'),
                              primary_key=True,
                              nullable=False)

    # user
    # to_user

    owed_amount = db.Column(db.Float(asdecimal=True),
                            nullable=False,
                            default=0.0)

    paid_amount = db.Column(db.Float(asdecimal=True),
                            nullable=False,
                            default=0.0)

    diff_amount = column_property(owed_amount - paid_amount)

    def get_balances_to_pay(self, user_id):
        if not is_valid_user(self, user_id):
            # if from or to balance user is invalid, then return false
            return []

        other_user_id = self.get_other_user_id(user_id)

        app.logger.debug("Balances get_balances_to_pay other_user_id %s",
                         other_user_id)

        balances = Balance.query.filter_by(from_user_id=user_id,
                                           to_user_id=other_user_id,
                                           paid=False).all()

        app.logger.debug("Balances get_balances_to_pay %s", balances)

        return balances

    def get_balances_owed(self, user_id):
        if not is_valid_user(self, user_id):
            # if from or to balance user is invalid, then return false
            return []

        other_user_id = self.get_other_user_id(user_id)

        balances = Balance.query.filter_by(from_user_id=other_user_id,
                                           to_user_id=user_id,
                                           paid=False).all()

        return balances

# TODO
# https://stackoverflow.com/questions/34057756/how-to-combine-sqlalchemys-hybrid-property-decorator-with-werkzeugs-cached-pr
# def update_settlement(self):
#     """should be legacy"""
#     # get balance from adding up unpaid balances
#     from_balances = db.session.query(
#         func.coalesce(
#             func.sum(Balance.amount), literal_column("0.0")
#         ).label("sum")
#     ).filter_by(
#         from_user_id=self.left_user_id,
#         to_user_id=self.right_user_id,
#         paid=False
#     ).subquery()

#     to_balances = db.session.query(
#         func.coalesce(
#             func.sum(Balance.amount), literal_column("0.0")
#         ).label("sum")
#     ).filter_by(
#         from_user_id=self.right_user_id,
#         to_user_id=self.left_user_id,
#         paid=False
#     ).subquery()

#     result = db.session.query(
#         (from_balances.c.sum - to_balances.c.sum).label("result")
#     ).scalar()

#     app.logger.info("NEW BALANCE AMOUNT %s", result)

#     self.owed_amount = result

#     app.logger.info("NEW BALANCE owed AMOUNT %s", self.owed_amount)

#     # self.owed_amount = s
#     # self.balance_amount = s

    def get(from_user_id, to_user_id):
        app.logger.debug("GET SETTLEMENT %s %s", from_user_id, to_user_id)
        s1 = Settlement.query.get({
            "left_user_id": from_user_id,
            "right_user_id": to_user_id,
        })
        if s1 is None:
            s2 = Settlement.query.get({
                "left_user_id": to_user_id,
                "right_user_id": from_user_id,
            })
            if s2 is None:
                return None
            else:
                return s2
        else:
            return s1

    def get_other_user_id(self, user_id):
        if user_id == self.left_user_id:
            return self.right_user_id
        elif user_id == self.right_user_id:
            return self.left_user_id
        return None

    def get_paid_amount(self, user_id):
        return get(self, user_id, "paid_amount", Decimal(0.0))

    def get_owed_amount(self, user_id):
        return get(self, user_id, "owed_amount", Decimal(0.0))

    def get_diff_amount(self, user_id):
        return get(self, user_id, "diff_amount", Decimal(0.0))

    def add_payment(self, payment):
        """
        add payment and apply balances if possible.
        return true if payment added
        """
        if add(self, payment.from_user_id, "paid_amount", payment.amount):
            self.pay_balances()
            return True
        return False

    def remove_payment(self, payment):
        """
        remove payment and apply balances if possible.
        return true if payment removed successfully
        """
        if add(self, payment.from_user_id, "paid_amount", -1 * payment.amount):
            self.pay_balances()
            return True
        return False

    def apply_balance(self, balance):
        """
        applies balance to settlement
        return False if did not add balance to settlement, else return True
        """

        if balance.paid:
            # dont apply if paid
            app.logger.debug("APPLY BALANCE balance already paid")
            return False

        if not is_valid_user(self, balance.from_user_id) or \
                not is_valid_user(self, balance.to_user_id):
            # if from or to balance user is invalid, then return false
            return False

        if balance.to_user_id == balance.from_user_id:
            return False

        # balance: from_user pays to_user
        paid_amount = self.get_paid_amount(balance.from_user_id)

        app.logger.debug("APPLY BALANCE paid %s bal %s", paid_amount,
                         balance.amount)

        if paid_amount < balance.amount:
            # if cannot pay right away for balance
            # add to balance owed_amount
            add(self, balance.from_user_id, "owed_amount", balance.amount)
            return True

        # otherwise, subtract from paid_amount for the balance and set balance
        # to paid. Do not change owed_amount

        add(self, balance.from_user_id, "paid_amount", -1 * balance.amount)
        balance.paid = True

        app.logger.debug("bal_OK! paid %s, owed %s cur_bal %s",
                         self.paid_amount, self.owed_amount, balance.amount)
        return True

    def add_balance_back(self, balance):
        """
        reverts added balance from settlement
        return False if did not revert to settlement, else delete and return
        True
        """

        app.logger.debug("ADD_BALANCE_BACK")

        app.logger.debug("\tbal_id=%s settlement %s->%s", balance.id,
                         balance.from_user_id, balance.to_user_id)

        if not is_valid_user(self, balance.from_user_id) or \
                not is_valid_user(self, balance.to_user_id):
            # if from or to balance user is invalid, then return false
            return False

        if balance.to_user_id == balance.from_user_id:
            app.logger.debug("\tDid not add back, as balance is to self")

            db.session.delete(balance)
            return True

        if balance.paid:
            app.logger.debug("\tbalance paid, so add back to paid_amount")
            add(self, balance.from_user_id, "paid_amount", balance.amount)
        else:
            # if paid balance, remove from balance in settlement.
            add(self, balance.from_user_id, "owed_amount", -1 * balance.amount)

        db.session.delete(balance)
        return True

    def pay_balances(self):
        app.logger.debug("PAY_BALANCES")

        balances = []

        check_user_id = None

        app.logger.debug("\t(before) paid_amount=%s balance_amount=%s",
                         self.paid_amount, self.owed_amount)

        if self.paid_amount > 0:
            # left user has payment to adjust balances
            check_user_id = self.left_user_id
            app.logger.debug("\tcheck_user left %s", check_user_id)
        elif self.paid_amount < 0:
            check_user_id = self.right_user_id
            app.logger.debug("\tcheck_user right %s", check_user_id)

        if check_user_id is not None:
            balances = Balance.query.filter(
                Balance.from_user_id == check_user_id,
                # balances user must pay
                Balance.to_user_id != check_user_id,
                # no self addressed
                Balance.paid.is_(False)  # only get unpaid balances
            ).order_by(Balance.created_on).all()
        else:
            app.logger.debug("\tERROR check_user_id is None")
            return

        for balance in balances:
            # balances are all "check_user_id pays to"

            app.logger.debug("\tbalance from %s->%s", balance.from_user_id,
                             balance.to_user_id)

            app.logger.debug("\t\tpaid_amount=%s balance_amount=%s",
                             self.get_paid_amount(check_user_id),
                             balance.amount)

            if self.get_paid_amount(check_user_id) >= balance.amount:
                add(self, check_user_id, "paid_amount", -1 * balance.amount)
                add(self, check_user_id, "owed_amount", -1 * balance.amount)
                balance.paid = True

        app.logger.debug("\t(final) paid_amount=%s balance_amount=%s",
                         self.paid_amount, self.owed_amount)