Files
TimeTracker/app/models/recurring_invoice.py
T
Dries Peeters b4486a627f fix: CI tests, code quality, and duplicate DB indexes
- Webhook models: remove duplicate index definitions so db.create_all()
  no longer raises 'index already exists' (columns already have index=True)
- ImportService: fix circular import by late-importing ClientService,
  ProjectService, TimeTrackingService in __init__
- reports: fix F823 by renaming unpack variable _ to _entry_count to avoid
  shadowing gettext _ in export_task_excel()
- Code quality: add .flake8 with extend-ignore so flake8 CI passes;
  simplify pyproject.toml isort config (drop unsupported options)
- Format: run black and isort on app/
- tests: restore minimal app fixture in test_import_export_models
2026-03-15 10:51:52 +01:00

145 lines
6.6 KiB
Python

from datetime import datetime, timedelta
from decimal import Decimal
from dateutil.relativedelta import relativedelta
from app import db
class RecurringInvoice(db.Model):
"""Recurring invoice template model for automated billing"""
__tablename__ = "recurring_invoices"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(200), nullable=False) # Template name/description
project_id = db.Column(db.Integer, db.ForeignKey("projects.id"), nullable=False, index=True)
client_id = db.Column(db.Integer, db.ForeignKey("clients.id"), nullable=False, index=True)
# Recurrence settings
frequency = db.Column(db.String(20), nullable=False) # 'daily', 'weekly', 'monthly', 'yearly'
interval = db.Column(db.Integer, nullable=False, default=1) # Every N periods (e.g., every 2 weeks)
next_run_date = db.Column(db.Date, nullable=False) # Next date to generate invoice
end_date = db.Column(db.Date, nullable=True) # Optional end date for recurrence
# Invoice template settings (copied to generated invoices)
client_name = db.Column(db.String(200), nullable=False)
client_email = db.Column(db.String(200), nullable=True)
client_address = db.Column(db.Text, nullable=True)
due_date_days = db.Column(db.Integer, nullable=False, default=30) # Days from issue date to due date
tax_rate = db.Column(db.Numeric(5, 2), nullable=False, default=0)
currency_code = db.Column(db.String(3), nullable=False, default="EUR")
notes = db.Column(db.Text, nullable=True)
terms = db.Column(db.Text, nullable=True)
template_id = db.Column(db.Integer, db.ForeignKey("invoice_templates.id"), nullable=True, index=True)
# Auto-send settings
auto_send = db.Column(db.Boolean, nullable=False, default=False) # Automatically send via email when generated
auto_include_time_entries = db.Column(db.Boolean, nullable=False, default=True) # Include unbilled time entries
# Status
is_active = db.Column(db.Boolean, nullable=False, default=True)
# Metadata
created_by = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
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)
last_generated_at = db.Column(db.DateTime, nullable=True) # Last time an invoice was generated
# Relationships
project = db.relationship("Project", backref="recurring_invoices")
client = db.relationship("Client", backref="recurring_invoices")
creator = db.relationship("User", backref="created_recurring_invoices")
template = db.relationship("InvoiceTemplate", backref="recurring_invoices")
generated_invoices = db.relationship(
"Invoice", backref="recurring_invoice_template", lazy="dynamic", foreign_keys="[Invoice.recurring_invoice_id]"
)
def __init__(self, name, project_id, client_id, frequency, next_run_date, created_by, **kwargs):
self.name = name
self.project_id = project_id
self.client_id = client_id
self.frequency = frequency
self.next_run_date = next_run_date
self.created_by = created_by
# Set optional fields
self.interval = kwargs.get("interval", 1)
self.end_date = kwargs.get("end_date")
self.client_name = kwargs.get("client_name", "")
self.client_email = kwargs.get("client_email")
self.client_address = kwargs.get("client_address")
self.due_date_days = kwargs.get("due_date_days", 30)
self.tax_rate = Decimal(str(kwargs.get("tax_rate", 0)))
self.currency_code = kwargs.get("currency_code", "EUR")
self.notes = kwargs.get("notes")
self.terms = kwargs.get("terms")
self.template_id = kwargs.get("template_id")
self.auto_send = kwargs.get("auto_send", False)
self.auto_include_time_entries = kwargs.get("auto_include_time_entries", True)
self.is_active = kwargs.get("is_active", True)
def __repr__(self):
return f"<RecurringInvoice {self.name} ({self.frequency})>"
def calculate_next_run_date(self, from_date=None):
"""Calculate the next run date based on frequency and interval"""
if from_date is None:
from_date = datetime.utcnow().date()
if self.frequency == "daily":
return from_date + timedelta(days=self.interval)
elif self.frequency == "weekly":
return from_date + timedelta(weeks=self.interval)
elif self.frequency == "monthly":
return from_date + relativedelta(months=self.interval)
elif self.frequency == "yearly":
return from_date + relativedelta(years=self.interval)
else:
raise ValueError(f"Invalid frequency: {self.frequency}")
def should_generate_today(self):
"""Check if invoice should be generated today"""
if not self.is_active:
return False
today = datetime.utcnow().date()
# Check if we've reached the end date
if self.end_date and today > self.end_date:
return False
# Check if it's time to generate
return today >= self.next_run_date
def generate_invoice(self):
"""Generate an invoice from this recurring template. Delegates to RecurringInvoiceService."""
from app.services.recurring_invoice_service import RecurringInvoiceService
return RecurringInvoiceService().generate_invoice(self)
def to_dict(self):
"""Convert recurring invoice to dictionary"""
return {
"id": self.id,
"name": self.name,
"project_id": self.project_id,
"client_id": self.client_id,
"frequency": self.frequency,
"interval": self.interval,
"next_run_date": self.next_run_date.isoformat() if self.next_run_date else None,
"end_date": self.end_date.isoformat() if self.end_date else None,
"client_name": self.client_name,
"client_email": self.client_email,
"due_date_days": self.due_date_days,
"tax_rate": float(self.tax_rate),
"currency_code": self.currency_code,
"auto_send": self.auto_send,
"auto_include_time_entries": self.auto_include_time_entries,
"is_active": self.is_active,
"created_by": self.created_by,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
"last_generated_at": self.last_generated_at.isoformat() if self.last_generated_at else None,
}