Files
TimeTracker/app/models/client.py
Dries Peeters 8a378b7078 feat(clients,license,db): add client management, enhanced DB init, and tests
- Clients: add model, routes, and templates
  - app/models/client.py
  - app/routes/clients.py
  - templates/clients/{create,edit,list,view}.html
  - docs/CLIENT_MANAGEMENT_README.md
- Database: add enhanced init/verify scripts, migrations, and docs
  - docker/{init-database-enhanced.py,start-enhanced.py,verify-database.py}
  - docs/ENHANCED_DATABASE_STARTUP.md
  - migrations/{add_analytics_column.sql,add_analytics_setting.py,migrate_to_client_model.py}
- Scripts: add version manager and docker network test helpers
  - scripts/version-manager.{bat,ps1,py,sh}
  - scripts/test-docker-network.{bat,sh}
  - docs/VERSION_MANAGEMENT.md
- UI: tweak base stylesheet
  - app/static/base.css
- Tests: add client system test
  - test_client_system.py
2025-09-01 11:34:45 +02:00

96 lines
3.7 KiB
Python

from datetime import datetime
from decimal import Decimal
from app import db
class Client(db.Model):
"""Client model for managing client information and rates"""
__tablename__ = 'clients'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(200), nullable=False, unique=True, index=True)
description = db.Column(db.Text, nullable=True)
contact_person = db.Column(db.String(200), nullable=True)
email = db.Column(db.String(200), nullable=True)
phone = db.Column(db.String(50), nullable=True)
address = db.Column(db.Text, nullable=True)
default_hourly_rate = db.Column(db.Numeric(9, 2), nullable=True)
status = db.Column(db.String(20), default='active', nullable=False) # 'active' or 'inactive'
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
projects = db.relationship('Project', backref='client_obj', lazy='dynamic', cascade='all, delete-orphan')
def __init__(self, name, description=None, contact_person=None, email=None, phone=None, address=None, default_hourly_rate=None):
self.name = name.strip()
self.description = description.strip() if description else None
self.contact_person = contact_person.strip() if contact_person else None
self.email = email.strip() if email else None
self.phone = phone.strip() if phone else None
self.address = address.strip() if address else None
self.default_hourly_rate = Decimal(str(default_hourly_rate)) if default_hourly_rate else None
def __repr__(self):
return f'<Client {self.name}>'
@property
def is_active(self):
"""Check if client is active"""
return self.status == 'active'
@property
def total_projects(self):
"""Get total number of projects for this client"""
return self.projects.count()
@property
def active_projects(self):
"""Get number of active projects for this client"""
return self.projects.filter_by(status='active').count()
@property
def total_hours(self):
"""Calculate total hours across all projects for this client"""
total_seconds = 0
for project in self.projects:
total_seconds += project.total_hours * 3600 # Convert hours to seconds
return round(total_seconds / 3600, 2)
@property
def total_billable_hours(self):
"""Calculate total billable hours across all projects for this client"""
total_seconds = 0
for project in self.projects:
total_seconds += project.total_billable_hours * 3600 # Convert hours to seconds
return round(total_seconds / 3600, 2)
@property
def estimated_total_cost(self):
"""Calculate estimated total cost based on billable hours and rates"""
total_cost = 0.0
for project in self.projects:
if project.billable and project.hourly_rate:
total_cost += project.estimated_cost
return total_cost
def archive(self):
"""Archive the client"""
self.status = 'inactive'
self.updated_at = datetime.utcnow()
def activate(self):
"""Activate the client"""
self.status = 'active'
self.updated_at = datetime.utcnow()
@classmethod
def get_active_clients(cls):
"""Get all active clients ordered by name"""
return cls.query.filter_by(status='active').order_by(cls.name).all()
@classmethod
def get_all_clients(cls):
"""Get all clients ordered by name"""
return cls.query.order_by(cls.name).all()