694 lines
32 KiB
Python
694 lines
32 KiB
Python
"""
|
|
Period handling functionality for XBRL statements.
|
|
|
|
This module provides functions for handling periods in XBRL statements, including:
|
|
- Determining available period views for different statement types
|
|
- Selecting appropriate periods for display
|
|
- Handling fiscal year and quarter information
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
# Configuration for different statement types
|
|
STATEMENT_TYPE_CONFIG = {
|
|
'BalanceSheet': {
|
|
'period_type': 'instant',
|
|
'max_periods': 3,
|
|
'allow_annual_comparison': True,
|
|
'views': [
|
|
{
|
|
'name': 'Three Recent Periods',
|
|
'description': 'Shows three most recent reporting periods',
|
|
'max_periods': 3,
|
|
'requires_min_periods': 3
|
|
},
|
|
{
|
|
'name': 'Current vs. Previous Period',
|
|
'description': 'Shows the current period and the previous period',
|
|
'max_periods': 2,
|
|
'requires_min_periods': 1
|
|
},
|
|
{
|
|
'name': 'Three-Year Annual Comparison',
|
|
'description': 'Shows three fiscal years for comparison',
|
|
'max_periods': 3,
|
|
'requires_min_periods': 3,
|
|
'annual_only': True
|
|
}
|
|
]
|
|
},
|
|
'IncomeStatement': {
|
|
'period_type': 'duration',
|
|
'max_periods': 3,
|
|
'allow_annual_comparison': True,
|
|
'views': [
|
|
{
|
|
'name': 'Three Recent Periods',
|
|
'description': 'Shows three most recent reporting periods',
|
|
'max_periods': 3,
|
|
'requires_min_periods': 3
|
|
},
|
|
{
|
|
'name': 'YTD and Quarterly Breakdown',
|
|
'description': 'Shows YTD figures and quarterly breakdown',
|
|
'max_periods': 5,
|
|
'requires_min_periods': 2,
|
|
'mixed_view': True
|
|
}
|
|
]
|
|
},
|
|
'StatementOfEquity': {
|
|
'period_type': 'duration',
|
|
'max_periods': 3,
|
|
'views': [
|
|
{
|
|
'name': 'Three Recent Periods',
|
|
'description': 'Shows three most recent reporting periods',
|
|
'max_periods': 3,
|
|
'requires_min_periods': 1
|
|
}
|
|
]
|
|
},
|
|
'ComprehensiveIncome': {
|
|
'period_type': 'duration',
|
|
'max_periods': 3,
|
|
'views': [
|
|
{
|
|
'name': 'Three Recent Periods',
|
|
'description': 'Shows three most recent reporting periods',
|
|
'max_periods': 3,
|
|
'requires_min_periods': 1
|
|
}
|
|
]
|
|
},
|
|
'CoverPage': {
|
|
'period_type': 'instant',
|
|
'max_periods': 1,
|
|
'views': [
|
|
{
|
|
'name': 'Current Period',
|
|
'description': 'Shows the current reporting period',
|
|
'max_periods': 1,
|
|
'requires_min_periods': 1
|
|
}
|
|
]
|
|
},
|
|
'Notes': {
|
|
'period_type': 'instant',
|
|
'max_periods': 1,
|
|
'views': [
|
|
{
|
|
'name': 'Current Period',
|
|
'description': 'Shows the current reporting period',
|
|
'max_periods': 1,
|
|
'requires_min_periods': 1
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
def sort_periods(periods: List[Dict], period_type: str) -> List[Dict]:
|
|
"""Sort periods by date, with most recent first."""
|
|
if period_type == 'instant':
|
|
return sorted(periods, key=lambda x: x['date'], reverse=True)
|
|
return sorted(periods, key=lambda x: (x['end_date'], x['start_date']), reverse=True)
|
|
|
|
def filter_periods_by_document_end_date(periods: List[Dict], document_period_end_date: str, period_type: str) -> List[Dict]:
|
|
"""Filter periods to only include those that end on or before the document period end date."""
|
|
if not document_period_end_date:
|
|
return periods
|
|
|
|
try:
|
|
doc_end_date = datetime.strptime(document_period_end_date, '%Y-%m-%d').date()
|
|
except (ValueError, TypeError):
|
|
# If we can't parse the document end date, return all periods
|
|
return periods
|
|
|
|
filtered_periods = []
|
|
for period in periods:
|
|
try:
|
|
if period_type == 'instant':
|
|
period_date = datetime.strptime(period['date'], '%Y-%m-%d').date()
|
|
if period_date <= doc_end_date:
|
|
filtered_periods.append(period)
|
|
else: # duration
|
|
period_end_date = datetime.strptime(period['end_date'], '%Y-%m-%d').date()
|
|
if period_end_date <= doc_end_date:
|
|
filtered_periods.append(period)
|
|
except (ValueError, TypeError):
|
|
# If we can't parse the period date, include it to be safe
|
|
filtered_periods.append(period)
|
|
|
|
return filtered_periods
|
|
|
|
def filter_periods_by_type(periods: List[Dict], period_type: str) -> List[Dict]:
|
|
"""Filter periods by their type (instant or duration)."""
|
|
return [p for p in periods if p['type'] == period_type]
|
|
|
|
def calculate_fiscal_alignment_score(end_date: datetime.date, fiscal_month: int, fiscal_day: int) -> int:
|
|
"""Calculate how well a date aligns with fiscal year end."""
|
|
if end_date.month == fiscal_month and end_date.day == fiscal_day:
|
|
return 100
|
|
if end_date.month == fiscal_month and abs(end_date.day - fiscal_day) <= 15:
|
|
return 75
|
|
if abs(end_date.month - fiscal_month) <= 1 and abs(end_date.day - fiscal_day) <= 15:
|
|
return 50
|
|
return 0
|
|
|
|
|
|
def generate_period_view(view_config: Dict[str, Any], periods: List[Dict], is_annual: bool = False) -> Optional[Dict[str, Any]]:
|
|
"""Generate a period view based on configuration and available periods.
|
|
|
|
Args:
|
|
view_config: Configuration for the view (from STATEMENT_TYPE_CONFIG)
|
|
periods: List of periods to choose from
|
|
is_annual: Whether this is an annual report
|
|
|
|
Returns:
|
|
Dictionary with view name, description, and period keys if view is valid,
|
|
None if view cannot be generated with available periods
|
|
"""
|
|
if len(periods) < view_config['requires_min_periods']:
|
|
return None
|
|
|
|
if view_config.get('annual_only', False) and not is_annual:
|
|
return None
|
|
|
|
max_periods = min(view_config['max_periods'], len(periods))
|
|
return {
|
|
'name': view_config['name'],
|
|
'description': view_config['description'],
|
|
'period_keys': [p['key'] for p in periods[:max_periods]]
|
|
}
|
|
|
|
|
|
def generate_mixed_view(view_config: Dict[str, Any], ytd_periods: List[Dict],
|
|
quarterly_periods: List[Dict]) -> Optional[Dict[str, Any]]:
|
|
"""Generate a mixed view combining YTD and quarterly periods.
|
|
|
|
Args:
|
|
view_config: Configuration for the view
|
|
ytd_periods: List of year-to-date periods
|
|
quarterly_periods: List of quarterly periods
|
|
|
|
Returns:
|
|
Dictionary with view configuration if valid, None otherwise
|
|
"""
|
|
if not ytd_periods or not quarterly_periods:
|
|
return None
|
|
|
|
mixed_keys = []
|
|
|
|
# Add current YTD
|
|
mixed_keys.append(ytd_periods[0]['key'])
|
|
|
|
# Add recent quarters
|
|
for q in quarterly_periods[:min(4, len(quarterly_periods))]:
|
|
if q['key'] not in mixed_keys:
|
|
mixed_keys.append(q['key'])
|
|
|
|
if len(mixed_keys) >= view_config['requires_min_periods']:
|
|
return {
|
|
'name': view_config['name'],
|
|
'description': view_config['description'],
|
|
'period_keys': mixed_keys[:view_config['max_periods']]
|
|
}
|
|
|
|
return None
|
|
|
|
|
|
def get_period_views(xbrl_instance, statement_type: str) -> List[Dict[str, Any]]:
|
|
"""
|
|
Get available period views for a statement type.
|
|
|
|
Args:
|
|
xbrl_instance: XBRL instance with context and entity information
|
|
statement_type: Type of statement to get period views for
|
|
|
|
Returns:
|
|
List of period view options with name, description, and period keys
|
|
"""
|
|
period_views = []
|
|
|
|
# Get statement configuration
|
|
config = STATEMENT_TYPE_CONFIG.get(statement_type)
|
|
if not config:
|
|
return period_views
|
|
|
|
# Get useful entity info for period selection
|
|
entity_info = xbrl_instance.entity_info
|
|
fiscal_period_focus = entity_info.get('fiscal_period')
|
|
annual_report = fiscal_period_focus == 'FY'
|
|
|
|
# Get all periods
|
|
all_periods = xbrl_instance.reporting_periods
|
|
document_period_end_date = xbrl_instance.period_of_report
|
|
|
|
# Filter and sort periods by type
|
|
period_type = config['period_type']
|
|
periods = filter_periods_by_type(all_periods, period_type)
|
|
# Filter by document period end date to exclude periods after the reporting period
|
|
periods = filter_periods_by_document_end_date(periods, document_period_end_date, period_type)
|
|
periods = sort_periods(periods, period_type)
|
|
|
|
# If this statement type allows annual comparison and this is an annual report,
|
|
# filter for annual periods
|
|
annual_periods = []
|
|
if config.get('allow_annual_comparison') and annual_report:
|
|
fiscal_month = entity_info.get('fiscal_year_end_month')
|
|
fiscal_day = entity_info.get('fiscal_year_end_day')
|
|
|
|
if fiscal_month is not None and fiscal_day is not None:
|
|
for period in periods:
|
|
try:
|
|
date_field = 'date' if period_type == 'instant' else 'end_date'
|
|
end_date = datetime.strptime(period[date_field], '%Y-%m-%d').date()
|
|
score = calculate_fiscal_alignment_score(end_date, fiscal_month, fiscal_day)
|
|
if score > 0: # Any alignment is good enough for a view
|
|
annual_periods.append(period)
|
|
except (ValueError, TypeError):
|
|
continue
|
|
|
|
# Generate views based on configuration
|
|
for view_config in config.get('views', []):
|
|
if view_config.get('mixed_view'):
|
|
# Special handling for mixed YTD/quarterly views
|
|
ytd_periods = [p for p in periods if p.get('ytd')]
|
|
quarterly_periods = [p for p in periods if p.get('quarterly')]
|
|
view = generate_mixed_view(view_config, ytd_periods, quarterly_periods)
|
|
elif view_config.get('annual_only'):
|
|
# Views that should only show annual periods
|
|
view = generate_period_view(view_config, annual_periods, annual_report)
|
|
else:
|
|
# Standard views using all periods
|
|
view = generate_period_view(view_config, periods, annual_report)
|
|
|
|
if view:
|
|
period_views.append(view)
|
|
|
|
return period_views
|
|
|
|
def determine_periods_to_display(
|
|
xbrl_instance,
|
|
statement_type: str,
|
|
period_filter: Optional[str] = None,
|
|
period_view: Optional[str] = None
|
|
) -> List[Tuple[str, str]]:
|
|
"""
|
|
Determine which periods should be displayed for a statement.
|
|
|
|
Uses smart period selection, which balances investor needs
|
|
with data availability for optimal financial analysis.
|
|
|
|
Args:
|
|
xbrl_instance: XBRL instance with context and entity information
|
|
statement_type: Type of statement ('BalanceSheet', 'IncomeStatement', etc.)
|
|
period_filter: Optional period key to filter by specific reporting period
|
|
period_view: Optional name of a predefined period view
|
|
|
|
Returns:
|
|
List of tuples with period keys and labels to display
|
|
"""
|
|
periods_to_display = []
|
|
|
|
# If a specific period is requested, use only that
|
|
if period_filter:
|
|
for period in xbrl_instance.reporting_periods:
|
|
if period['key'] == period_filter:
|
|
periods_to_display.append((period_filter, period['label']))
|
|
break
|
|
return periods_to_display
|
|
|
|
# If a period view is specified, use that
|
|
if period_view:
|
|
available_views = get_period_views(xbrl_instance, statement_type)
|
|
matching_view = next((view for view in available_views if view['name'] == period_view), None)
|
|
|
|
if matching_view:
|
|
for period_key in matching_view['period_keys']:
|
|
for period in xbrl_instance.reporting_periods:
|
|
if period['key'] == period_key:
|
|
periods_to_display.append((period_key, period['label']))
|
|
break
|
|
return periods_to_display
|
|
|
|
# Use unified period selection system with fallback to legacy logic
|
|
try:
|
|
from edgar.xbrl.period_selector import select_periods
|
|
return select_periods(xbrl_instance, statement_type)
|
|
except Exception as e:
|
|
# Log the error and fall back to legacy logic
|
|
import logging
|
|
logging.warning("Unified period selection failed, using legacy logic: %s", e)
|
|
# Continue to legacy logic below
|
|
|
|
# If no specific periods requested, use default logic based on statement type
|
|
all_periods = xbrl_instance.reporting_periods
|
|
entity_info = xbrl_instance.entity_info
|
|
fiscal_period_focus = entity_info.get('fiscal_period')
|
|
document_period_end_date = xbrl_instance.period_of_report
|
|
|
|
# Filter periods by statement type
|
|
if statement_type == 'BalanceSheet':
|
|
instant_periods = filter_periods_by_type(all_periods, 'instant')
|
|
# Filter by document period end date to exclude periods after the reporting period
|
|
instant_periods = filter_periods_by_document_end_date(instant_periods, document_period_end_date, 'instant')
|
|
instant_periods = sort_periods(instant_periods, 'instant')
|
|
|
|
# Get fiscal information for better period matching
|
|
fiscal_period_focus = entity_info.get('fiscal_period')
|
|
fiscal_year_focus = entity_info.get('fiscal_year')
|
|
fiscal_year_end_month = entity_info.get('fiscal_year_end_month')
|
|
fiscal_year_end_day = entity_info.get('fiscal_year_end_day')
|
|
|
|
if instant_periods:
|
|
# Take latest instant period that is not later than document_period_end_date
|
|
current_period = instant_periods[0] # Most recent
|
|
period_key = current_period['key']
|
|
periods_to_display.append((period_key, current_period['label']))
|
|
|
|
# Try to find appropriate comparison period
|
|
try:
|
|
current_date = datetime.strptime(current_period['date'], '%Y-%m-%d').date()
|
|
|
|
# Use fiscal information if available for better matching
|
|
if fiscal_year_end_month is not None and fiscal_year_end_day is not None:
|
|
# Check if this is a fiscal year end report
|
|
is_fiscal_year_end = False
|
|
if fiscal_period_focus == 'FY' or (
|
|
current_date.month == fiscal_year_end_month and
|
|
abs(current_date.day - fiscal_year_end_day) <= 7):
|
|
is_fiscal_year_end = True
|
|
|
|
if is_fiscal_year_end and fiscal_year_focus:
|
|
# For fiscal year end, find the previous fiscal year end period
|
|
prev_fiscal_year = int(fiscal_year_focus) - 1 if isinstance(fiscal_year_focus,
|
|
(int, str)) and str(
|
|
fiscal_year_focus).isdigit() else current_date.year - 1
|
|
|
|
# Look for a comparable period from previous fiscal year
|
|
for period in instant_periods[1:]: # Skip the current one
|
|
try:
|
|
period_date = datetime.strptime(period['date'], '%Y-%m-%d').date()
|
|
# Check if this period is from the previous fiscal year and around fiscal year end
|
|
if (period_date.year == prev_fiscal_year and
|
|
period_date.month == fiscal_year_end_month and
|
|
abs(period_date.day - fiscal_year_end_day) <= 15):
|
|
periods_to_display.append((period['key'], period['label']))
|
|
break
|
|
except (ValueError, TypeError):
|
|
continue
|
|
|
|
# If no appropriate period found yet, try generic date-based comparison
|
|
if len(periods_to_display) == 1:
|
|
# Look for a period from previous year with similar date pattern
|
|
prev_year = current_date.year - 1
|
|
|
|
for period in instant_periods[1:]: # Skip the current one
|
|
try:
|
|
period_date = datetime.strptime(period['date'], '%Y-%m-%d').date()
|
|
# If from previous year with similar month/day
|
|
if period_date.year == prev_year:
|
|
periods_to_display.append((period['key'], period['label']))
|
|
break
|
|
except (ValueError, TypeError):
|
|
continue
|
|
|
|
# Only add additional comparable periods (up to a total of 3)
|
|
# For annual reports, only add periods that are also fiscal year ends
|
|
is_annual_report = (fiscal_period_focus == 'FY')
|
|
added_period_keys = [key for key, _ in periods_to_display]
|
|
|
|
for period in instant_periods[1:]: # Skip current period
|
|
if len(periods_to_display) >= 3:
|
|
break # Stop when we have 3 periods
|
|
|
|
# For annual reports, only add periods that are fiscal year ends
|
|
# ENHANCED: Ensure we're selecting true annual period ends, not quarterly
|
|
if is_annual_report and fiscal_year_end_month is not None and fiscal_year_end_day is not None:
|
|
try:
|
|
# Check if this period is close to the fiscal year end
|
|
period_date = datetime.strptime(period['date'], '%Y-%m-%d').date()
|
|
|
|
# STRICT CHECK: For annual reports, be more selective
|
|
# The period should be within a reasonable range of fiscal year end
|
|
is_fiscal_year_end = (
|
|
period_date.month == fiscal_year_end_month and
|
|
abs(period_date.day - fiscal_year_end_day) <= 15 # Allow some flexibility
|
|
)
|
|
|
|
# Additional check: Ensure this is approximately 1 year before previous periods
|
|
if is_fiscal_year_end and len(periods_to_display) > 0:
|
|
prev_date_str = periods_to_display[-1][0].split('_')[-1] if '_' in periods_to_display[-1][0] else None
|
|
if prev_date_str:
|
|
try:
|
|
prev_date = datetime.strptime(prev_date_str, '%Y-%m-%d').date()
|
|
year_diff = abs((prev_date - period_date).days)
|
|
# Should be approximately 365 days apart (allow 350-380 range)
|
|
if not (350 <= year_diff <= 380):
|
|
is_fiscal_year_end = False
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
# Only include this period if it's a fiscal year end
|
|
if not is_fiscal_year_end:
|
|
continue # Skip non-fiscal-year-end periods
|
|
except (ValueError, TypeError):
|
|
continue # Skip periods with invalid dates
|
|
|
|
# Don't add periods we've already added
|
|
period_key = period['key']
|
|
if period_key not in added_period_keys:
|
|
periods_to_display.append((period_key, period['label']))
|
|
|
|
except (ValueError, TypeError):
|
|
# If date parsing failed, still try to select appropriate periods
|
|
# For annual reports, we should only show fiscal year end periods
|
|
is_annual_report = (fiscal_period_focus == 'FY')
|
|
|
|
added_count = 0
|
|
for i, period in enumerate(instant_periods):
|
|
if i == 0:
|
|
continue # Skip first period which should already be added
|
|
|
|
if added_count >= 2: # Already added 2 more (for a total of 3)
|
|
break
|
|
|
|
# For annual reports, only add periods that are close to fiscal year end
|
|
if (is_annual_report and fiscal_year_end_month is not None and
|
|
fiscal_year_end_day is not None):
|
|
try:
|
|
period_date = datetime.strptime(period['date'], '%Y-%m-%d').date()
|
|
# Only add periods close to fiscal year end
|
|
if (period_date.month != fiscal_year_end_month or
|
|
abs(period_date.day - fiscal_year_end_day) > 15):
|
|
continue # Skip periods that aren't fiscal year ends
|
|
except (ValueError, TypeError):
|
|
continue # Skip periods with invalid dates
|
|
|
|
periods_to_display.append((period['key'], period['label']))
|
|
added_count += 1
|
|
|
|
elif statement_type in ['IncomeStatement', 'CashFlowStatement']:
|
|
duration_periods = filter_periods_by_type(all_periods, 'duration')
|
|
# Filter by document period end date to exclude periods after the reporting period
|
|
duration_periods = filter_periods_by_document_end_date(duration_periods, document_period_end_date, 'duration')
|
|
duration_periods = sort_periods(duration_periods, 'duration')
|
|
if duration_periods:
|
|
# For annual reports, prioritize annual periods
|
|
if fiscal_period_focus == 'FY':
|
|
# Get fiscal year end information if available
|
|
fiscal_year_end_month = entity_info.get('fiscal_year_end_month')
|
|
fiscal_year_end_day = entity_info.get('fiscal_year_end_day')
|
|
|
|
# First pass: Find all periods that are approximately a year long
|
|
# CRITICAL FIX: Apply strict duration filtering to ensure we only get annual periods
|
|
# Some facts are marked as FY but are actually quarterly (90 days vs 363+ days)
|
|
candidate_annual_periods = []
|
|
for period in duration_periods:
|
|
try:
|
|
start_date = datetime.strptime(period['start_date'], '%Y-%m-%d').date()
|
|
end_date = datetime.strptime(period['end_date'], '%Y-%m-%d').date()
|
|
days = (end_date - start_date).days
|
|
# STRICT CHECK: Annual periods must be > 300 days
|
|
# This filters out quarterly periods incorrectly marked as FY
|
|
if days > 300: # Truly annual period (not quarterly)
|
|
# Add a score to each period for later sorting
|
|
# Default score is 0 (will be increased for fiscal year matches)
|
|
period_with_score = period.copy()
|
|
period_with_score['fiscal_alignment_score'] = 0
|
|
period_with_score['duration_days'] = days # Store for debugging
|
|
candidate_annual_periods.append(period_with_score)
|
|
except (ValueError, TypeError):
|
|
continue
|
|
|
|
# Second pass: Score periods based on alignment with fiscal year pattern
|
|
if fiscal_year_end_month is not None and fiscal_year_end_day is not None:
|
|
for period in candidate_annual_periods:
|
|
try:
|
|
# Check how closely the end date aligns with fiscal year end
|
|
end_date = datetime.strptime(period['end_date'], '%Y-%m-%d').date()
|
|
|
|
# Perfect match: Same month and day as fiscal year end
|
|
if end_date.month == fiscal_year_end_month and end_date.day == fiscal_year_end_day:
|
|
period['fiscal_alignment_score'] = 100
|
|
# Strong match: Same month and within 15 days
|
|
elif end_date.month == fiscal_year_end_month and abs(end_date.day - fiscal_year_end_day) <= 15:
|
|
period['fiscal_alignment_score'] = 75
|
|
# Moderate match: Month before/after and close to the day
|
|
elif abs(end_date.month - fiscal_year_end_month) <= 1 and abs(end_date.day - fiscal_year_end_day) <= 15:
|
|
period['fiscal_alignment_score'] = 50
|
|
except (ValueError, TypeError):
|
|
continue
|
|
|
|
# Sort periods by fiscal alignment (higher score first) and then by recency (end date)
|
|
annual_periods = sorted(
|
|
candidate_annual_periods,
|
|
key=lambda x: (x['fiscal_alignment_score'], x['end_date']),
|
|
reverse=True # Highest score and most recent first
|
|
)
|
|
|
|
if annual_periods:
|
|
# Take up to 3 best matching annual periods (prioritizing fiscal year alignment)
|
|
for period in annual_periods[:3]:
|
|
periods_to_display.append((period['key'], period['label']))
|
|
return periods_to_display
|
|
|
|
# For quarterly reports, apply intelligent period selection
|
|
else:
|
|
# First, categorize periods by duration to identify meaningful financial periods
|
|
quarterly_periods = [] # 85-95 days (one quarter)
|
|
ytd_periods = [] # 175-185 days (two quarters), 265-275 days (three quarters)
|
|
annual_periods = [] # 350-380 days (full year for comparisons)
|
|
|
|
current_year = None
|
|
if document_period_end_date:
|
|
try:
|
|
current_year = datetime.strptime(document_period_end_date, '%Y-%m-%d').year
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
# Categorize all duration periods by their length
|
|
# ENHANCED: More strict duration checking to avoid misclassification
|
|
for period in duration_periods:
|
|
try:
|
|
start_date = datetime.strptime(period['start_date'], '%Y-%m-%d').date()
|
|
end_date = datetime.strptime(period['end_date'], '%Y-%m-%d').date()
|
|
days = (end_date - start_date).days
|
|
|
|
# Skip single-day or very short periods (less than 30 days)
|
|
if days < 30:
|
|
continue
|
|
|
|
# Categorize by duration with stricter checks
|
|
if 80 <= days <= 100: # Quarterly period (~90 days), slightly wider range
|
|
period['period_type'] = 'quarterly'
|
|
period['days'] = days
|
|
quarterly_periods.append(period)
|
|
elif 170 <= days <= 190: # Semi-annual/YTD for Q2 (~180 days)
|
|
period['period_type'] = 'semi-annual'
|
|
period['days'] = days
|
|
ytd_periods.append(period)
|
|
elif 260 <= days <= 280: # YTD for Q3 (~270 days)
|
|
period['period_type'] = 'three-quarters'
|
|
period['days'] = days
|
|
ytd_periods.append(period)
|
|
elif days > 300: # Annual period for comparisons (strict check)
|
|
period['period_type'] = 'annual'
|
|
period['days'] = days
|
|
annual_periods.append(period)
|
|
except (ValueError, TypeError):
|
|
continue
|
|
|
|
# Build the optimal set of periods for quarterly reporting
|
|
selected_periods = []
|
|
|
|
# 1. Add the most recent quarterly period (current quarter)
|
|
if quarterly_periods:
|
|
# Find the most recent quarterly period
|
|
recent_quarterly = quarterly_periods[0] # Already sorted by end date
|
|
selected_periods.append(recent_quarterly)
|
|
|
|
# Try to find the same quarter from previous year for comparison
|
|
if current_year:
|
|
for qp in quarterly_periods[1:]:
|
|
try:
|
|
qp_end = datetime.strptime(qp['end_date'], '%Y-%m-%d').date()
|
|
recent_end = datetime.strptime(recent_quarterly['end_date'], '%Y-%m-%d').date()
|
|
# Same quarter, previous year (within 15 days tolerance)
|
|
if (qp_end.year == current_year - 1 and
|
|
qp_end.month == recent_end.month and
|
|
abs(qp_end.day - recent_end.day) <= 15):
|
|
selected_periods.append(qp)
|
|
break
|
|
except (ValueError, TypeError):
|
|
continue
|
|
|
|
# 2. Add the most recent YTD period if available
|
|
if ytd_periods:
|
|
# Find the YTD period that ends closest to the document period end
|
|
selected_periods.append(ytd_periods[0])
|
|
|
|
# 3. If we don't have enough periods yet, add more quarterly periods
|
|
if len(selected_periods) < 3:
|
|
for period in quarterly_periods:
|
|
if period not in selected_periods and len(selected_periods) < 3:
|
|
selected_periods.append(period)
|
|
|
|
# 4. If still not enough, consider annual periods for year-over-year comparison
|
|
if len(selected_periods) < 3 and annual_periods:
|
|
for period in annual_periods:
|
|
if len(selected_periods) < 3:
|
|
selected_periods.append(period)
|
|
|
|
# Convert selected periods to display format
|
|
for period in selected_periods[:3]: # Limit to 3 periods
|
|
periods_to_display.append((period['key'], period['label']))
|
|
|
|
# For other statement types (not covered by specific logic above)
|
|
else:
|
|
# Get configuration for this statement type, or use defaults
|
|
statement_info = STATEMENT_TYPE_CONFIG.get(statement_type, {})
|
|
|
|
if not statement_info:
|
|
# For unknown statement types, use heuristics based on available periods
|
|
|
|
# For unknown statement types, determine preferences based on fiscal period
|
|
if fiscal_period_focus == 'FY':
|
|
# For annual reports, prefer duration periods and show comparisons
|
|
statement_info = {
|
|
'period_type': 'duration',
|
|
'max_periods': 3,
|
|
'allow_annual_comparison': True
|
|
}
|
|
else:
|
|
# For interim reports, accept either type but limit to current period
|
|
statement_info = {
|
|
'period_type': 'either',
|
|
'max_periods': 1,
|
|
'allow_annual_comparison': False
|
|
}
|
|
|
|
# Select periods based on determined preferences
|
|
period_type = statement_info.get('period_type', 'either')
|
|
max_periods = statement_info.get('max_periods', 1)
|
|
|
|
if period_type == 'instant' or period_type == 'either':
|
|
instant_periods = filter_periods_by_type(all_periods, 'instant')
|
|
instant_periods = filter_periods_by_document_end_date(instant_periods, document_period_end_date, 'instant')
|
|
instant_periods = sort_periods(instant_periods, 'instant')
|
|
if instant_periods:
|
|
for period in instant_periods[:max_periods]:
|
|
periods_to_display.append((period['key'], period['label']))
|
|
|
|
if (period_type == 'duration' or (period_type == 'either' and not periods_to_display)):
|
|
duration_periods = filter_periods_by_type(all_periods, 'duration')
|
|
duration_periods = filter_periods_by_document_end_date(duration_periods, document_period_end_date, 'duration')
|
|
duration_periods = sort_periods(duration_periods, 'duration')
|
|
if duration_periods:
|
|
for period in duration_periods[:max_periods]:
|
|
periods_to_display.append((period['key'], period['label']))
|
|
|
|
return periods_to_display
|