feat: complete issue #114 - Issue #114

Automated issue wrap-up including:
- Implementation completion verification
- Test execution and validation
- Cost tracking and note generation
- Repository state commit

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-05 00:31:10 +02:00
parent d24479b8a2
commit 20e7f0f5bd
6 changed files with 1816 additions and 2 deletions

View File

@@ -908,4 +908,217 @@ def current_period(date_str: Optional[str], db_path: Optional[str]):
except Exception as e:
click.echo(f"Error getting current period: {e}", err=True)
sys.exit(1)
@cost_commands.group(name='allocate')
def cost_allocate():
"""Allocate costs to active issues for specified periods."""
pass
@cost_allocate.command('period')
@click.argument('period_id', type=int)
@click.option('--database', 'db_path', help='Database path (defaults to config)')
@click.option('--dry-run', is_flag=True, help='Show what would be allocated without making changes')
def allocate_period(period_id: int, db_path: Optional[str], dry_run: bool):
"""Allocate costs for a specific period to active issues."""
try:
# Import allocation engine
from .allocation_engine import AllocationEngine, AllocationStatus
# Get database path
if not db_path:
config_manager = ConfigurationManager()
config = config_manager.get_current_config()
db_path = config.get('database_path')
if not db_path:
click.echo("Error: No database path specified.", err=True)
sys.exit(1)
# Initialize allocation engine
engine = AllocationEngine(db_path)
if dry_run:
# TODO: Implement dry-run functionality
click.echo(f"🔍 Dry run for period {period_id} allocation")
click.echo("(Dry-run functionality will be implemented in future version)")
return
# Perform allocation
result = engine.allocate_period_costs(period_id)
# Display results based on status
if result.status == AllocationStatus.SUCCESS:
click.echo(f"✅ Cost Allocation Complete - Period {period_id}")
click.echo("=" * 50)
click.echo(f"Total Costs Allocated: €{result.total_costs:.2f}")
click.echo(f"Active Issues: {len(result.active_issues)}")
click.echo(f"Cost Per Issue: €{result.cost_per_issue:.2f}")
click.echo(f"Allocations Created: {result.allocations_created}")
click.echo(f"Transactions Created: {result.transactions_created}")
if result.active_issues:
click.echo(f"\nIssues that received allocations:")
for issue_id in result.active_issues:
click.echo(f" Issue #{issue_id}: €{result.cost_per_issue:.2f}")
elif result.status == AllocationStatus.NO_ACTIVE_ISSUES:
click.echo(f"⚠️ No Active Issues Found - Period {period_id}")
click.echo("=" * 45)
click.echo(f"Total Costs: €{result.total_costs:.2f}")
click.echo(f"Loss Carried Forward: €{result.loss_carried_forward:.2f}")
click.echo("All costs have been carried forward to the next period.")
elif result.status == AllocationStatus.NO_COSTS_TO_ALLOCATE:
click.echo(f" No Costs to Allocate - Period {period_id}")
click.echo("=" * 40)
click.echo("Period has no costs to allocate.")
elif result.status == AllocationStatus.PERIOD_CLOSED:
click.echo(f"⚠️ Period Already Closed - Period {period_id}")
click.echo("=" * 40)
click.echo("This period has already been processed and closed.")
else:
click.echo(f"❌ Allocation Failed - Period {period_id}")
click.echo("=" * 35)
click.echo(f"Error: {result.message}")
sys.exit(1)
except Exception as e:
click.echo(f"Error performing allocation: {e}", err=True)
sys.exit(1)
@cost_allocate.command('show')
@click.argument('target', type=str)
@click.option('--format', 'output_format',
type=click.Choice(['table', 'json']),
default='table', help='Output format')
@click.option('--database', 'db_path', help='Database path (defaults to config)')
def show_allocations(target: str, output_format: str, db_path: Optional[str]):
"""Show allocations for an issue (issue:ID) or period (period:ID)."""
try:
# Import allocation engine
from .allocation_engine import AllocationEngine
# Get database path
if not db_path:
config_manager = ConfigurationManager()
config = config_manager.get_current_config()
db_path = config.get('database_path')
if not db_path:
click.echo("Error: No database path specified.", err=True)
sys.exit(1)
# Parse target (issue:123 or period:456)
if ':' not in target:
click.echo("Error: Target must be in format 'issue:ID' or 'period:ID'", err=True)
sys.exit(1)
target_type, target_id_str = target.split(':', 1)
try:
target_id = int(target_id_str)
except ValueError:
click.echo("Error: Target ID must be a number", err=True)
sys.exit(1)
# Initialize allocation engine
engine = AllocationEngine(db_path)
# Get allocations based on target type
if target_type == 'issue':
allocations = engine.get_issue_allocations(target_id)
title = f"Cost Allocations for Issue #{target_id}"
elif target_type == 'period':
allocations = engine.get_period_allocations(target_id)
title = f"Cost Allocations for Period {target_id}"
else:
click.echo("Error: Target type must be 'issue' or 'period'", err=True)
sys.exit(1)
if not allocations:
click.echo(f"📝 No allocations found for {target}")
return
# Display results
if output_format == 'json':
import json
click.echo(json.dumps(allocations, indent=2, default=str))
else:
# Table format
from tabulate import tabulate
click.echo(f"\n💰 {title}\n")
if target_type == 'issue':
headers = ['ID', 'Period', 'Amount', 'Date', 'Period Range', 'Transaction']
rows = []
for alloc in allocations:
rows.append([
alloc['id'],
alloc['period_id'],
f"{alloc['allocated_amount']:.2f}",
alloc['allocation_date'],
f"{alloc['period_start']} to {alloc['period_end']}",
alloc['transaction_id'] or 'N/A'
])
else:
headers = ['ID', 'Issue', 'Amount', 'Date', 'Transaction']
rows = []
for alloc in allocations:
rows.append([
alloc['id'],
f"#{alloc['issue_id']}",
f"{alloc['allocated_amount']:.2f}",
alloc['allocation_date'],
alloc['transaction_id'] or 'N/A'
])
click.echo(tabulate(rows, headers=headers, tablefmt='grid'))
click.echo(f"\n📊 Total: {len(allocations)} allocations")
except Exception as e:
click.echo(f"Error showing allocations: {e}", err=True)
sys.exit(1)
@cost_allocate.command('reverse')
@click.argument('allocation_id', type=int)
@click.option('--database', 'db_path', help='Database path (defaults to config)')
@click.confirmation_option(prompt='Are you sure you want to reverse this allocation?')
def reverse_allocation(allocation_id: int, db_path: Optional[str]):
"""Reverse a cost allocation (for corrections)."""
try:
# Import allocation engine
from .allocation_engine import AllocationEngine
# Get database path
if not db_path:
config_manager = ConfigurationManager()
config = config_manager.get_current_config()
db_path = config.get('database_path')
if not db_path:
click.echo("Error: No database path specified.", err=True)
sys.exit(1)
# Initialize allocation engine
engine = AllocationEngine(db_path)
# Perform reversal
success = engine.reverse_allocation(allocation_id)
if success:
click.echo(f"✅ Successfully reversed allocation #{allocation_id}")
click.echo("A reversal transaction has been created in the audit trail.")
else:
click.echo(f"❌ Failed to reverse allocation #{allocation_id}")
click.echo("Allocation may not exist or may already be reversed.")
sys.exit(1)
except Exception as e:
click.echo(f"Error reversing allocation: {e}", err=True)
sys.exit(1)