Files
TimeTracker/app/models/quote_version.py
Dries Peeters 90dde470da style: standardize code formatting and normalize line endings
- Normalize line endings from CRLF to LF across all files to match .editorconfig
- Standardize quote style from single quotes to double quotes
- Normalize whitespace and formatting throughout codebase
- Apply consistent code style across 372 files including:
  * Application code (models, routes, services, utils)
  * Test files
  * Configuration files
  * CI/CD workflows

This ensures consistency with the project's .editorconfig settings and
improves code maintainability.
2025-11-28 20:05:37 +01:00

129 lines
5.3 KiB
Python

from datetime import datetime
from app import db
from app.utils.timezone import now_in_app_timezone
import json
def local_now():
"""Get current time in local timezone as naive datetime (for database storage)"""
return now_in_app_timezone().replace(tzinfo=None)
class QuoteVersion(db.Model):
"""Model for tracking quote version history"""
__tablename__ = "quote_versions"
id = db.Column(db.Integer, primary_key=True)
quote_id = db.Column(db.Integer, db.ForeignKey("quotes.id", ondelete="CASCADE"), nullable=False, index=True)
version_number = db.Column(db.Integer, nullable=False) # 1, 2, 3, etc.
# Snapshot of quote data at this version (stored as JSON)
quote_data = db.Column(db.Text, nullable=False) # JSON string with complete quote state
# Change information
changed_by = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, index=True)
changed_at = db.Column(db.DateTime, default=local_now, nullable=False)
change_summary = db.Column(db.String(500), nullable=True) # Brief description of changes
# What changed (for quick reference)
fields_changed = db.Column(db.String(500), nullable=True) # Comma-separated list of changed fields
# Relationships
quote = db.relationship("Quote", backref="versions")
changer = db.relationship("User", foreign_keys=[changed_by], backref="quote_version_changes")
def __init__(self, quote_id, version_number, quote_data, changed_by, **kwargs):
self.quote_id = quote_id
self.version_number = version_number
self.quote_data = quote_data if isinstance(quote_data, str) else json.dumps(quote_data)
self.changed_by = changed_by
self.change_summary = kwargs.get("change_summary", "").strip() if kwargs.get("change_summary") else None
self.fields_changed = kwargs.get("fields_changed", "").strip() if kwargs.get("fields_changed") else None
def __repr__(self):
return f"<QuoteVersion {self.version_number} for Quote {self.quote_id}>"
@property
def data_dict(self):
"""Get quote data as a dictionary"""
try:
return json.loads(self.quote_data)
except (json.JSONDecodeError, TypeError):
return {}
def to_dict(self):
"""Convert version to dictionary for API responses"""
return {
"id": self.id,
"quote_id": self.quote_id,
"version_number": self.version_number,
"quote_data": self.data_dict,
"changed_by": self.changed_by,
"changer": self.changer.username if self.changer else None,
"changed_at": self.changed_at.isoformat() if self.changed_at else None,
"change_summary": self.change_summary,
"fields_changed": self.fields_changed.split(",") if self.fields_changed else [],
}
@classmethod
def create_version(cls, quote, changed_by, change_summary=None, fields_changed=None):
"""Create a new version snapshot of a quote"""
# Get current version number
last_version = cls.query.filter_by(quote_id=quote.id).order_by(cls.version_number.desc()).first()
version_number = (last_version.version_number + 1) if last_version else 1
# Create snapshot of quote data
quote_data = {
"title": quote.title,
"description": quote.description,
"status": quote.status,
"subtotal": float(quote.subtotal),
"tax_rate": float(quote.tax_rate),
"tax_amount": float(quote.tax_amount),
"total_amount": float(quote.total_amount),
"currency_code": quote.currency_code,
"discount_type": quote.discount_type,
"discount_amount": float(quote.discount_amount) if quote.discount_amount else None,
"discount_reason": quote.discount_reason,
"coupon_code": quote.coupon_code,
"payment_terms": quote.payment_terms,
"valid_until": quote.valid_until.isoformat() if quote.valid_until else None,
"notes": quote.notes,
"terms": quote.terms,
"visible_to_client": quote.visible_to_client,
"requires_approval": quote.requires_approval,
"approval_status": quote.approval_status,
"items": [
{
"description": item.description,
"quantity": float(item.quantity),
"unit_price": float(item.unit_price),
"unit": item.unit,
}
for item in quote.items
],
}
version = cls(
quote_id=quote.id,
version_number=version_number,
quote_data=json.dumps(quote_data),
changed_by=changed_by,
change_summary=change_summary,
fields_changed=",".join(fields_changed) if fields_changed else None,
)
db.session.add(version)
return version
@classmethod
def get_quote_versions(cls, quote_id):
"""Get all versions for a quote"""
return cls.query.filter_by(quote_id=quote_id).order_by(cls.version_number.desc()).all()
@classmethod
def get_latest_version(cls, quote_id):
"""Get the latest version of a quote"""
return cls.query.filter_by(quote_id=quote_id).order_by(cls.version_number.desc()).first()