@@ -3575,6 +3575,85 @@ def generate_financial_plan_pdf(q: dict, analysis: dict, output_path: str, doc_i
35753575 ]
35763576 for item in recalc_items :
35773577 story .append (Paragraph (f"• { item } " , styles ["BodyText" ]))
3578+ story .append (Spacer (1 , 12 ))
3579+
3580+ # =========================================================================
3581+ # ADVANCED RISK ASSESSMENT SECTION
3582+ # =========================================================================
3583+ story .append (Paragraph ("Advanced Risk Assessment" , styles ["h2" ]))
3584+
3585+ # Extract risk assessment values
3586+ risk_score = advanced_risk .get ("score" , 0 )
3587+ risk_appetite = advanced_risk .get ("riskAppetite" , "Moderate" )
3588+ tenure_limit = advanced_risk .get ("tenureLimit" , "Moderate" )
3589+ baseline_category = advanced_risk .get ("baselineCategory" , "Moderate" )
3590+ final_category = advanced_risk .get ("finalCategory" , "Moderate" )
3591+ rec_equity_min = rec_band .get ("min" , 40 )
3592+ rec_equity_max = rec_band .get ("max" , 60 )
3593+ rec_equity_mid = (rec_equity_min + rec_equity_max ) / 2
3594+
3595+ risk_assessment_rows = [
3596+ ["Calculated Score" , f"{ risk_score :.1f} " if isinstance (risk_score , (int , float )) else str (risk_score )],
3597+ ["Risk Appetite" , risk_appetite ],
3598+ ["Tenure Limit" , tenure_limit ],
3599+ ["Baseline Category" , baseline_category ],
3600+ ["Final Category" , final_category ],
3601+ ["Recommended Equity Band" , f"{ rec_equity_min } %-{ rec_equity_max } % (mid { rec_equity_mid :.1f} %)" ],
3602+ ]
3603+
3604+ risk_table = Table (
3605+ [["Parameter" , "Value" ]] + risk_assessment_rows ,
3606+ hAlign = "LEFT" ,
3607+ colWidths = [180 , 280 ]
3608+ )
3609+ risk_table .setStyle (TableStyle ([
3610+ ('BACKGROUND' , (0 ,0 ), (- 1 ,0 ), colors .HexColor ('#9D4B45' )),
3611+ ('TEXTCOLOR' , (0 ,0 ), (- 1 ,0 ), colors .white ),
3612+ ('GRID' , (0 ,0 ), (- 1 ,- 1 ), 0.5 , colors .grey ),
3613+ ('FONTNAME' , (0 ,0 ), (- 1 ,0 ), 'Helvetica-Bold' ),
3614+ ('BOTTOMPADDING' , (0 ,0 ), (- 1 ,0 ), 6 ),
3615+ ]))
3616+ story .append (risk_table )
3617+ story .append (Spacer (1 , 6 ))
3618+
3619+ # Reasoning line
3620+ reasoning = f"Reasoning: Score { risk_score :.2f} → Appetite { risk_appetite } | Tenure limit { tenure_limit } | Baseline after adjustments { baseline_category } | Final { final_category } "
3621+ story .append (Paragraph (f"<i>{ reasoning } </i>" , styles ["BodyText" ]))
3622+ story .append (Spacer (1 , 12 ))
3623+
3624+ # Assessment Summary
3625+ story .append (Paragraph ("Assessment Summary" , styles ["h2" ]))
3626+
3627+ # Determine status labels
3628+ surplus_status = "Adequate" if monthly_surplus > 0 else ("Tight" if monthly_surplus >= - 5000 else "Deficit" )
3629+ insurance_status = "Adequate" if (life_cover >= required_term_cover * 0.8 and health_cover >= required_health_cover * 0.8 ) else "Underinsured"
3630+ debt_status = "Low" if monthly_emi <= monthly_income * 0.2 else ("Moderate" if monthly_emi <= monthly_income * 0.4 else "High" )
3631+ liquidity_status = "Sufficient" if comp ["liquidity" ]["score" ] >= 60 else "Insufficient"
3632+ ihs_band = health_score .get ("overall_label" , "Unknown" )
3633+
3634+ summary_assessment_rows = [
3635+ ["Risk Profile" , final_category ],
3636+ ["Surplus Level" , surplus_status ],
3637+ ["Insurance Status" , insurance_status ],
3638+ ["Debt Position" , debt_status ],
3639+ ["Liquidity" , liquidity_status ],
3640+ ["IHS Band" , ihs_band ],
3641+ ]
3642+
3643+ summary_table2 = Table (
3644+ [["Assessment" , "Result" ]] + summary_assessment_rows ,
3645+ hAlign = "LEFT" ,
3646+ colWidths = [180 , 280 ]
3647+ )
3648+ summary_table2 .setStyle (TableStyle ([
3649+ ('BACKGROUND' , (0 ,0 ), (- 1 ,0 ), colors .HexColor ('#457B9D' )),
3650+ ('TEXTCOLOR' , (0 ,0 ), (- 1 ,0 ), colors .white ),
3651+ ('GRID' , (0 ,0 ), (- 1 ,- 1 ), 0.5 , colors .grey ),
3652+ ('FONTNAME' , (0 ,0 ), (- 1 ,0 ), 'Helvetica-Bold' ),
3653+ ('BOTTOMPADDING' , (0 ,0 ), (- 1 ,0 ), 6 ),
3654+ ]))
3655+ story .append (summary_table2 )
3656+
35783657 story .append (PageBreak ())
35793658
35803659 # ==========================================================================
@@ -3800,7 +3879,7 @@ def generate_financial_plan_pdf(q: dict, analysis: dict, output_path: str, doc_i
38003879 # Determine funding status
38013880 available_for_goals = monthly_surplus - total_monthly_sip # What's left after existing SIPs
38023881 if available_for_goals < 0 :
3803- available_for_goals = monthly_surplus # If already over-commited, use full surplus
3882+ available_for_goals = max ( 0 , monthly_surplus ) # If over-committed or negative, floor at 0
38043883
38053884 funding_pct = (available_for_goals / total_required_sip * 100 ) if total_required_sip > 0 else 100
38063885 shortfall = max (0 , total_required_sip - available_for_goals )
@@ -3814,7 +3893,10 @@ def generate_financial_plan_pdf(q: dict, analysis: dict, output_path: str, doc_i
38143893
38153894 # Calculate per-goal funding status
38163895 if total_required_sip > 0 and required_sip > 0 :
3896+ # Proportionally allocate available funds to this goal
38173897 allocated_sip = (required_sip / total_required_sip ) * available_for_goals
3898+ allocated_sip = max (0 , allocated_sip ) # Never negative
3899+
38183900 if allocated_sip >= required_sip * 0.95 :
38193901 status = "Fully Funded"
38203902 status_color = colors .HexColor ('#2D6A4F' ) # Green
@@ -3824,7 +3906,9 @@ def generate_financial_plan_pdf(q: dict, analysis: dict, output_path: str, doc_i
38243906 else :
38253907 status = "Gap Exists"
38263908 status_color = colors .HexColor ('#9D4B45' ) # Red
3827- gap = max (0 , required_sip - allocated_sip )
3909+
3910+ # Gap is difference, capped at required_sip (can never be more than 100% shortfall)
3911+ gap = min (required_sip , max (0 , required_sip - allocated_sip ))
38283912 else :
38293913 status = "Not Calculable"
38303914 status_color = colors .HexColor ('#457B9D' )
@@ -3902,15 +3986,107 @@ def generate_financial_plan_pdf(q: dict, analysis: dict, output_path: str, doc_i
39023986 ]))
39033987 story .append (summary_table )
39043988
3905- # Bridge Options if shortfall exists
3906- if shortfall > 0 :
3907- story .append (Spacer (1 , 12 ))
3908- story .append (Paragraph ("<b>Options to Bridge the Gap:</b>" , styles ["BodyText" ]))
3909- story .append (Paragraph (f"1. <b>Reduce expenses</b>: Cut Rs. { shortfall :,.0f} /month from non-essential spending" , styles ["BodyText" ]))
3910- story .append (Paragraph (f"2. <b>Increase income</b>: Add Rs. { shortfall * 1.3 :,.0f} /month gross (accounting for taxes)" , styles ["BodyText" ]))
3911- story .append (Paragraph ("3. <b>Extend timelines</b>: Push goal deadlines by 3-5 years to reduce monthly requirement" , styles ["BodyText" ]))
3912- story .append (Paragraph ("4. <b>Reduce targets</b>: Adjust goal amounts to match available capacity" , styles ["BodyText" ]))
3913- story .append (Paragraph (f"5. <b>Start partial SIPs</b>: Begin with Rs. { available_for_goals :,.0f} /month and increase annually" , styles ["BodyText" ]))
3989+ # =========================================================================
3990+ # REALISTIC ACTION PLAN - What you CAN do with what you HAVE
3991+ # =========================================================================
3992+ story .append (Spacer (1 , 12 ))
3993+ story .append (Paragraph ("<b>Your Realistic Action Plan</b>" , styles ["h2" ]))
3994+
3995+ # Calculate insurance premium estimates
3996+ term_gap = max (0 , required_term_cover - life_cover )
3997+ health_gap = max (0 , required_health_cover - health_cover )
3998+
3999+ # Estimate monthly premiums (rough estimates)
4000+ term_premium_yearly = (term_gap / 100000 ) * 700 if term_gap > 0 else 0 # ~Rs.700/lakh/year
4001+ health_premium_yearly = (health_gap / 100000 ) * 3000 if health_gap > 0 else 0 # ~Rs.3000/lakh/year
4002+ total_insurance_yearly = term_premium_yearly + health_premium_yearly
4003+ monthly_insurance_equivalent = total_insurance_yearly / 12
4004+
4005+ # Available for goals after insurance provision
4006+ surplus_after_insurance = max (0 , monthly_surplus - monthly_insurance_equivalent )
4007+
4008+ # Phase 1: Insurance (if needed)
4009+ if term_gap > 0 or health_gap > 0 :
4010+ story .append (Paragraph ("<b>Phase 1: Protection First (Months 1-6)</b>" , styles ["BodyText" ]))
4011+ story .append (Paragraph ("Before investing in goals, secure your family's protection:" , styles ["BodyText" ]))
4012+
4013+ if term_gap > 0 :
4014+ story .append (Paragraph (f"• Get Term Insurance: Rs. { _format_indian_amount (term_gap )} cover → ~Rs. { term_premium_yearly :,.0f} /year premium" , styles ["BodyText" ]))
4015+ if health_gap > 0 :
4016+ story .append (Paragraph (f"• Get Health Insurance: Rs. { _format_indian_amount (health_gap )} cover → ~Rs. { health_premium_yearly :,.0f} /year premium" , styles ["BodyText" ]))
4017+
4018+ story .append (Paragraph (f"<i>Total insurance cost: ~Rs. { total_insurance_yearly :,.0f} /year (Rs. { monthly_insurance_equivalent :,.0f} /month equivalent)</i>" , styles ["BodyText" ]))
4019+ story .append (Spacer (1 , 8 ))
4020+
4021+ # Phase 2: Goal SIPs with realistic allocation
4022+ story .append (Paragraph ("<b>Phase 2: Goal-Based SIPs (After Insurance)</b>" , styles ["BodyText" ]))
4023+
4024+ if len (goal_analysis ) > 0 and surplus_after_insurance > 0 :
4025+ story .append (Paragraph (f"Available for goals after insurance: <b>Rs. { surplus_after_insurance :,.0f} /month</b>" , styles ["BodyText" ]))
4026+ story .append (Paragraph (f"Split across { len (goal_analysis )} goals proportionally:" , styles ["BodyText" ]))
4027+ story .append (Spacer (1 , 8 ))
4028+
4029+ # Build allocation table with projected outcomes
4030+ allocation_rows = [["Goal" , "Allocated SIP" , "Required SIP" , "What You'll Achieve" , "Gap" ]]
4031+
4032+ for ga in goal_analysis :
4033+ g_name = ga ["name" ][:20 ] # Truncate long names
4034+ g_target = ga ["target" ]
4035+ g_horizon = ga ["horizon" ]
4036+ required_sip = ga ["required_sip" ]
4037+
4038+ # Proportional allocation
4039+ if total_required_sip > 0 and required_sip > 0 :
4040+ allocated = (required_sip / total_required_sip ) * surplus_after_insurance
4041+ else :
4042+ allocated = surplus_after_insurance / max (1 , len (goal_analysis ))
4043+
4044+ allocated = max (0 , allocated )
4045+
4046+ # Calculate what this SIP will actually achieve
4047+ if g_horizon and allocated > 0 :
4048+ # Future Value formula: FV = P * [((1+r)^n - 1) / r]
4049+ # Using ~9% annual return (0.75% monthly)
4050+ r = 0.0075
4051+ n = int (g_horizon ) * 12
4052+ if n > 0 :
4053+ projected_value = allocated * (((1 + r ) ** n - 1 ) / r )
4054+ else :
4055+ projected_value = 0
4056+ else :
4057+ projected_value = 0
4058+
4059+ # Calculate achievement percentage
4060+ achievement_pct = (projected_value / g_target * 100 ) if g_target > 0 else 0
4061+ gap_amount = max (0 , g_target - projected_value )
4062+
4063+ allocation_rows .append ([
4064+ sanitize_pdf_text (g_name ),
4065+ f"Rs. { allocated :,.0f} " ,
4066+ f"Rs. { required_sip :,.0f} " ,
4067+ f"Rs. { _format_indian_amount (projected_value )} ({ achievement_pct :.0f} %)" ,
4068+ f"Rs. { _format_indian_amount (gap_amount )} "
4069+ ])
4070+
4071+ allocation_table = Table (allocation_rows , hAlign = "LEFT" , colWidths = [100 , 80 , 80 , 120 , 80 ])
4072+ allocation_table .setStyle (TableStyle ([
4073+ ('BACKGROUND' , (0 ,0 ), (- 1 ,0 ), colors .HexColor ('#1D3557' )),
4074+ ('TEXTCOLOR' , (0 ,0 ), (- 1 ,0 ), colors .white ),
4075+ ('GRID' , (0 ,0 ), (- 1 ,- 1 ), 0.5 , colors .grey ),
4076+ ('FONTNAME' , (0 ,0 ), (- 1 ,0 ), 'Helvetica-Bold' ),
4077+ ('FONTSIZE' , (0 ,0 ), (- 1 ,- 1 ), 8 ),
4078+ ('BOTTOMPADDING' , (0 ,0 ), (- 1 ,0 ), 6 ),
4079+ ]))
4080+ story .append (allocation_table )
4081+ story .append (Spacer (1 , 8 ))
4082+
4083+ story .append (Paragraph ("<b>Key Insight:</b> Starting with available surplus is better than waiting. Increase SIPs annually as income grows." , styles ["BodyText" ]))
4084+
4085+ elif surplus_after_insurance <= 0 :
4086+ story .append (Paragraph ("<i>After insurance provision, no surplus remains for goal SIPs. Focus on increasing income or reducing expenses first.</i>" , styles ["BodyText" ]))
4087+
4088+ else :
4089+ story .append (Paragraph ("<i>No goals defined. Add financial goals to see allocation recommendations.</i>" , styles ["BodyText" ]))
39144090
39154091 story .append (PageBreak ())
39164092
@@ -4052,12 +4228,13 @@ def generate_financial_plan_pdf(q: dict, analysis: dict, output_path: str, doc_i
40524228 else :
40534229 marginal_rate = 0.0
40544230
4055- # Calculate gaps and recommendations (only show if Old regime is beneficial)
4231+ # Calculate gaps and recommendations
40564232 recommendations = []
40574233 total_potential_saving = 0
40584234
4059- # Only show deduction recommendations if Old Regime is better or income is moderate
4060- show_deduction_advice = regime_optimal ['better_regime' ] == 'old' or gross_income > 700000
4235+ # Only show deduction recommendations if Old Regime is ACTUALLY recommended
4236+ # If New Regime is recommended, these deductions are IRRELEVANT
4237+ show_deduction_advice = regime_current ['better_regime' ] == 'old'
40614238
40624239 if show_deduction_advice :
40634240 # 80C Gap
@@ -4110,8 +4287,8 @@ def generate_financial_plan_pdf(q: dict, analysis: dict, output_path: str, doc_i
41104287
41114288 # Show personalized recommendations
41124289 if recommendations :
4113- story .append (Paragraph ("Your Tax Saving Opportunities" , styles ["h2" ]))
4114- story .append (Paragraph (f"<b>Based on your ITR , you can save up to Rs. { _format_indian_amount (total_potential_saving )} in tax next year! </b>" , styles ["BodyText" ]))
4290+ story .append (Paragraph ("Your Tax Saving Opportunities (Old Regime) " , styles ["h2" ]))
4291+ story .append (Paragraph (f"<b>If you stay in Old Regime , you can save up to Rs. { _format_indian_amount (total_potential_saving )} by maximizing deductions. </b>" , styles ["BodyText" ]))
41154292 story .append (Spacer (1 , 8 ))
41164293
41174294 rec_rows = []
@@ -4144,6 +4321,16 @@ def generate_financial_plan_pdf(q: dict, analysis: dict, output_path: str, doc_i
41444321 story .append (Spacer (1 , 8 ))
41454322
41464323 story .append (Paragraph (f"<b>Total Potential Tax Saving: Rs. { _format_indian_amount (total_potential_saving )} </b>" , styles ["BodyText" ]))
4324+ elif regime_current ['better_regime' ] == 'new' :
4325+ # New Regime recommended - explain that deductions don't apply
4326+ story .append (Paragraph ("Tax Optimization under New Regime" , styles ["h2" ]))
4327+ story .append (Paragraph ("<b>Since New Regime is recommended for you, Section 80C, 80D, and 80CCD deductions do NOT apply.</b>" , styles ["BodyText" ]))
4328+ story .append (Spacer (1 , 8 ))
4329+ story .append (Paragraph ("Under New Regime, focus on:" , styles ["BodyText" ]))
4330+ story .append (Paragraph ("• <b>Employer NPS contribution</b> (Section 80CCD-2) - still allowed up to 14% of basic" , styles ["BodyText" ]))
4331+ story .append (Paragraph ("• <b>Standard deduction</b> of Rs. 75,000 - automatically applied" , styles ["BodyText" ]))
4332+ story .append (Paragraph ("• <b>Tax-efficient investments</b> - equity funds (held >1 year for LTCG exemption)" , styles ["BodyText" ]))
4333+ story .append (Paragraph ("• <b>Health insurance</b> - still important for protection, even without tax benefit" , styles ["BodyText" ]))
41474334 else :
41484335 story .append (Paragraph ("Great! You have maximized all major tax deductions." , styles ["BodyText" ]))
41494336 else :
0 commit comments