devZenaight commited on
Commit
c0fc75b
·
2 Parent(s): d4b61861b6c693

Merge branch 'main' of https://huggingface.co/spaces/Zenaight/planify

Browse files
app/CurrencyMapping/__init__.py DELETED
@@ -1,17 +0,0 @@
1
- # Currency Mapping Package
2
-
3
- import os
4
- import sys
5
-
6
- # Add the current directory to sys.path to handle the space in folder name
7
- current_dir = os.path.dirname(__file__)
8
- if current_dir not in sys.path:
9
- sys.path.insert(0, current_dir)
10
-
11
- # Import the main function for easy access
12
- from currency_mapping import convert_currency_symbols_to_iso, canonical_currency_map
13
-
14
- __all__ = [
15
- 'convert_currency_symbols_to_iso',
16
- 'canonical_currency_map'
17
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/CurrencyMapping/currency_mapping.py DELETED
@@ -1,538 +0,0 @@
1
- # Currency Mapping Module
2
- # Contains canonical_currency_map dictionary and convert_currency_symbols_to_iso() function
3
-
4
- # Canonical currency map for symbol resolution (matches TypeScript version)
5
- canonical_currency_map = {
6
- # Standard currency symbols
7
- "$": "USD",
8
- "£": "GBP",
9
- "€": "EUR",
10
- "¥": "JPY",
11
- "₹": "INR",
12
- "₽": "RUB",
13
- "₩": "KRW",
14
- "₪": "ILS",
15
- "₺": "TRY",
16
- "₴": "UAH",
17
- "₼": "AZN",
18
- "₸": "KZT",
19
- "₾": "GEL",
20
- "֏": "AMD",
21
- "₲": "PYG",
22
- "₡": "CRC",
23
- "₣": "XPF",
24
- "₦": "NGN",
25
- "₵": "GHS",
26
- "₨": "PKR",
27
- "₱": "PHP",
28
- "₫": "VND",
29
- "₭": "LAK",
30
- "៛": "KHR",
31
- "৳": "BDT",
32
- "؋": "AFN",
33
- "﷼": "IRR",
34
-
35
- # Custom symbols
36
- "‡": "CUSTOM_DAGGER",
37
- "†": "CUSTOM_DAGGER_SINGLE",
38
-
39
- # Letter-based currencies
40
- "R": "ZAR", # South African Rand
41
- "C$": "CAD", # Canadian Dollar
42
- "A$": "AUD", # Australian Dollar
43
- "R$": "BRL", # Brazilian Real
44
- "S/": "PEN", # Peruvian Sol
45
- "Bs": "BOB", # Bolivian Boliviano
46
- "Q": "GTQ", # Guatemalan Quetzal
47
- "L": "HNL", # Honduran Lempira
48
- "ƒ": "AWG", # Aruban Florin
49
- "Vt": "VUV", # Vanuatu Vatu
50
- "T$": "TOP", # Tongan Pa'anga
51
- "T": "WST", # Samoan Tala
52
- "лв": "BGN", # Bulgarian Lev
53
- "Kč": "CZK", # Czech Koruna
54
- "kr": "DKK", # Danish Krone
55
- "Ft": "HUF", # Hungarian Forint
56
- "zł": "PLN", # Polish Zloty
57
- "lei": "RON", # Romanian Leu
58
- "CHF": "CHF", # Swiss Franc
59
- "KM": "BAM", # Bosnian Marka
60
- "ден": "MKD", # North Macedonian Denar
61
- "дин": "RSD", # Serbian Dinar
62
- "so'm": "UZS", # Uzbekistani Som
63
- "NT$": "TWD", # Taiwan Dollar
64
- "HK$": "HKD", # Hong Kong Dollar
65
- "S$": "SGD", # Singapore Dollar
66
- "RM": "MYR", # Malaysian Ringgit
67
- "฿": "THB", # Thai Baht
68
- "Rp": "IDR", # Indonesian Rupiah
69
- "K": "MMK", # Myanmar Kyat
70
- "Rs": "LKR", # Sri Lankan Rupee
71
- "Nu": "BTN", # Bhutanese Ngultrum
72
- "MVR": "MVR", # Maldivian Rufiyaa
73
-
74
- # Middle Eastern currencies
75
- "ع.د": "IQD", # Iraqi Dinar
76
- "ل.ل": "LBP", # Lebanese Pound
77
- "د.ا": "JOD", # Jordanian Dinar
78
- "ر.س": "SAR", # Saudi Riyal
79
- "د.إ": "AED", # UAE Dirham
80
- "ر.ق": "QAR", # Qatari Riyal
81
- "د.ك": "KWD", # Kuwaiti Dinar
82
- "د.ب": "BHD", # Bahraini Dinar
83
- "ر.ع.": "OMR", # Omani Rial
84
- "ر.ي": "YER", # Yemeni Rial
85
- "ج.م": "EGP", # Egyptian Pound
86
- "ل.د": "LYD", # Libyan Dinar
87
- "د.ت": "TND", # Tunisian Dinar
88
- "د.ج": "DZD", # Algerian Dinar
89
- "د.م.": "MAD", # Moroccan Dirham
90
- "ج.س.": "SDG", # Sudanese Pound
91
- "SSP": "SSP", # South Sudanese Pound
92
-
93
- # African currencies
94
- "KSh": "KES", # Kenyan Shilling
95
- "USh": "UGX", # Ugandan Shilling
96
- "TSh": "TZS", # Tanzanian Shilling
97
- "FRw": "RWF", # Rwandan Franc
98
- "FBu": "BIF", # Burundian Franc
99
- "MT": "MZN", # Mozambican Metical
100
- "P": "BWP", # Botswanan Pula
101
- "N$": "NAD", # Namibian Dollar
102
- "Ar": "MGA", # Malagasy Ariary
103
- "CF": "KMF", # Comorian Franc
104
- "MK": "MWK", # Malawian Kwacha
105
- "ZK": "ZMW", # Zambian Kwacha
106
- "Kz": "AOA", # Angolan Kwanza
107
- "FC": "CDF", # Congolese Franc
108
- "FCFA": "XAF", # Central African Franc
109
- "Db": "STD", # Sao Tomean Dobra
110
- "D": "GMD", # Gambian Dalasi
111
- "FG": "GNF", # Guinean Franc
112
- "CFA": "XOF", # West African Franc
113
- "Le": "SLL", # Sierra Leonean Leone
114
- "L$": "LRD", # Liberian Dollar
115
- "Br": "ETB", # Ethiopian Birr
116
- "Nfk": "ERN", # Eritrean Nakfa
117
- "Fdj": "DJF", # Djiboutian Franc
118
- "S": "SOS", # Somali Shilling
119
-
120
- # Caribbean & Central American currencies
121
- "J$": "JMD", # Jamaican Dollar
122
- "BZ$": "BZD", # Belize Dollar
123
- "TT$": "TTD", # Trinidad and Tobago Dollar
124
- "Bds$": "BBD", # Barbadian Dollar
125
- "EC$": "XCD", # East Caribbean Dollar
126
- "GY$": "GYD", # Guyanese Dollar
127
- "SRD": "SRD", # Surinamese Dollar
128
- "RD$": "DOP", # Dominican Peso
129
-
130
- # Pacific Island currencies
131
- "FJ$": "FJD", # Fijian Dollar
132
- "PGK": "PGK", # Papua New Guinean Kina
133
- "SI$": "SBD", # Solomon Islands Dollar
134
- "WS$": "WST", # Samoan Tala
135
- "VT": "VUV", # Vanuatu Vatu
136
-
137
- # European currencies
138
- "ISK": "ISK", # Icelandic Krona
139
- "Lt": "LTL", # Lithuanian Litas
140
- "Ls": "LVL", # Latvian Lats
141
- "EEK": "EEK", # Estonian Kroon
142
- "NOK": "NOK", # Norwegian Krone
143
- "SEK": "SEK", # Swedish Krona
144
- "ALL": "ALL", # Albanian Lek
145
-
146
- # South American currencies
147
- "Gs": "PYG", # Paraguayan Guarani
148
- "Bs.S": "VES", # Venezuelan Bolívar Soberano
149
- "Bs": "VEF", # Venezuelan Bolívar
150
- "$U": "UYU", # Uruguayan Peso
151
- "$a": "ARS", # Argentine Peso
152
- "$b": "BOB", # Bolivian Boliviano
153
- "CLP": "CLP", # Chilean Peso
154
- "COP": "COP", # Colombian Peso
155
- "MXN": "MXN", # Mexican Peso
156
-
157
- # Small Island Nations
158
- "BMD": "BMD", # Bermudian Dollar
159
- "BSD": "BSD", # Bahamian Dollar
160
- "KYD": "KYD", # Cayman Islands Dollar
161
- "TVD": "TVD", # Tuvaluan Dollar
162
- "ZWL": "ZWL", # Zimbabwean Dollar
163
-
164
- # Additional African currencies
165
- "LSL": "LSL", # Lesotho Loti
166
- "SZL": "SZL", # Swazi Lilangeni
167
- "CVE": "CVE", # Cape Verdean Escudo
168
- "MRO": "MRO", # Mauritanian Ouguiya
169
- "MRU": "MRU", # Mauritanian Ouguiya
170
- "SCR": "SCR", # Seychellois Rupee
171
- "SHP": "SHP", # Saint Helena Pound
172
-
173
- # Additional Asian currencies
174
- "BND": "BND", # Brunei Dollar
175
- "MOP": "MOP", # Macanese Pataca
176
- "NPR": "NPR", # Nepalese Rupee
177
-
178
- # Additional Pacific currencies
179
- "NZD": "NZD", # New Zealand Dollar
180
-
181
- # Alternative $ symbols for countries (context-dependent)
182
- "$BMD": "BMD", # Bermudian Dollar
183
- "$BSD": "BSD", # Bahamian Dollar
184
- "$BZD": "BZD", # Belize Dollar
185
- "$KYD": "KYD", # Cayman Islands Dollar
186
- "$FJD": "FJD", # Fijian Dollar
187
- "$GYD": "GYD", # Guyanese Dollar
188
- "$JMD": "JMD", # Jamaican Dollar
189
- "$LRD": "LRD", # Liberian Dollar
190
- "$NAD": "NAD", # Namibian Dollar
191
- "$SBD": "SBD", # Solomon Islands Dollar
192
- "$SRD": "SRD", # Surinamese Dollar
193
- "$TTD": "TTD", # Trinidad and Tobago Dollar
194
- "$TVD": "TVD", # Tuvaluan Dollar
195
- "$VUV": "VUV", # Vanuatu Vatu
196
- "$WST": "WST", # Samoan Tala
197
- "$XCD": "XCD", # East Caribbean Dollar
198
- "$ZWL": "ZWL", # Zimbabwean Dollar
199
- "$BWP": "BWP", # Botswanan Pula
200
- "$LSL": "LSL", # Lesotho Loti
201
- "$SZL": "SZL", # Swazi Lilangeni
202
- "$ZAR": "ZAR", # South African Rand
203
- "$HKD": "HKD", # Hong Kong Dollar
204
- "$SGD": "SGD", # Singapore Dollar
205
- "$TWD": "TWD", # Taiwan Dollar
206
- "$CHF": "CHF", # Swiss Franc
207
- "$DKK": "DKK", # Danish Krone
208
- "$NOK": "NOK", # Norwegian Krone
209
- "$SEK": "SEK", # Swedish Krona
210
- "$ARS": "ARS", # Argentine Peso
211
- "$BOB": "BOB", # Bolivian Boliviano
212
- "$CLP": "CLP", # Chilean Peso
213
- "$COP": "COP", # Colombian Peso
214
- "$MXN": "MXN", # Mexican Peso
215
- "$PEN": "PEN", # Peruvian Sol
216
- "$UYU": "UYU", # Uruguayan Peso
217
- "$VES": "VES", # Venezuelan Bolívar Soberano
218
-
219
- # Cryptocurrencies (for modern business plans)
220
- "₿": "BTC", # Bitcoin
221
- "Ξ": "ETH", # Ethereum
222
- "Ł": "LTC", # Litecoin
223
-
224
- # Special Drawing Rights
225
- "XDR": "XDR", # Special Drawing Rights
226
-
227
- # Precious Metals
228
- "XAU": "XAU", # Gold (troy ounce)
229
- "XAG": "XAG", # Silver (troy ounce)
230
- "XPT": "XPT", # Platinum (troy ounce)
231
- "XPD": "XPD", # Palladium (troy ounce)
232
-
233
- # Other
234
- "XTS": "XTS", # Testing Currency Code
235
- "XXX": "XXX", # No Currency
236
-
237
- # Missing currencies from canonical_currency_map.py
238
- "ADP": "ADP", # Andorran Peseta
239
- "AFA": "AFA", # Afghan Afghani (1927–2002)
240
- "ALK": "ALK", # Albanian Lek (1946–1965)
241
- "ANG": "ANG", # Netherlands Antillean Guilder
242
- "AOK": "AOK", # Angolan Kwanza (1977–1991)
243
- "AON": "AON", # Angolan New Kwanza (1990–2000)
244
- "AOR": "AOR", # Angolan Readjusted Kwanza (1995–1999)
245
- "ARA": "ARA", # Argentine Austral
246
- "ARL": "ARL", # Argentine Peso Ley (1970–1983)
247
- "ARM": "ARM", # Argentine Peso (1881–1970)
248
- "ARP": "ARP", # Argentine Peso (1983–1985)
249
- "ATS": "ATS", # Austrian Schilling
250
- "AZM": "AZM", # Azerbaijani Manat (1993–2006)
251
- "BAD": "BAD", # Bosnia-Herzegovina Dinar (1992–1994)
252
- "BAN": "BAN", # Bosnia-Herzegovina New Dinar (1994–1997)
253
- "BEC": "BEC", # Belgian Franc (convertible)
254
- "BEF": "BEF", # Belgian Franc
255
- "BEL": "BEL", # Belgian Franc (financial)
256
- "BGL": "BGL", # Bulgarian Hard Lev
257
- "BGM": "BGM", # Bulgarian Socialist Lev
258
- "BGO": "BGO", # Bulgarian Lev (1879–1952)
259
- "BOL": "BOL", # Bolivian Boliviano (1863–1963)
260
- "BOP": "BOP", # Bolivian Peso
261
- "BOV": "BOV", # Bolivian Mvdol
262
- "BRB": "BRB", # Brazilian New Cruzeiro (1967–1986)
263
- "BRC": "BRC", # Brazilian Cruzado (1986–1989)
264
- "BRE": "BRE", # Brazilian Cruzeiro (1990–1993)
265
- "BRN": "BRN", # Brazilian New Cruzado (1989–1990)
266
- "BRR": "BRR", # Brazilian Cruzeiro (1993–1994)
267
- "BRZ": "BRZ", # Brazilian Cruzeiro (1942–1967)
268
- "BUK": "BUK", # Burmese Kyat
269
- "BYB": "BYB", # Belarusian Ruble (1994–1999)
270
- "BYN": "BYN", # Belarusian Ruble
271
- "BYR": "BYR", # Belarusian Ruble (2000–2016)
272
- "CA$": "CAD", # Canadian Dollar
273
- "CHE": "CHE", # WIR Euro
274
- "CHW": "CHW", # WIR Franc
275
- "CLE": "CLE", # Chilean Escudo
276
- "CLF": "CLF", # Chilean Unit of Account (UF)
277
- "CNH": "CNH", # Chinese Yuan (offshore)
278
- "CNX": "CNX", # Chinese People's Bank Dollar
279
- "CN¥": "CNY", # Chinese Yuan
280
- "COU": "COU", # Colombian Real Value Unit
281
- "CSD": "CSD", # Serbian Dinar (2002–2006)
282
- "CSK": "CSK", # Czechoslovak Hard Koruna
283
- "CUC": "CUC", # Cuban Convertible Peso
284
- "CUP": "CUP", # Cuban Peso
285
- "CYP": "CYP", # Cypriot Pound
286
- "DDM": "DDM", # East German Mark
287
- "DEM": "DEM", # German Mark
288
- "ECS": "ECS", # Ecuadorian Sucre
289
- "ECV": "ECV", # Ecuadorian Unit of Constant Value
290
- "ESA": "ESA", # Spanish Peseta (A account)
291
- "ESB": "ESB", # Spanish Peseta (convertible account)
292
- "ESP": "ESP", # Spanish Peseta
293
- "FIM": "FIM", # Finnish Markka
294
- "FKP": "FKP", # Falkland Islands Pound
295
- "FRF": "FRF", # French Franc
296
- "GEK": "GEK", # Georgian Kupon Larit
297
- "GHC": "GHC", # Ghanaian Cedi (1979–2007)
298
- "GIP": "GIP", # Gibraltar Pound
299
- "GNS": "GNS", # Guinean Syli
300
- "GQE": "GQE", # Equatorial Guinean Ekwele
301
- "GRD": "GRD", # Greek Drachma
302
- "GWE": "GWE", # Portuguese Guinea Escudo
303
- "GWP": "GWP", # Guinea-Bissau Peso
304
- "HRD": "HRD", # Croatian Dinar
305
- "HRK": "HRK", # Croatian Kuna
306
- "HTG": "HTG", # Haitian Gourde
307
- "IEP": "IEP", # Irish Pound
308
- "ILP": "ILP", # Israeli Pound
309
- "ILR": "ILR", # Israeli Shekel (1980–1985)
310
- "ISJ": "ISJ", # Icelandic Króna (1918–1981)
311
- "ITL": "ITL", # Italian Lira
312
- "KGS": "KGS", # Kyrgystani Som
313
- "KPW": "KPW", # North Korean Won
314
- "KRH": "KRH", # South Korean Hwan (1953–1962)
315
- "KRO": "KRO", # South Korean Won (1945–1953)
316
- "LTT": "LTT", # Lithuanian Talonas
317
- "LUC": "LUC", # Luxembourgian Convertible Franc
318
- "LUF": "LUF", # Luxembourgian Franc
319
- "LUL": "LUL", # Luxembourg Financial Franc
320
- "LVR": "LVR", # Latvian Ruble
321
- "MAF": "MAF", # Moroccan Franc
322
- "MCF": "MCF", # Monegasque Franc
323
- "MDC": "MDC", # Moldovan Cupon
324
- "MDL": "MDL", # Moldovan Leu
325
- "MGF": "MGF", # Malagasy Franc
326
- "MKN": "MKN", # Macedonian Denar (1992–1993)
327
- "MLF": "MLF", # Malian Franc
328
- "MNT": "MNT", # Mongolian Tugrik
329
- "MTL": "MTL", # Maltese Lira
330
- "MTP": "MTP", # Maltese Pound
331
- "MUR": "MUR", # Mauritian Rupee
332
- "MVP": "MVP", # Maldivian Rupee (1947–1981)
333
- "MX$": "MXN", # Mexican Peso
334
- "MXP": "MXP", # Mexican Silver Peso (1861–1992)
335
- "MXV": "MXV", # Mexican Investment Unit
336
- "MZE": "MZE", # Mozambican Escudo
337
- "MZM": "MZM", # Mozambican Metical (1980–2006)
338
- "NIC": "NIC", # Nicaraguan Córdoba (1988–1991)
339
- "NIO": "NIO", # Nicaraguan Córdoba
340
- "NLG": "NLG", # Dutch Guilder
341
- "NZ$": "NZD", # New Zealand Dollar
342
- "PAB": "PAB", # Panamanian Balboa
343
- "PEI": "PEI", # Peruvian Inti
344
- "PES": "PES", # Peruvian Sol (1863–1965)
345
- "PLZ": "PLZ", # Polish Zloty (1950–1995)
346
- "PTE": "PTE", # Portuguese Escudo
347
- "RHD": "RHD", # Rhodesian Dollar
348
- "ROL": "ROL", # Romanian Leu (1952–2006)
349
- "RUR": "RUR", # Russian Ruble (1991–1998)
350
- "SDD": "SDD", # Sudanese Dinar (1992–2007)
351
- "SDP": "SDP", # Sudanese Pound (1957–1998)
352
- "SIT": "SIT", # Slovenian Tolar
353
- "SKK": "SKK", # Slovak Koruna
354
- "SLE": "SLE", # Sierra Leonean Leone
355
- "SRG": "SRG", # Surinamese Guilder
356
- "STN": "STN", # São Tomé & Príncipe Dobra
357
- "SUR": "SUR", # Soviet Rouble
358
- "SVC": "SVC", # Salvadoran Colón
359
- "SYP": "SYP", # Syrian Pound
360
- "TJR": "TJR", # Tajikistani Ruble
361
- "TJS": "TJS", # Tajikistani Somoni
362
- "TMM": "TMM", # Turkmenistani Manat (1993–2009)
363
- "TMT": "TMT", # Turkmenistani Manat
364
- "TPE": "TPE", # Timorese Escudo
365
- "TRL": "TRL", # Turkish Lira (1922–2005)
366
- "UAK": "UAK", # Ukrainian Karbovanets
367
- "UGS": "UGS", # Ugandan Shilling (1966–1987)
368
- "USN": "USN", # US Dollar (Next day)
369
- "USS": "USS", # US Dollar (Same day)
370
- "UYI": "UYI", # Uruguayan Peso (Indexed Units)
371
- "UYP": "UYP", # Uruguayan Peso (1975–1993)
372
- "UYW": "UYW", # Uruguayan Nominal Wage Index Unit
373
- "VEB": "VEB", # Venezuelan Bolívar (1871–2008)
374
- "VED": "VED", # Bolívar Soberano
375
- "VNN": "VNN", # Vietnamese Dong (1978–1985)
376
- "XBA": "XBA", # European Composite Unit
377
- "XBB": "XBB", # European Monetary Unit
378
- "XBC": "XBC", # European Unit of Account (XBC)
379
- "XBD": "XBD", # European Unit of Account (XBD)
380
- "Cg.": "XCG", # Caribbean guilder
381
- "XEU": "XEU", # European Currency Unit
382
- "XFO": "XFO", # French Gold Franc
383
- "XFU": "XFU", # French UIC-Franc
384
- "F CFA": "XOF", # West African CFA Franc
385
- "XRE": "XRE", # RINET Funds
386
- "XSU": "XSU", # Sucre
387
- "XUA": "XUA", # ADB Unit of Account
388
- "¤": "XXX", # Unknown Currency
389
- "YDD": "YDD", # Yemeni Dinar
390
- "YUD": "YUD", # Yugoslavian Hard Dinar (1966–1990)
391
- "YUM": "YUM", # Yugoslavian New Dinar (1994–2002)
392
- "YUN": "YUN", # Yugoslavian Convertible Dinar (1990–1992)
393
- "YUR": "YUR", # Yugoslavian Reformed Dinar (1992–1993)
394
- "ZAL": "ZAL", # South African Rand (financial)
395
- "ZMK": "ZMK", # Zambian Kwacha (1968–2012)
396
- "ZRN": "ZRN", # Zairean New Zaire (1993–1998)
397
- "ZRZ": "ZRZ", # Zairean Zaire (1971–1993)
398
- "ZWD": "ZWD", # Zimbabwean Dollar (1980–2008)
399
- "ZWG": "ZWG", # Zimbabwean Gold
400
- "ZWR": "ZWR", # Zimbabwean Dollar (2008)
401
- "US$": "USD", # US Dollar
402
- "C$": "NIO", # Nicaraguan Córdoba
403
- "TT$": "TTD", # Trinidad & Tobago Dollar
404
- "BD$": "BBD", # Barbadian Dollar
405
- "B$": "BND", # Brunei Dollar
406
- "SI$": "SBD", # Solomon Islands Dollar
407
- "G$": "GYD", # Guyanaese Dollar
408
- "SR$": "SRD", # Surinamese Dollar
409
- "CLP$": "CLP", # Chilean Peso
410
- "Bs.": "VES", # Venezuelan Bolívar
411
- "Rf": "MVR", # Maldivian Rufiyaa
412
- "SSh": "SOS", # Somali Shilling
413
- "GH₵": "GHS", # Ghanaian Cedi
414
- "Br": "BYN", # Belarusian Ruble
415
- "CFA F": "XOF", # West African CFA Franc
416
- "CFAF": "XAF", # Central African CFA Franc
417
- "JP¥": "JPY", # Japanese Yen
418
- "元": "TWD", # New Taiwan Dollar
419
- "TL": "TRY", # Turkish Lira
420
- "лв.": "BGN", # Bulgarian Lev
421
- "₮": "MNT", # Mongolian Tugrik
422
- "﷼": "SAR", # Saudi Riyal
423
- ".د.ب": "BHD", # Bahraini Dinar
424
- "ل.س": "SYP", # Syrian Pound
425
- "﷼‎": "IRR", # Iranian Rial
426
- "ש״ח": "ILS", # Israeli New Shekel
427
- "Fr": "CHF", # Swiss Franc
428
- "CFP": "XPF", # CFP Franc
429
- "E": "SZL", # Swazi Lilangeni
430
- "Rf.": "MVR", # Maldivian Rufiyaa
431
- "QR": "QAR", # Qatari Riyal
432
- "SR": "SAR", # Saudi Riyal
433
- "BD": "BHD", # Bahraini Dinar
434
- "KD": "KWD", # Kuwaiti Dinar
435
- "₯": "GRD", # Greek Drachma
436
- "ЅМ": "TJS", # Tajikistani Somoni
437
- "Br (BYN)": "BYN", # Belarusian Ruble
438
- "B/.": "PAB", # Panamanian Balboa
439
- "Gs.": "PYG", # Paraguayan Guarani
440
- "Afl.": "AWG", # Aruban Florin
441
- "NAƒ": "ANG", # Netherlands Antillean Guilder
442
- "lei (MDL)": "MDL", # Moldovan Leu
443
- "KM (BAM)": "BAM", # Bosnia-Herzegovina Convertible Mark
444
- "ден (MKD)": "MKD", # Macedonian Denar
445
- "kn": "HRK", # Croatian Kuna
446
- "Z$": "ZWL", # Zimbabwean Dollar (2009–2024)
447
- "RTGS$": "ZWL", # Zimbabwean Dollar (2009–2024)
448
- "Skr": "SEK", # Swedish Krona
449
- "Nkr": "NOK", # Norwegian Krone
450
- "Dkr": "DKK", # Danish Krone
451
- "Íkr": "ISK", # Icelandic Króna
452
- "AU$": "AUD", # Australian Dollar
453
- "SG$": "SGD", # Singapore Dollar
454
- "BB$": "BBD", # Barbadian Dollar
455
- "BS$": "BSD", # Bahamian Dollar
456
- "KY$": "KYD", # Cayman Islands Dollar
457
- "BM$": "BMD", # Bermudan Dollar
458
- "JM$": "JMD", # Jamaican Dollar
459
- }
460
-
461
- def convert_currency_symbols_to_iso(text: str) -> str:
462
- """
463
- Convert currency symbols to ISO codes for PDF compatibility
464
- Matches the TypeScript canonicalCurrencyMap logic
465
- """
466
- import re # Import at function level to avoid scope issues
467
- processed_text = text
468
-
469
- # Track all conversions for detailed logging
470
- conversions_made = []
471
- total_conversions = 0
472
-
473
- # Process each currency symbol in the canonical map
474
- for symbol, iso_code in canonical_currency_map.items():
475
- if symbol in processed_text:
476
- # Check if it's a single character
477
- is_single_character = len(symbol) == 1
478
- is_unicode_currency = is_single_character and ord(symbol) > 127 # Non-ASCII
479
- is_latin_single_letter = is_single_character and symbol.isalpha() and ord(symbol) < 128
480
-
481
- # Handle multi-character symbols (like "Le", "KSh", etc.)
482
- is_multi_char_symbol = len(symbol) > 1
483
-
484
- # Skip plain Latin single letters (to avoid converting words like "A", "R", etc.)
485
- # but DO convert single-character Unicode currency symbols like €, £, ¥, ؋, etc.
486
- if not is_unicode_currency and is_latin_single_letter:
487
- # Only convert if it's followed by a number (currency context)
488
- # Pattern: R500, R 500, R500,000 etc.
489
- pattern = rf'\b{re.escape(symbol)}\s*(\d{{1,3}}(?:,?\d{{3}})*(?:\.\d{{2}})?)\b'
490
- matches = re.findall(pattern, processed_text)
491
-
492
- if matches:
493
- old_text = processed_text
494
- processed_text = re.sub(pattern, rf'{iso_code} \1', processed_text)
495
-
496
- if old_text != processed_text:
497
- conversions_made.append(f"{symbol} → {iso_code} (context: {matches})")
498
- total_conversions += len(matches)
499
- continue
500
-
501
- # For multi-character symbols like "Ar", "Le", etc., be very careful
502
- # Only convert if they appear as standalone words or in currency contexts
503
- if is_multi_char_symbol and len(symbol) == 2 and symbol.isalpha():
504
- # Only convert if it's a standalone word or followed by a number
505
- # Pattern: \bAr\b or \bAr\s*\d or \bLe\b or \bLe\s*\d
506
- standalone_pattern = rf'\b{re.escape(symbol)}\b'
507
- currency_pattern = rf'\b{re.escape(symbol)}\s*(\d{{1,3}}(?:,?\d{{3}})*(?:\.\d{{2}})?)'
508
-
509
- # Check for standalone occurrences first
510
- standalone_matches = re.findall(standalone_pattern, processed_text)
511
- currency_matches = re.findall(currency_pattern, processed_text)
512
-
513
- if standalone_matches or currency_matches:
514
- old_text = processed_text
515
- # Replace standalone occurrences
516
- processed_text = re.sub(standalone_pattern, iso_code, processed_text)
517
- # Replace currency context occurrences
518
- processed_text = re.sub(currency_pattern, rf'{iso_code} \1', processed_text)
519
-
520
- if old_text != processed_text:
521
- conversions_made.append(f"{symbol} → {iso_code} (standalone/currency context)")
522
- total_conversions += len(standalone_matches) + len(currency_matches)
523
- continue
524
-
525
- # For all other symbols (Unicode currency symbols, multi-character symbols)
526
- # Count occurrences before replacement
527
- old_count = processed_text.count(symbol)
528
-
529
- # Replace all occurrences
530
- old_text = processed_text
531
- processed_text = processed_text.replace(symbol, iso_code)
532
-
533
- # Check if replacement actually happened
534
- if old_text != processed_text:
535
- conversions_made.append(f"{symbol} → {iso_code} ({old_count} instances)")
536
- total_conversions += old_count
537
-
538
- return processed_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/DocumentGeneration/__init__.py DELETED
@@ -1,56 +0,0 @@
1
- # Document Generation Package
2
-
3
- import os
4
- import sys
5
-
6
- # Add the current directory to sys.path to handle the space in folder name
7
- current_dir = os.path.dirname(__file__)
8
- if current_dir not in sys.path:
9
- sys.path.insert(0, current_dir)
10
-
11
- # Import all the main functions for easy access
12
- from pdf_generator import create_pdf_document
13
- from word_generator import create_word_document
14
- from chart_generator import parse_embedded_chart_data, create_chart_from_data
15
- from table_of_content import generate_table_of_contents_after_content, add_table_of_contents_page
16
- from document_helpers import (
17
- extract_business_name_from_content,
18
- filter_empty_sections,
19
- extract_subheadings,
20
- remove_duplicate_headings
21
- )
22
- from grouped import (
23
- parse_content_into_groups,
24
- extract_key_phrases,
25
- get_content_groups_info
26
- )
27
- from markdown_renderer import (
28
- clean_text_for_pdf,
29
- render_markdown_to_pdf,
30
- render_markdown_to_pdf_grouped,
31
- render_markdown_to_docx,
32
- render_markdown_to_docx_grouped,
33
- clean_text_for_docx
34
- )
35
-
36
- __all__ = [
37
- 'create_pdf_document',
38
- 'create_word_document',
39
- 'parse_embedded_chart_data',
40
- 'create_chart_from_data',
41
- 'generate_table_of_contents_after_content',
42
- 'add_table_of_contents_page',
43
- 'extract_business_name_from_content',
44
- 'filter_empty_sections',
45
- 'extract_subheadings',
46
- 'parse_content_into_groups',
47
- 'extract_key_phrases',
48
- 'get_content_groups_info',
49
- 'remove_duplicate_headings',
50
- 'clean_text_for_pdf',
51
- 'render_markdown_to_pdf',
52
- 'render_markdown_to_pdf_grouped',
53
- 'render_markdown_to_docx',
54
- 'render_markdown_to_docx_grouped',
55
- 'clean_text_for_docx'
56
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/DocumentGeneration/chart_generator.py DELETED
@@ -1,384 +0,0 @@
1
- # Chart Generator Module
2
- # Contains parse_embedded_chart_data() and create_chart_from_data() functions
3
-
4
- import re
5
- import io
6
- import os
7
- import sys
8
- import base64
9
- import matplotlib.pyplot as plt
10
- import matplotlib.patches as patches
11
- from typing import List, Dict, Any, Optional
12
-
13
- # Add the parent directory to sys.path to import from models.py
14
- sys.path.append(os.path.dirname(os.path.dirname(__file__)))
15
- from models import ExportSection
16
-
17
- def parse_embedded_chart_data(sections: List[ExportSection]) -> List[Dict[str, Any]]:
18
- """Parse chart data embedded in section content using CHARTDATA markers"""
19
- charts = []
20
-
21
- for section in sections:
22
- if section.content:
23
- # Look for chart data markers - handle both formats
24
- chart_matches = re.findall(r'<!--CHART_DATA_START-->(.*?)<!--CHART_DATA_END-->', section.content, re.DOTALL)
25
-
26
- # Also check for alternative markers as fallback
27
- alt_chart_matches = re.findall(r'<!--CHARTDATASTART-->(.*?)<!--CHARTDATAEND-->', section.content, re.DOTALL)
28
- chart_matches.extend(alt_chart_matches)
29
-
30
- # logging.info(f"Section '{section.title}': Found {len(chart_matches)} chart data markers")
31
-
32
- for chart_data in chart_matches:
33
- try:
34
- # Parse the chart data array
35
- chart_array = eval(chart_data.strip())
36
- # logging.info(f"Parsed chart array with {len(chart_array)} items")
37
- # logging.info(f"Chart array type: {type(chart_array)}")
38
-
39
- # Each chart is an array with: [Section, ChartType, Title, Period, Data]
40
- for chart_item in chart_array:
41
- if len(chart_item) >= 5:
42
- section_name = chart_item[0]
43
- chart_type = chart_item[1] # DC=Doughnut, BG=Bar Graph, etc.
44
- chart_title = chart_item[2]
45
- period = chart_item[3]
46
- data = chart_item[4]
47
-
48
- chart_info = {
49
- "section": section_name,
50
- "type": chart_type,
51
- "title": chart_title,
52
- "period": period,
53
- "data": data,
54
- "source_section": section.title
55
- }
56
-
57
- charts.append(chart_info)
58
- # logging.info(f"Added chart: {chart_type} - {chart_title} for section {section_name}")
59
- else:
60
- # logging.warning(f"Chart item has insufficient data: {len(chart_item)} items, expected 5")
61
- pass
62
-
63
- except Exception as e:
64
- # logging.error(f"Failed to parse chart data for section '{section.title}': {str(e)}")
65
- continue
66
-
67
- # Additional debugging: Check for any chart-related content
68
- if not chart_matches:
69
- # Look for any chart-related markers or text
70
- chart_indicators = re.findall(r'<!--.*?CHART.*?-->', section.content, re.DOTALL)
71
- if chart_indicators:
72
- # logging.info(f"Section '{section.title}': Found chart indicators: {chart_indicators}")
73
- pass
74
-
75
- # Check for general chart text
76
- if 'CHART' in section.content.upper() or 'chart' in section.content.lower():
77
- # logging.info(f"Section '{section.title}': Contains chart-related text")
78
- pass
79
-
80
- # logging.info(f"Total charts parsed: {len(charts)}")
81
- return charts
82
-
83
- def create_enhanced_chart_style(chart_type, data, title, business_idea):
84
- """Create charts with enhanced professional styling"""
85
- # Set modern style
86
- plt.style.use('default')
87
-
88
- # Design palette from image
89
- colors = ['#6A2EAB', '#5A2E9B', '#C0C0C0', '#FF8C00', '#FFD700', '#696969', '#FF4500']
90
-
91
- if chart_type == "DC": # Doughnut Chart
92
- fig, ax = plt.subplots(figsize=(14, 10))
93
-
94
- # Extract labels and values from data
95
- labels = [item[0] for item in data]
96
- values = [item[1] for item in data]
97
-
98
- # Create doughnut chart with enhanced styling
99
- wedges, texts, autotexts = ax.pie(values, labels=labels, colors=colors[:len(values)],
100
- autopct='%1.1f%%', startangle=90,
101
- wedgeprops=dict(width=0.6, edgecolor='white', linewidth=2),
102
- textprops={'fontsize': 18})
103
-
104
- # Enhanced title styling
105
- ax.set_title(title, fontsize=24, fontweight='bold', pad=20, color='#374151')
106
-
107
- # Add legend with enhanced styling
108
- legend = ax.legend(wedges, labels, title="Categories",
109
- loc="center left", bbox_to_anchor=(1, 0, 0.5, 1),
110
- fontsize=16, title_fontsize=18)
111
- legend.get_title().set_color('#FF6B6B')
112
-
113
- # Add business name watermark
114
- fig.text(0.5, 0.02, f"Generated for: {business_idea}",
115
- ha='center', va='bottom', fontsize=8, color='#9CA3AF', style='italic')
116
-
117
- plt.tight_layout()
118
- return fig
119
-
120
- elif chart_type == "BG": # Bar Graph
121
- fig, ax = plt.subplots(figsize=(16, 10))
122
-
123
- # Extract labels and values
124
- labels = [item[0] for item in data]
125
- values = [item[1] for item in data]
126
-
127
- # Create bar chart with enhanced styling
128
- bars = ax.bar(labels, values, color=colors[:len(values)],
129
- edgecolor='white', linewidth=1, alpha=0.8)
130
-
131
- # Add value labels on bars
132
- for bar, value in zip(bars, values):
133
- height = bar.get_height()
134
- ax.text(bar.get_x() + bar.get_width()/2., height + max(values)*0.01,
135
- f'{value}', ha='center', va='bottom', fontweight='bold', fontsize=18)
136
-
137
- # Enhanced styling
138
- ax.set_title(title, fontsize=24, fontweight='bold', pad=20, color='#374151')
139
- ax.set_xlabel('Categories', fontsize=18, color='#6B7280')
140
- ax.set_ylabel('Values', fontsize=18, color='#6B7280')
141
-
142
- # Rotate x-axis labels for better readability
143
- plt.setp(ax.get_xticklabels(), rotation=45, ha='right', fontsize=16)
144
- plt.setp(ax.get_yticklabels(), fontsize=16)
145
-
146
- # Add grid for better readability
147
- ax.yaxis.grid(True, alpha=0.3)
148
- ax.set_axisbelow(True)
149
-
150
- # Add business name watermark
151
- fig.text(0.5, 0.02, f"Generated for: {business_idea}",
152
- ha='center', va='bottom', fontsize=8, color='#9CA3AF', style='italic')
153
-
154
- plt.tight_layout()
155
- return fig
156
-
157
- elif chart_type == "LG": # Line Graph
158
- fig, ax = plt.subplots(figsize=(16, 10))
159
-
160
- # Extract data
161
- x_values = [item[0] for item in data]
162
- y_values = [item[1] for item in data]
163
-
164
- # Create line chart with enhanced styling
165
- ax.plot(x_values, y_values, color='#FF6B6B', linewidth=3, marker='o',
166
- markersize=8, markerfacecolor='white', markeredgecolor='#FF6B6B')
167
-
168
- # Fill area under line
169
- ax.fill_between(x_values, y_values, alpha=0.3, color='#4ECDC4')
170
-
171
- # Enhanced styling
172
- ax.set_title(title, fontsize=24, fontweight='bold', pad=20, color='#374151')
173
- ax.set_xlabel('Time Period', fontsize=18, color='#6B7280')
174
- ax.set_ylabel('Values', fontsize=18, color='#6B7280')
175
-
176
- # Add grid
177
- ax.grid(True, alpha=0.3)
178
- ax.set_axisbelow(True)
179
-
180
- # Increase tick label font sizes
181
- plt.setp(ax.get_xticklabels(), fontsize=16)
182
- plt.setp(ax.get_yticklabels(), fontsize=16)
183
-
184
- # Add business name watermark
185
- fig.text(0.5, 0.02, f"Generated for: {business_idea}",
186
- ha='center', va='bottom', fontsize=8, color='#9CA3AF', style='italic')
187
-
188
- plt.tight_layout()
189
- return fig
190
-
191
- elif chart_type == "PG": # Pie Graph
192
- fig, ax = plt.subplots(figsize=(14, 10))
193
-
194
- # Extract labels and values
195
- labels = [item[0] for item in data]
196
- values = [item[1] for item in data]
197
-
198
- # Create pie chart with enhanced styling
199
- wedges, texts, autotexts = ax.pie(values, labels=labels, colors=colors[:len(values)],
200
- autopct='%1.1f%%', startangle=90,
201
- wedgeprops=dict(edgecolor='white', linewidth=2),
202
- textprops={'fontsize': 18})
203
-
204
- # Enhanced title styling
205
- ax.set_title(title, fontsize=24, fontweight='bold', pad=20, color='#374151')
206
-
207
- # Add business name watermark
208
- fig.text(0.5, 0.02, f"Generated for: {business_idea}",
209
- ha='center', va='bottom', fontsize=8, color='#9CA3AF', style='italic')
210
-
211
- plt.tight_layout()
212
- return fig
213
-
214
- return None
215
-
216
- def create_chart_from_data(chart_info: Dict[str, Any], business_idea: str) -> bytes:
217
- """Create a matplotlib chart from parsed chart data with enhanced styling"""
218
- try:
219
- chart_type = chart_info["type"]
220
- chart_title = chart_info["title"]
221
- data = chart_info["data"]
222
-
223
- # logging.info(f"Creating chart: {chart_type} - {chart_title} with {len(data)} data points")
224
-
225
- # Use enhanced styling function
226
- fig = create_enhanced_chart_style(chart_type, data, chart_title, business_idea)
227
-
228
- if fig is None:
229
- # Fallback to original method if enhanced styling fails
230
- if chart_type == "DC": # Doughnut Chart
231
- fig, ax = plt.subplots(figsize=(14, 10)) # Larger size for better visibility
232
-
233
- # Extract labels and values from data
234
- labels = [item[0] for item in data]
235
- values = [item[1] for item in data]
236
-
237
- # High contrast color scheme
238
- colors = ['#6A2EAB', '#5A2E9B', '#C0C0C0', '#FF8C00', '#FFD700', '#696969', '#FF4500']
239
-
240
- # Create doughnut chart
241
- wedges, texts, autotexts = ax.pie(values, labels=labels, colors=colors[:len(values)],
242
- autopct='%1.1f%%', startangle=90,
243
- wedgeprops=dict(width=0.6, edgecolor='white', linewidth=2),
244
- textprops={'fontsize': 18})
245
-
246
- # Enhanced title
247
- ax.set_title(chart_title, fontsize=22, fontweight='bold', pad=15, color='#374151')
248
-
249
- # Add legend
250
- ax.legend(wedges, labels, title="Categories", loc="center left", bbox_to_anchor=(1, 0, 0.5, 1), fontsize=16)
251
-
252
- plt.tight_layout()
253
- elif chart_type == "BG": # Bar Graph
254
- fig, ax = plt.subplots(figsize=(16, 10))
255
-
256
- # Extract labels and values
257
- labels = [item[0] for item in data]
258
- values = [item[1] for item in data]
259
-
260
- # High contrast color scheme
261
- colors = ['#6A2EAB', '#5A2E9B', '#C0C0C0', '#FF8C00', '#FFD700', '#696969', '#FF4500']
262
-
263
- # Create bar chart
264
- bars = ax.bar(labels, values, color=colors[:len(values)],
265
- edgecolor='white', linewidth=1, alpha=0.8)
266
-
267
- # Add value labels on bars
268
- for bar, value in zip(bars, values):
269
- height = bar.get_height()
270
- ax.text(bar.get_x() + bar.get_width()/2., height + max(values)*0.01,
271
- f'{value}', ha='center', va='bottom', fontweight='bold', fontsize=18)
272
-
273
- ax.set_title(chart_title, fontsize=22, fontweight='bold', pad=15, color='#374151')
274
- ax.set_xlabel('Categories', fontsize=18, color='#6B7280')
275
- ax.set_ylabel('Values', fontsize=18, color='#6B7280')
276
-
277
- # Rotate x-axis labels
278
- plt.setp(ax.get_xticklabels(), rotation=45, ha='right', fontsize=16)
279
- plt.setp(ax.get_yticklabels(), fontsize=16)
280
-
281
- # Add grid
282
- ax.yaxis.grid(True, alpha=0.3)
283
- ax.set_axisbelow(True)
284
-
285
- plt.tight_layout()
286
- elif chart_type == "LG": # Line Graph
287
- fig, ax = plt.subplots(figsize=(16, 10))
288
-
289
- # Extract data
290
- x_values = [item[0] for item in data]
291
- y_values = [item[1] for item in data]
292
-
293
- # Create line chart
294
- ax.plot(x_values, y_values, color='#FF6B6B', linewidth=3, marker='o',
295
- markersize=8, markerfacecolor='white', markeredgecolor='#FF6B6B')
296
-
297
- # Fill area under line
298
- ax.fill_between(x_values, y_values, alpha=0.3, color='#4ECDC4')
299
-
300
- ax.set_title(chart_title, fontsize=22, fontweight='bold', pad=15, color='#374151')
301
- ax.set_xlabel('Time Period', fontsize=18, color='#6B7280')
302
- ax.set_ylabel('Values', fontsize=18, color='#6B7280')
303
-
304
- # Add grid
305
- ax.grid(True, alpha=0.3)
306
- ax.set_axisbelow(True)
307
-
308
- # Increase tick label font sizes
309
- plt.setp(ax.get_xticklabels(), fontsize=16)
310
- plt.setp(ax.get_yticklabels(), fontsize=16)
311
-
312
- plt.tight_layout()
313
- elif chart_type == "PG": # Pie Graph
314
- fig, ax = plt.subplots(figsize=(14, 10))
315
-
316
- # Extract labels and values
317
- labels = [item[0] for item in data]
318
- values = [item[1] for item in data]
319
-
320
- # High contrast color scheme
321
- colors = ['#6A2EAB', '#5A2E9B', '#C0C0C0', '#FF8C00', '#FFD700', '#696969', '#FF4500']
322
-
323
- # Create pie chart
324
- wedges, texts, autotexts = ax.pie(values, labels=labels, colors=colors[:len(values)],
325
- autopct='%1.1f%%', startangle=90,
326
- wedgeprops=dict(edgecolor='white', linewidth=2),
327
- textprops={'fontsize': 18})
328
-
329
- ax.set_title(chart_title, fontsize=22, fontweight='bold', pad=15, color='#374151')
330
-
331
- plt.tight_layout()
332
- else:
333
- # logging.warning(f"Unknown chart type: {chart_type}")
334
- return None
335
-
336
- # Save to bytes
337
- buffer = io.BytesIO()
338
- fig.savefig(buffer, format='png', dpi=300, bbox_inches='tight',
339
- facecolor='white', edgecolor='none')
340
- buffer.seek(0)
341
- chart_bytes = buffer.getvalue()
342
- buffer.close()
343
-
344
- # Close the figure to free memory
345
- plt.close(fig)
346
-
347
- return chart_bytes
348
-
349
- except Exception as e:
350
- # logging.error(f"Failed to create chart: {str(e)}")
351
- return None
352
-
353
- def fix_base64_padding(base64_string: str) -> str:
354
- """Fix common base64 padding issues"""
355
- # Remove any whitespace or newlines
356
- base64_string = base64_string.strip()
357
-
358
- # Add padding if needed
359
- missing_padding = len(base64_string) % 4
360
- if missing_padding:
361
- base64_string += '=' * (4 - missing_padding)
362
-
363
- return base64_string
364
-
365
- def robust_base64_decode(base64_string: str) -> Optional[bytes]:
366
- """Robustly decode base64 string with error handling and padding correction"""
367
- try:
368
- # First try direct decoding
369
- return base64.b64decode(base64_string)
370
- except Exception as e1:
371
- try:
372
- # Try with padding correction
373
- fixed_string = fix_base64_padding(base64_string)
374
- return base64.b64decode(fixed_string)
375
- except Exception as e2:
376
- try:
377
- # Try with URL-safe base64
378
- return base64.urlsafe_b64decode(fix_base64_padding(base64_string))
379
- except Exception as e3:
380
- try:
381
- # Try with standard base64 ignoring padding
382
- return base64.b64decode(base64_string + '==', validate=False)
383
- except Exception as e4:
384
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/DocumentGeneration/document_helpers.py DELETED
@@ -1,159 +0,0 @@
1
- # Document Helper Functions
2
- # Contains helper functions like filter_empty_sections(), extract_business_name_from_content(), etc.
3
-
4
- import re
5
- import os
6
- import sys
7
- from typing import List, Dict, Any
8
-
9
- # Add the parent directory to sys.path to import from models.py
10
- sys.path.append(os.path.dirname(os.path.dirname(__file__)))
11
- from models import ExportSection
12
-
13
- def extract_business_name_from_content(sections: List[ExportSection]) -> str:
14
- """Extract business name from markdown content"""
15
- for section in sections:
16
- if section.content:
17
- lines = section.content.split('\n')
18
- for i, line in enumerate(lines):
19
- line = line.strip()
20
- # Look for "Business Name:" pattern
21
- if '**Business Name:**' in line or 'Business Name:' in line:
22
- # Extract the business name from the line
23
- if '**Business Name:**' in line:
24
- business_name = line.split('**Business Name:**')[1].strip()
25
- else:
26
- business_name = line.split('Business Name:')[1].strip()
27
-
28
- # Keep markdown formatting - don't strip ** symbols
29
- # business_name = re.sub(r'[*_`~]', '', business_name)
30
- business_name = business_name.strip()
31
-
32
- if business_name:
33
- return business_name
34
-
35
- # If not found on same line, check next line
36
- if i + 1 < len(lines):
37
- next_line = lines[i + 1].strip()
38
- business_name = next_line # Keep ** symbols
39
- business_name = business_name.strip()
40
- if business_name:
41
- return business_name
42
-
43
- return None
44
-
45
- def extract_subheadings(content: str) -> List[str]:
46
- """Extract subheadings from markdown content"""
47
- subheadings = []
48
- lines = content.split('\n')
49
-
50
- for line in lines:
51
- line = line.strip()
52
- # Look for various markdown header patterns
53
- if (line.startswith('##') and not line.startswith('###')) or \
54
- line.startswith('**') and line.endswith('**') and len(line) > 4:
55
- # Remove markdown formatting and clean up
56
- if line.startswith('##'):
57
- subheading = re.sub(r'^##+\s*', '', line)
58
- else:
59
- # Handle bold text that might be headings - keep ** symbols
60
- subheading = line # Keep ** symbols instead of stripping them
61
-
62
- # subheading = re.sub(r'[*_`~]', '', subheading) # Commented out to preserve ** symbols
63
- if subheading.strip() and len(subheading.strip()) > 3: # Avoid very short headings
64
- subheadings.append(subheading.strip())
65
-
66
- return subheadings
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
- def filter_empty_sections(sections: List[ExportSection]) -> List[ExportSection]:
79
- """Filter out sections with no content to prevent empty pages"""
80
- filtered_sections = []
81
-
82
- for section in sections:
83
- if section.content and section.content.strip():
84
- # Clean the content to check if it's actually meaningful
85
- cleaned_content = section.content # No clean_text_for_pdf import, so use raw content
86
- if cleaned_content and len(cleaned_content.strip()) > 10: # At least 10 characters
87
- filtered_sections.append(section)
88
- # logging.info(f"Section '{section.title}' passed filtering with {len(cleaned_content)} characters")
89
- else:
90
- # logging.info(f"Section '{section.title}' filtered out - content too short: {len(cleaned_content) if cleaned_content else 0} chars")
91
- pass
92
- else:
93
- # logging.info(f"Section '{section.title}' filtered out - no content")
94
- pass
95
-
96
- # logging.info(f"Filtered sections: {len(filtered_sections)} out of {len(sections)} total")
97
- return filtered_sections
98
-
99
-
100
- def remove_duplicate_headings(content: str) -> str:
101
- """Remove duplicate headings from content"""
102
- if not content:
103
- return content
104
-
105
- lines = content.split('\n')
106
- cleaned_lines = []
107
- seen_headings = set()
108
-
109
- for line in lines:
110
- line = line.strip()
111
- if not line:
112
- cleaned_lines.append('')
113
- continue
114
-
115
- # Check if this is any type of heading
116
- is_heading = False
117
- heading_text = ""
118
-
119
- # Markdown headings (# ## ### etc.)
120
- if line.startswith('#'):
121
- heading_text = re.sub(r'^#+\s*', '', line).strip()
122
- is_heading = True
123
-
124
- # Bold headings (**text**)
125
- elif line.startswith('**') and line.endswith('**') and len(line) > 4:
126
- heading_text = line[2:-2].strip() # Remove ** symbols
127
- is_heading = True
128
-
129
- # Check for numbered headings (e.g., "8.1 Management Team")
130
- elif re.match(r'^\d+\.?\d*\s+', line):
131
- heading_text = re.sub(r'^\d+\.?\d*\s+', '', line).strip()
132
- is_heading = True
133
-
134
- if is_heading and heading_text:
135
- # Normalize the heading text for comparison
136
- normalized_text = heading_text.lower().strip()
137
-
138
- # Also check for variations with "and" vs "&"
139
- variations = [
140
- normalized_text,
141
- normalized_text.replace(' & ', ' and '),
142
- normalized_text.replace(' and ', ' & '),
143
- normalized_text.replace('&', 'and'),
144
- normalized_text.replace('and', '&')
145
- ]
146
-
147
- # Check if any variation has been seen before
148
- is_duplicate = any(var in seen_headings for var in variations)
149
-
150
- if not is_duplicate:
151
- # Add all variations to seen set
152
- for var in variations:
153
- seen_headings.add(var)
154
- cleaned_lines.append(line)
155
- # Skip duplicate headings
156
- else:
157
- cleaned_lines.append(line)
158
-
159
- return '\n'.join(cleaned_lines)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/DocumentGeneration/grouped.py DELETED
@@ -1,304 +0,0 @@
1
- import re
2
- from typing import List, Dict, Any
3
-
4
- def calculate_group_height(pdf, group: Dict[str, Any]) -> int:
5
- """Calculate the estimated height needed for a complete group"""
6
- subheading_type = group['subheading_type']
7
- content_lines = group['content'].split('\n') if group['content'] else []
8
-
9
- # Base height for subheading
10
- if subheading_type == 'h1':
11
- subheading_height = 25 # 20pt font + spacing
12
- elif subheading_type == 'h2':
13
- subheading_height = 20 # 16pt font + underline + spacing
14
- elif subheading_type == 'h3':
15
- subheading_height = 16 # 14pt font + spacing
16
- elif subheading_type == 'h4':
17
- subheading_height = 14 # 12pt font + spacing
18
- elif subheading_type == 'bold':
19
- subheading_height = 20 # 16pt font + spacing
20
- elif subheading_type == 'numbered':
21
- subheading_height = 20 # 16pt font + spacing
22
- else:
23
- subheading_height = 16
24
-
25
- # Calculate content height more accurately
26
- content_height = 0
27
- for line in content_lines:
28
- if line.strip():
29
- # More accurate line height calculation
30
- words = line.split()
31
- if len(words) > 0:
32
- # Estimate characters per line (roughly 80-90 chars per line)
33
- chars_per_line = 85
34
- estimated_lines = max(1, len(line) // chars_per_line + 1)
35
- line_height = estimated_lines * 8 # 8pt per line
36
- content_height += line_height
37
- else:
38
- content_height += 8 # Empty line
39
- else:
40
- content_height += 3 # Empty line spacing
41
-
42
- # Add spacing between groups
43
- group_spacing = 8
44
-
45
- total_height = subheading_height + content_height + group_spacing
46
-
47
- print(f"DEBUG: Group '{group.get('subheading_clean', 'Unknown')[:30]}...' height calculation:")
48
- print(f" Subheading height: {subheading_height}")
49
- print(f" Content height: {content_height}")
50
- print(f" Group spacing: {group_spacing}")
51
- print(f" Total height: {total_height}")
52
-
53
- return total_height
54
-
55
- def should_break_page_for_group(pdf, group: Dict[str, Any], current_y: int, page_height: int = 280) -> bool:
56
- """Determine if we should break to a new page for this group"""
57
- estimated_height = calculate_group_height(pdf, group)
58
-
59
- print(f"DEBUG: should_break_page_for_group called")
60
- print(f"DEBUG: Subheading: '{group.get('subheading_clean', 'Unknown')[:30]}...'")
61
- print(f"DEBUG: Current Y: {current_y}")
62
- print(f"DEBUG: Estimated height: {estimated_height}")
63
- print(f"DEBUG: Page height: {page_height}")
64
- print(f"DEBUG: Available space: {page_height - current_y}")
65
- print(f"DEBUG: Will fit? {current_y + estimated_height <= page_height}")
66
-
67
- # CRITICAL FIX: Only break if the group absolutely won't fit
68
- # This prevents splitting subheadings from their content
69
-
70
- # If the group won't fit on current page, break
71
- if current_y + estimated_height > page_height:
72
- print(f"DEBUG: -> BREAKING: Group won't fit ({current_y} + {estimated_height} > {page_height})")
73
- return True
74
-
75
- # If we're very close to the end of the page (less than 50mm remaining) and group is significant, break
76
- if current_y > page_height - 50 and estimated_height > 30:
77
- print(f"DEBUG: -> BREAKING: Very close to end of page ({current_y} > {page_height - 50}) and group significant ({estimated_height} > 30)")
78
- return True
79
-
80
- print(f"DEBUG: -> NO BREAK: Group will fit on current page")
81
- return False
82
-
83
- def clean_markdown_from_line(line: str) -> str:
84
- """Clean markdown formatting from a single line"""
85
- if not line:
86
- return ""
87
-
88
- cleaned = line
89
-
90
- # Remove bold, italic formatting
91
- cleaned = re.sub(r'\*\*(.*?)\*\*', r'\1', cleaned) # Remove bold
92
- cleaned = re.sub(r'\*(.*?)\*', r'\1', cleaned) # Remove italic
93
- cleaned = re.sub(r'`(.*?)`', r'\1', cleaned) # Remove code
94
- cleaned = re.sub(r'~~(.*?)~~', r'\1', cleaned) # Remove strikethrough
95
-
96
- # Remove emphasis markers
97
- cleaned = re.sub(r'_{1,2}(.*?)_{1,2}', r'\1', cleaned) # Remove underscores
98
- cleaned = re.sub(r'\*{1,2}(.*?)\*{1,2}', r'\1', cleaned) # Remove asterisks
99
-
100
- # Remove links (keep text)
101
- cleaned = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', cleaned) # Remove links, keep text
102
-
103
- # Remove any remaining HTML-like tags
104
- cleaned = re.sub(r'<[^>]+>', '', cleaned)
105
-
106
- # CRITICAL FIX: Replace multiple spaces with single space to prevent double spaces
107
- cleaned = re.sub(r' +', ' ', cleaned) # Replace multiple spaces with single space
108
-
109
- return cleaned.strip()
110
-
111
- def parse_content_into_groups(content: str) -> List[Dict[str, Any]]:
112
- """Parse markdown content into subheading-content groups"""
113
- if not content:
114
- return []
115
-
116
- print(f"DEBUG: parse_content_into_groups called with content length: {len(content)}")
117
-
118
- lines = content.split('\n')
119
- groups = []
120
- current_group = None
121
-
122
- for i, line in enumerate(lines):
123
- line = line.strip()
124
- if not line:
125
- if current_group:
126
- current_group['content_lines'].append('') # Preserve empty lines
127
- continue
128
-
129
- # Check if this is a heading (H1, H2, H3, H4, or numbered)
130
- is_heading = (
131
- line.startswith('# ') or # H1
132
- (line.startswith('## ') and not line.startswith('### ')) or # H2
133
- (line.startswith('### ') and not line.startswith('#### ')) or # H3
134
- (line.startswith('#### ') and not line.startswith('##### ')) or # H4
135
- re.match(r'^\d+\.\d+\s+', line) # Numbered subheadings like "3.6 SWOT Analysis"
136
- )
137
-
138
- # Check if this is bold text
139
- is_bold_text = (
140
- (line.startswith('**') and line.endswith('**') and len(line) > 4) or # Bold headings
141
- (re.match(r'^\*\*.*?\*\*:', line) and len(line) > 4) # Bold text before colon as heading
142
- )
143
-
144
- # Check if this is a bold list item (e.g., "- **Item**")
145
- is_bold_list_item = re.match(r'^[-*]\s*\*\*(.*?)\*\*$', line)
146
-
147
- # Special handling for bold text following numbered subheadings
148
- should_create_new_group = False
149
-
150
- if is_heading:
151
- # Always create new group for regular headings
152
- should_create_new_group = True
153
- print(f"DEBUG: Line {i+1} '{line[:50]}...' - Regular heading, creating new group")
154
- elif is_bold_text:
155
- # Check if we should treat this bold text as content for a numbered subheading
156
- if groups and groups[-1].get('subheading_type') == 'h3':
157
- # Check if the previous group's subheading looks like a numbered subheading
158
- previous_subheading = groups[-1].get('subheading', '')
159
- if re.match(r'^\d+\.\d+\s+', previous_subheading):
160
- # This is content for a numbered subheading, add to previous group
161
- groups[-1]['content_lines'].append(line)
162
- print(f"DEBUG: Line {i+1} '{line[:50]}...' - Adding to previous numbered subheading '{previous_subheading[:30]}...'")
163
- continue # Skip creating new group
164
- else:
165
- # Not a numbered subheading, treat as new heading
166
- should_create_new_group = True
167
- print(f"DEBUG: Line {i+1} '{line[:50]}...' - Treating as NEW HEADING (not numbered subheading)")
168
- else:
169
- # No previous group or not a numbered subheading, treat as new heading
170
- should_create_new_group = True
171
- print(f"DEBUG: Line {i+1} '{line[:50]}...' - Treating as NEW HEADING (no numbered subheading)")
172
- elif is_bold_list_item:
173
- # If it's a bold list item, treat it as a new heading
174
- should_create_new_group = True
175
- print(f"DEBUG: Line {i+1} '{line[:50]}...' - Bold list item, creating new group")
176
-
177
- if should_create_new_group:
178
- # Save previous group if it exists
179
- if current_group:
180
- current_group['content'] = '\n'.join(current_group['content_lines'])
181
- groups.append(current_group)
182
- print(f"DEBUG: Saved group '{current_group.get('subheading_clean', 'Unknown')[:30]}...' with {len(current_group['content_lines'])} content lines")
183
-
184
- # Clean markdown symbols BEFORE determining type and storing
185
- clean_text = line
186
- heading_type = 'text'
187
-
188
- if line.startswith('# '):
189
- clean_text = line[2:].strip() # Remove "# "
190
- heading_type = 'h1'
191
- elif line.startswith('## '):
192
- clean_text = line[3:].strip() # Remove "## "
193
- heading_type = 'h2'
194
- elif line.startswith('### '):
195
- clean_text = line[4:].strip() # Remove "### "
196
- heading_type = 'h3'
197
- elif line.startswith('#### '):
198
- clean_text = line[5:].strip() # Remove "#### "
199
- heading_type = 'h4'
200
- elif re.match(r'^\d+\.\d+\s+', line):
201
- clean_text = line.strip() # Keep numbered headings as-is
202
- heading_type = 'numbered'
203
- elif is_bold_text:
204
- # Remove ** markers from bold text
205
- clean_text = clean_markdown_from_line(line.strip())
206
- heading_type = 'bold'
207
- elif is_bold_list_item:
208
- # Extract the bold text from list item and treat as H3 subheading
209
- # Remove the list marker and ** markers
210
- clean_text = re.sub(r'^[-*]\s*\*\*(.*?)\*\*', r'\1', line.strip())
211
- heading_type = 'h3' # Treat as H3 subheading
212
-
213
- print(f"DEBUG: Creating new group '{clean_text[:30]}...' (type: {heading_type})")
214
-
215
- # Start new group with cleaned text
216
- current_group = {
217
- 'subheading': line, # Keep original for reference
218
- 'content_lines': [],
219
- 'content': '',
220
- 'subheading_clean': clean_text, # Pre-cleaned text
221
- 'subheading_type': heading_type
222
- }
223
- else:
224
- # Add to current group's content - clean the line
225
- if current_group:
226
- cleaned_line = clean_markdown_from_line(line)
227
- current_group['content_lines'].append(cleaned_line)
228
- if i < 5: # Log first few content lines
229
- print(f"DEBUG: Added content line to group '{current_group.get('subheading_clean', 'Unknown')[:30]}...': '{cleaned_line[:50]}...'")
230
- else:
231
- # Content before any subheading - create a default group
232
- if not groups or groups[-1].get('subheading') != '## Introduction':
233
- cleaned_line = clean_markdown_from_line(line)
234
- current_group = {
235
- 'subheading': '## Introduction',
236
- 'content_lines': [cleaned_line],
237
- 'content': '',
238
- 'subheading_clean': 'Introduction',
239
- 'subheading_type': 'h2'
240
- }
241
-
242
- # Don't forget the last group
243
- if current_group:
244
- current_group['content'] = '\n'.join(current_group['content_lines'])
245
- groups.append(current_group)
246
- print(f"DEBUG: Saved final group '{current_group.get('subheading_clean', 'Unknown')[:30]}...' with {len(current_group['content_lines'])} content lines")
247
-
248
- # Add metadata to each group
249
- for i, group in enumerate(groups):
250
- group['group_id'] = f"group_{i}"
251
- group['content_length'] = len(group['content'])
252
- group['word_count'] = len(group['content'].split()) if group['content'] else 0
253
- group['has_charts'] = 'CHART' in group['content'].upper() if group['content'] else False
254
- group['line_count'] = len([line for line in group['content'].split('\n') if line.strip()])
255
-
256
- print(f"DEBUG: Created {len(groups)} groups total")
257
- return groups
258
-
259
- def extract_key_phrases(content: str) -> List[tuple]:
260
- """Extract key phrases from content for grouping metadata"""
261
- if not content:
262
- return []
263
-
264
- # Simple key phrase extraction
265
- words = re.findall(r'\b\w+\b', content.lower())
266
- word_freq = {}
267
-
268
- for word in words:
269
- if len(word) > 3: # Only words longer than 3 characters
270
- word_freq[word] = word_freq.get(word, 0) + 1
271
-
272
- # Return top 5 most frequent words
273
- return sorted(word_freq.items(), key=lambda x: x[1], reverse=True)[:5]
274
-
275
- def get_content_groups_info(content: str) -> Dict[str, Any]:
276
- """Get detailed information about content groups for analysis and debugging"""
277
- if not content:
278
- return {"groups": [], "summary": {"total_groups": 0, "total_content_length": 0}}
279
-
280
- groups = parse_content_into_groups(content)
281
-
282
- # Add key phrases to each group
283
- for group in groups:
284
- group['key_phrases'] = extract_key_phrases(group['content'])
285
-
286
- # Calculate summary statistics
287
- total_content_length = sum(group['content_length'] for group in groups)
288
- total_word_count = sum(group['word_count'] for group in groups)
289
- groups_with_charts = sum(1 for group in groups if group['has_charts'])
290
-
291
- summary = {
292
- "total_groups": len(groups),
293
- "total_content_length": total_content_length,
294
- "total_word_count": total_word_count,
295
- "groups_with_charts": groups_with_charts,
296
- "average_group_size": total_content_length / len(groups) if groups else 0,
297
- "largest_group": max(groups, key=lambda g: g['content_length'])['subheading_clean'] if groups else None,
298
- "smallest_group": min(groups, key=lambda g: g['content_length'])['subheading_clean'] if groups else None
299
- }
300
-
301
- return {
302
- "groups": groups,
303
- "summary": summary
304
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/DocumentGeneration/live_pdf_generator.py DELETED
@@ -1,126 +0,0 @@
1
- # Live PDF Generator Module
2
- # Enables real-time PDF updates as content changes
3
-
4
- import asyncio
5
- import io
6
- import tempfile
7
- import os
8
- from typing import Dict, Any, Optional, AsyncGenerator
9
- from fpdf import FPDF
10
- import base64
11
-
12
- import sys
13
- import os
14
- sys.path.append(os.path.dirname(__file__))
15
- from pdf_generator import create_pdf_document
16
- from models import ExportRequest
17
-
18
- class LivePDFGenerator:
19
- """Generates and streams PDF updates in real-time"""
20
-
21
- def __init__(self):
22
- self.active_sessions: Dict[str, 'PDFSession'] = {}
23
-
24
- async def create_live_session(self, business_plan_id: str, initial_request: ExportRequest) -> str:
25
- """Create a new live PDF session"""
26
- session_id = f"pdf_session_{business_plan_id}_{int(asyncio.get_event_loop().time())}"
27
-
28
- session = PDFSession(session_id, business_plan_id, initial_request)
29
- self.active_sessions[session_id] = session
30
-
31
- # Generate initial PDF
32
- await session.update_pdf(initial_request)
33
-
34
- return session_id
35
-
36
- async def update_session(self, session_id: str, updated_request: ExportRequest) -> bool:
37
- """Update an existing PDF session with new content"""
38
- if session_id not in self.active_sessions:
39
- return False
40
-
41
- session = self.active_sessions[session_id]
42
- await session.update_pdf(updated_request)
43
- return True
44
-
45
- async def stream_pdf_updates(self, session_id: str) -> AsyncGenerator[bytes, None]:
46
- """Stream PDF updates as they occur"""
47
- if session_id not in self.active_sessions:
48
- return
49
-
50
- session = self.active_sessions[session_id]
51
- async for pdf_chunk in session.stream_updates():
52
- yield pdf_chunk
53
-
54
- def close_session(self, session_id: str):
55
- """Close and cleanup a PDF session"""
56
- if session_id in self.active_sessions:
57
- del self.active_sessions[session_id]
58
-
59
- class PDFSession:
60
- """Individual PDF session that manages real-time updates"""
61
-
62
- def __init__(self, session_id: str, business_plan_id: str, initial_request: ExportRequest):
63
- self.session_id = session_id
64
- self.business_plan_id = business_plan_id
65
- self.current_request = initial_request
66
- self.update_queue = asyncio.Queue()
67
- self.is_active = True
68
- self.last_pdf_bytes: Optional[bytes] = None
69
-
70
- async def update_pdf(self, new_request: ExportRequest):
71
- """Update the PDF with new content"""
72
- self.current_request = new_request
73
-
74
- try:
75
- # Generate new PDF
76
- pdf_bytes, filename = create_pdf_document(new_request)
77
- self.last_pdf_bytes = pdf_bytes
78
-
79
- # Notify subscribers of update
80
- await self.update_queue.put({
81
- 'type': 'pdf_update',
82
- 'data': base64.b64encode(pdf_bytes).decode('utf-8'),
83
- 'filename': filename,
84
- 'timestamp': asyncio.get_event_loop().time()
85
- })
86
-
87
- except Exception as e:
88
- # Notify subscribers of error
89
- await self.update_queue.put({
90
- 'type': 'pdf_error',
91
- 'error': str(e),
92
- 'timestamp': asyncio.get_event_loop().time()
93
- })
94
-
95
- async def stream_updates(self) -> AsyncGenerator[bytes, None]:
96
- """Stream PDF updates as they occur"""
97
- while self.is_active:
98
- try:
99
- # Wait for updates with timeout
100
- update = await asyncio.wait_for(self.update_queue.get(), timeout=30.0)
101
-
102
- if update['type'] == 'pdf_update':
103
- # Send SSE event for PDF update
104
- event_data = f"event: pdf_update\ndata: {{\"data\": \"{update['data']}\", \"filename\": \"{update['filename']}\"}}\n\n"
105
- yield event_data.encode('utf-8')
106
-
107
- elif update['type'] == 'pdf_error':
108
- # Send SSE event for error
109
- error_data = f"event: pdf_error\ndata: {{\"error\": \"{update['error']}\"}}\n\n"
110
- yield error_data.encode('utf-8')
111
-
112
- except asyncio.TimeoutError:
113
- # Send keep-alive
114
- yield b": keep-alive\n\n"
115
- except Exception as e:
116
- # Send error event
117
- error_data = f"event: stream_error\ndata: {str(e)}\n\n"
118
- yield error_data.encode('utf-8')
119
- break
120
-
121
- def close(self):
122
- """Close this session"""
123
- self.is_active = False
124
-
125
- # Global instance
126
- live_pdf_generator = LivePDFGenerator()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/DocumentGeneration/markdown_renderer.py DELETED
@@ -1,759 +0,0 @@
1
- # Markdown Renderer Module
2
- # Contains all markdown rendering functions for both PDF and Word
3
-
4
- import re
5
- import base64
6
- import os
7
- import sys
8
- import tempfile
9
- from typing import List, Dict, Any, Optional
10
-
11
- # Import from the same directory (Document Generation)
12
- from grouped import parse_content_into_groups, should_break_page_for_group
13
- from chart_generator import create_chart_from_data
14
-
15
- def clean_markdown_text(text: str) -> str:
16
- """Clean markdown formatting for PDF generation - PRESERVE HEADINGS"""
17
- if not text:
18
- return ""
19
-
20
- # Remove markdown formatting BUT PRESERVE HEADING STRUCTURE
21
- cleaned = text
22
-
23
- # PRESERVE HEADINGS - don't strip the # markers, keep them for rendering
24
- # We'll handle styling at render time instead of stripping them
25
-
26
- # Remove bold, italic formatting - strip ** or * symbols
27
- cleaned = re.sub(r'\*\*(.*?)\*\*', r'\1', cleaned) # Remove bold
28
- cleaned = re.sub(r'\*(.*?)\*', r'\1', cleaned) # Remove italic
29
- cleaned = re.sub(r'`(.*?)`', r'\1', cleaned) # Remove code
30
- cleaned = re.sub(r'~~(.*?)~~', r'\1', cleaned) # Remove strikethrough
31
-
32
- # Remove links (keep text)
33
- cleaned = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', cleaned) # Remove links, keep text
34
- cleaned = re.sub(r'!\[([^\]]*)\]\([^)]+\)', '', cleaned) # Remove images
35
-
36
- # Remove list markers (more aggressive)
37
- cleaned = re.sub(r'^\s*[-*+]\s*', '', cleaned, flags=re.MULTILINE) # Remove list markers
38
- cleaned = re.sub(r'^\s*\d+\.\s*', '', cleaned, flags=re.MULTILINE) # Remove numbered list markers
39
- cleaned = re.sub(r'^\s*[-*+]\s*', '', cleaned, flags=re.MULTILINE) # Remove any remaining list markers
40
-
41
- # Remove blockquotes
42
- cleaned = re.sub(r'^\s*>\s*', '', cleaned, flags=re.MULTILINE) # Remove blockquotes
43
-
44
- # Remove horizontal rules
45
- cleaned = re.sub(r'^\s*[-*_]{3,}\s*$', '', cleaned, flags=re.MULTILINE) # Remove horizontal rules
46
-
47
- # Remove code blocks
48
- cleaned = re.sub(r'```[\s\S]*?```', '', cleaned) # Remove code blocks
49
- cleaned = re.sub(r'`.*?`', '', cleaned) # Remove any remaining inline code
50
-
51
- # Remove emphasis markers - strip ** or * symbols
52
- cleaned = re.sub(r'_{1,2}(.*?)_{1,2}', r'\1', cleaned) # Remove underscores
53
- cleaned = re.sub(r'\*{1,2}(.*?)\*{1,2}', r'\1', cleaned) # Remove asterisks
54
-
55
- # PRESERVE CHART DATA MARKERS for inline processing
56
- # DO NOT REMOVE chart markers - they will be processed inline
57
- # cleaned = re.sub(r'<!--CHARTDATASTART-->.*?<!--CHARTDATAEND-->', '', cleaned, flags=re.DOTALL)
58
- # cleaned = re.sub(r'<!--CHARTDATASTART-->.*$', '', cleaned, flags=re.DOTALL)
59
- # cleaned = re.sub(r'<!--CHARTDATAEND-->', '', cleaned)
60
-
61
- # REMOVE CHART DATA MARKERS for PDF rendering since they're not being processed
62
- cleaned = re.sub(r'<!--CHART_DATA_START-->.*?<!--CHART_DATA_END-->', '', cleaned, flags=re.DOTALL)
63
- cleaned = re.sub(r'<!--CHARTDATASTART-->.*?<!--CHARTDATAEND-->', '', cleaned, flags=re.DOTALL)
64
-
65
- # Remove any remaining HTML-like tags
66
- cleaned = re.sub(r'<[^>]+>', '', cleaned)
67
-
68
- # Clean up extra whitespace and formatting
69
- cleaned = re.sub(r'\n\s*\n\s*\n+', '\n\n', cleaned) # Remove excessive line breaks
70
- cleaned = re.sub(r'^\s+', '', cleaned, flags=re.MULTILINE) # Remove leading whitespace
71
- cleaned = re.sub(r'\s+$', '', cleaned, flags=re.MULTILINE) # Remove trailing whitespace
72
- cleaned = re.sub(r' +', ' ', cleaned) # Replace multiple spaces with single space
73
-
74
- # Final cleanup
75
- cleaned = cleaned.strip()
76
-
77
- return cleaned
78
-
79
- def clean_text_for_pdf(text: str) -> str:
80
- """Clean text specifically for PDF generation, handling Unicode issues"""
81
- if not text:
82
- return ""
83
-
84
- # First clean markdown
85
- cleaned = clean_markdown_text(text)
86
-
87
- # Replace problematic Unicode characters with ASCII equivalents
88
- unicode_replacements = {
89
- '\u2019': "'", # Right single quotation mark
90
- '\u2018': "'", # Left single quotation mark
91
- '\u201C': '"', # Left double quotation mark
92
- '\u201D': '"', # Right double quotation mark
93
- '\u2013': '-', # En dash
94
- '\u2014': '--', # Em dash
95
- '\u2022': '•', # Bullet
96
- '\u2026': '...', # Horizontal ellipsis
97
- '\u00A0': ' ', # Non-breaking space
98
- '\u00B0': '°', # Degree sign
99
- '\u00AE': '(R)', # Registered trademark
100
- '\u2122': '(TM)', # Trademark
101
- '\u00A9': '(C)', # Copyright
102
- }
103
-
104
- for unicode_char, replacement in unicode_replacements.items():
105
- cleaned = cleaned.replace(unicode_char, replacement)
106
-
107
- # Additional space normalization after Unicode replacements to prevent double spaces
108
- cleaned = re.sub(r' +', ' ', cleaned) # Replace multiple spaces with single space
109
-
110
- return cleaned
111
-
112
- def process_content_with_inline_charts(doc, content: str, business_idea: str):
113
- """Process content and render charts inline where they appear in the text"""
114
- try:
115
- from docx.shared import Inches
116
- except ImportError:
117
- raise Exception("python-docx is not installed. Please install it with: pip install python-docx")
118
-
119
- # Split content by chart markers - handle both formats
120
- chart_pattern = r'<!--CHART_DATA_START-->(.*?)<!--CHART_DATA_END-->'
121
- parts = re.split(chart_pattern, content, flags=re.DOTALL)
122
-
123
- # If no charts found, try alternative format
124
- if len(parts) == 1:
125
- chart_pattern = r'<!--CHARTDATASTART-->(.*?)<!--CHARTDATAEND-->'
126
- parts = re.split(chart_pattern, content, flags=re.DOTALL)
127
-
128
- for i, part in enumerate(parts):
129
- if i % 2 == 0: # Regular content
130
- if part.strip():
131
- render_markdown_to_docx_grouped(doc, part.strip())
132
- else: # Chart data
133
- try:
134
- chart_array = eval(part.strip())
135
- for chart_item in chart_array:
136
- if len(chart_item) >= 5:
137
- chart_type = chart_item[1]
138
- chart_title = chart_item[2]
139
- data = chart_item[4]
140
-
141
- # Add chart title
142
- doc.add_heading(f"{chart_title}", level=2)
143
-
144
- # Create and add chart
145
- chart_info = {
146
- "type": chart_type,
147
- "title": chart_title,
148
- "data": data
149
- }
150
-
151
- chart_bytes = create_chart_from_data(chart_info, business_idea)
152
-
153
- if chart_bytes:
154
- with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_img:
155
- tmp_img.write(chart_bytes)
156
- temp_img_path = tmp_img.name
157
-
158
- # Add image to Word document
159
- doc.add_picture(temp_img_path, width=Inches(6.0)) # 6 inches width
160
-
161
- # Clean up temp file
162
- os.remove(temp_img_path)
163
- except Exception as e:
164
- # If chart processing fails, just render the raw content
165
- if part.strip():
166
- render_markdown_to_docx_grouped(doc, part.strip())
167
-
168
-
169
- def render_markdown_to_pdf(pdf, content: str, primary_color, accent_color, text_color):
170
- """Render markdown content to PDF with proper heading styling and page break logic"""
171
- if not content:
172
- return
173
-
174
- lines = content.split('\n')
175
- current_y = pdf.get_y()
176
-
177
- for line in lines:
178
- line = line.strip()
179
- if not line:
180
- # Check if we need a page break for empty line
181
- if current_y + 10 > 280: # 280mm is roughly where we want to break
182
- pdf.add_page()
183
- current_y = 25
184
- else:
185
- pdf.ln(3) # Consistent space for empty lines
186
- current_y = pdf.get_y()
187
- continue
188
-
189
- # Calculate estimated height needed for this line
190
- estimated_height = 12 # Base height for text
191
-
192
- # Handle different heading levels
193
- if line.startswith("# "): # H1 - Main heading (like "Company Profile")
194
- estimated_height = 25 # Heading + line + spacing
195
- if current_y + estimated_height > 280:
196
- pdf.add_page()
197
- current_y = 25
198
-
199
- text = line[2:].strip()
200
- pdf.set_font("Arial", "B", 20) # Bold, size 20
201
- pdf.set_text_color(*primary_color) # Black
202
- pdf.set_xy(25, current_y)
203
- pdf.multi_cell(160, 10, text)
204
-
205
- # Calculate text width and make line autofit
206
- text_width = pdf.get_string_width(text)
207
- line_width = min(text_width + 12, 160) # Add 12mm padding, max 160mm
208
-
209
- # Add subtle line under main heading - consistent with section titles
210
- pdf.set_draw_color(221, 221, 221) # Lighter gray line (#DDDDDD)
211
- pdf.set_line_width(0.3) # Thin line
212
- pdf.line(25, current_y + 10, 25 + line_width, current_y + 10)
213
-
214
- current_y = pdf.get_y()
215
- pdf.ln(8) # Consistent space after main heading
216
- current_y = pdf.get_y()
217
-
218
- elif line.startswith("## ") or re.match(r'^\d+\.\d+\s+', line): # H2 - Subheading or numbered subheading
219
- estimated_height = 20 # Subheading + line + spacing
220
- if current_y + estimated_height > 280:
221
- pdf.add_page()
222
- current_y = 25
223
-
224
- text = line[3:].strip() if line.startswith("## ") else line.strip()
225
- pdf.set_font("Arial", "B", 16) # Bold, size 16
226
- pdf.set_text_color(*primary_color) # Black
227
- pdf.set_xy(25, current_y)
228
- pdf.multi_cell(160, 8, text)
229
-
230
- # Calculate text width and make line autofit
231
- text_width = pdf.get_string_width(text)
232
- line_width = min(text_width + 10, 160) # Add 10mm padding, max 160mm
233
-
234
- # Add subtle line under subheading - consistent styling
235
- pdf.set_draw_color(221, 221, 221) # Lighter gray line (#DDDDDD)
236
- pdf.set_line_width(0.3) # Thin line
237
- pdf.line(25, current_y + 8, 25 + line_width, current_y + 8)
238
-
239
- current_y = pdf.get_y()
240
- pdf.ln(6) # Consistent space after subheading
241
- current_y = pdf.get_y()
242
-
243
- elif re.match(r'^\d+\.\d+\s+[A-Z]', line): # Numbered subheadings like "3.6 SWOT Analysis"
244
- estimated_height = 20 # Subheading + line + spacing
245
- if current_y + estimated_height > 280:
246
- pdf.add_page()
247
- current_y = 25
248
-
249
- text = line.strip()
250
- pdf.set_font("Arial", "B", 16) # Bold, size 16
251
- pdf.set_text_color(*primary_color) # Black
252
- pdf.set_xy(25, current_y)
253
- pdf.multi_cell(160, 8, text)
254
-
255
-
256
- current_y = pdf.get_y()
257
- pdf.ln(6) # Consistent space after subheading
258
- current_y = pdf.get_y()
259
-
260
- elif line.startswith("### ") or line.startswith("#### "): # H3/H4 - Smaller subheadings
261
- estimated_height = 18 # Subheading + spacing
262
- if current_y + estimated_height > 280:
263
- pdf.add_page()
264
- current_y = 25
265
-
266
- text = line[4:].strip() if line.startswith("### ") else line[5:].strip()
267
- pdf.set_font("Arial", "B", 14) # Bold, size 14
268
- pdf.set_text_color(*primary_color) # Black
269
- pdf.set_xy(25, current_y)
270
- pdf.multi_cell(160, 8, text)
271
-
272
- current_y = pdf.get_y()
273
- pdf.ln(4) # Consistent space after subheading
274
- current_y = pdf.get_y()
275
-
276
- elif line.startswith("**") and line.endswith("**"): # Bold text that might be a heading
277
- text = line.strip() # Keep ** markers
278
- # Check if this looks like a main heading (no numbers, not too long)
279
- if not re.match(r'^\d+\.', text) and len(text) < 50:
280
- estimated_height = 22 # Heading + line + spacing
281
- if current_y + estimated_height > 280:
282
- pdf.add_page()
283
- current_y = 25
284
-
285
- pdf.set_font("Arial", "B", 18) # Bold, size 18
286
- pdf.set_text_color(*primary_color) # Black
287
- pdf.set_xy(25, current_y)
288
- pdf.multi_cell(160, 9, text)
289
-
290
- current_y = pdf.get_y()
291
- pdf.ln(6) # Consistent space after heading
292
- current_y = pdf.get_y()
293
- else:
294
- # Regular bold text
295
- estimated_height = 12
296
- if current_y + estimated_height > 280:
297
- pdf.add_page()
298
- current_y = 25
299
-
300
- pdf.set_font("Arial", "B", 12) # Bold, size 12
301
- pdf.set_text_color(*text_color) # Dark gray
302
- pdf.set_xy(25, current_y)
303
- pdf.multi_cell(160, 8, text)
304
- current_y = pdf.get_y()
305
-
306
- else: # Normal paragraph text
307
- # For long paragraphs, estimate height based on text length
308
- estimated_height = max(12, len(line) // 80 * 12) # Rough estimate
309
- if current_y + estimated_height > 280:
310
- pdf.add_page()
311
- current_y = 25
312
-
313
- pdf.set_font("Arial", "", 12) # Regular font, size 12
314
- pdf.set_text_color(*text_color) # Dark gray
315
- pdf.set_xy(25, current_y)
316
- pdf.multi_cell(160, 8, line, align='L') # Left align to prevent text justification
317
- current_y = pdf.get_y()
318
-
319
-
320
- def render_markdown_to_pdf_grouped(pdf, content: str, primary_color, accent_color, text_color):
321
- """Render markdown content to PDF with subheading-content grouping and smart page breaks"""
322
- if not content:
323
- return
324
-
325
- print(f"DEBUG: render_markdown_to_pdf_grouped called with content length: {len(content)}")
326
- print(f"DEBUG: FULL CONTENT:")
327
- print("=" * 80)
328
- print(content)
329
- print("=" * 80)
330
-
331
- # Parse content into groups
332
- groups = parse_content_into_groups(content)
333
-
334
- for group in groups:
335
- subheading = group['subheading']
336
- group_content = group['content']
337
- subheading_type = group['subheading_type']
338
-
339
- # Get current Y position from PDF
340
- current_y = pdf.get_y()
341
- page_height = 280 # Approximate usable page height
342
-
343
- # Use smart page break logic
344
- if should_break_page_for_group(pdf, group, current_y, page_height):
345
- pdf.add_page()
346
- current_y = 25
347
- pdf.set_y(current_y) # Set the PDF's Y position
348
-
349
- # Render subheading based on type - using original styling
350
- # Use the pre-cleaned text from the group
351
- text = group['subheading_clean']
352
-
353
- if subheading_type == 'h1':
354
- pdf.set_font("Arial", "B", 20) # Bold, size 20 (original size)
355
- pdf.set_text_color(*primary_color) # Black
356
- pdf.set_xy(25, current_y)
357
- pdf.multi_cell(160, 10, text)
358
-
359
- # Calculate text width and make line autofit
360
- text_width = pdf.get_string_width(text)
361
- line_width = min(text_width + 12, 160) # Add 12mm padding, max 160mm
362
-
363
- # Add subtle line under main heading - consistent with section titles
364
- pdf.set_draw_color(221, 221, 221) # Lighter gray line (#DDDDDD)
365
- pdf.set_line_width(0.3) # Thin line
366
- pdf.line(25, current_y + 10, 25 + line_width, current_y + 10)
367
-
368
- current_y = pdf.get_y()
369
- pdf.ln(8) # Consistent space after main heading
370
- current_y = pdf.get_y()
371
-
372
- elif subheading_type == 'h2':
373
- pdf.set_font("Arial", "B", 18) # Bold, size 18 (original size)
374
- pdf.set_text_color(*primary_color) # Black
375
- pdf.set_xy(25, current_y)
376
- pdf.multi_cell(160, 9, text)
377
-
378
- # Add underline (original styling)
379
- text_width = pdf.get_string_width(text)
380
- line_width = min(text_width + 12, 160)
381
- pdf.set_draw_color(221, 221, 221) # Light gray line
382
- pdf.set_line_width(0.3)
383
- pdf.line(25, current_y + 9, 25 + line_width, current_y + 9)
384
-
385
- current_y = pdf.get_y()
386
- pdf.ln(6) # Space after subheading
387
- current_y = pdf.get_y()
388
-
389
- elif subheading_type == 'h3':
390
- pdf.set_font("Arial", "B", 14) # Bold, size 14 (original size)
391
- pdf.set_text_color(*primary_color) # Black
392
- pdf.set_xy(25, current_y)
393
- pdf.multi_cell(160, 8, text)
394
-
395
- current_y = pdf.get_y()
396
- pdf.ln(4) # Space after subheading
397
- current_y = pdf.get_y()
398
-
399
- elif subheading_type == 'h4':
400
- pdf.set_font("Arial", "B", 12) # Bold, size 12 (original size)
401
- pdf.set_text_color(*primary_color) # Black
402
- pdf.set_xy(25, current_y)
403
- pdf.multi_cell(160, 7, text)
404
-
405
- current_y = pdf.get_y()
406
- pdf.ln(3) # Space after subheading
407
- current_y = pdf.get_y()
408
-
409
- elif subheading_type == 'bold':
410
- line_height_heading = 9 # For size 18 heading
411
- line_height_text = 8 # For size 12 text
412
-
413
- if not re.match(r'^\d+\.', text) and len(text) < 80:
414
- # Main heading style
415
- pdf.set_font("Arial", "B", 12) # Bold, size 12
416
- pdf.set_text_color(*primary_color) # Black
417
- pdf.set_xy(25, current_y)
418
- pdf.multi_cell(160, 8, text) # Multi-cell for text
419
- current_y = pdf.get_y() # Get updated Y position
420
- pdf.ln(6)
421
- current_y = pdf.get_y()
422
- else:
423
- # Regular bold text
424
- pdf.set_font("Arial", "B", 12) # Bold, size 12
425
- pdf.set_text_color(*text_color) # Dark gray
426
- pdf.set_xy(25, current_y)
427
- pdf.multi_cell(160, 8, text) # Multi-cell for text
428
- current_y = pdf.get_y() # Get updated Y position
429
-
430
- elif subheading_type == 'numbered':
431
- pdf.set_font("Arial", "B", 18) # Bold, size 18 (original size)
432
- pdf.set_text_color(*primary_color) # Black
433
- pdf.set_xy(25, current_y)
434
- pdf.multi_cell(160, 9, text)
435
-
436
- # Add underline (original styling)
437
- text_width = pdf.get_string_width(text)
438
- line_width = min(text_width + 12, 160)
439
- pdf.set_draw_color(221, 221, 221) # Light gray line
440
- pdf.set_line_width(0.3)
441
- pdf.line(25, current_y + 9, 25 + line_width, current_y + 9)
442
-
443
- current_y = pdf.get_y()
444
- pdf.ln(6) # Space after subheading
445
- current_y = pdf.get_y()
446
-
447
- # Render grouped content with simple styling
448
- if group_content:
449
- content_lines = group_content.split('\n')
450
- for line in content_lines:
451
- if line.strip():
452
- # Check if line contains text ending with ":" that should be bold
453
- if ':' in line:
454
- # Split the line by ":" to separate the label from the content
455
- parts = line.split(':', 1) # Split only on first ":"
456
- if len(parts) == 2:
457
- label = parts[0].strip()
458
- content = parts[1].strip()
459
-
460
- # Render label in bold
461
- pdf.set_font("Arial", "B", 12) # Bold
462
- pdf.set_text_color(*text_color)
463
- pdf.set_xy(25, current_y)
464
- pdf.multi_cell(160, 8, label + ":")
465
- current_y = pdf.get_y()
466
-
467
- # Render content in regular font
468
- if content:
469
- pdf.set_font("Arial", "", 12) # Regular
470
- pdf.set_text_color(*text_color)
471
- pdf.set_xy(25, current_y)
472
- pdf.multi_cell(160, 8, content, align='L') # Left align to prevent text justification
473
- current_y = pdf.get_y()
474
- else:
475
- # No content after ":", just render the whole line
476
- pdf.set_font("Arial", "B", 12) # Bold for labels ending with ":"
477
- pdf.set_text_color(*text_color)
478
- pdf.set_xy(25, current_y)
479
- pdf.multi_cell(160, 8, line, align='L') # Left align to prevent text justification
480
- current_y = pdf.get_y()
481
- elif ' - ' in line:
482
- # Split the line by " - " to separate the label from the content
483
- parts = line.split(' - ', 1) # Split only on first " - "
484
- if len(parts) == 2:
485
- label = parts[0].strip()
486
- content = parts[1].strip()
487
-
488
- # Render label in bold
489
- pdf.set_font("Arial", "B", 12) # Bold
490
- pdf.set_text_color(*text_color)
491
- pdf.set_xy(25, current_y)
492
- pdf.multi_cell(160, 8, label + " -")
493
- current_y = pdf.get_y()
494
-
495
- # Render content in regular font
496
- if content:
497
- pdf.set_font("Arial", "", 12) # Regular
498
- pdf.set_text_color(*text_color)
499
- pdf.set_xy(25, current_y)
500
- pdf.multi_cell(160, 8, content, align='L') # Left align to prevent text justification
501
- current_y = pdf.get_y()
502
- else:
503
- # No content after " - ", just render the whole line
504
- pdf.set_font("Arial", "B", 12) # Bold for labels ending with " -"
505
- pdf.set_text_color(*text_color)
506
- pdf.set_xy(25, current_y)
507
- pdf.multi_cell(160, 8, line, align='L') # Left align to prevent text justification
508
- current_y = pdf.get_y()
509
- else:
510
- # Regular paragraph rendering without mixed text processing
511
- pdf.set_font("Arial", "", 12)
512
- pdf.set_text_color(*text_color)
513
- pdf.set_xy(25, current_y)
514
- pdf.multi_cell(160, 8, line, align='L') # Left align to prevent text justification
515
- current_y = pdf.get_y() # Update current_y after each line
516
- else:
517
- # Empty line spacing
518
- pdf.ln(3)
519
- current_y = pdf.get_y() # Update current_y after spacing
520
-
521
-
522
- def render_markdown_to_docx(doc, content: str):
523
- """Render markdown content to Word document with proper heading styling"""
524
- if not content:
525
- return
526
-
527
- lines = content.split('\n')
528
-
529
- for line in lines:
530
- line = line.strip()
531
- if not line:
532
- doc.add_paragraph() # Empty paragraph for spacing
533
- continue
534
-
535
- # Handle different heading levels
536
- if line.startswith("# "): # H1 - Main heading (like "Company Profile")
537
- text = line[2:].strip()
538
- doc.add_heading(text, level=1)
539
- elif line.startswith("## ") or re.match(r'^\d+\.\d+\s+', line): # H2 - Subheading or numbered subheading
540
- text = line[3:].strip() if line.startswith("## ") else line.strip()
541
- doc.add_heading(text, level=2)
542
- elif re.match(r'^\d+\.\d+\s+[A-Z]', line): # Numbered subheadings like "3.6 SWOT Analysis"
543
- text = line.strip()
544
- doc.add_heading(text, level=2)
545
- elif line.startswith("### ") or line.startswith("#### "): # H3/H4 - Smaller subheadings
546
- text = line[4:].strip() if line.startswith("### ") else line[5:].strip()
547
- doc.add_heading(text, level=3)
548
- elif line.startswith("**") and line.endswith("**"): # Bold text that might be a heading
549
- text = line.strip() # Keep ** markers
550
- # Check if this looks like a main heading (no numbers, not too long)
551
- if not re.match(r'^\d+\.', text) and len(text) < 50:
552
- doc.add_heading(text, level=1) # Treat as main heading
553
- else:
554
- # Regular bold text
555
- doc.add_paragraph(text)
556
- else: # Normal paragraph text
557
- # Check if line contains text ending with ":" that should be bold
558
- if ':' in line:
559
- # Split the line by ":" to separate the label from the content
560
- parts = line.split(':', 1) # Split only on first ":"
561
- if len(parts) == 2:
562
- label = parts[0].strip()
563
- content = parts[1].strip()
564
-
565
- # Create paragraph with mixed formatting
566
- paragraph = doc.add_paragraph()
567
- # Add label in bold
568
- run = paragraph.add_run(label + ":")
569
- run.bold = True
570
- # Add content in regular font
571
- if content:
572
- run = paragraph.add_run(" " + content)
573
- run.bold = False
574
- else:
575
- # No content after ":", just render the whole line in bold
576
- paragraph = doc.add_paragraph(line)
577
- paragraph.runs[0].bold = True
578
- elif ' - ' in line:
579
- # Split the line by " - " to separate the label from the content
580
- parts = line.split(' - ', 1) # Split only on first " - "
581
- if len(parts) == 2:
582
- label = parts[0].strip()
583
- content = parts[1].strip()
584
-
585
- # Create paragraph with mixed formatting
586
- paragraph = doc.add_paragraph()
587
- # Add label in bold
588
- run = paragraph.add_run(label + " -")
589
- run.bold = True
590
- # Add content in regular font
591
- if content:
592
- run = paragraph.add_run(" " + content)
593
- run.bold = False
594
- else:
595
- # No content after " - ", just render the whole line in bold
596
- paragraph = doc.add_paragraph(line)
597
- paragraph.runs[0].bold = True
598
- else:
599
- doc.add_paragraph(line)
600
-
601
-
602
- def render_markdown_to_docx_grouped(doc, content: str):
603
- """Render markdown content to Word document with subheading-content grouping and smart page breaks"""
604
- if not content:
605
- return
606
-
607
- # Parse content into groups
608
- groups = parse_content_into_groups(content)
609
-
610
- for i, group in enumerate(groups):
611
- subheading = group['subheading']
612
- group_content = group['content']
613
- subheading_type = group['subheading_type']
614
-
615
- # Add page break before group if it's not the first group and the previous group was large
616
- if i > 0:
617
- # Check if we should add a page break to keep groups together
618
- # This is a simple heuristic - in Word, we rely more on the natural flow
619
- # but we can add manual page breaks for very large groups
620
- if group['content_length'] > 1000: # Large group threshold
621
- doc.add_page_break()
622
-
623
- # Render subheading based on type - using original styling
624
- # Use the pre-cleaned text from the group
625
- text = group['subheading_clean']
626
-
627
- if subheading_type == 'h1':
628
- doc.add_heading(text, level=1)
629
- elif subheading_type == 'h2':
630
- doc.add_heading(text, level=2)
631
- elif subheading_type == 'h3':
632
- doc.add_heading(text, level=3)
633
- elif subheading_type == 'h4':
634
- doc.add_heading(text, level=4)
635
- elif subheading_type == 'bold':
636
- if not re.match(r'^\d+\.', text) and len(text) < 80:
637
- # Simple paragraph with bold text
638
- paragraph = doc.add_paragraph(text)
639
- paragraph.style = doc.styles['Heading 1'] # Apply heading style
640
- else:
641
- # Regular paragraph
642
- doc.add_paragraph(text)
643
- elif subheading_type == 'numbered':
644
- doc.add_heading(text, level=2)
645
-
646
- # Render grouped content with simple styling
647
- if group_content:
648
- content_lines = group_content.split('\n')
649
- for line in content_lines:
650
- if line.strip():
651
- # Check if line contains text ending with ":" that should be bold
652
- if ':' in line:
653
- # Split the line by ":" to separate the label from the content
654
- parts = line.split(':', 1) # Split only on first ":"
655
- if len(parts) == 2:
656
- label = parts[0].strip()
657
- content = parts[1].strip()
658
-
659
- # Create paragraph with mixed formatting
660
- paragraph = doc.add_paragraph()
661
- # Add label in bold
662
- run = paragraph.add_run(label + ":")
663
- run.bold = True
664
- # Add content in regular font
665
- if content:
666
- run = paragraph.add_run(" " + content)
667
- run.bold = False
668
- else:
669
- # No content after ":", just render the whole line in bold
670
- paragraph = doc.add_paragraph(line)
671
- paragraph.runs[0].bold = True
672
- elif ' - ' in line:
673
- # Split the line by " - " to separate the label from the content
674
- parts = line.split(' - ', 1) # Split only on first " - "
675
- if len(parts) == 2:
676
- label = parts[0].strip()
677
- content = parts[1].strip()
678
-
679
- # Create paragraph with mixed formatting
680
- paragraph = doc.add_paragraph()
681
- # Add label in bold
682
- run = paragraph.add_run(label + " -")
683
- run.bold = True
684
- # Add content in regular font
685
- if content:
686
- run = paragraph.add_run(" " + content)
687
- run.bold = False
688
- else:
689
- # No content after " - ", just render the whole line in bold
690
- paragraph = doc.add_paragraph(line)
691
- paragraph.runs[0].bold = True
692
- else:
693
- # Simple paragraph addition without mixed text processing
694
- doc.add_paragraph(line)
695
- else:
696
- doc.add_paragraph() # Empty paragraph for spacing
697
-
698
-
699
-
700
- def clean_text_for_docx(text: str) -> str:
701
- # Replace problematic Unicode characters with ASCII equivalents
702
- unicode_replacements = {
703
- '\u2019': "'", # Right single quotation mark
704
- '\u2018': "'", # Left single quotation mark
705
- '\u201C': '"', # Left double quotation mark
706
- '\u201D': '"', # Right double quotation mark
707
- '\u2013': '-', # En dash
708
- '\u2014': '--', # Em dash
709
- '\u2022': '•', # Bullet
710
- '\u2026': '...', # Horizontal ellipsis
711
- '\u00A0': ' ', # Non-breaking space
712
- '\u00B0': '°', # Degree sign
713
- '\u00AE': '(R)', # Registered trademark
714
- '\u2122': '(TM)', # Trademark
715
- '\u00A9': '(C)', # Copyright
716
- }
717
-
718
- for unicode_char, replacement in unicode_replacements.items():
719
- text = text.replace(unicode_char, replacement)
720
-
721
- # Simple text cleaning - keep basic punctuation and common symbols
722
- text = ''.join(char for char in text if ord(char) < 128 or char in '•°$€£¥₹₩₽₪₺₴₼₸₾֏₲₡₣₦₵₨₱₫₭៛৳؋﷼%')
723
-
724
- return text
725
-
726
-
727
-
728
-
729
-
730
-
731
-
732
-
733
-
734
- def clean_unicode_for_pdf(text: str) -> str:
735
- # Replace problematic Unicode characters with ASCII equivalents
736
- unicode_replacements = {
737
- '\u2019': "'", # Right single quotation mark
738
- '\u2018': "'", # Left single quotation mark
739
- '\u201C': '"', # Left double quotation mark
740
- '\u201D': '"', # Right double quotation mark
741
- '\u2013': '-', # En dash
742
- '\u2014': '--', # Em dash
743
- '\u2022': '•', # Bullet
744
- '\u2026': '...', # Horizontal ellipsis
745
- '\u00A0': ' ', # Non-breaking space
746
- '\u00B0': '°', # Degree sign
747
- '\u00AE': '(R)', # Registered trademark
748
- '\u2122': '(TM)', # Trademark
749
- '\u00A9': '(C)', # Copyright
750
- }
751
-
752
- for unicode_char, replacement in unicode_replacements.items():
753
- text = text.replace(unicode_char, replacement)
754
-
755
- # Simple text cleaning - keep basic punctuation and common symbols
756
- text = ''.join(char for char in text if ord(char) < 128 or char in '•°$€£¥₹₩₽₪₺₴₼₸₾֏₲₡₣₦₵₨₱₫₭៛৳؋﷼%')
757
-
758
- return text
759
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/DocumentGeneration/mixed.py DELETED
@@ -1,6 +0,0 @@
1
- # This file has been removed as all its functions are not present in maintest.txt
2
- # Functions removed:
3
- # - make_sentences_ending_with_colon_bold()
4
- # - _render_mixed_text_to_pdf()
5
- # - clean_bold_text_for_pdf()
6
- # - process_bold_text_for_docx()
 
 
 
 
 
 
 
app/DocumentGeneration/pdf_generator.py DELETED
@@ -1,682 +0,0 @@
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
285
- project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
286
- logo_path = os.path.join(project_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/table_of_content.py DELETED
@@ -1,205 +0,0 @@
1
- # Table of Contents Module
2
- # Contains functions to generate TOC AFTER content is placed with real page numbers
3
-
4
- import re
5
- import os
6
- import sys
7
- from typing import List, Dict, Any
8
-
9
- # Add the parent directory to sys.path to import from models.py and constants.py
10
- sys.path.append(os.path.dirname(os.path.dirname(__file__)))
11
- from models import ExportSection
12
- from constants import SECTION_ORDER
13
-
14
- def extract_first_heading_from_content(content: str) -> str:
15
- """Extract the first heading from markdown content"""
16
- if not content:
17
- return ""
18
-
19
- lines = content.split('\n')
20
- for line in lines:
21
- line = line.strip()
22
- if not line:
23
- continue
24
-
25
- # Check for markdown headings (# ## ### etc.)
26
- if line.startswith('#'):
27
- # Extract heading text without the # symbols
28
- heading_text = re.sub(r'^#+\s*', '', line).strip()
29
- if heading_text:
30
- return heading_text
31
-
32
- # Check for bold headings (**text**)
33
- elif line.startswith('**') and line.endswith('**') and len(line) > 4:
34
- # Extract text from bold markers
35
- heading_text = line[2:-2].strip()
36
- if heading_text:
37
- return heading_text
38
-
39
- return ""
40
-
41
- def generate_table_of_contents_after_content(sections: List[ExportSection], section_page_map: Dict[str, int], business_idea: str) -> List[Dict[str, Any]]:
42
- """Generate TOC AFTER all content is placed, using REAL page numbers from section_page_map
43
- This ensures accurate page numbers based on actual content placement"""
44
- toc_items = []
45
-
46
- # Create a mapping from section titles to section objects for easy lookup
47
- section_map = {section.title: section for section in sections}
48
-
49
- # Add business plan sections in the predefined SECTION_ORDER
50
- for i, expected_title in enumerate(SECTION_ORDER):
51
- # Find the corresponding section - try multiple matching strategies
52
- section = None
53
-
54
- # Strategy 1: Exact title match
55
- section = section_map.get(expected_title)
56
-
57
- # Strategy 2: Case-insensitive match
58
- if not section:
59
- for section_title, section_obj in section_map.items():
60
- if section_title.lower() == expected_title.lower():
61
- section = section_obj
62
- break
63
-
64
- # Strategy 3: Partial match (e.g., "Company Profile" matches "Company Profile Section")
65
- if not section:
66
- for section_title, section_obj in section_map.items():
67
- if (expected_title.lower() in section_title.lower() or
68
- section_title.lower() in expected_title.lower()):
69
- section = section_obj
70
- break
71
-
72
- # Strategy 4: Key-based match (remove 'prompt_' prefix and match)
73
- if not section:
74
- for section_obj in sections:
75
- if section_obj.key:
76
- key_clean = section_obj.key.replace('prompt_', '') if section_obj.key.startswith('prompt_') else section_obj.key
77
- if key_clean.lower() == expected_title.lower().replace(' ', '').replace('&', ''):
78
- section = section_obj
79
- break
80
-
81
- # Only add sections that have actual content AND real page numbers - NO DUMMY DATA
82
- has_content = bool(section and section.content and section.content.strip())
83
- if has_content:
84
- # Get the REAL page number from section_page_map
85
- real_page = section_page_map.get(section.title, 0)
86
- if real_page > 0: # Only add if we have a real page number
87
- # Extract the actual heading from markdown content
88
- actual_heading = extract_first_heading_from_content(section.content)
89
-
90
- # Use the actual heading from markdown content, fallback to expected title
91
- toc_title = actual_heading if actual_heading else expected_title
92
-
93
- toc_items.append({
94
- "title": toc_title, # Use actual heading from markdown content
95
- "level": 1,
96
- "page": real_page, # REAL page number from actual content placement
97
- "section_type": "section",
98
- "has_content": True,
99
- "section_obj": section # Store reference to actual section
100
- })
101
-
102
- return toc_items
103
-
104
- def add_table_of_contents_page(pdf, toc_items: List[Dict[str, Any]], business_idea: str):
105
- """Add a professional table of contents page with REAL page numbers"""
106
-
107
- # TOC items are already in the correct order and have real page numbers
108
- sorted_toc_items = toc_items
109
-
110
- # Professional color scheme - Refined for bank/investor presentation
111
- primary_color = (0, 0, 0) # Black - for headings
112
- accent_color = (75, 0, 130) # Navy purple - for accent lines and highlights
113
- text_color = (51, 51, 51) # Dark gray - for body text
114
- light_gray = (221, 221, 221) # Lighter gray - for subtle lines (#DDDDDD)
115
- navy_color = (25, 25, 112) # Navy blue - for TOC and emphasis
116
-
117
- # Page title - more compact
118
- pdf.set_font("Arial", "B", 20) # Reduced from 24 to 20
119
- pdf.set_text_color(*primary_color)
120
- pdf.set_xy(25, 20) # Reduced from 25 to 20
121
- pdf.cell(0, 15, "Table of Contents", ln=True) # Reduced from 18 to 15
122
-
123
- # Calculate text width and make line autofit
124
- text_width = pdf.get_string_width("Table of Contents")
125
- line_width = min(text_width + 12, 160) # Add 12mm padding, max 160mm
126
-
127
- # Removed purple accent line under TOC title
128
-
129
- # TOC content - optimized for single page layout
130
- y_position = 50 # Reduced from 60 to 50
131
- pages_used = 1
132
-
133
- for item in sorted_toc_items:
134
- # Only add new page if absolutely necessary (very close to bottom)
135
- if y_position > 290: # Very close to bottom margin
136
- pdf.add_page()
137
- y_position = 25
138
- pages_used += 1
139
-
140
- # Indent based on level - better indentation
141
- indent = 0 if item["level"] == 1 else 20 # Increased indent for subsections
142
-
143
- # Font and color based on level - ALL sections same color
144
- if item["level"] == 1:
145
- pdf.set_font("Arial", "B", 12)
146
- pdf.set_text_color(*navy_color) # Navy blue for all main entries
147
- else:
148
- pdf.set_font("Arial", "", 10)
149
- pdf.set_text_color(*text_color)
150
-
151
- # Title with hyperlink
152
- title_width = pdf.get_string_width(item["title"])
153
- pdf.set_xy(25 + indent, y_position)
154
- try:
155
- # FPDF2 hyperlinks: test both 0-based and 1-based indexing
156
- # First try 1-based (no subtraction)
157
- target_page = item["page"]
158
- # Try different FPDF2 link methods
159
- if hasattr(pdf, 'add_link'):
160
- link_id = pdf.add_link()
161
- pdf.set_link(link_id, page=target_page)
162
- pdf.link(25 + indent, y_position, title_width, 8, link_id)
163
- else:
164
- pdf.link(25 + indent, y_position, title_width, 8, dest=target_page)
165
- except:
166
- pass # If hyperlink fails, continue without it
167
- pdf.cell(title_width, 8, item["title"], ln=False)
168
-
169
- # Dots leading to page number
170
- pdf.set_font("Arial", "", 8)
171
- pdf.set_text_color(128, 128, 128) # Medium gray for dots
172
-
173
- # Calculate dots position
174
- title_width = pdf.get_string_width(item["title"])
175
- dots_start = 25 + indent + title_width + 5
176
- dots_end = 160
177
-
178
- # Draw dots
179
- for x in range(int(dots_start), int(dots_end), 3):
180
- pdf.set_xy(x, y_position + 3)
181
- pdf.cell(1, 1, ".", ln=False)
182
-
183
- # Page number with hyperlink - NOW USING REAL PAGE NUMBERS
184
- page_number_width = pdf.get_string_width(str(item["page"]))
185
- pdf.set_xy(160, y_position)
186
- try:
187
- # FPDF2 hyperlinks: test both 0-based and 1-based indexing
188
- # First try 1-based (no subtraction)
189
- target_page = item["page"]
190
- # Try different FPDF2 link methods
191
- if hasattr(pdf, 'add_link'):
192
- link_id = pdf.add_link()
193
- pdf.set_link(link_id, page=target_page)
194
- pdf.link(160, y_position, page_number_width, 8, link_id)
195
- else:
196
- pdf.link(160, y_position, page_number_width, 8, dest=target_page)
197
- except:
198
- pass # If hyperlink fails, continue without it
199
- pdf.cell(0, 8, str(item["page"]), ln=False)
200
-
201
- # Reduced spacing between items for better page utilization
202
- y_position += 8 # Reduced from 10 to 8 for even tighter spacing
203
-
204
- # Add some spacing at the end
205
- pdf.ln(20)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/DocumentGeneration/word_generator.py DELETED
@@ -1,279 +0,0 @@
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
66
- project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
67
- logo_path = os.path.join(project_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/__init__.py DELETED
@@ -1,29 +0,0 @@
1
- # App Package
2
- # This file makes the app directory a proper Python package
3
-
4
- import os
5
- import sys
6
-
7
- # Add the current directory to sys.path
8
- current_dir = os.path.dirname(__file__)
9
- if current_dir not in sys.path:
10
- sys.path.insert(0, current_dir)
11
-
12
- # Import main modules for easy access
13
- from models import ExportRequest, ExportSection, ExportResponse, SectionJob, BatchGenerateRequest, GenerateRequest, CreatePlanRequest
14
- from constants import SECTION_ORDER, SECTION_MAP, SECTION_COLUMNS
15
- from utils import sort_sections_by_order
16
-
17
- __all__ = [
18
- 'ExportRequest',
19
- 'ExportSection',
20
- 'ExportResponse',
21
- 'SectionJob',
22
- 'BatchGenerateRequest',
23
- 'GenerateRequest',
24
- 'CreatePlanRequest',
25
- 'SECTION_ORDER',
26
- 'SECTION_MAP',
27
- 'SECTION_COLUMNS',
28
- 'sort_sections_by_order'
29
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/constants.py DELETED
@@ -1,41 +0,0 @@
1
- # Constants and Configuration
2
- # Contains constants and configuration values to avoid circular imports
3
-
4
- # Human-readable section titles for TOC and display
5
- SECTION_ORDER = [
6
- "Executive Summary",
7
- "Company Profile",
8
- "Market Analysis",
9
- "Product or Service",
10
- "Business Model",
11
- "Marketing & Growth",
12
- "Operations Plan",
13
- "Management Team",
14
- "Financial Plan & Funding"
15
- ]
16
-
17
- # Database helper constants
18
- SECTION_MAP = {
19
- "prompt_ExecutiveSummary": ("executiveSummary", "executiveSummaryComplete", "executiveSummaryGenerating"),
20
- "prompt_CompanyProfile": ("companyProfile", "companyProfileComplete", "companyProfileGenerating"),
21
- "prompt_MarketAnalysis": ("marketAnalysis", "marketAnalysisComplete", "marketAnalysisGenerating"),
22
- "prompt_ProductOrService": ("productOrService", "productOrServiceComplete", "productOrServiceGenerating"),
23
- "prompt_BusinessModel": ("businessModel", "businessModelComplete", "businessModelGenerating"),
24
- "prompt_MarketingGrowth": ("marketingGrowth", "marketingGrowthComplete", "marketingGrowthGenerating"),
25
- "prompt_OperationsPlan": ("operationsPlan", "operationsPlanComplete", "operationsPlanGenerating"),
26
- "prompt_ManagementTeam": ("managementTeam", "managementTeamComplete", "managementTeamGenerating"),
27
- "prompt_FinancialPlanFunding": ("financialPlanFunding", "financialPlanFundingComplete", "financialPlanFundingGenerating"),
28
- }
29
-
30
- # Database column names for sections
31
- SECTION_COLUMNS = [
32
- "executiveSummary",
33
- "companyProfile",
34
- "marketAnalysis",
35
- "productOrService",
36
- "businessModel",
37
- "marketingGrowth",
38
- "operationsPlan",
39
- "managementTeam",
40
- "financialPlanFunding",
41
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/main.py CHANGED
The diff for this file is too large to render. See raw diff
 
app/models.py DELETED
@@ -1,61 +0,0 @@
1
- # Models and Data Structures
2
- # Contains Pydantic models and data structures to avoid circular imports
3
-
4
- from pydantic import BaseModel
5
- from typing import List, Dict, Any, Optional, Literal
6
-
7
- class ExportSection(BaseModel):
8
- """Represents a section of content for export"""
9
- title: str
10
- content: str
11
- key: Optional[str] = None
12
- order: Optional[int] = None
13
-
14
- class ExportRequest(BaseModel):
15
- """Request model for document export"""
16
- sections: List[ExportSection]
17
- businessIdea: str
18
- summary: str
19
- format: Literal["pdf", "word"] = "pdf"
20
- chartImages: Optional[List[str]] = None
21
- includeCharts: bool = True
22
- includeTOC: bool = True
23
-
24
- class ExportResponse(BaseModel):
25
- """Response model for document export"""
26
- success: bool
27
- message: str
28
- filename: Optional[str] = None
29
- size: Optional[int] = None
30
- downloadUrl: Optional[str] = None
31
- documentData: Optional[str] = None
32
-
33
- class SectionJob(BaseModel):
34
- """Job model for section generation"""
35
- businessPlanId: str
36
- sectionKey: str
37
- businessIdea: str
38
- countryCode: Optional[str] = None
39
- defaultCountry: Optional[str] = None
40
-
41
- class BatchGenerateRequest(BaseModel):
42
- """Request model for batch generation"""
43
- businessPlanId: str
44
- businessIdea: str
45
- sections: List[str]
46
- countryCode: Optional[str] = None
47
- defaultCountry: Optional[str] = None
48
-
49
- class GenerateRequest(BaseModel):
50
- """Request model for single section generation"""
51
- businessPlanId: str
52
- sectionKey: str
53
- businessIdea: str
54
- countryCode: Optional[str] = None
55
- defaultCountry: Optional[str] = None
56
-
57
- class CreatePlanRequest(BaseModel):
58
- """Request model for creating a new business plan"""
59
- businessIdea: str
60
- countryCode: Optional[str] = None
61
- defaultCountry: Optional[str] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/utils.py DELETED
@@ -1,42 +0,0 @@
1
- # Utility functions shared across modules
2
- # This file helps break circular imports between main.py and pdf_generator.py
3
-
4
- from typing import List
5
- from models import ExportSection
6
- from constants import SECTION_ORDER
7
-
8
- def sort_sections_by_order(sections: List[ExportSection]) -> List[ExportSection]:
9
- """Sort sections according to the predefined SECTION_ORDER to ensure correct numerical ordering"""
10
- # Create a mapping from section title to order index
11
- order_map = {title: index for index, title in enumerate(SECTION_ORDER)}
12
-
13
- def get_section_order(section: ExportSection) -> int:
14
- # Try to match by exact title first
15
- if section.title in order_map:
16
- order_index = order_map[section.title]
17
- return order_index
18
-
19
- # Try case-insensitive matching
20
- section_title_lower = section.title.lower()
21
- for order_title, order_index in order_map.items():
22
- if order_title.lower() == section_title_lower:
23
- return order_index
24
-
25
- # Try to match by key (remove 'prompt_' prefix if present)
26
- key_clean = section.key.replace('prompt_', '') if section.key.startswith('prompt_') else section.key
27
- if key_clean in order_map:
28
- order_index = order_map[key_clean]
29
- return order_index
30
-
31
- # Try partial matching for keys
32
- for order_title, order_index in order_map.items():
33
- if key_clean.lower() in order_title.lower() or order_title.lower() in key_clean.lower():
34
- return order_index
35
-
36
- # If no match found, put at the end
37
- return len(SECTION_ORDER)
38
-
39
- # Sort sections by their order
40
- sorted_sections = sorted(sections, key=get_section_order)
41
-
42
- return sorted_sections
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/logos/MBD logo.png DELETED
Binary file (23.2 kB)
 
package-lock.json DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "name": "planify",
3
- "lockfileVersion": 3,
4
- "requires": true,
5
- "packages": {}
6
- }