11import logging
22
3+ from markupsafe import Markup
4+
35from odoo import Command , _ , api , fields , models
46from odoo .exceptions import UserError
57
@@ -20,7 +22,7 @@ def _compute_display_name(self):
2022 if record .scenario_id and record .executed_at :
2123 date_str = record .executed_at .strftime ("%b %d, %Y %H:%M" )
2224 record .display_name = (
23- f"{ record .scenario_id .name } - { date_str } " f" ({ record .beneficiary_count :,} beneficiaries)"
25+ f"{ record .scenario_id .name } - { date_str } ({ record .beneficiary_count :,} beneficiaries)"
2426 )
2527 elif record .scenario_id :
2628 record .display_name = f"{ record .scenario_id .name } - Run #{ record .id } "
@@ -233,22 +235,29 @@ def _compute_summary_html(self):
233235 target_type_label = "households" if record .scenario_id .target_type == "group" else "individuals"
234236 coverage_pct = f"{ record .coverage_rate :.1f} " if record .coverage_rate else "0.0"
235237 parts = [
236- f"<strong>{ scenario_name } </strong> targets "
237- f"<strong>{ record .beneficiary_count :,} </strong> { target_type_label } "
238- f"({ coverage_pct } % of registry)." ,
238+ Markup ("<strong>{}</strong> targets <strong>{}</strong> {} ({}% of registry)." ).format (
239+ scenario_name ,
240+ f"{ record .beneficiary_count :,} " ,
241+ target_type_label ,
242+ coverage_pct ,
243+ ),
239244 ]
240245 if record .total_cost :
241- parts .append (f " Total estimated cost: <strong>{ record .total_cost :,.2f} </strong>." )
246+ parts .append (Markup ( " Total estimated cost: <strong>{}</strong>." ). format ( f" { record .total_cost :,.2f} " ) )
242247 if record .budget_utilization and record .scenario_id .budget_amount :
243- parts .append (f " Budget utilization: { record .budget_utilization :.1f} %." )
248+ parts .append (Markup ( " Budget utilization: {}%." ). format ( f" { record .budget_utilization :.1f} " ) )
244249 if record .equity_score :
245250 parity_label = (
246251 "proportional"
247252 if record .equity_score >= 80
248253 else ("some variation" if record .equity_score >= 60 else "significant variation" )
249254 )
250- parts .append (f" Parity score: <strong>{ record .equity_score :.0f} /100</strong> ({ parity_label } )." )
251- record .summary_html = "<p>" + "" .join (parts ) + "</p>"
255+ parts .append (
256+ Markup (" Parity score: <strong>{}/100</strong> ({})." ).format (
257+ f"{ record .equity_score :.0f} " , parity_label
258+ )
259+ )
260+ record .summary_html = Markup ("<p>{}</p>" ).format (Markup ("" ).join (parts ))
252261
253262 @api .depends ("gini_coefficient" , "distribution_json" )
254263 def _compute_distribution_summary_html (self ):
@@ -261,7 +270,7 @@ def _compute_distribution_summary_html(self):
261270 gini_label = (
262271 "nearly equal" if gini < 0.2 else ("moderately distributed" if gini < 0.4 else "unequally distributed" )
263272 )
264- parts = [f"Benefits are <strong>{ gini_label } </strong> " f" (Gini coefficient: { gini :.2f} )." ]
273+ parts = [f"Benefits are <strong>{ gini_label } </strong> (Gini coefficient: { gini :.2f} )." ]
265274 minimum = distribution .get ("minimum" , 0 )
266275 maximum = distribution .get ("maximum" , 0 )
267276 mean = distribution .get ("mean" , 0 )
@@ -344,10 +353,10 @@ def _compute_geographic_html(self):
344353 continue
345354
346355 html_parts = [
347- '<table class="table table-sm table-bordered">' ,
348- "<thead><tr>" ,
349- "<th>Area</th><th>Beneficiaries</th><th>Amount</th><th>Coverage</th>" ,
350- "</tr></thead><tbody>" ,
356+ Markup ( '<table class="table table-sm table-bordered">' ) ,
357+ Markup ( "<thead><tr>" ) ,
358+ Markup ( "<th>Area</th><th>Beneficiaries</th><th>Amount</th><th>Coverage</th>" ) ,
359+ Markup ( "</tr></thead><tbody>" ) ,
351360 ]
352361 # Sort by beneficiary count descending
353362 sorted_areas = sorted (geo_data .items (), key = lambda x : x [1 ].get ("count" , 0 ), reverse = True )
@@ -357,10 +366,12 @@ def _compute_geographic_html(self):
357366 amount = area_info .get ("amount" , 0 )
358367 coverage = area_info .get ("coverage_rate" , 0 )
359368 html_parts .append (
360- f"<tr><td>{ name } </td><td>{ count :,} </td>" f"<td>{ amount :,.2f} </td><td>{ coverage :.1f} %</td></tr>"
369+ Markup ("<tr><td>{}</td><td>{}</td><td>{}</td><td>{}%</td></tr>" ).format (
370+ name , f"{ count :,} " , f"{ amount :,.2f} " , f"{ coverage :.1f} "
371+ )
361372 )
362- html_parts .append ("</tbody></table>" )
363- record .geographic_html = "" .join (html_parts )
373+ html_parts .append (Markup ( "</tbody></table>" ) )
374+ record .geographic_html = Markup ( "" ) .join (html_parts )
364375
365376 @api .depends ("metric_results_json" )
366377 def _compute_metric_results_html (self ):
@@ -375,10 +386,10 @@ def _compute_metric_results_html(self):
375386 continue
376387
377388 html_parts = [
378- '<table class="table table-sm table-bordered">' ,
379- "<thead><tr>" ,
380- "<th>Metric</th><th>Value</th><th>Type</th>" ,
381- "</tr></thead><tbody>" ,
389+ Markup ( '<table class="table table-sm table-bordered">' ) ,
390+ Markup ( "<thead><tr>" ) ,
391+ Markup ( "<th>Metric</th><th>Value</th><th>Type</th>" ) ,
392+ Markup ( "</tr></thead><tbody>" ) ,
382393 ]
383394 for metric_name , metric_data in metrics .items ():
384395 value = metric_data .get ("value" , 0 )
@@ -392,9 +403,13 @@ def _compute_metric_results_html(self):
392403 formatted_value = f"{ value :,.2f} "
393404 else :
394405 formatted_value = str (value )
395- html_parts .append (f"<tr><td>{ metric_name } </td><td>{ formatted_value } </td><td>{ metric_type } </td></tr>" )
396- html_parts .append ("</tbody></table>" )
397- record .metric_results_html = "" .join (html_parts )
406+ html_parts .append (
407+ Markup ("<tr><td>{}</td><td>{}</td><td>{}</td></tr>" ).format (
408+ metric_name , formatted_value , metric_type
409+ )
410+ )
411+ html_parts .append (Markup ("</tbody></table>" ))
412+ record .metric_results_html = Markup ("" ).join (html_parts )
398413
399414 @api .depends ("targeting_efficiency_json" )
400415 def _compute_targeting_efficiency_html (self ):
@@ -474,11 +489,11 @@ def _compute_scenario_snapshot_fields(self):
474489 rules = snapshot .get ("entitlement_rules" ) or []
475490 if rules :
476491 html_parts = [
477- '<table class="table table-sm table-bordered">' ,
478- "<thead><tr>" ,
479- "<th>Mode</th><th>Amount</th><th>Multiplier Field</th><th>Max Multiplier</th>" ,
480- "<th>CEL Expression</th><th>Condition</th>" ,
481- "</tr></thead><tbody>" ,
492+ Markup ( '<table class="table table-sm table-bordered">' ) ,
493+ Markup ( "<thead><tr>" ) ,
494+ Markup ( "<th>Mode</th><th>Amount</th><th>Multiplier Field</th><th>Max Multiplier</th>" ) ,
495+ Markup ( "<th>CEL Expression</th><th>Condition</th>" ) ,
496+ Markup ( "</tr></thead><tbody>" ) ,
482497 ]
483498 amount_mode_labels = {
484499 "fixed" : "Fixed" ,
@@ -493,12 +508,21 @@ def _compute_scenario_snapshot_fields(self):
493508 cel_expr = rule .get ("amount_cel_expression" ) or "-"
494509 condition = rule .get ("condition_cel_expression" ) or "-"
495510 html_parts .append (
496- f"<tr><td>{ mode } </td><td>{ amount :,.2f} </td><td>{ mult_field } </td>"
497- f"<td>{ max_mult } </td><td><code>{ cel_expr } </code></td>"
498- f"<td><code>{ condition } </code></td></tr>"
511+ Markup (
512+ "<tr><td>{}</td><td>{}</td><td>{}</td>"
513+ "<td>{}</td><td><code>{}</code></td>"
514+ "<td><code>{}</code></td></tr>"
515+ ).format (
516+ mode ,
517+ f"{ amount :,.2f} " ,
518+ mult_field ,
519+ max_mult ,
520+ cel_expr ,
521+ condition ,
522+ )
499523 )
500- html_parts .append ("</tbody></table>" )
501- record .scenario_snapshot_entitlement_rules_html = "" .join (html_parts )
524+ html_parts .append (Markup ( "</tbody></table>" ) )
525+ record .scenario_snapshot_entitlement_rules_html = Markup ( "" ) .join (html_parts )
502526 else :
503527 record .scenario_snapshot_entitlement_rules_html = (
504528 "<p class='text-muted'>No entitlement rules defined</p>"
0 commit comments