Spaces:
Sleeping
Sleeping
import os | |
import json | |
from datetime import datetime | |
from typing import Dict, Optional | |
import logging | |
try: | |
from reportlab.lib.pagesizes import letter, A4 | |
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle | |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |
from reportlab.lib.units import inch | |
from reportlab.lib import colors | |
from reportlab.lib.enums import TA_CENTER, TA_LEFT | |
REPORTLAB_AVAILABLE = True | |
except ImportError: | |
REPORTLAB_AVAILABLE = False | |
# Configure logging | |
logging.basicConfig(level=logging.INFO) | |
logger = logging.getLogger(__name__) | |
class PDFGeneratorService: | |
"""Service for generating PDF reports for yearly plans and other documents""" | |
def __init__(self, output_dir: str = "generated_pdfs"): | |
self.output_dir = output_dir | |
os.makedirs(output_dir, exist_ok=True) | |
if not REPORTLAB_AVAILABLE: | |
logger.warning("ReportLab not available. PDF generation will use fallback HTML to PDF") | |
def generate_yearly_plan_pdf(self, farmer_data: Dict, plan_data: Dict) -> Optional[str]: | |
"""Generate PDF for yearly plan""" | |
try: | |
if REPORTLAB_AVAILABLE: | |
return self._generate_with_reportlab(farmer_data, plan_data) | |
else: | |
return self._generate_with_html_fallback(farmer_data, plan_data) | |
except Exception as e: | |
logger.error(f"Error generating yearly plan PDF: {str(e)}") | |
return None | |
def _generate_with_reportlab(self, farmer_data: Dict, plan_data: Dict) -> str: | |
"""Generate PDF using ReportLab""" | |
try: | |
# Create filename | |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
filename = f"yearly_plan_{farmer_data.get('name', 'farmer')}_{timestamp}.pdf" | |
filepath = os.path.join(self.output_dir, filename) | |
# Create document | |
doc = SimpleDocTemplate(filepath, pagesize=A4, topMargin=0.5*inch) | |
styles = getSampleStyleSheet() | |
# Custom styles | |
title_style = ParagraphStyle( | |
'CustomTitle', | |
parent=styles['Heading1'], | |
fontSize=24, | |
textColor=colors.darkgreen, | |
spaceAfter=20, | |
alignment=TA_CENTER | |
) | |
heading_style = ParagraphStyle( | |
'CustomHeading', | |
parent=styles['Heading2'], | |
fontSize=16, | |
textColor=colors.blue, | |
spaceAfter=12, | |
spaceBefore=20 | |
) | |
story = [] | |
# Title | |
story.append(Paragraph("🌾 Comprehensive Yearly Farming Plan", title_style)) | |
story.append(Spacer(1, 20)) | |
# Farmer Information | |
story.append(Paragraph("Farmer Information", heading_style)) | |
farmer_info = [ | |
['Name:', farmer_data.get('name', 'N/A')], | |
['Contact:', farmer_data.get('contact_number', 'N/A')], | |
['Address:', farmer_data.get('address', 'N/A')], | |
['Plan Year:', plan_data.get('year', datetime.now().year)] | |
] | |
farmer_table = Table(farmer_info, colWidths=[2*inch, 4*inch]) | |
farmer_table.setStyle(TableStyle([ | |
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'), | |
('FONTSIZE', (0, 0), (-1, -1), 12), | |
('TEXTCOLOR', (0, 0), (0, -1), colors.darkblue), | |
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'), | |
('VALIGN', (0, 0), (-1, -1), 'TOP'), | |
('BOTTOMPADDING', (0, 0), (-1, -1), 8), | |
])) | |
story.append(farmer_table) | |
story.append(Spacer(1, 20)) | |
# Farm Plans | |
farms = plan_data.get('farms', []) | |
for i, farm in enumerate(farms): | |
story.append(Paragraph(f"Farm {i+1}: {farm.get('farm_name', 'Unknown Farm')}", heading_style)) | |
# Parse farm plan | |
try: | |
farm_plan = json.loads(farm.get('plan', '{}')) if isinstance(farm.get('plan'), str) else farm.get('plan', {}) | |
# Monthly Plan Table | |
if 'monthly_plan' in farm_plan: | |
story.append(Paragraph("Monthly Farming Schedule", styles['Heading3'])) | |
monthly_data = [['Month', 'Planned Activities']] | |
for month_data in farm_plan['monthly_plan']: | |
month = month_data.get('month', '') | |
tasks = month_data.get('tasks', []) | |
tasks_text = '\n'.join(tasks[:3]) # Show max 3 tasks | |
monthly_data.append([month, tasks_text]) | |
monthly_table = Table(monthly_data, colWidths=[1.5*inch, 4.5*inch]) | |
monthly_table.setStyle(TableStyle([ | |
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), | |
('FONTSIZE', (0, 0), (-1, -1), 10), | |
('BACKGROUND', (0, 0), (-1, 0), colors.lightgreen), | |
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), | |
('ALIGN', (0, 0), (-1, -1), 'LEFT'), | |
('VALIGN', (0, 0), (-1, -1), 'TOP'), | |
('GRID', (0, 0), (-1, -1), 1, colors.black), | |
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey]), | |
])) | |
story.append(monthly_table) | |
story.append(Spacer(1, 15)) | |
# Crop Information | |
crops = farm_plan.get('crops', []) | |
if crops: | |
story.append(Paragraph("Crop Details", styles['Heading3'])) | |
crop_data = [['Crop Name', 'Sowing Month', 'Area']] | |
for crop in crops: | |
crop_data.append([ | |
crop.get('name', 'Unknown'), | |
crop.get('sowing_month', 'N/A'), | |
crop.get('area', 'N/A') | |
]) | |
crop_table = Table(crop_data, colWidths=[2*inch, 2*inch, 2*inch]) | |
crop_table.setStyle(TableStyle([ | |
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), | |
('BACKGROUND', (0, 0), (-1, 0), colors.lightblue), | |
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), | |
('ALIGN', (0, 0), (-1, -1), 'CENTER'), | |
('GRID', (0, 0), (-1, -1), 1, colors.black), | |
])) | |
story.append(crop_table) | |
story.append(Spacer(1, 15)) | |
except Exception as e: | |
logger.error(f"Error processing farm plan: {str(e)}") | |
story.append(Paragraph("Plan details could not be processed", styles['Normal'])) | |
story.append(Spacer(1, 10)) | |
# Summary and Recommendations | |
story.append(Paragraph("Summary & Recommendations", heading_style)) | |
summary_text = plan_data.get('summary', 'Comprehensive yearly plan generated with AI analysis.') | |
story.append(Paragraph(summary_text, styles['Normal'])) | |
story.append(Spacer(1, 10)) | |
# Footer | |
story.append(Spacer(1, 30)) | |
footer_text = f"Generated on: {datetime.now().strftime('%B %d, %Y at %I:%M %p')}" | |
story.append(Paragraph(footer_text, styles['Normal'])) | |
story.append(Paragraph("🌱 Powered by AI Agriculture Assistant", styles['Normal'])) | |
# Build PDF | |
doc.build(story) | |
logger.info(f"Successfully generated PDF: {filepath}") | |
return filepath | |
except Exception as e: | |
logger.error(f"Error generating ReportLab PDF: {str(e)}") | |
return None | |
def _generate_with_html_fallback(self, farmer_data: Dict, plan_data: Dict) -> str: | |
"""Generate HTML file as fallback when ReportLab is not available""" | |
try: | |
# Create filename | |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
filename = f"yearly_plan_{farmer_data.get('name', 'farmer')}_{timestamp}.html" | |
filepath = os.path.join(self.output_dir, filename) | |
# Generate HTML content | |
html_content = self._create_html_report(farmer_data, plan_data) | |
# Save HTML file | |
with open(filepath, 'w', encoding='utf-8') as f: | |
f.write(html_content) | |
logger.info(f"Successfully generated HTML report: {filepath}") | |
return filepath | |
except Exception as e: | |
logger.error(f"Error generating HTML fallback: {str(e)}") | |
return None | |
def _create_html_report(self, farmer_data: Dict, plan_data: Dict) -> str: | |
"""Create HTML report content""" | |
html = f""" | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Yearly Farming Plan - {farmer_data.get('name', 'Farmer')}</title> | |
<style> | |
body {{ | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
line-height: 1.6; | |
margin: 20px; | |
background-color: #f9f9f9; | |
}} | |
.container {{ | |
max-width: 800px; | |
margin: 0 auto; | |
background: white; | |
padding: 30px; | |
border-radius: 10px; | |
box-shadow: 0 0 20px rgba(0,0,0,0.1); | |
}} | |
.header {{ | |
text-align: center; | |
color: #2c5530; | |
border-bottom: 3px solid #4CAF50; | |
padding-bottom: 20px; | |
margin-bottom: 30px; | |
}} | |
.section {{ | |
margin-bottom: 30px; | |
}} | |
.section h2 {{ | |
color: #1976d2; | |
border-left: 4px solid #4CAF50; | |
padding-left: 15px; | |
}} | |
table {{ | |
width: 100%; | |
border-collapse: collapse; | |
margin: 15px 0; | |
}} | |
th, td {{ | |
padding: 12px; | |
text-align: left; | |
border: 1px solid #ddd; | |
}} | |
th {{ | |
background-color: #4CAF50; | |
color: white; | |
}} | |
tr:nth-child(even) {{ | |
background-color: #f2f2f2; | |
}} | |
.info-grid {{ | |
display: grid; | |
grid-template-columns: 1fr 2fr; | |
gap: 10px; | |
margin: 15px 0; | |
}} | |
.info-label {{ | |
font-weight: bold; | |
color: #333; | |
}} | |
.footer {{ | |
text-align: center; | |
margin-top: 40px; | |
padding-top: 20px; | |
border-top: 2px solid #eee; | |
color: #666; | |
}} | |
.highlight {{ | |
background-color: #fff3cd; | |
padding: 15px; | |
border-left: 4px solid #ffc107; | |
margin: 15px 0; | |
}} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="header"> | |
<h1>🌾 Comprehensive Yearly Farming Plan</h1> | |
<p>AI-Generated Agricultural Strategy</p> | |
</div> | |
<div class="section"> | |
<h2>Farmer Information</h2> | |
<div class="info-grid"> | |
<div class="info-label">Name:</div> | |
<div>{farmer_data.get('name', 'N/A')}</div> | |
<div class="info-label">Contact:</div> | |
<div>{farmer_data.get('contact_number', 'N/A')}</div> | |
<div class="info-label">Address:</div> | |
<div>{farmer_data.get('address', 'N/A')}</div> | |
<div class="info-label">Plan Year:</div> | |
<div>{plan_data.get('year', datetime.now().year)}</div> | |
</div> | |
</div> | |
""" | |
# Add farm details | |
farms = plan_data.get('farms', []) | |
for i, farm in enumerate(farms): | |
html += f""" | |
<div class="section"> | |
<h2>Farm {i+1}: {farm.get('farm_name', 'Unknown Farm')}</h2> | |
""" | |
# Parse farm plan | |
try: | |
farm_plan = json.loads(farm.get('plan', '{}')) if isinstance(farm.get('plan'), str) else farm.get('plan', {}) | |
# Monthly Plan | |
if 'monthly_plan' in farm_plan: | |
html += """ | |
<h3>Monthly Farming Schedule</h3> | |
<table> | |
<thead> | |
<tr> | |
<th>Month</th> | |
<th>Planned Activities</th> | |
</tr> | |
</thead> | |
<tbody> | |
""" | |
for month_data in farm_plan['monthly_plan']: | |
month = month_data.get('month', '') | |
tasks = month_data.get('tasks', []) | |
tasks_html = '<br>'.join(tasks[:4]) # Show max 4 tasks | |
html += f""" | |
<tr> | |
<td><strong>{month}</strong></td> | |
<td>{tasks_html}</td> | |
</tr> | |
""" | |
html += """ | |
</tbody> | |
</table> | |
""" | |
# Crop Information | |
crops = farm_plan.get('crops', []) | |
if crops: | |
html += """ | |
<h3>Crop Details</h3> | |
<table> | |
<thead> | |
<tr> | |
<th>Crop Name</th> | |
<th>Sowing Month</th> | |
<th>Area</th> | |
</tr> | |
</thead> | |
<tbody> | |
""" | |
for crop in crops: | |
html += f""" | |
<tr> | |
<td>{crop.get('name', 'Unknown')}</td> | |
<td>{crop.get('sowing_month', 'N/A')}</td> | |
<td>{crop.get('area', 'N/A')}</td> | |
</tr> | |
""" | |
html += """ | |
</tbody> | |
</table> | |
""" | |
# AI Generation Note | |
if farm_plan.get('ai_generated'): | |
html += """ | |
<div class="highlight"> | |
<strong>🤖 AI-Generated Plan:</strong> This plan was created using advanced AI analysis of soil conditions, weather patterns, and agricultural best practices. | |
</div> | |
""" | |
except Exception as e: | |
html += f"<p><em>Plan details could not be processed: {str(e)}</em></p>" | |
html += " </div>\n" | |
# Summary | |
summary_text = plan_data.get('summary', 'Comprehensive yearly plan generated with AI analysis.') | |
html += f""" | |
<div class="section"> | |
<h2>Summary & Recommendations</h2> | |
<p>{summary_text}</p> | |
<div class="highlight"> | |
<strong>📱 Next Steps:</strong> Access your daily advisories through the farmer dashboard. Monitor weather alerts and market prices for optimal farming decisions. | |
</div> | |
</div> | |
<div class="footer"> | |
<p>Generated on: {datetime.now().strftime('%B %d, %Y at %I:%M %p')}</p> | |
<p>🌱 <strong>Powered by AI Agriculture Assistant</strong></p> | |
<p><em>This plan is generated based on AI analysis and should be used in conjunction with local agricultural expertise.</em></p> | |
</div> | |
</div> | |
</body> | |
</html> | |
""" | |
return html | |
def cleanup_old_files(self, days_old: int = 30): | |
"""Clean up old generated files""" | |
try: | |
import time | |
cutoff_time = time.time() - (days_old * 24 * 60 * 60) | |
for filename in os.listdir(self.output_dir): | |
filepath = os.path.join(self.output_dir, filename) | |
if os.path.isfile(filepath) and os.path.getctime(filepath) < cutoff_time: | |
os.remove(filepath) | |
logger.info(f"Removed old file: {filename}") | |
except Exception as e: | |
logger.error(f"Error cleaning up old files: {str(e)}") | |