""" Cost Report Template Generator for MarkiTect. This module generates professional markdown cost reports from database data with proper frontmatter/contentmatter structure. Reports include cost breakdowns, issue allocations, and audit trails. Supports multiple formats: - Summary: High-level overview with totals - Detailed: Complete breakdown by category and item - Audit: Full transaction history and audit trail """ import json import sqlite3 from datetime import date, datetime from decimal import Decimal from typing import Dict, Any, List, Optional, Union from dataclasses import dataclass from pathlib import Path from .cost_manager import CostItemManager from .models import FinanceModels @dataclass class ReportConfig: """Configuration for report generation.""" format: str = "summary" # summary, detailed, audit period_start: date = None period_end: date = None include_inactive: bool = False currency: str = "EUR" output_path: Optional[str] = None class CostReportGenerator: """Generator for cost reports with MarkiTect integration.""" def __init__(self, db_path: str): """ Initialize report generator. Args: db_path: Path to SQLite database file """ self.db_path = db_path self.cost_manager = CostItemManager(db_path) self.finance_models = FinanceModels(db_path) def generate_report(self, config: ReportConfig) -> str: """ Generate a cost report based on configuration. Args: config: Report configuration Returns: Markdown content with frontmatter and contentmatter """ if config.format == "summary": return self.generate_summary_report(config) elif config.format == "detailed": return self.generate_detailed_report(config) elif config.format == "audit": return self.generate_audit_report(config) else: raise ValueError(f"Unknown report format: {config.format}") def generate_summary_report(self, config: ReportConfig) -> str: """Generate a summary cost report.""" # Calculate period costs calculations = self.cost_manager.calculate_period_costs( config.period_start, config.period_end ) # Get active cost items for the period active_costs = self.cost_manager.get_active_costs_for_period( config.period_start, config.period_end ) # Prepare frontmatter frontmatter = { "report_type": "cost_summary", "period_start": config.period_start.isoformat(), "period_end": config.period_end.isoformat(), "total_costs": calculations["total_period"], "currency": config.currency, "generated_at": datetime.now().isoformat(), "active_items": calculations["active_cost_items"] } # Prepare contentmatter contentmatter = { "cost_data": { "total_monthly": calculations["total_monthly"], "total_one_time": calculations["total_one_time"], "total": calculations["total_period"], "categories": [ { "name": name, "monthly": breakdown["monthly"], "one_time": breakdown["one_time"], "total": breakdown["total"] } for name, breakdown in calculations["category_breakdown"].items() ], "active_items": len(active_costs) } } # Generate report content period_str = f"{config.period_start.strftime('%B %Y')}" content = self._build_summary_content( period_str, calculations, active_costs, config ) return self._assemble_markdown(frontmatter, content, contentmatter) def generate_detailed_report(self, config: ReportConfig) -> str: """Generate a detailed cost report with full breakdowns.""" # Calculate period costs calculations = self.cost_manager.calculate_period_costs( config.period_start, config.period_end ) # Get active cost items grouped by category active_costs = self.cost_manager.get_active_costs_for_period( config.period_start, config.period_end ) # Group costs by category costs_by_category = {} for cost in active_costs: category = cost['category_name'] or 'Uncategorized' if category not in costs_by_category: costs_by_category[category] = [] costs_by_category[category].append(cost) # Prepare frontmatter frontmatter = { "report_type": "cost_detailed", "period_start": config.period_start.isoformat(), "period_end": config.period_end.isoformat(), "total_costs": calculations["total_period"], "currency": config.currency, "generated_at": datetime.now().isoformat(), "categories_count": len(costs_by_category), "active_items": calculations["active_cost_items"] } # Prepare detailed contentmatter contentmatter = { "cost_data": { "summary": { "total_monthly": calculations["total_monthly"], "total_one_time": calculations["total_one_time"], "total": calculations["total_period"] }, "categories": {}, "items": [] } } for category, costs in costs_by_category.items(): cat_total = sum(float(cost['amount_eur']) for cost in costs) contentmatter["cost_data"]["categories"][category] = { "total": cat_total, "items_count": len(costs) } for cost in costs: contentmatter["cost_data"]["items"].append({ "id": cost['id'], "name": cost['name'], "category": category, "amount": float(cost['amount_eur']), "type": cost['cost_type'], "active": bool(cost['is_active']) }) # Generate report content period_str = f"{config.period_start.strftime('%B %Y')}" content = self._build_detailed_content( period_str, calculations, costs_by_category, config ) return self._assemble_markdown(frontmatter, content, contentmatter) def generate_audit_report(self, config: ReportConfig) -> str: """Generate an audit report with transaction history.""" # Get cost calculations calculations = self.cost_manager.calculate_period_costs( config.period_start, config.period_end ) # Get all cost items for the period active_costs = self.cost_manager.get_active_costs_for_period( config.period_start, config.period_end ) # Get transaction history (simplified - would be enhanced with actual transaction data) transactions = self._get_transaction_history(config.period_start, config.period_end) # Prepare frontmatter frontmatter = { "report_type": "cost_audit", "period_start": config.period_start.isoformat(), "period_end": config.period_end.isoformat(), "total_costs": calculations["total_period"], "currency": config.currency, "generated_at": datetime.now().isoformat(), "transactions_count": len(transactions), "audit_trail": True } # Prepare audit contentmatter (ensure all dates are serialized) period_summary = calculations.copy() period_summary['period_start'] = period_summary['period_start'].isoformat() period_summary['period_end'] = period_summary['period_end'].isoformat() contentmatter = { "audit_data": { "period_summary": period_summary, "transactions": transactions, "cost_items": [ { "id": cost['id'], "name": cost['name'], "amount": float(cost['amount_eur']), "start_date": str(cost['starting_from_date']) if cost['starting_from_date'] else None, "end_date": str(cost['ending_date']) if cost['ending_date'] else None } for cost in active_costs ] } } # Generate report content period_str = f"{config.period_start.strftime('%B %Y')}" content = self._build_audit_content( period_str, calculations, active_costs, transactions, config ) return self._assemble_markdown(frontmatter, content, contentmatter) def _build_summary_content(self, period_str: str, calculations: Dict[str, Any], active_costs: List[Dict], config: ReportConfig) -> str: """Build summary report content.""" content = [ f"# Cost Summary Report - {period_str}", "", "## Overview", f"- **Period**: {config.period_start.strftime('%Y-%m-%d')} to {config.period_end.strftime('%Y-%m-%d')}", f"- **Total Costs**: €{calculations['total_period']:.2f}", f"- **Monthly Recurring**: €{calculations['total_monthly']:.2f}", f"- **One-time Expenses**: €{calculations['total_one_time']:.2f}", f"- **Active Cost Items**: {calculations['active_cost_items']}", "" ] if calculations['category_breakdown']: content.extend([ "## Cost Breakdown by Category", "", "| Category | Monthly | One-time | Total |", "|----------|---------|----------|-------|" ]) for category, breakdown in calculations['category_breakdown'].items(): content.append( f"| {category} | €{breakdown['monthly']:.2f} | " f"€{breakdown['one_time']:.2f} | €{breakdown['total']:.2f} |" ) content.append("") # Add top cost items if active_costs: content.extend([ "## Top Cost Items", "" ]) # Sort by amount (descending) sorted_costs = sorted(active_costs, key=lambda x: float(x['amount_eur']), reverse=True) for cost in sorted_costs[:5]: # Top 5 content.append( f"- **{cost['name']}**: €{float(cost['amount_eur']):.2f}/{cost['cost_type']} " f"({cost['category_name'] or 'Uncategorized'})" ) content.append("") return "\n".join(content) def _build_detailed_content(self, period_str: str, calculations: Dict[str, Any], costs_by_category: Dict[str, List], config: ReportConfig) -> str: """Build detailed report content.""" content = [ f"# Detailed Cost Report - {period_str}", "", "## Executive Summary", f"- **Period**: {config.period_start.strftime('%Y-%m-%d')} to {config.period_end.strftime('%Y-%m-%d')}", f"- **Total Costs**: €{calculations['total_period']:.2f}", f"- **Monthly Recurring**: €{calculations['total_monthly']:.2f}", f"- **One-time Expenses**: €{calculations['total_one_time']:.2f}", f"- **Categories**: {len(costs_by_category)}", f"- **Active Items**: {calculations['active_cost_items']}", "" ] # Add detailed breakdown by category for category, costs in costs_by_category.items(): category_total = sum(float(cost['amount_eur']) for cost in costs) content.extend([ f"## {category}", f"**Category Total**: €{category_total:.2f}", "", "| Name | Type | Amount | Status | Start Date |", "|------|------|--------|--------|------------|" ]) for cost in sorted(costs, key=lambda x: float(x['amount_eur']), reverse=True): status = "Active" if cost['is_active'] else "Inactive" content.append( f"| {cost['name']} | {cost['cost_type']} | " f"€{float(cost['amount_eur']):.2f} | {status} | " f"{cost['starting_from_date']} |" ) content.extend(["", ""]) return "\n".join(content) def _build_audit_content(self, period_str: str, calculations: Dict[str, Any], active_costs: List[Dict], transactions: List[Dict], config: ReportConfig) -> str: """Build audit report content.""" content = [ f"# Cost Audit Report - {period_str}", "", "## Audit Summary", f"- **Period**: {config.period_start.strftime('%Y-%m-%d')} to {config.period_end.strftime('%Y-%m-%d')}", f"- **Total Costs**: €{calculations['total_period']:.2f}", f"- **Audit Date**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", f"- **Transaction Records**: {len(transactions)}", f"- **Cost Items Reviewed**: {len(active_costs)}", "", "## Cost Verification", "", "### Active Cost Items", "", "| ID | Name | Category | Amount | Type | Start Date | End Date |", "|----|----- |----------|--------|------|------------|----------|" ] for cost in active_costs: end_date = cost['ending_date'] if cost['ending_date'] else "Ongoing" content.append( f"| {cost['id']} | {cost['name']} | " f"{cost['category_name'] or 'N/A'} | €{float(cost['amount_eur']):.2f} | " f"{cost['cost_type']} | {cost['starting_from_date']} | {end_date} |" ) content.extend(["", "### Transaction History", ""]) if transactions: content.extend([ "| Date | Type | Amount | Description |", "|------|------|--------|-------------|" ]) for transaction in transactions: content.append( f"| {transaction['date']} | {transaction['type']} | " f"€{transaction['amount']:.2f} | {transaction['description']} |" ) else: content.append("*No transaction records found for this period.*") content.extend(["", "### Audit Trail", ""]) content.append(f"This report was generated automatically from the MarkiTect cost tracking database.") content.append(f"All amounts are in {config.currency} and reflect active cost items for the specified period.") return "\n".join(content) def _get_transaction_history(self, period_start: date, period_end: date) -> List[Dict]: """Get transaction history for audit report (placeholder implementation).""" # This would query actual transaction data when implemented # For now, return sample data return [ { "date": period_start.isoformat(), "type": "cost_incurred", "amount": 87.00, "description": "Monthly recurring costs applied" } ] def _assemble_markdown(self, frontmatter: Dict[str, Any], content: str, contentmatter: Dict[str, Any]) -> str: """Assemble complete markdown document with frontmatter and contentmatter.""" # Convert frontmatter to YAML fm_lines = ["---"] for key, value in frontmatter.items(): if isinstance(value, str): fm_lines.append(f'{key}: "{value}"') else: fm_lines.append(f'{key}: {value}') fm_lines.append("---") # Convert contentmatter to HTML comment cm_json = json.dumps(contentmatter, indent=2) cm_lines = [ "" ] # Assemble final document return "\n".join([ "\n".join(fm_lines), "", content, "", "\n".join(cm_lines) ]) def save_report(self, report_content: str, output_path: str) -> None: """Save report to file.""" output_file = Path(output_path) output_file.parent.mkdir(parents=True, exist_ok=True) with open(output_file, 'w', encoding='utf-8') as f: f.write(report_content) def generate_period_report(self, year: int, month: int, format: str = "summary") -> str: """ Generate report for a specific month. Args: year: Year (e.g., 2025) month: Month (1-12) format: Report format Returns: Generated report content """ from calendar import monthrange period_start = date(year, month, 1) _, last_day = monthrange(year, month) period_end = date(year, month, last_day) config = ReportConfig( format=format, period_start=period_start, period_end=period_end ) return self.generate_report(config)