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"" 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, }