mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-20 21:30:12 -05:00
ad9bfbf1ed
This commit fixes multiple issues preventing client deletion and adds proper validation to prevent deletion when invoices exist. Database Schema Fixes: - Migration 103: Add missing quote_number column to quotes table - Handles migration from offer_number to quote_number - Generates quote numbers for existing quotes if needed - Creates required unique index - Migration 104: Add all missing columns to quotes table - Adds subtotal, tax_amount, visible_to_client columns - Adds discount fields (discount_type, discount_amount, discount_reason, coupon_code) - Adds payment_terms column - Adds approval workflow columns (approval_status, approved_by, approved_at, rejected_by, rejection_reason) - Creates required indexes and foreign keys - Migration 105: Fix client_notifications foreign key cascade - Updates client_notifications.client_id FK to ON DELETE CASCADE - Updates client_notification_preferences.client_id FK to ON DELETE CASCADE - Prevents NOT NULL constraint violations during client deletion Model Updates: - Add passive_deletes=True to ClientNotification.client relationship - Add passive_deletes=True to ClientNotificationPreferences.client relationship - Add passive_deletes=True to ClientAttachment.client relationship - Update ClientNote.client relationship to use passive_deletes Route Updates: - Add invoice validation to delete_client() and bulk_delete_clients() - Manually delete notifications before client deletion to prevent SQLAlchemy update issues Fixes: - Resolves IntegrityError when deleting clients with notifications - Resolves missing quote_number column errors - Resolves missing quotes table columns errors - Prevents deletion of clients with invoices (data integrity)
150 lines
5.2 KiB
Python
150 lines
5.2 KiB
Python
from datetime import datetime
|
|
from app import db
|
|
from app.utils.timezone import now_in_app_timezone
|
|
|
|
|
|
class ClientNote(db.Model):
|
|
"""ClientNote model for internal notes about clients"""
|
|
|
|
__tablename__ = "client_notes"
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
content = db.Column(db.Text, nullable=False)
|
|
|
|
# Reference to client
|
|
client_id = db.Column(db.Integer, db.ForeignKey("clients.id"), nullable=False, index=True)
|
|
|
|
# Author of the note
|
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, index=True)
|
|
|
|
# Internal flag - these notes are always internal and not visible to clients
|
|
is_important = db.Column(db.Boolean, default=False, nullable=False)
|
|
|
|
# Timestamps
|
|
created_at = db.Column(db.DateTime, default=now_in_app_timezone, nullable=False)
|
|
updated_at = db.Column(db.DateTime, default=now_in_app_timezone, onupdate=now_in_app_timezone, nullable=False)
|
|
|
|
# Relationships
|
|
author = db.relationship("User", backref="client_notes")
|
|
client = db.relationship("Client", backref=db.backref("notes", lazy="dynamic"), passive_deletes=True)
|
|
|
|
def __init__(self, content, user_id, client_id, is_important=False):
|
|
"""Create a client note.
|
|
|
|
Args:
|
|
content: The note text
|
|
user_id: ID of the user creating the note
|
|
client_id: ID of the client
|
|
is_important: Whether this note is marked as important
|
|
"""
|
|
if not client_id:
|
|
raise ValueError("Note must be associated with a client")
|
|
|
|
if not content or not content.strip():
|
|
raise ValueError("Note content cannot be empty")
|
|
|
|
self.content = content.strip()
|
|
self.user_id = user_id
|
|
self.client_id = client_id
|
|
self.is_important = is_important
|
|
|
|
def __repr__(self):
|
|
return f'<ClientNote by {self.author.username if self.author else "Unknown"} for Client {self.client_id}>'
|
|
|
|
@property
|
|
def author_name(self):
|
|
"""Get the author's display name"""
|
|
if self.author:
|
|
return self.author.full_name if self.author.full_name else self.author.username
|
|
return "Unknown"
|
|
|
|
@property
|
|
def client_name(self):
|
|
"""Get the client name"""
|
|
return self.client.name if self.client else "Unknown"
|
|
|
|
def can_edit(self, user):
|
|
"""Check if a user can edit this note"""
|
|
return user.id == self.user_id or user.is_admin
|
|
|
|
def can_delete(self, user):
|
|
"""Check if a user can delete this note"""
|
|
return user.id == self.user_id or user.is_admin
|
|
|
|
def edit_content(self, new_content, user, is_important=None):
|
|
"""Edit the note content
|
|
|
|
Args:
|
|
new_content: New content for the note
|
|
user: User making the edit
|
|
is_important: Optional new importance flag
|
|
"""
|
|
if not self.can_edit(user):
|
|
raise PermissionError("User does not have permission to edit this note")
|
|
|
|
if not new_content or not new_content.strip():
|
|
raise ValueError("Note content cannot be empty")
|
|
|
|
self.content = new_content.strip()
|
|
if is_important is not None:
|
|
self.is_important = is_important
|
|
self.updated_at = now_in_app_timezone()
|
|
|
|
def to_dict(self):
|
|
"""Convert note to dictionary for API responses"""
|
|
return {
|
|
"id": self.id,
|
|
"content": self.content,
|
|
"client_id": self.client_id,
|
|
"client_name": self.client_name,
|
|
"user_id": self.user_id,
|
|
"author": self.author.username if self.author else None,
|
|
"author_full_name": self.author.full_name if self.author and self.author.full_name else None,
|
|
"author_name": self.author_name,
|
|
"is_important": self.is_important,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
@classmethod
|
|
def get_client_notes(cls, client_id, order_by_important=False):
|
|
"""Get all notes for a client
|
|
|
|
Args:
|
|
client_id: ID of the client
|
|
order_by_important: If True, important notes appear first
|
|
"""
|
|
query = cls.query.filter_by(client_id=client_id)
|
|
|
|
if order_by_important:
|
|
query = query.order_by(cls.is_important.desc(), cls.created_at.desc())
|
|
else:
|
|
query = query.order_by(cls.created_at.desc())
|
|
|
|
return query.all()
|
|
|
|
@classmethod
|
|
def get_important_notes(cls, client_id=None):
|
|
"""Get all important notes, optionally filtered by client"""
|
|
query = cls.query.filter_by(is_important=True)
|
|
|
|
if client_id:
|
|
query = query.filter_by(client_id=client_id)
|
|
|
|
return query.order_by(cls.created_at.desc()).all()
|
|
|
|
@classmethod
|
|
def get_user_notes(cls, user_id, limit=None):
|
|
"""Get recent notes by a user"""
|
|
query = cls.query.filter_by(user_id=user_id).order_by(cls.created_at.desc())
|
|
|
|
if limit:
|
|
query = query.limit(limit)
|
|
|
|
return query.all()
|
|
|
|
@classmethod
|
|
def get_recent_notes(cls, limit=10):
|
|
"""Get recent notes across all clients"""
|
|
return cls.query.order_by(cls.created_at.desc()).limit(limit).all()
|