devZenaight commited on
Commit
b8737d9
·
2 Parent(s): 891dd937b50d28

Merge branch 'live-test-branch'

Browse files
app/DocumentGeneration/pdf_generator.py ADDED
@@ -0,0 +1,682 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PDF Generator Module
2
+ # Contains create_pdf_document() and all PDF-related functions
3
+
4
+ import re
5
+ import os
6
+ import tempfile
7
+ import time
8
+ import sys
9
+ from typing import List, Dict, Any
10
+
11
+ # Add the parent directory to sys.path to import from models.py
12
+ sys.path.append(os.path.dirname(os.path.dirname(__file__)))
13
+ from models import ExportRequest
14
+
15
+ # Import from the same directory (Document Generation)
16
+ from document_helpers import (
17
+ filter_empty_sections,
18
+ extract_business_name_from_content,
19
+ remove_duplicate_headings
20
+ )
21
+ from table_of_content import generate_table_of_contents_after_content, add_table_of_contents_page
22
+ from chart_generator import parse_embedded_chart_data, create_chart_from_data
23
+ from markdown_renderer import (
24
+ clean_text_for_pdf,
25
+ render_markdown_to_pdf_grouped
26
+ )
27
+
28
+ # Add Currency Mapping path
29
+ currency_mapping_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'Currency Mapping')
30
+ if currency_mapping_path not in sys.path:
31
+ sys.path.append(currency_mapping_path)
32
+ from CurrencyMapping.currency_mapping import convert_currency_symbols_to_iso
33
+
34
+ # Import sort_sections_by_order from utils module
35
+ from utils import sort_sections_by_order
36
+
37
+ def calculate_real_page_numbers(request: ExportRequest, filtered_sections):
38
+ """Calculate real page numbers by creating a temporary PDF with all content INCLUDING charts"""
39
+ try:
40
+ from fpdf import FPDF
41
+ except ImportError:
42
+ raise Exception("fpdf2 is not installed. Please install it with: pip install fpdf2")
43
+
44
+ # Create temporary PDF to calculate page numbers
45
+ temp_pdf = FPDF()
46
+ temp_pdf.set_auto_page_break(auto=True, margin=15)
47
+
48
+ # Add cover page
49
+ temp_pdf.add_page()
50
+
51
+ # Add TOC page (placeholder)
52
+ temp_pdf.add_page()
53
+
54
+ # Parse charts from embedded chart data in section content
55
+ embedded_charts = parse_embedded_chart_data(filtered_sections)
56
+
57
+ # Create charts and map them to sections
58
+ chart_info_map = {}
59
+ if embedded_charts:
60
+ for chart_info in embedded_charts:
61
+ chart_bytes = create_chart_from_data(chart_info, request.businessIdea or "Business")
62
+ if chart_bytes:
63
+ source_section = chart_info["source_section"]
64
+ if source_section not in chart_info_map:
65
+ chart_info_map[source_section] = []
66
+ chart_info_map[source_section].append({
67
+ "chart_bytes": chart_bytes,
68
+ "title": chart_info["title"],
69
+ "type": chart_info["type"]
70
+ })
71
+
72
+ # Add all sections to calculate real page numbers INCLUDING charts
73
+ section_page_map = {}
74
+ sorted_sections = sort_sections_by_order(filtered_sections)
75
+
76
+ for section in sorted_sections:
77
+ temp_pdf.add_page()
78
+ current_page = temp_pdf.page_no()
79
+ section_page_map[section.title] = current_page
80
+
81
+ # Don't add section title manually - let the markdown content handle headings
82
+
83
+ # Add section content
84
+ temp_pdf.set_font("Arial", "", 12)
85
+ temp_pdf.set_text_color(51, 51, 51)
86
+ temp_pdf.set_xy(25, temp_pdf.get_y())
87
+
88
+ # Process content
89
+ try:
90
+ content_text = clean_text_for_pdf(section.content)
91
+ except Exception as e:
92
+ content_text = section.content or ""
93
+
94
+ # Remove chart data markers
95
+ content_text = re.sub(r'<!--CHARTDATASTART-->.*?<!--CHARTDATAEND-->', '', content_text, flags=re.DOTALL)
96
+ content_text = re.sub(r'<!--CHARTDATASTART-->.*$', '', content_text, flags=re.DOTALL)
97
+
98
+ # No need to filter section titles since we're not adding them manually
99
+
100
+ # Add content with proper markdown rendering
101
+ if content_text and content_text.strip():
102
+ render_markdown_to_pdf_grouped(temp_pdf, content_text, (0,0,0), (75,0,130), (51,51,51))
103
+
104
+ # Add charts for this section (same logic as in actual PDF generation)
105
+ section_charts = chart_info_map.get(section.title, [])
106
+
107
+ # Try different matching strategies for charts
108
+ if not section_charts:
109
+ # Case-insensitive match
110
+ for chart_source, charts in chart_info_map.items():
111
+ if chart_source.lower() == section.title.lower():
112
+ section_charts = charts
113
+ break
114
+
115
+ # Partial match
116
+ if not section_charts:
117
+ for chart_source, charts in chart_info_map.items():
118
+ if (section.title.lower() in chart_source.lower() or
119
+ chart_source.lower() in section.title.lower()):
120
+ section_charts = charts
121
+ break
122
+
123
+ # Key-based match
124
+ if not section_charts and hasattr(section, 'key'):
125
+ for chart_source, charts in chart_info_map.items():
126
+ if section.key and section.key.replace('prompt_', '') in chart_source.lower():
127
+ section_charts = charts
128
+ break
129
+
130
+ # Add charts with same page break logic as actual PDF
131
+ if section_charts:
132
+ charts_per_page = 0
133
+ max_charts_per_page = 5
134
+
135
+ for chart_info in section_charts:
136
+ try:
137
+ chart_height_needed = 140 # Chart height (120) + title (20) + minimal spacing
138
+ current_y = temp_pdf.get_y()
139
+ charts_per_page += 1
140
+
141
+ # Only force page break if absolutely necessary
142
+ if (current_y + chart_height_needed > 290) or (charts_per_page > 5):
143
+ temp_pdf.add_page()
144
+ y_title = 25
145
+ charts_per_page = 1
146
+ else:
147
+ y_title = current_y
148
+
149
+ # Add minimal spacing before chart if not at top of page
150
+ if y_title > 25:
151
+ temp_pdf.ln(3)
152
+
153
+ # Chart title is already included in the chart itself, no need for separate title
154
+
155
+ # Add chart image
156
+ img_width = 160 # Much larger chart width
157
+ x = (210 - img_width) / 2
158
+ y_img = y_title
159
+
160
+ # Save image to temp file
161
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_img:
162
+ tmp_img.write(chart_info["chart_bytes"])
163
+ temp_img_path = tmp_img.name
164
+
165
+ # Add chart
166
+ chart_height = 120 # Much larger chart height
167
+ temp_pdf.image(temp_img_path, x=x, y=y_img, w=img_width)
168
+ temp_pdf.ln(chart_height + 5)
169
+
170
+ # Clean up temp file
171
+ try:
172
+ os.remove(temp_img_path)
173
+ except Exception:
174
+ pass
175
+
176
+ except Exception:
177
+ # Skip faulty chart and continue
178
+ continue
179
+
180
+ return section_page_map
181
+
182
+ def create_pdf_document(request: ExportRequest):
183
+ """Create a PROFESSIONAL PDF document from the business plan data using FPDF2
184
+ NOTE: Now uses matplotlib charts exclusively - old base64 chart logic commented out"""
185
+ try:
186
+ from fpdf import FPDF
187
+ except ImportError:
188
+ raise Exception("fpdf2 is not installed. Please install it with: pip install fpdf2")
189
+
190
+ # Filter out empty sections to prevent blank pages
191
+ try:
192
+ filtered_sections = filter_empty_sections(request.sections)
193
+ except Exception as e:
194
+ raise Exception(f"Failed to filter sections: {str(e)}")
195
+ if not filtered_sections:
196
+ raise Exception("No valid sections found after filtering. All sections appear to be empty.")
197
+
198
+ # First pass: Calculate real page numbers by creating a temporary PDF
199
+ section_page_map = calculate_real_page_numbers(request, filtered_sections)
200
+
201
+ # Generate TOC with real page numbers
202
+ toc_items = generate_table_of_contents_after_content(filtered_sections, section_page_map, request.businessIdea)
203
+
204
+ # Second pass: Create final PDF with TOC on page 2
205
+ pdf = FPDF()
206
+ pdf.set_auto_page_break(auto=True, margin=15) # Reduced margin to allow more charts per page
207
+
208
+ # Add confidentiality footer to every page
209
+ def add_confidentiality_footer(pdf):
210
+ pdf.set_font("Arial", "", 8)
211
+ pdf.set_text_color(100, 100, 100) # Gray text
212
+ pdf.set_y(-15) # 15mm from bottom
213
+ pdf.cell(0, 5, "CONFIDENTIAL - This document contains proprietary information and may not be shared without consent", 0, 0, 'C')
214
+
215
+ # Override the footer method
216
+ pdf.footer = lambda: add_confidentiality_footer(pdf)
217
+ pdf.add_page()
218
+
219
+ # Professional color scheme - Refined for bank/investor presentation
220
+ primary_color = (0, 0, 0) # Black - for headings
221
+ accent_color = (75, 0, 130) # Navy purple - for accent lines and highlights
222
+ text_color = (51, 51, 51) # Dark gray - for body text
223
+ light_gray = (221, 221, 221) # Lighter gray - for subtle lines (#DDDDDD)
224
+ navy_color = (25, 25, 112) # Navy blue - for TOC and emphasis
225
+
226
+ # Professional fonts - Arial for body, optional serif for main titles
227
+ title_font = 'Arial' # Keep Arial for consistency
228
+ body_font = 'Arial' # Modern business feel
229
+
230
+ # Helper functions for consistent styling
231
+ def add_header(pdf, text, size=24, y_offset=25, primary_color=(0,0,0), accent_color=(75,0,130)):
232
+ pdf.set_font("Arial", "B", size)
233
+ pdf.set_text_color(*primary_color)
234
+ pdf.set_xy(25, y_offset)
235
+ pdf.cell(0, 18, text, ln=True)
236
+
237
+ # Calculate text width and make line autofit
238
+ text_width = pdf.get_string_width(text)
239
+ line_width = min(text_width + 10, 160) # Add 10mm padding, max 160mm (page width - margins)
240
+
241
+ # Removed purple accent line
242
+ return y_offset + 25
243
+
244
+ def add_subheader(pdf, text, size=16, y_offset=None, primary_color=(0,0,0)):
245
+ if y_offset is None:
246
+ y_offset = pdf.get_y() + 10
247
+ pdf.set_font("Arial", "B", size)
248
+ pdf.set_text_color(*primary_color)
249
+ pdf.set_xy(25, y_offset)
250
+ pdf.cell(0, 14, text, ln=True)
251
+
252
+ # Calculate text width and make line autofit
253
+ text_width = pdf.get_string_width(text)
254
+ line_width = min(text_width + 8, 160) # Add 8mm padding, max 160mm (page width - margins)
255
+
256
+ pdf.set_draw_color(221, 221, 221) # Lighter gray (#DDDDDD) for subtle lines
257
+ pdf.set_line_width(0.3) # Thinner line
258
+ pdf.line(25, y_offset + 14, 25 + line_width, y_offset + 14)
259
+ return y_offset + 20
260
+
261
+ def add_body_text(pdf, text, y_offset=None, text_color=(51,51,51)):
262
+ if y_offset is None:
263
+ y_offset = pdf.get_y() + 5
264
+ pdf.set_font("Arial", "", 12)
265
+ pdf.set_text_color(*text_color)
266
+ pdf.set_xy(25, y_offset)
267
+ pdf.multi_cell(160, 8, text, align='L') # Left align to prevent text justification
268
+ return pdf.get_y()
269
+
270
+ def check_page_break(pdf, required_height=120, bottom_margin=300):
271
+ """Check if we need a page break and return the Y position to use"""
272
+ y = pdf.get_y()
273
+ if y + required_height > bottom_margin:
274
+ pdf.add_page()
275
+ return 25
276
+ return y
277
+
278
+ # ===== COVER PAGE =====
279
+ def add_cover_page():
280
+ # Clean white background - no purple background
281
+
282
+ # Add company logo at the top
283
+ try:
284
+ # Use the MBD logo PNG file - now in app folder for HF deployment
285
+ app_root = os.path.dirname(os.path.dirname(__file__))
286
+ logo_path = os.path.join(app_root, 'assets', 'logos', 'MBD logo.png')
287
+
288
+ if os.path.exists(logo_path):
289
+ # Add logo to PDF (centered, top of page) - LOGO FIRST, THEN TITLE BELOW
290
+ # Center the logo: (210 - 70) / 2 = 70, so logo starts at x=70
291
+ pdf.image(logo_path, x=70, y=20, w=70) # Even larger size to show full logo
292
+ print(f"Logo added successfully from: {logo_path}")
293
+ else:
294
+ print(f"Logo file not found at: {logo_path}")
295
+ except Exception as e:
296
+ # Log any errors with logo
297
+ print(f"Logo error: {e}")
298
+ pass
299
+
300
+ # Extract business name from content, fallback to businessIdea
301
+ extracted_business_name = extract_business_name_from_content(filtered_sections)
302
+ business_name = extracted_business_name or request.businessIdea or "Business Plan"
303
+
304
+ # Company/Project name - DARK TEXT (moved down below logo)
305
+ pdf.set_font(title_font, "B", 32)
306
+ pdf.set_text_color(0, 0, 0) # Black text for visibility on white background
307
+ pdf.ln(60) # More space to move title below logo
308
+ pdf.cell(0, 25, business_name, ln=True, align='C')
309
+
310
+ # Subtitle - DARK GRAY, BOLD, NO ITALICS
311
+ pdf.set_font(title_font, "B", 16) # Reduced from 18pt, removed italics
312
+ pdf.set_text_color(51, 51, 51) # Dark gray for visibility on white background
313
+ pdf.cell(0, 12, "Business Plan", ln=True, align='C')
314
+
315
+ # Add some space
316
+ pdf.ln(40)
317
+
318
+
319
+ pdf.set_font(body_font, "", 12)
320
+ pdf.set_text_color(51, 51, 51) # Dark text for content
321
+ pdf.set_xy(35, 145)
322
+ pdf.cell(0, 8, f"Created: {time.strftime('%B %d, %Y')}", ln=True)
323
+ pdf.set_xy(35, 153)
324
+ pdf.cell(0, 8, f"Format: {request.format.upper()}", ln=True)
325
+ pdf.set_xy(35, 161)
326
+ pdf.cell(0, 8, f"Sections: {len(request.sections)}", ln=True)
327
+
328
+ # Footer - simple footer since confidentiality is now in page footer
329
+ pdf.set_font(body_font, "", 10)
330
+ pdf.set_text_color(51, 51, 51) # Dark gray text for visibility on white background
331
+ pdf.set_xy(0, 280)
332
+ pdf.cell(0, 8, "Business Plan Document", ln=True, align='C')
333
+
334
+
335
+
336
+ # ===== SECTION PAGES =====
337
+ def add_sections_with_charts():
338
+ # Parse charts from embedded chart data in section content
339
+ embedded_charts = parse_embedded_chart_data(filtered_sections)
340
+
341
+ if embedded_charts:
342
+ # Create actual charts from the parsed data
343
+ matplotlib_charts = []
344
+ chart_info_map = {} # Map section names to their charts
345
+
346
+ # logging.info(f"Chart info map keys: {list(chart_info_map.keys())}")
347
+
348
+ for chart_info in embedded_charts:
349
+ chart_bytes = create_chart_from_data(chart_info, request.businessIdea or "Business")
350
+ if chart_bytes:
351
+ matplotlib_charts.append(chart_bytes)
352
+ # Map this chart to its source section
353
+ source_section = chart_info["source_section"]
354
+ # logging.info(f"Chart '{chart_info['title']}' has source_section: '{source_section}'")
355
+
356
+ if source_section not in chart_info_map:
357
+ chart_info_map[source_section] = []
358
+ chart_info_map[source_section].append({
359
+ "chart_bytes": chart_bytes,
360
+ "title": chart_info["title"],
361
+ "type": chart_info["type"]
362
+ })
363
+ # logging.info(f"Mapping chart '{chart_info['title']}' to source section: '{source_section}'")
364
+
365
+ # CRITICAL DEBUG: Show the exact mapping being created
366
+ # logging.info(f" -> Chart '{chart_info['title']}' mapped to key '{source_section}' in chart_info_map")
367
+ else:
368
+ pass
369
+
370
+ # logging.info(f"Total charts created: {len(matplotlib_charts)}")
371
+ # logging.info(f"Final chart_info_map: {chart_info_map}")
372
+
373
+ # Debug: Show all chart mappings
374
+ for source_section, charts in chart_info_map.items():
375
+ # logging.info(f"Source section '{source_section}' has {len(charts)} charts:")
376
+ for chart in charts:
377
+ # logging.info(f" - {chart['title']} ({chart['type']})")
378
+ pass
379
+
380
+ else:
381
+ matplotlib_charts = []
382
+ chart_info_map = {}
383
+
384
+ total_charts_placed = 0
385
+
386
+ # Sort sections according to predefined order
387
+ sorted_sections = sort_sections_by_order(filtered_sections)
388
+ # logging.info(f"Section titles in sorted order: {[s.title for s in sorted_sections]}")
389
+
390
+ # CRITICAL DEBUG: Show exact section titles and their keys
391
+ for i, section in enumerate(sorted_sections):
392
+ # logging.info(f"Section {i+1}: '{section.title}' (key: {getattr(section, 'key', 'NO_KEY')})")
393
+ pass
394
+
395
+ for i, section in enumerate(sorted_sections):
396
+
397
+ # Always start each section on a new page
398
+ pdf.add_page()
399
+
400
+ # Don't add section title manually - let the markdown content handle headings
401
+
402
+ # Section content (properly positioned)
403
+ pdf.set_font(body_font, "", 12)
404
+ pdf.set_text_color(*text_color)
405
+ pdf.set_xy(25, pdf.get_y()) # Use current Y position for consistent spacing
406
+
407
+ # Process content
408
+ try:
409
+ content_text = clean_text_for_pdf(section.content)
410
+ except Exception as e:
411
+ content_text = section.content or ""
412
+ try:
413
+ content_text = convert_currency_symbols_to_iso(content_text)
414
+ except Exception:
415
+ # If currency conversion fails, keep original text
416
+ pass
417
+
418
+ # Remove chart data markers
419
+ content_text = re.sub(r'<!--CHARTDATASTART-->.*?<!--CHARTDATAEND-->', '', content_text, flags=re.DOTALL)
420
+ content_text = re.sub(r'<!--CHARTDATASTART-->.*$', '', content_text, flags=re.DOTALL)
421
+
422
+ # Remove chart titles from content to prevent duplicate headings
423
+ # Get chart titles for this section
424
+ section_charts = chart_info_map.get(section.title, [])
425
+ if not section_charts:
426
+ # Try case-insensitive match
427
+ for chart_source, charts in chart_info_map.items():
428
+ if chart_source.lower() == section.title.lower():
429
+ section_charts = charts
430
+ break
431
+
432
+ # Remove chart titles from content
433
+ for chart_info in section_charts:
434
+ chart_title = chart_info["title"]
435
+ # Remove various markdown heading formats for the chart title
436
+ patterns_to_remove = [
437
+ rf'^#+\s*{re.escape(chart_title)}\s*$', # # Title, ## Title, etc.
438
+ rf'^\*\*{re.escape(chart_title)}\*\*\s*$', # **Title**
439
+ rf'^{re.escape(chart_title)}\s*$', # Just the title
440
+ ]
441
+ for pattern in patterns_to_remove:
442
+ content_text = re.sub(pattern, '', content_text, flags=re.MULTILINE | re.IGNORECASE)
443
+
444
+ # No need to filter section titles since we're not adding them manually
445
+
446
+ # Skip enhance_subheadings_with_bold for grouped rendering to preserve ** markers
447
+ # content_text = enhance_subheadings_with_bold(content_text)
448
+ content_text = remove_duplicate_headings(content_text)
449
+
450
+ # Add content with proper markdown rendering using grouped approach
451
+ if content_text and content_text.strip():
452
+ render_markdown_to_pdf_grouped(pdf, content_text, primary_color, accent_color, text_color)
453
+ else:
454
+ # Add placeholder text to prevent blank page
455
+ pdf.multi_cell(160, 8, f"Content for {section.title} section is being processed...", align='L')
456
+
457
+ # Add visual separation between content and charts
458
+ if content_text and content_text.strip():
459
+ pdf.ln(15) # More generous spacing for better visual separation
460
+
461
+ # ---- Charts for this section ----
462
+ charts_added = 0 # IMPORTANT: initialize to avoid NameError
463
+
464
+ # CRITICAL DEBUG: Show exactly what charts exist and their source_section values
465
+ if section.title == "Financial Plan & Funding":
466
+ # logging.info(f"=== LAST SECTION DEBUG ===")
467
+ # logging.info(f"Section title: '{section.title}'")
468
+ # logging.info(f"All chart_info_map keys: {list(chart_info_map.keys())}")
469
+ for key, charts in chart_info_map.items():
470
+ # logging.info(f" Key '{key}' has {len(charts)} charts")
471
+ for chart in charts:
472
+ # logging.info(f" Chart: {chart['title']} ({chart['type']})")
473
+ pass
474
+ # logging.info(f"=== END LAST SECTION DEBUG ===")
475
+ pass
476
+
477
+ section_charts = chart_info_map.get(section.title, [])
478
+ # logging.info(f"Section '{section.title}': Direct lookup found {len(section_charts)} charts")
479
+
480
+ # Strategy 1: Exact title match
481
+ if not section_charts:
482
+ # logging.info(f"No exact match found for '{section.title}'")
483
+ pass
484
+
485
+ # Strategy 2: Case-insensitive match
486
+ for chart_source, charts in chart_info_map.items():
487
+ if chart_source.lower() == section.title.lower():
488
+ section_charts = charts
489
+ # logging.info(f"Found charts via case-insensitive match: '{chart_source}' -> {len(charts)} charts")
490
+ break
491
+
492
+ # Strategy 3: Partial match
493
+ if not section_charts:
494
+ for chart_source, charts in chart_info_map.items():
495
+ if (section.title.lower() in chart_source.lower() or
496
+ chart_source.lower() in section.title.lower()):
497
+ section_charts = charts
498
+ # logging.info(f"Found charts via partial match: '{chart_source}' -> {len(charts)} charts")
499
+ break
500
+
501
+ # Strategy 4: Key-based match (for prompt_ prefixed sections)
502
+ if not section_charts and hasattr(section, 'key'):
503
+ for chart_source, charts in chart_info_map.items():
504
+ if section.key and section.key.replace('prompt_', '') in chart_source.lower():
505
+ section_charts = charts
506
+ # logging.info(f"Found charts via key match: '{section.key}' -> '{chart_source}' -> {len(charts)} charts")
507
+ break
508
+
509
+ # logging.info(f"Section '{section.title}': Final chart count: {len(section_charts)}")
510
+
511
+ if section_charts:
512
+
513
+ # Group charts that can fit on the same page
514
+ charts_per_page = 0
515
+ max_charts_per_page = 5 # Allow up to 5 charts per page
516
+
517
+ for chart_info in section_charts:
518
+ try:
519
+ # Check if we need a new page for this chart
520
+ chart_height_needed = 140 # Chart height (120) + title (20) + minimal spacing
521
+
522
+ # Smart page break logic - allow multiple charts per page
523
+ current_y = pdf.get_y()
524
+ charts_per_page += 1
525
+
526
+ # Only force page break if absolutely necessary - allow up to 5 charts per page
527
+ if (current_y + chart_height_needed > 290) or (charts_per_page > 5):
528
+ pdf.add_page()
529
+ # Header/footer removed for now
530
+ y_title = 25 # Start at normal position
531
+ charts_per_page = 1 # Reset counter for new page
532
+ else:
533
+ y_title = current_y
534
+
535
+ # Add minimal spacing before chart if not at top of page
536
+ if y_title > 25: # If not at top of page, add minimal space
537
+ pdf.ln(3) # Minimal spacing to fit more charts per page
538
+
539
+ # Chart title is already included in the chart itself, no need for separate subheader
540
+
541
+ # Centered image with larger dimensions for better visibility
542
+ img_width = 160 # Much larger chart width for better visibility
543
+ x = (210 - img_width) / 2
544
+ y_img = y_title
545
+
546
+ # Save image to temp file
547
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_img:
548
+ tmp_img.write(chart_info["chart_bytes"])
549
+ temp_img_path = tmp_img.name
550
+
551
+ # Chart dimensions (no border needed)
552
+ chart_height = 120 # Much larger height for better visibility
553
+ chart_width = img_width
554
+
555
+ # Image
556
+ pdf.image(temp_img_path, x=x, y=y_img, w=chart_width)
557
+
558
+ # Move below image and clean up (spacing based on actual chart height)
559
+ pdf.ln(chart_height + 5) # Chart height (120) + minimal spacing between charts
560
+ try:
561
+ os.remove(temp_img_path)
562
+ except Exception:
563
+ pass
564
+
565
+ charts_added += 1
566
+ total_charts_placed += 1
567
+ except Exception:
568
+ # Skip faulty chart and continue rendering others
569
+ continue
570
+ else:
571
+ # Debug: Check if there are any charts that might match this section
572
+ potential_matches = []
573
+ for chart_source, charts in chart_info_map.items():
574
+ if (section.title.lower() in chart_source.lower() or
575
+ chart_source.lower() in section.title.lower()):
576
+ potential_matches.append(f"'{chart_source}' -> {len(charts)} charts")
577
+
578
+ if potential_matches:
579
+ # logging.info(f"Section '{section.title}': Potential chart matches: {potential_matches}")
580
+ pass
581
+ else:
582
+ # logging.info(f"Section '{section.title}': No charts found and no potential matches")
583
+ pass
584
+
585
+ # Each section is already on its own page, no need for complex page break logic
586
+
587
+
588
+
589
+
590
+ # ===== BUILD THE PDF =====
591
+ try:
592
+ # Add all sections
593
+ try:
594
+ add_cover_page()
595
+ except Exception as e:
596
+ raise Exception(f"Cover page failed: {str(e)}")
597
+
598
+ # Add TOC on page 2 with REAL page numbers (already calculated above)
599
+ try:
600
+ if toc_items:
601
+ add_table_of_contents_page(pdf, toc_items, request.businessIdea)
602
+ else:
603
+ # Add a basic TOC page if no items were generated
604
+ pdf.add_page()
605
+ pdf.set_font("Arial", "B", 20)
606
+ pdf.set_text_color(0, 0, 0)
607
+ pdf.set_xy(25, 25)
608
+ pdf.cell(0, 18, "Table of Contents", ln=True)
609
+ pdf.set_font("Arial", "", 12)
610
+ pdf.set_text_color(51, 51, 51)
611
+ pdf.set_xy(25, 50)
612
+ pdf.multi_cell(160, 8, "No sections available for table of contents.", align='L')
613
+ except Exception as e:
614
+ # logging.error(f"Table of contents failed: {str(e)}")
615
+ pass
616
+ # Add a basic TOC page as fallback
617
+ pdf.add_page()
618
+ pdf.set_font("Arial", "B", 20)
619
+ pdf.set_text_color(0, 0, 0)
620
+ pdf.set_xy(25, 25)
621
+ pdf.cell(0, 18, "Table of Contents", ln=True)
622
+ pdf.set_font("Arial", "", 12)
623
+ pdf.set_text_color(51, 51, 51)
624
+ pdf.set_xy(25, 50)
625
+ pdf.multi_cell(160, 8, "Table of contents could not be generated.", align='L')
626
+
627
+ # Executive summary is now handled as part of the sections
628
+ # No separate page needed - the first section contains the executive summary
629
+
630
+ try:
631
+ add_sections_with_charts()
632
+ except Exception as e:
633
+ raise Exception(f"Sections with charts failed: {str(e)}")
634
+
635
+ # CRITICAL CHECK: Ensure we have content in the PDF
636
+ total_pages = pdf.page_no()
637
+
638
+ if total_pages < 2: # Should have at least cover + 1 content page
639
+ # Add a content page to prevent blank PDF
640
+ pdf.add_page()
641
+ pdf.set_font("Arial", "B", 16)
642
+ pdf.set_text_color(255, 0, 0)
643
+ pdf.cell(0, 20, "Content Processing Issue", ln=True, align='C')
644
+ pdf.set_font("Arial", "", 12)
645
+ pdf.set_text_color(0, 0, 0)
646
+ pdf.cell(0, 10, "The PDF generation encountered an issue with content processing.", ln=True, align='C')
647
+ pdf.cell(0, 10, "Please check the server logs for detailed information.", ln=True, align='C')
648
+
649
+ # Get PDF output and convert to bytes
650
+ try:
651
+ pdf_output = pdf.output(dest='S')
652
+ if isinstance(pdf_output, bytearray):
653
+ pdf_bytes = bytes(pdf_output)
654
+ else:
655
+ pdf_bytes = pdf_output.encode('latin-1') if isinstance(pdf_output, str) else pdf_output
656
+
657
+ # CRITICAL SAFETY CHECK: Ensure PDF is not empty
658
+ if len(pdf_bytes) < 1000: # PDF should be at least 1KB
659
+ # Try to add some content to prevent blank PDF
660
+ pdf.add_page()
661
+ pdf.set_font("Arial", "B", 16)
662
+ pdf.set_text_color(255, 0, 0) # Red text
663
+ pdf.cell(0, 20, "PDF Generation Issue Detected", ln=True, align='C')
664
+ pdf.set_font("Arial", "", 12)
665
+ pdf.set_text_color(0, 0, 0)
666
+ pdf.cell(0, 10, "If you see this message, there was an issue with content processing.", ln=True, align='C')
667
+ pdf.cell(0, 10, "Please check the server logs for details.", ln=True, align='C')
668
+
669
+ # Regenerate PDF with error message
670
+ pdf_output = pdf.output(dest='S')
671
+ if isinstance(pdf_output, bytearray):
672
+ pdf_bytes = bytes(pdf_output)
673
+ else:
674
+ pdf_bytes = pdf_output.encode('latin-1') if isinstance(pdf_output, str) else pdf_output
675
+
676
+ return pdf_bytes, f"business_plan_{request.businessIdea or 'export'}.pdf"
677
+ except Exception as e:
678
+ raise Exception(f"PDF conversion failed: {str(e)}")
679
+
680
+ except Exception as e:
681
+ # logging.error(f"PDF generation failed: {e}")
682
+ raise Exception(f"Failed to generate professional PDF: {str(e)}")
app/DocumentGeneration/word_generator.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Word Generator Module
2
+ # Contains create_word_document() and all Word-related functions
3
+
4
+ import re
5
+ import os
6
+ import tempfile
7
+ import time
8
+ import io
9
+ import sys
10
+ from typing import List, Dict, Any
11
+
12
+ # Add the parent directory to sys.path to import from models.py
13
+ sys.path.append(os.path.dirname(os.path.dirname(__file__)))
14
+ from models import ExportRequest
15
+
16
+ # Import from the same directory (Document Generation)
17
+ from document_helpers import (
18
+ filter_empty_sections,
19
+ extract_business_name_from_content
20
+ )
21
+ # from table_of_content import generate_table_of_contents # Not used in word generator
22
+ from chart_generator import parse_embedded_chart_data, create_chart_from_data
23
+ from markdown_renderer import (
24
+ render_markdown_to_docx_grouped,
25
+ process_content_with_inline_charts
26
+ )
27
+
28
+ # Add Currency Mapping path
29
+ sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'Currency Mapping'))
30
+ from CurrencyMapping.currency_mapping import convert_currency_symbols_to_iso
31
+
32
+ def create_word_document(request: ExportRequest):
33
+ """Create a Word document from the business plan data with front page, TOC, and proper page breaks"""
34
+ try:
35
+ from docx import Document
36
+ from docx.shared import Inches, Pt, RGBColor
37
+ from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_TAB_ALIGNMENT
38
+ from docx.oxml import OxmlElement
39
+ from docx.oxml.ns import qn
40
+ except ImportError:
41
+ raise Exception("python-docx is not installed. Please install it with: pip install python-docx")
42
+
43
+ # Filter out empty sections to prevent blank pages
44
+ try:
45
+ filtered_sections = filter_empty_sections(request.sections)
46
+ except Exception as e:
47
+ raise Exception(f"Failed to filter sections: {str(e)}")
48
+ if not filtered_sections:
49
+ raise Exception("No valid sections found after filtering. All sections appear to be empty.")
50
+
51
+ doc = Document()
52
+
53
+ # Add confidentiality footer to every page
54
+ section = doc.sections[0]
55
+ footer = section.footer
56
+ footer_para = footer.paragraphs[0]
57
+ footer_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
58
+ footer_run = footer_para.add_run("CONFIDENTIAL - This document contains proprietary information and may not be shared without consent")
59
+ footer_run.font.size = Pt(8)
60
+ footer_run.font.color.rgb = RGBColor(100, 100, 100) # Gray text
61
+
62
+ # ===== FRONT PAGE =====
63
+ # Add company logo at the top
64
+ try:
65
+ # Use the MBD logo PNG file - now in app folder for HF deployment
66
+ app_root = os.path.dirname(os.path.dirname(__file__))
67
+ logo_path = os.path.join(app_root, 'assets', 'logos', 'MBD logo.png')
68
+
69
+ if os.path.exists(logo_path):
70
+ # Add logo to Word document (centered, top of page)
71
+ logo_para = doc.add_paragraph()
72
+ logo_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
73
+ logo_run = logo_para.add_run()
74
+ logo_run.add_picture(logo_path, width=Inches(3.0)) # Even larger size to show full logo
75
+ print(f"Logo added successfully from: {logo_path}")
76
+ else:
77
+ print(f"Logo file not found at: {logo_path}")
78
+ except Exception as e:
79
+ # Log any errors with logo
80
+ print(f"Logo error: {e}")
81
+ pass
82
+
83
+ # Add top spacing
84
+ doc.add_paragraph()
85
+ doc.add_paragraph()
86
+
87
+ # Extract business name from content, fallback to businessIdea
88
+ extracted_business_name = extract_business_name_from_content(filtered_sections)
89
+ business_name = extracted_business_name or request.businessIdea or "Business Plan"
90
+
91
+ # Business name as main title with enhanced styling - PURPLE THEME
92
+ title = doc.add_heading(business_name, 0)
93
+ title.alignment = WD_ALIGN_PARAGRAPH.CENTER
94
+
95
+ # Style the title with larger font and PURPLE color
96
+ for run in title.runs:
97
+ run.font.size = Pt(36)
98
+ run.font.color.rgb = RGBColor(139, 92, 246) # PURPLE
99
+
100
+ # Removed decorative line under title
101
+
102
+ # Add subtitle with enhanced styling - PURPLE
103
+ doc.add_paragraph()
104
+ subtitle = doc.add_paragraph("Professional Business Plan Document")
105
+ subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
106
+ subtitle_run = subtitle.runs[0]
107
+ subtitle_run.font.size = Pt(18)
108
+ subtitle_run.font.color.rgb = RGBColor(124, 58, 237) # DARKER PURPLE
109
+ subtitle_run.italic = True
110
+
111
+ # Add more spacing
112
+ doc.add_paragraph()
113
+ doc.add_paragraph()
114
+ doc.add_paragraph()
115
+
116
+ # Add document info in a styled box - PURPLE BORDER
117
+ info_para = doc.add_paragraph()
118
+ info_para.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
119
+
120
+ # Add background color and border styling
121
+ info_run = info_para.add_run("Prepared by: Business Plan Generator")
122
+ info_run.font.size = Pt(14)
123
+ info_run.font.color.rgb = RGBColor(139, 92, 246) # PURPLE
124
+
125
+ doc.add_paragraph()
126
+ date_para = doc.add_paragraph()
127
+ date_para.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
128
+ date_run = date_para.add_run(f"Date: {time.strftime('%B %Y')}")
129
+ date_run.font.size = Pt(14)
130
+ date_run.font.color.rgb = RGBColor(139, 92, 246) # PURPLE
131
+
132
+ # Confidentiality statement removed - now in footer of every page
133
+
134
+ # Add more spacing before footer
135
+ doc.add_paragraph()
136
+ doc.add_paragraph()
137
+ doc.add_paragraph()
138
+ doc.add_paragraph()
139
+
140
+ # Add footer with enhanced styling - PURPLE
141
+ footer = doc.add_paragraph("Confidential Business Information")
142
+ footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
143
+ footer_run = footer.runs[0]
144
+ footer_run.font.size = Pt(12)
145
+ footer_run.font.color.rgb = RGBColor(139, 92, 246) # PURPLE
146
+ footer_run.bold = True
147
+
148
+ # Removed decorative line above footer
149
+
150
+ # ===== TABLE OF CONTENTS =====
151
+ # Add page break before TOC
152
+ doc.add_page_break()
153
+
154
+ # Add TOC title
155
+ toc_title = doc.add_heading("Table of Contents", level=1)
156
+ toc_title.alignment = WD_ALIGN_PARAGRAPH.CENTER
157
+
158
+ # Removed decorative line under TOC title
159
+
160
+ # Add spacing before TOC entries
161
+ doc.add_paragraph()
162
+
163
+ # Sort sections according to predefined order (do this before TOC generation)
164
+ try:
165
+ sorted_sections = sort_sections_by_order(filtered_sections)
166
+ except Exception as e:
167
+ # Fallback: use filtered order if sorting fails
168
+ sorted_sections = filtered_sections
169
+
170
+ # Generate TOC entries with actual page numbers
171
+ # Since each section starts on a new page, we can calculate page numbers
172
+ toc_items = []
173
+ current_page = 3 # Start after cover (1) and TOC (2)
174
+
175
+ for section in sorted_sections:
176
+ if section.content and section.content.strip():
177
+ # Add main section
178
+ toc_items.append({
179
+ "title": section.title,
180
+ "level": 1,
181
+ "page": current_page
182
+ })
183
+
184
+ # Skip subheadings and chart titles - only include main section headings
185
+
186
+ current_page += 1 # Each section starts on a new page
187
+
188
+ # Add the actual TOC entries
189
+ for item in toc_items:
190
+ # Create TOC entry paragraph
191
+ toc_para = doc.add_paragraph()
192
+ toc_para.paragraph_format.space_after = Pt(6)
193
+
194
+ # Add section title
195
+ title_run = toc_para.add_run(item["title"])
196
+ title_run.font.size = Pt(11)
197
+
198
+ # Add dots and page number
199
+ if item["level"] == 1:
200
+ # Main section - bold
201
+ title_run.bold = True
202
+ # Add dots
203
+ dots_run = toc_para.add_run(" " + "." * (60 - len(item["title"])) + " ")
204
+ dots_run.font.size = Pt(11)
205
+ dots_run.font.color.rgb = RGBColor(128, 128, 128) # Gray
206
+ # Add page number
207
+ page_run = toc_para.add_run(str(item["page"]))
208
+ page_run.font.size = Pt(11)
209
+ page_run.bold = True
210
+ else:
211
+ # Subsection - indented
212
+ toc_para.paragraph_format.left_indent = Inches(0.3)
213
+ # Add dots
214
+ dots_run = toc_para.add_run(" " + "." * (50 - len(item["title"])) + " ")
215
+ dots_run.font.size = Pt(11)
216
+ dots_run.font.color.rgb = RGBColor(128, 128, 128) # Gray
217
+ # Add page number
218
+ page_run = toc_para.add_run(str(item["page"]))
219
+ page_run.font.size = Pt(11)
220
+
221
+ # logging.info(f"Generated {len(toc_items)} TOC items with actual page numbers")
222
+
223
+ # Executive Summary is now handled as a regular section above
224
+
225
+ # ===== MAIN SECTIONS =====
226
+ for i, section in enumerate(sorted_sections):
227
+ if section.content.strip():
228
+ # Add page break before each section (including Executive Summary)
229
+ doc.add_page_break()
230
+
231
+ # Add section heading
232
+ doc.add_heading(section.title, level=1)
233
+
234
+ # Clean and process content WITH inline charts
235
+ try:
236
+ clean_content = clean_text_for_pdf(section.content)
237
+ except Exception:
238
+ clean_content = section.content or ""
239
+ try:
240
+ clean_content = convert_currency_symbols_to_iso(clean_content)
241
+ except Exception:
242
+ pass
243
+
244
+ # Remove the section title from content to prevent duplication
245
+ section_title_clean = section.title.strip()
246
+ content_lines = clean_content.split('\n')
247
+ filtered_lines = []
248
+ for line in content_lines:
249
+ line_clean = line.strip()
250
+ # Skip lines that match the section title (with or without markdown)
251
+ # Check for various markdown formats and variations
252
+ if (line_clean == section_title_clean or
253
+ line_clean == f"**{section_title_clean}**" or
254
+ line_clean == f"# {section_title_clean}" or
255
+ line_clean == f"## {section_title_clean}" or
256
+ line_clean == f"### {section_title_clean}" or
257
+ line_clean == f"#### {section_title_clean}" or
258
+ # Also check if the line starts with the section title (for numbered headings)
259
+ line_clean.startswith(f"{section_title_clean}") and len(line_clean) <= len(section_title_clean) + 10):
260
+ continue
261
+ filtered_lines.append(line)
262
+ clean_content = '\n'.join(filtered_lines)
263
+
264
+ # Process content with inline charts - THIS IS THE KEY CHANGE
265
+ process_content_with_inline_charts(doc, clean_content, request.businessIdea or "Business")
266
+
267
+ doc.add_paragraph()
268
+
269
+ # Charts are now handled inline with the sections above
270
+
271
+ # Save to bytes
272
+ buffer = io.BytesIO()
273
+ doc.save(buffer)
274
+ buffer.seek(0)
275
+ doc_bytes = buffer.getvalue()
276
+ buffer.close()
277
+
278
+ filename = f"business_plan_{request.businessIdea or 'export'}.docx"
279
+ return doc_bytes, filename
app/assets/logos/MBD logo.png ADDED