mirror of
https://github.com/hanzoskill/research-company.git
synced 2026-10-30 21:04:52 +00:00
530 lines
19 KiB
Python
530 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Company Research Report PDF Generator
|
|
|
|
Generates professionally styled PDF reports from research data.
|
|
Uses reportlab for high-quality PDF output with proper typography and layout.
|
|
|
|
Usage:
|
|
python generate_report.py input.json output.pdf
|
|
|
|
Input JSON structure: See references/data-schema.md
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from datetime import datetime
|
|
from reportlab.lib import colors
|
|
from reportlab.lib.pagesizes import letter
|
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
from reportlab.lib.units import inch
|
|
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
|
|
from reportlab.platypus import (
|
|
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
|
|
KeepTogether, HRFlowable, CondPageBreak
|
|
)
|
|
from reportlab.pdfbase import pdfmetrics
|
|
from reportlab.pdfbase.ttfonts import TTFont
|
|
|
|
# Color palette (professional slate-based)
|
|
COLORS = {
|
|
'primary': colors.HexColor('#1e293b'), # Dark slate
|
|
'secondary': colors.HexColor('#334155'), # Medium slate
|
|
'muted': colors.HexColor('#64748b'), # Light slate
|
|
'accent_blue': colors.HexColor('#3b82f6'),
|
|
'accent_emerald': colors.HexColor('#10b981'),
|
|
'accent_violet': colors.HexColor('#8b5cf6'),
|
|
'accent_amber': colors.HexColor('#f59e0b'),
|
|
'accent_teal': colors.HexColor('#14b8a6'),
|
|
'accent_indigo': colors.HexColor('#6366f1'),
|
|
'accent_rose': colors.HexColor('#f43f5e'),
|
|
'bg_light': colors.HexColor('#f8fafc'),
|
|
'border': colors.HexColor('#e2e8f0'),
|
|
'white': colors.white,
|
|
}
|
|
|
|
|
|
def create_styles():
|
|
"""Create custom paragraph styles for the report."""
|
|
styles = getSampleStyleSheet()
|
|
|
|
# Title style (for header)
|
|
styles.add(ParagraphStyle(
|
|
'ReportTitle',
|
|
parent=styles['Heading1'],
|
|
fontSize=24,
|
|
textColor=COLORS['white'],
|
|
spaceAfter=6,
|
|
fontName='Helvetica-Bold'
|
|
))
|
|
|
|
# Section heading
|
|
styles.add(ParagraphStyle(
|
|
'SectionHeading',
|
|
parent=styles['Heading2'],
|
|
fontSize=14,
|
|
textColor=COLORS['primary'],
|
|
spaceBefore=16,
|
|
spaceAfter=8,
|
|
borderPadding=(0, 0, 4, 0),
|
|
fontName='Helvetica-Bold'
|
|
))
|
|
|
|
# Subsection heading
|
|
styles.add(ParagraphStyle(
|
|
'SubHeading',
|
|
parent=styles['Heading3'],
|
|
fontSize=11,
|
|
textColor=COLORS['secondary'],
|
|
spaceBefore=12,
|
|
spaceAfter=6,
|
|
fontName='Helvetica-Bold'
|
|
))
|
|
|
|
# Body text (custom)
|
|
styles.add(ParagraphStyle(
|
|
'ReportBody',
|
|
parent=styles['Normal'],
|
|
fontSize=10,
|
|
textColor=COLORS['secondary'],
|
|
spaceAfter=8,
|
|
leading=14,
|
|
alignment=TA_JUSTIFY
|
|
))
|
|
|
|
# Muted text (for metadata, footer)
|
|
styles.add(ParagraphStyle(
|
|
'MutedText',
|
|
parent=styles['Normal'],
|
|
fontSize=9,
|
|
textColor=COLORS['muted'],
|
|
spaceAfter=4
|
|
))
|
|
|
|
# Tag/pill style text
|
|
styles.add(ParagraphStyle(
|
|
'TagText',
|
|
parent=styles['Normal'],
|
|
fontSize=9,
|
|
textColor=COLORS['secondary'],
|
|
))
|
|
|
|
# Item title (for bordered items)
|
|
styles.add(ParagraphStyle(
|
|
'ItemTitle',
|
|
parent=styles['Normal'],
|
|
fontSize=10,
|
|
textColor=COLORS['primary'],
|
|
fontName='Helvetica-Bold',
|
|
spaceAfter=2
|
|
))
|
|
|
|
# Item description
|
|
styles.add(ParagraphStyle(
|
|
'ItemDesc',
|
|
parent=styles['Normal'],
|
|
fontSize=9,
|
|
textColor=COLORS['secondary'],
|
|
leading=12
|
|
))
|
|
|
|
return styles
|
|
|
|
|
|
def create_header_table(data, styles):
|
|
"""Create the dark header section."""
|
|
company = data.get('company_name', 'Unknown Company')
|
|
date = data.get('report_date', datetime.now().strftime('%B %d, %Y'))
|
|
url = data.get('source_url', '')
|
|
|
|
header_content = [
|
|
[Paragraph('<font color="white" size="9">ACCOUNT RESEARCH REPORT</font>', styles['Normal'])],
|
|
[Paragraph(f'<font color="white" size="20"><b>{company}</b></font>', styles['Normal'])],
|
|
[Spacer(1, 8)],
|
|
[Paragraph(f'<font color="#94a3b8" size="9"><b>Date:</b> {date} | <b>Source:</b> {url} | <b>Analyst:</b> Claude AI</font>', styles['Normal'])]
|
|
]
|
|
|
|
header_table = Table(header_content, colWidths=[7*inch])
|
|
header_table.setStyle(TableStyle([
|
|
('BACKGROUND', (0, 0), (-1, -1), COLORS['primary']),
|
|
('LEFTPADDING', (0, 0), (-1, -1), 24),
|
|
('RIGHTPADDING', (0, 0), (-1, -1), 24),
|
|
('TOPPADDING', (0, 0), (0, 0), 20),
|
|
('BOTTOMPADDING', (0, -1), (-1, -1), 20),
|
|
]))
|
|
|
|
return header_table
|
|
|
|
|
|
def create_section_heading(title, styles):
|
|
"""Create a section heading with bottom border."""
|
|
elements = []
|
|
elements.append(Paragraph(title, styles['SectionHeading']))
|
|
elements.append(HRFlowable(width="100%", thickness=2, color=COLORS['border'], spaceAfter=8))
|
|
return elements
|
|
|
|
|
|
def create_profile_table(profile_data):
|
|
"""Create the company profile table."""
|
|
rows = []
|
|
fields = [
|
|
('Company Name', 'name'),
|
|
('Industry', 'industry'),
|
|
('Headquarters', 'headquarters'),
|
|
('Founded', 'founded'),
|
|
('CEO/Founder', 'ceo'),
|
|
('Company Size', 'size'),
|
|
('Website', 'website'),
|
|
('Recent Funding', 'funding'),
|
|
]
|
|
|
|
for label, key in fields:
|
|
value = profile_data.get(key, '')
|
|
if value:
|
|
rows.append([label, value])
|
|
|
|
if not rows:
|
|
return None
|
|
|
|
table = Table(rows, colWidths=[1.8*inch, 5.2*inch])
|
|
table.setStyle(TableStyle([
|
|
('BACKGROUND', (0, 0), (0, -1), COLORS['bg_light']),
|
|
('TEXTCOLOR', (0, 0), (0, -1), COLORS['secondary']),
|
|
('TEXTCOLOR', (1, 0), (1, -1), COLORS['secondary']),
|
|
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
|
|
('FONTSIZE', (0, 0), (-1, -1), 9),
|
|
('PADDING', (0, 0), (-1, -1), 8),
|
|
('GRID', (0, 0), (-1, -1), 0.5, COLORS['border']),
|
|
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
|
('ROWBACKGROUNDS', (0, 0), (-1, -1), [COLORS['bg_light'], COLORS['white']]),
|
|
]))
|
|
|
|
return table
|
|
|
|
|
|
def create_bordered_item(title, description, accent_color, styles):
|
|
"""Create a left-bordered item block."""
|
|
content = []
|
|
if title:
|
|
content.append([Paragraph(f'<b>{title}</b>', styles['ItemTitle'])])
|
|
if description:
|
|
content.append([Paragraph(description, styles['ItemDesc'])])
|
|
|
|
if not content:
|
|
return None
|
|
|
|
inner_table = Table(content, colWidths=[6.6*inch])
|
|
inner_table.setStyle(TableStyle([
|
|
('LEFTPADDING', (0, 0), (-1, -1), 8),
|
|
('RIGHTPADDING', (0, 0), (-1, -1), 4),
|
|
('TOPPADDING', (0, 0), (-1, -1), 4),
|
|
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
|
|
]))
|
|
|
|
outer_table = Table([[inner_table]], colWidths=[7*inch])
|
|
outer_table.setStyle(TableStyle([
|
|
('LINEAFTER', (0, 0), (0, -1), 0, COLORS['white']),
|
|
('LINEBEFORE', (0, 0), (0, -1), 4, accent_color),
|
|
('BACKGROUND', (0, 0), (-1, -1), colors.HexColor('#fafafa')),
|
|
('TOPPADDING', (0, 0), (-1, -1), 0),
|
|
('BOTTOMPADDING', (0, 0), (-1, -1), 0),
|
|
('LEFTPADDING', (0, 0), (-1, -1), 0),
|
|
]))
|
|
|
|
return outer_table
|
|
|
|
|
|
def create_competitor_table(competitors, styles):
|
|
"""Create the competitor comparison table with proper text wrapping."""
|
|
if not competitors:
|
|
return None
|
|
|
|
# Create cell style for wrapping text
|
|
cell_style = ParagraphStyle(
|
|
'TableCell',
|
|
parent=styles['Normal'],
|
|
fontSize=9,
|
|
textColor=COLORS['secondary'],
|
|
leading=12
|
|
)
|
|
cell_style_bold = ParagraphStyle(
|
|
'TableCellBold',
|
|
parent=cell_style,
|
|
fontName='Helvetica-Bold'
|
|
)
|
|
header_style = ParagraphStyle(
|
|
'TableHeader',
|
|
parent=cell_style,
|
|
fontName='Helvetica-Bold',
|
|
textColor=COLORS['secondary']
|
|
)
|
|
|
|
# Create header row with Paragraphs
|
|
header = [
|
|
Paragraph('Competitor', header_style),
|
|
Paragraph('Key Strengths', header_style),
|
|
Paragraph('Differentiation', header_style)
|
|
]
|
|
rows = [header]
|
|
|
|
# Create data rows with Paragraphs for text wrapping
|
|
for comp in competitors:
|
|
rows.append([
|
|
Paragraph(comp.get('name', ''), cell_style_bold),
|
|
Paragraph(comp.get('strengths', ''), cell_style),
|
|
Paragraph(comp.get('differentiation', ''), cell_style)
|
|
])
|
|
|
|
table = Table(rows, colWidths=[1.6*inch, 2.7*inch, 2.7*inch])
|
|
table.setStyle(TableStyle([
|
|
('BACKGROUND', (0, 0), (-1, 0), COLORS['bg_light']),
|
|
('PADDING', (0, 0), (-1, -1), 8),
|
|
('GRID', (0, 0), (-1, -1), 0.5, COLORS['border']),
|
|
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
|
]))
|
|
|
|
return table
|
|
|
|
|
|
def create_tags(keywords, color_map, styles):
|
|
"""Create keyword tags as a wrapped paragraph."""
|
|
if not keywords:
|
|
return None
|
|
|
|
tags_text = ' '.join([f'<font color="{color_map.get(kw, "#334155")}" size="9">[{kw}]</font>' for kw in keywords])
|
|
return Paragraph(tags_text, styles['TagText'])
|
|
|
|
|
|
def create_bullet_list(items, bullet_color, styles):
|
|
"""Create a simple bullet list."""
|
|
if not items:
|
|
return None
|
|
|
|
elements = []
|
|
for item in items:
|
|
bullet_html = f'<font color="{bullet_color}">•</font> {item}'
|
|
elements.append(Paragraph(bullet_html, styles['ReportBody']))
|
|
|
|
return elements
|
|
|
|
|
|
def generate_pdf(data, output_path):
|
|
"""Generate the complete PDF report."""
|
|
styles = create_styles()
|
|
|
|
doc = SimpleDocTemplate(
|
|
output_path,
|
|
pagesize=letter,
|
|
rightMargin=0.5*inch,
|
|
leftMargin=0.5*inch,
|
|
topMargin=0.5*inch,
|
|
bottomMargin=0.5*inch
|
|
)
|
|
|
|
story = []
|
|
|
|
# Header
|
|
story.append(create_header_table(data, styles))
|
|
story.append(Spacer(1, 16))
|
|
|
|
# Executive Summary
|
|
if data.get('executive_summary'):
|
|
story.extend(create_section_heading('Executive Summary', styles))
|
|
story.append(Paragraph(data['executive_summary'], styles['ReportBody']))
|
|
story.append(Spacer(1, 8))
|
|
|
|
# Company Profile
|
|
if data.get('profile'):
|
|
story.extend(create_section_heading('Company Profile', styles))
|
|
profile_table = create_profile_table(data['profile'])
|
|
if profile_table:
|
|
story.append(KeepTogether([profile_table]))
|
|
story.append(Spacer(1, 8))
|
|
|
|
# Products & Services
|
|
if data.get('products'):
|
|
story.extend(create_section_heading('Products & Services', styles))
|
|
|
|
if data['products'].get('offerings'):
|
|
story.append(Paragraph('<b>Core Offerings</b>', styles['SubHeading']))
|
|
for item in data['products']['offerings']:
|
|
block = create_bordered_item(item.get('name'), item.get('description'), COLORS['accent_blue'], styles)
|
|
if block:
|
|
story.append(KeepTogether([block, Spacer(1, 6)]))
|
|
|
|
if data['products'].get('differentiators'):
|
|
story.append(Paragraph('<b>Key Differentiators</b>', styles['SubHeading']))
|
|
for diff in data['products']['differentiators']:
|
|
block = create_bordered_item(diff, None, COLORS['accent_emerald'], styles)
|
|
if block:
|
|
story.append(block)
|
|
story.append(Spacer(1, 4))
|
|
|
|
if data['products'].get('tech_stack'):
|
|
story.append(Paragraph('<b>Technology Stack</b>', styles['SubHeading']))
|
|
tech_text = ' | '.join([f'<b>{k}:</b> {v}' for k, v in data['products']['tech_stack'].items()])
|
|
story.append(Paragraph(tech_text, styles['MutedText']))
|
|
|
|
story.append(Spacer(1, 8))
|
|
|
|
# Target Market
|
|
if data.get('target_market'):
|
|
story.extend(create_section_heading('Target Market', styles))
|
|
|
|
if data['target_market'].get('segments'):
|
|
story.append(Paragraph(data['target_market']['segments'], styles['ReportBody']))
|
|
|
|
if data['target_market'].get('verticals'):
|
|
story.append(Paragraph('<b>Industry Verticals</b>', styles['SubHeading']))
|
|
verticals_text = ' '.join([f'[{v}]' for v in data['target_market']['verticals']])
|
|
story.append(Paragraph(verticals_text, styles['MutedText']))
|
|
|
|
if data['target_market'].get('personas'):
|
|
story.append(Paragraph('<b>Buyer Personas</b>', styles['SubHeading']))
|
|
for persona in data['target_market']['personas']:
|
|
block = create_bordered_item(persona.get('title'), persona.get('description'), COLORS['accent_violet'], styles)
|
|
if block:
|
|
story.append(block)
|
|
story.append(Spacer(1, 4))
|
|
|
|
if data['target_market'].get('business_model'):
|
|
story.append(Paragraph('<b>Business Model</b>', styles['SubHeading']))
|
|
story.append(Paragraph(data['target_market']['business_model'], styles['ReportBody']))
|
|
|
|
story.append(Spacer(1, 8))
|
|
|
|
# Use Cases & Pain Points (conditional page break only if needed)
|
|
if data.get('use_cases'):
|
|
# Use CondPageBreak to only break if less than 2 inches remain
|
|
story.append(CondPageBreak(2*inch))
|
|
story.extend(create_section_heading('Use Cases & Pain Points', styles))
|
|
for uc in data['use_cases']:
|
|
block = create_bordered_item(uc.get('title'), uc.get('description'), COLORS['accent_amber'], styles)
|
|
if block:
|
|
story.append(KeepTogether([block, Spacer(1, 6)]))
|
|
story.append(Spacer(1, 8))
|
|
|
|
# Competitive Landscape
|
|
if data.get('competitors'):
|
|
story.extend(create_section_heading('Competitive Landscape', styles))
|
|
comp_table = create_competitor_table(data['competitors'], styles)
|
|
if comp_table:
|
|
story.append(KeepTogether([comp_table]))
|
|
|
|
if data.get('competitive_positioning'):
|
|
story.append(Spacer(1, 8))
|
|
story.append(Paragraph('<b>Competitive Positioning</b>', styles['SubHeading']))
|
|
story.append(Paragraph(data['competitive_positioning'], styles['ReportBody']))
|
|
|
|
story.append(Spacer(1, 8))
|
|
|
|
# Industry Dynamics
|
|
if data.get('industry'):
|
|
story.extend(create_section_heading('Industry Dynamics', styles))
|
|
|
|
if data['industry'].get('trends'):
|
|
story.append(Paragraph('<b>Market Trends</b>', styles['SubHeading']))
|
|
for trend in data['industry']['trends']:
|
|
story.append(Paragraph(f'<font color="#3b82f6">•</font> {trend}', styles['ReportBody']))
|
|
|
|
if data['industry'].get('opportunities'):
|
|
story.append(Paragraph('<b>Growth Opportunities</b>', styles['SubHeading']))
|
|
for opp in data['industry']['opportunities']:
|
|
story.append(Paragraph(f'<font color="#10b981">•</font> {opp}', styles['ReportBody']))
|
|
|
|
if data['industry'].get('challenges'):
|
|
story.append(Paragraph('<b>Challenges</b>', styles['SubHeading']))
|
|
for ch in data['industry']['challenges']:
|
|
story.append(Paragraph(f'<font color="#f59e0b">•</font> {ch}', styles['ReportBody']))
|
|
|
|
story.append(Spacer(1, 8))
|
|
|
|
# Recent Developments
|
|
if data.get('developments'):
|
|
story.extend(create_section_heading('Recent Developments', styles))
|
|
for dev in data['developments']:
|
|
date_str = dev.get('date', '')
|
|
title = dev.get('title', '')
|
|
desc = dev.get('description', '')
|
|
block = create_bordered_item(f'<font color="#14b8a6">{date_str}</font> — {title}', desc, COLORS['accent_teal'], styles)
|
|
if block:
|
|
story.append(KeepTogether([block, Spacer(1, 6)]))
|
|
story.append(Spacer(1, 8))
|
|
|
|
# Lead Generation Intelligence (conditional page break only if needed)
|
|
if data.get('lead_gen'):
|
|
# Use CondPageBreak to only break if less than 2 inches remain
|
|
story.append(CondPageBreak(2*inch))
|
|
story.extend(create_section_heading('Lead Generation Intelligence', styles))
|
|
|
|
kw = data['lead_gen'].get('keywords', {})
|
|
if kw.get('primary'):
|
|
story.append(Paragraph('<b>Primary Service Keywords</b>', styles['SubHeading']))
|
|
story.append(Paragraph(' '.join([f'<font color="#3b82f6">[{k}]</font>' for k in kw['primary']]), styles['TagText']))
|
|
|
|
if kw.get('vertical'):
|
|
story.append(Paragraph('<b>Vertical Keywords</b>', styles['SubHeading']))
|
|
story.append(Paragraph(' '.join([f'<font color="#10b981">[{k}]</font>' for k in kw['vertical']]), styles['TagText']))
|
|
|
|
if kw.get('technology'):
|
|
story.append(Paragraph('<b>Technology Keywords</b>', styles['SubHeading']))
|
|
story.append(Paragraph(' '.join([f'<font color="#8b5cf6">[{k}]</font>' for k in kw['technology']]), styles['TagText']))
|
|
|
|
if kw.get('pain_point'):
|
|
story.append(Paragraph('<b>Pain Point Keywords</b>', styles['SubHeading']))
|
|
story.append(Paragraph(' '.join([f'<font color="#f59e0b">[{k}]</font>' for k in kw['pain_point']]), styles['TagText']))
|
|
|
|
if data['lead_gen'].get('outreach_angles'):
|
|
story.append(Spacer(1, 8))
|
|
story.append(Paragraph('<b>Outreach Angles</b>', styles['SubHeading']))
|
|
for angle in data['lead_gen']['outreach_angles']:
|
|
block = create_bordered_item(angle.get('title'), angle.get('description'), COLORS['accent_indigo'], styles)
|
|
if block:
|
|
story.append(block)
|
|
story.append(Spacer(1, 4))
|
|
|
|
if data['lead_gen'].get('partnership_targets'):
|
|
story.append(Spacer(1, 8))
|
|
story.append(Paragraph('<b>Partnership Targets</b>', styles['SubHeading']))
|
|
for partner in data['lead_gen']['partnership_targets']:
|
|
block = create_bordered_item(partner.get('name'), partner.get('rationale'), COLORS['accent_rose'], styles)
|
|
if block:
|
|
story.append(block)
|
|
story.append(Spacer(1, 4))
|
|
|
|
story.append(Spacer(1, 8))
|
|
|
|
# Information Gaps
|
|
if data.get('info_gaps'):
|
|
story.extend(create_section_heading('Information Gaps', styles))
|
|
story.append(Paragraph('The following information was not publicly available:', styles['MutedText']))
|
|
for gap in data['info_gaps']:
|
|
story.append(Paragraph(f'<font color="#94a3b8">•</font> {gap}', styles['MutedText']))
|
|
story.append(Spacer(1, 8))
|
|
|
|
# Footer
|
|
story.append(Spacer(1, 16))
|
|
story.append(HRFlowable(width="100%", thickness=1, color=COLORS['border']))
|
|
report_date = data.get('report_date', datetime.now().strftime('%B %d, %Y'))
|
|
footer_text = f'This report was prepared for business development and sales intelligence purposes. All information is based on publicly available sources and is current as of {report_date}. Verify critical details before making business decisions.'
|
|
story.append(Paragraph(footer_text, styles['MutedText']))
|
|
|
|
# Build PDF
|
|
doc.build(story)
|
|
return output_path
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if len(sys.argv) < 3:
|
|
print("Usage: python generate_report.py input.json output.pdf")
|
|
sys.exit(1)
|
|
|
|
input_file = sys.argv[1]
|
|
output_file = sys.argv[2]
|
|
|
|
with open(input_file, 'r') as f:
|
|
data = json.load(f)
|
|
|
|
generate_pdf(data, output_file)
|
|
print(f"Report generated: {output_file}")
|