112 lines
4.1 KiB
Python
112 lines
4.1 KiB
Python
"""
|
|
Transaction Model
|
|
|
|
SQLAlchemy model for financial transactions.
|
|
Handles income and expense records with precise decimal arithmetic.
|
|
"""
|
|
|
|
from datetime import datetime, date
|
|
from decimal import Decimal
|
|
from app.extensions import db
|
|
|
|
|
|
class Transaction(db.Model):
|
|
"""
|
|
Transaction model for financial records.
|
|
|
|
Attributes:
|
|
id (int): Primary key
|
|
description (str): Transaction description
|
|
amount (Decimal): Transaction amount (precise decimal)
|
|
type (str): Transaction type ('income' or 'expense')
|
|
category_id (int): Foreign key to Category
|
|
transaction_date (date): Date of transaction
|
|
notes (str): Optional additional notes
|
|
created_at (datetime): Creation timestamp
|
|
"""
|
|
|
|
__tablename__ = 'transactions'
|
|
|
|
# Primary key
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
|
|
# Transaction information
|
|
description = db.Column(db.String(255), nullable=False)
|
|
amount = db.Column(db.Numeric(10, 2), nullable=False) # DECIMAL(10,2) for precise money handling
|
|
type = db.Column(db.String(10), nullable=False) # 'income' or 'expense'
|
|
notes = db.Column(db.Text)
|
|
|
|
# Relationships
|
|
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True)
|
|
|
|
# Dates
|
|
transaction_date = db.Column(db.Date, nullable=False, default=date.today)
|
|
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
|
|
|
# Database constraints
|
|
__table_args__ = (
|
|
db.CheckConstraint('amount > 0', name='check_amount_positive'),
|
|
db.CheckConstraint("type IN ('income', 'expense')", name='check_type_valid'),
|
|
)
|
|
|
|
def __repr__(self):
|
|
"""String representation of Transaction."""
|
|
return f'<Transaction {self.type}: {self.amount} - {self.description}>'
|
|
|
|
def to_dict(self):
|
|
"""Convert transaction to dictionary for JSON serialization."""
|
|
return {
|
|
'id': self.id,
|
|
'description': self.description,
|
|
'amount': float(self.amount) if self.amount else 0,
|
|
'type': self.type,
|
|
'category_id': self.category_id,
|
|
'category_name': self.category.name if self.category else None,
|
|
'transaction_date': self.transaction_date.isoformat() if self.transaction_date else None,
|
|
'notes': self.notes,
|
|
'created_at': self.created_at.isoformat() if self.created_at else None
|
|
}
|
|
|
|
@property
|
|
def amount_decimal(self):
|
|
"""Get amount as Decimal object for precise calculations."""
|
|
return Decimal(str(self.amount)) if self.amount else Decimal('0')
|
|
|
|
@classmethod
|
|
def get_by_type(cls, transaction_type):
|
|
"""Get transactions by type (income/expense)."""
|
|
return cls.query.filter_by(type=transaction_type).all()
|
|
|
|
@classmethod
|
|
def get_by_date_range(cls, start_date, end_date):
|
|
"""Get transactions within date range."""
|
|
return cls.query.filter(
|
|
cls.transaction_date >= start_date,
|
|
cls.transaction_date <= end_date
|
|
).all()
|
|
|
|
@classmethod
|
|
def get_by_category(cls, category_id):
|
|
"""Get transactions by category."""
|
|
return cls.query.filter_by(category_id=category_id).all()
|
|
|
|
@classmethod
|
|
def calculate_total_by_type(cls, transaction_type):
|
|
"""Calculate total amount for a transaction type."""
|
|
result = db.session.query(db.func.sum(cls.amount)).filter_by(type=transaction_type).scalar()
|
|
return Decimal(str(result)) if result else Decimal('0')
|
|
|
|
@classmethod
|
|
def get_summary_stats(cls):
|
|
"""Get summary statistics for all transactions."""
|
|
total_income = cls.calculate_total_by_type('income')
|
|
total_expenses = cls.calculate_total_by_type('expense')
|
|
net_balance = total_income - total_expenses
|
|
transaction_count = cls.query.count()
|
|
|
|
return {
|
|
'total_income': float(total_income),
|
|
'total_expenses': float(total_expenses),
|
|
'net_balance': float(net_balance),
|
|
'transaction_count': transaction_count
|
|
}
|