@@ -3347,6 +3347,110 @@ def _format_rupee_match(match):
33473347 story .append (Paragraph ("No critical flags identified." , styles ["BodyText" ]))
33483348 story .append (Spacer (1 , 12 ))
33493349
3350+ # ==========================================================================
3351+ # CASHFLOW WATERFALL (from Sentinel)
3352+ # ==========================================================================
3353+ cashflow_waterfall = analysis .get ("cashflow_waterfall" ) or []
3354+ if cashflow_waterfall :
3355+ story .append (Paragraph ("Cashflow Waterfall" , styles ["h2" ]))
3356+ waterfall_rows = []
3357+ for item in cashflow_waterfall :
3358+ label = item .get ("label" , "" )
3359+ formatted = item .get ("formatted" , "" )
3360+ waterfall_rows .append ([label , formatted ])
3361+
3362+ if waterfall_rows :
3363+ wf_table = Table (
3364+ waterfall_rows ,
3365+ hAlign = "LEFT" ,
3366+ colWidths = [300 , 200 ],
3367+ )
3368+ wf_table .setStyle (TableStyle ([
3369+ ('GRID' , (0 ,0 ), (- 1 ,- 1 ), 0.5 , colors .grey ),
3370+ ('BACKGROUND' , (0 ,- 1 ), (- 1 ,- 1 ), colors .HexColor ('#E8F4EA' )), # Highlight net available
3371+ ('FONTNAME' , (0 ,- 1 ), (- 1 ,- 1 ), 'Helvetica-Bold' ),
3372+ ('BOTTOMPADDING' , (0 ,0 ), (- 1 ,- 1 ), 4 ),
3373+ ('TOPPADDING' , (0 ,0 ), (- 1 ,- 1 ), 4 ),
3374+ ]))
3375+ story .append (wf_table )
3376+ story .append (Spacer (1 , 12 ))
3377+
3378+ # ==========================================================================
3379+ # ALLOCATION PRIORITIES (from Sentinel)
3380+ # ==========================================================================
3381+ allocation_priorities = analysis .get ("allocation_priorities" ) or []
3382+ if allocation_priorities :
3383+ story .append (Paragraph ("Recommended Fund Allocation" , styles ["h2" ]))
3384+ priority_rows = []
3385+ for p in allocation_priorities :
3386+ priority = p .get ("priority" , "" )
3387+ label = p .get ("label" , "" )
3388+ formatted = p .get ("formatted" , "" )
3389+ reasoning = p .get ("reasoning" , "" )
3390+ priority_rows .append ([f"P{ priority } " , label , formatted , reasoning ])
3391+
3392+ if priority_rows :
3393+ pr_table = Table (
3394+ [["#" , "Category" , "Amount/mo" , "Reason" ]] + priority_rows ,
3395+ hAlign = "LEFT" ,
3396+ colWidths = [30 , 120 , 80 , 270 ],
3397+ )
3398+ pr_table .setStyle (TableStyle ([
3399+ ('BACKGROUND' , (0 ,0 ), (- 1 ,0 ), colors .HexColor ('#457B9D' )),
3400+ ('TEXTCOLOR' , (0 ,0 ), (- 1 ,0 ), colors .white ),
3401+ ('GRID' , (0 ,0 ), (- 1 ,- 1 ), 0.5 , colors .grey ),
3402+ ('FONTNAME' , (0 ,0 ), (- 1 ,0 ), 'Helvetica-Bold' ),
3403+ ('BOTTOMPADDING' , (0 ,0 ), (- 1 ,- 1 ), 4 ),
3404+ ('VALIGN' , (0 ,0 ), (- 1 ,- 1 ), 'TOP' ),
3405+ ]))
3406+ story .append (pr_table )
3407+ story .append (Spacer (1 , 12 ))
3408+
3409+ # ==========================================================================
3410+ # TAX EFFICIENCY (from Sentinel)
3411+ # ==========================================================================
3412+ tax_efficiency = analysis .get ("tax_efficiency" ) or {}
3413+ tax_recommendations = tax_efficiency .get ("recommendations" ) or []
3414+ total_tax_alpha = tax_efficiency .get ("total_tax_alpha" , 0 )
3415+
3416+ if tax_recommendations :
3417+ story .append (Paragraph ("Tax Savings Opportunities" , styles ["h2" ]))
3418+ if total_tax_alpha > 0 :
3419+ story .append (Paragraph (f"<b>Potential Tax Savings: Rs. { _format_indian_amount (total_tax_alpha )} </b>" , styles ["BodyText" ]))
3420+
3421+ tax_rows = []
3422+ for rec in tax_recommendations :
3423+ section = rec .get ("section" , "" )
3424+ gap = rec .get ("formatted_gap" , "" )
3425+ savings = rec .get ("formatted_savings" , "" )
3426+ deadline = rec .get ("deadline" , "" )
3427+ tax_rows .append ([section , gap , savings , f"by { deadline } " ])
3428+
3429+ if tax_rows :
3430+ tax_table = Table (
3431+ [["Section" , "Gap" , "Tax Saved" , "Deadline" ]] + tax_rows ,
3432+ hAlign = "LEFT" ,
3433+ colWidths = [80 , 100 , 100 , 220 ],
3434+ )
3435+ tax_table .setStyle (TableStyle ([
3436+ ('BACKGROUND' , (0 ,0 ), (- 1 ,0 ), colors .HexColor ('#2A9D8F' )),
3437+ ('TEXTCOLOR' , (0 ,0 ), (- 1 ,0 ), colors .white ),
3438+ ('GRID' , (0 ,0 ), (- 1 ,- 1 ), 0.5 , colors .grey ),
3439+ ('FONTNAME' , (0 ,0 ), (- 1 ,0 ), 'Helvetica-Bold' ),
3440+ ('BOTTOMPADDING' , (0 ,0 ), (- 1 ,- 1 ), 4 ),
3441+ ]))
3442+ story .append (tax_table )
3443+
3444+ # LTCG Harvest Recommendation
3445+ ltcg = tax_efficiency .get ("ltcg_harvest" )
3446+ if ltcg :
3447+ story .append (Spacer (1 , 6 ))
3448+ ltcg_rec = ltcg .get ("recommendation" , "" )
3449+ if ltcg_rec :
3450+ story .append (Paragraph (f"<b>LTCG Opportunity:</b> { ltcg_rec } " , styles ["BodyText" ]))
3451+
3452+ story .append (Spacer (1 , 12 ))
3453+
33503454 # Categorized Recommendations
33513455 story .append (Paragraph ("Recommendations" , styles ["h2" ]))
33523456 for cat , items in categorized .items ():
@@ -3369,6 +3473,7 @@ def _format_rupee_match(match):
33693473 "risk_rationale" ,
33703474 "goals_strategy" ,
33713475 "portfolio_rebalance" ,
3476+ "tax_efficiency" ,
33723477 ]
33733478 for key in section_order :
33743479 sec = narratives .get (key )
0 commit comments