from datetime import datetime from decimal import Decimal from app import db class ProjectCost(db.Model): """Project cost model for tracking expenses beyond hourly work""" __tablename__ = 'project_costs' id = db.Column(db.Integer, primary_key=True) project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=False, index=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True) # Cost details description = db.Column(db.String(500), nullable=False) category = db.Column(db.String(50), nullable=False) # 'travel', 'materials', 'services', 'equipment', 'other' amount = db.Column(db.Numeric(10, 2), nullable=False) currency_code = db.Column(db.String(3), nullable=False, default='EUR') # Billing billable = db.Column(db.Boolean, default=True, nullable=False) invoiced = db.Column(db.Boolean, default=False, nullable=False) invoice_id = db.Column(db.Integer, db.ForeignKey('invoices.id'), nullable=True, index=True) # Date and metadata cost_date = db.Column(db.Date, nullable=False, index=True) notes = db.Column(db.Text, nullable=True) receipt_path = db.Column(db.String(500), nullable=True) # Path to uploaded receipt created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) # Relationships # project and user relationships defined via backref def __init__(self, project_id, user_id, description, category, amount, cost_date, billable=True, notes=None, currency_code='EUR', receipt_path=None): self.project_id = project_id self.user_id = user_id self.description = description.strip() if description else None self.category = category self.amount = Decimal(str(amount)) self.cost_date = cost_date self.billable = billable self.notes = notes.strip() if notes else None self.currency_code = currency_code self.receipt_path = receipt_path def __repr__(self): return f'' @property def is_invoiced(self): """Check if this cost has been invoiced""" return self.invoiced and self.invoice_id is not None def mark_as_invoiced(self, invoice_id): """Mark this cost as invoiced""" self.invoiced = True self.invoice_id = invoice_id self.updated_at = datetime.utcnow() def unmark_as_invoiced(self): """Unmark this cost as invoiced (e.g., if invoice is deleted)""" self.invoiced = False self.invoice_id = None self.updated_at = datetime.utcnow() def to_dict(self): """Convert project cost to dictionary for API responses""" return { 'id': self.id, 'project_id': self.project_id, 'user_id': self.user_id, 'description': self.description, 'category': self.category, 'amount': float(self.amount), 'currency_code': self.currency_code, 'billable': self.billable, 'invoiced': self.invoiced, 'invoice_id': self.invoice_id, 'cost_date': self.cost_date.isoformat() if self.cost_date else None, 'notes': self.notes, 'receipt_path': self.receipt_path, 'created_at': self.created_at.isoformat() if self.created_at else None, 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 'project': self.project.name if self.project else None, 'user': self.user.username if self.user else None } @classmethod def get_project_costs(cls, project_id, start_date=None, end_date=None, user_id=None, billable_only=False): """Get costs for a specific project with optional filters""" query = cls.query.filter_by(project_id=project_id) if start_date: query = query.filter(cls.cost_date >= start_date) if end_date: query = query.filter(cls.cost_date <= end_date) if user_id: query = query.filter(cls.user_id == user_id) if billable_only: query = query.filter(cls.billable == True) return query.order_by(cls.cost_date.desc()).all() @classmethod def get_total_costs(cls, project_id, start_date=None, end_date=None, user_id=None, billable_only=False): """Calculate total costs for a project with optional filters""" query = db.session.query(db.func.sum(cls.amount)).filter_by(project_id=project_id) if start_date: query = query.filter(cls.cost_date >= start_date) if end_date: query = query.filter(cls.cost_date <= end_date) if user_id: query = query.filter(cls.user_id == user_id) if billable_only: query = query.filter(cls.billable == True) total = query.scalar() or Decimal('0') return float(total) @classmethod def get_uninvoiced_costs(cls, project_id): """Get all billable costs that haven't been invoiced yet""" return cls.query.filter_by( project_id=project_id, billable=True, invoiced=False ).order_by(cls.cost_date.desc()).all() @classmethod def get_costs_by_category(cls, project_id, start_date=None, end_date=None): """Get costs grouped by category""" query = db.session.query( cls.category, db.func.sum(cls.amount).label('total_amount'), db.func.count(cls.id).label('count') ).filter_by(project_id=project_id) if start_date: query = query.filter(cls.cost_date >= start_date) if end_date: query = query.filter(cls.cost_date <= end_date) results = query.group_by(cls.category).all() return [ { 'category': category, 'total_amount': float(total_amount), 'count': count } for category, total_amount, count in results ]