Coverage for ingadhoc-odoo-saas-adhoc / saas_provider_upgrade / models / helpdesk_ticket.py: 49%

282 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-09 18:05 +0000

1import logging 

2import math 

3from ast import literal_eval 

4from datetime import datetime, timedelta 

5from typing import Literal 

6 

7from markupsafe import Markup 

8from odoo import _, api, fields, models 

9from odoo.exceptions import RedirectWarning, UserError, ValidationError 

10from odoo.tools.safe_eval import safe_eval 

11from werkzeug.urls import url_encode 

12 

13_logger = logging.getLogger(__name__) 

14 

15 

16class HelpdeskTicket(models.Model): 

17 _name = "helpdesk.ticket" 

18 _inherit = ["helpdesk.ticket", "saas.provider.upgrade.util"] 

19 _mailing_enabled = True 

20 

21 upgrade_type_id = fields.Many2one("saas.upgrade.type", string="Upgrade Type", tracking=True) 

22 upgrade_team = fields.Boolean(related="team_id.upgrade_team", related_sudo=True, store=True) 

23 request_line_ids = fields.One2many( 

24 "helpdesk.ticket.upgrade.request", "ticket_id", "Requests", domain=[("next_request_id", "=", False)] 

25 ) 

26 all_request_line_ids = fields.One2many("helpdesk.ticket.upgrade.request", "ticket_id", "All Requests") 

27 customer_note_ids = fields.One2many("helpdesk.ticket.customer_note", "ticket_id", "Customer Notes") 

28 custom_script_ids = fields.Many2many("saas.upgrade.line") 

29 environment_variables_dict = fields.Text( 

30 default="{}", 

31 required=False, 

32 help="Dictionary to send environment variables to odoo upgrade service where the key is the variable " 

33 "and the value is the value", 

34 ) 

35 consultant_upgrade_id = fields.Many2one( 

36 "res.users", 

37 string="Upgrade Consultant", 

38 compute="_compute_consultant_upgrade_id", 

39 store=True, 

40 readonly=False, 

41 ) 

42 team_technician_ids = fields.Many2many( 

43 "res.users", 

44 related="team_id.technician_member_ids", 

45 ) 

46 assigned_technician_id = fields.Many2one( 

47 "res.users", 

48 string="Técnico asignado", 

49 tracking=True, 

50 compute="_compute_assigned_technician_id", 

51 store=True, 

52 readonly=False, 

53 help="Responsible technician who will be notified in case of errors during the execution of requests", 

54 domain="[('id', 'in', team_technician_ids)]", 

55 ) 

56 customer_note_count = fields.Integer(compute="_compute_customer_note_count", compute_sudo=True) 

57 total_customer_note_count = fields.Integer( 

58 compute="_compute_customer_note_count", 

59 compute_sudo=True, 

60 string="Total Customer Notes", 

61 ) 

62 pending_customer_notes = fields.Integer(compute="_compute_customer_note_count", compute_sudo=True) 

63 upgrade_pull_request_ids = fields.Many2many( 

64 "saas.pull.request", 

65 "saas_pull_request_ticket_upgrade_rel", 

66 "ticket_id", 

67 "pull_id", 

68 string="Upgrade PRs", 

69 ) 

70 upgrade_target = fields.Char( 

71 related="upgrade_type_id.target", 

72 string="Upgrade Major version", 

73 ) 

74 deadline_upgrade = fields.Date( 

75 string="Fecha deseada de actualización", 

76 help="Planned date for the upgrade (either requested by the customer or set by us).", 

77 tracking=True, 

78 ) 

79 date_upgrade_scheduled = fields.Datetime( 

80 string="Fecha agendada para actualización", 

81 compute="_compute_date_upgrade_scheduled", 

82 store=True, 

83 help="Scheduled date for the upgrade to production, based on the last production request.", 

84 ) 

85 upgrade_description = fields.Html(related="upgrade_type_id.portal_description") 

86 post_upgrade_description = fields.Html(related="upgrade_type_id.post_upgrade_portal_description") 

87 upgrade_line_ids = fields.Many2many( 

88 "saas.upgrade.line", 

89 "saas_upgrade_line_ticket_rel", 

90 "ticket_id", 

91 "upgrade_line_id", 

92 string="Upgrade Lines", 

93 help="Upgrade Lines asociadas al ticket.", 

94 domain=["|", ("active", "=", True), ("active", "=", False)], 

95 context={"active_test": False}, 

96 ) 

97 last_test_request_line_id = fields.Many2one( 

98 "helpdesk.ticket.upgrade.request", 

99 compute="_compute_last_test_request_line", 

100 store=True, 

101 ) 

102 last_test_upgraded_database_id = fields.Many2one( 

103 "saas.database", 

104 related="last_test_request_line_id.upgraded_database_id", 

105 string="Last Test Upgraded Database", 

106 related_sudo=True, 

107 ) 

108 last_production_request_line_id = fields.Many2one( 

109 "helpdesk.ticket.upgrade.request", 

110 compute="_compute_date_upgrade_scheduled", 

111 store=True, 

112 ) 

113 last_test_request_state = fields.Selection( 

114 related="last_test_request_line_id.state", 

115 string="Test Request State", 

116 related_sudo=True, 

117 ) 

118 last_production_request_state = fields.Selection( 

119 related="last_production_request_line_id.state", 

120 string="Production Request State", 

121 related_sudo=True, 

122 ) 

123 parameters = fields.Properties( 

124 definition="upgrade_type_id.parameters_definition", 

125 help="Parameters that can be used on scripts", 

126 ) 

127 custom_parameters_dict = fields.Text(compute="_compute_custom_parameters_dict") 

128 upgrade_upload_changes_ids = fields.One2many( 

129 "saas.upgrade.upload.changes", 

130 "ticket_id", 

131 ) 

132 upgrade_client_data_id = fields.Many2one("saas.upgrade.client.data") 

133 ticket_upgrade_status = fields.Selection( 

134 selection=[ 

135 ("no_upgraded", "Pending Upgrade"), 

136 ("upgraded", "Upgraded Database"), 

137 ], 

138 compute="_compute_request_state", 

139 store=True, 

140 ) 

141 active_production_freeze = fields.Boolean( 

142 compute="_compute_active_production_freeze", 

143 store=True, 

144 readonly=False, 

145 ) 

146 confirm_production_sent = fields.Boolean( 

147 default=False, 

148 help="Indicates whether the production update has already been committed and the customer has been notified.", 

149 ) 

150 is_last_upgrade_ticket_available = fields.Boolean(compute="_compute_is_last_upgrade_ticket") 

151 upgrade_classification = fields.Char( 

152 compute="_compute_upgrade_classification", 

153 store=True, 

154 ) 

155 gantt_date_stop = fields.Datetime( 

156 compute="_compute_gantt_date_stop", 

157 store=True, 

158 help="Planned end date for the upgrade. Calculated from the start date field specified in context (gantt_start_field) + 7 days. Used in Gantt view.", 

159 ) 

160 

161 _upgrade_uniq = models.Constraint( 

162 "unique (project_id, upgrade_type_id)", 

163 "Only one upgrade can exist per project and upgrade type.", 

164 ) 

165 

166 @api.depends("team_id", "team_id.upgrade_team") 

167 def _compute_assigned_technician_id(self): 

168 for ticket in self: 

169 if ticket.team_id.upgrade_team and not ticket.assigned_technician_id: 

170 technician_dict = ticket.team_id._determine_technician_to_assign() 

171 technician = technician_dict.get(ticket.team_id.id, self.env["res.users"]) 

172 if technician: 172 ↛ 173line 172 didn't jump to line 173 because the condition on line 172 was never true

173 ticket.assigned_technician_id = technician.id 

174 

175 @api.depends("upgrade_client_data_id.project_classification") 

176 def _compute_upgrade_classification(self): 

177 for rec in self: 

178 if rec.upgrade_client_data_id and rec.upgrade_client_data_id.last_ticket_id.id == rec.id: 

179 rec.upgrade_classification = rec.upgrade_client_data_id.project_classification 

180 else: 

181 rec.upgrade_classification = False 

182 

183 @api.depends("upgrade_type_id") 

184 def _compute_is_last_upgrade_ticket(self): 

185 last_upgrade_type = self.env["saas.upgrade.type"].search([("stable", "=", True)], limit=1, order="sequence") 

186 for rec in self: 

187 if rec.upgrade_team and rec.upgrade_type_id: 

188 if rec.upgrade_type_id == last_upgrade_type: 

189 rec.is_last_upgrade_ticket_available = True 

190 else: 

191 rec.is_last_upgrade_ticket_available = False 

192 else: 

193 rec.is_last_upgrade_ticket_available = False 

194 

195 @api.depends("last_production_request_line_id.state") 

196 def _compute_request_state(self): 

197 for rec in self: 

198 rec.ticket_upgrade_status = "no_upgraded" 

199 if rec.last_production_request_line_id and rec.last_production_request_line_id.state == "done": 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true

200 rec.ticket_upgrade_status = "upgraded" 

201 

202 @api.depends("upgrade_type_id.parameters_definition", "parameters", "upgrade_team") 

203 def _compute_custom_parameters_dict(self): 

204 for rec in self: 

205 new_dict = dict() 

206 parameters_definition = rec.upgrade_type_id.parameters_definition 

207 if not rec.upgrade_team or (not rec.upgrade_type_id.parameters_definition and not rec.parameters): 207 ↛ 210line 207 didn't jump to line 210 because the condition on line 207 was always true

208 rec.custom_parameters_dict = "{}" 

209 continue 

210 parameters_definition = list(filter(lambda x: x["type"] != "separator", parameters_definition)) 

211 if not rec.parameters: 

212 new_parameters = dict() 

213 for definition in parameters_definition: 

214 if "default" in definition.keys(): 

215 new_parameters[definition["name"]] = definition["default"] 

216 rec.write({"parameters": new_parameters}) 

217 rec.parameters = new_parameters 

218 

219 for key, value in rec.parameters.items(): 

220 parameter_definition = list(filter(lambda x: x["name"] == key, parameters_definition)) 

221 

222 if not parameter_definition: 

223 # Elimination case 

224 continue 

225 

226 parameter_definition = parameter_definition[0] 

227 new_key = parameter_definition["string"] 

228 new_value = value 

229 if not new_value: 

230 new_value = parameter_definition["default"] 

231 

232 if new_key in new_dict: 

233 raise UserError( 

234 _( 

235 "Duplicate key '%s' in custom_parameters_dict, please use a different name", 

236 new_key, 

237 ) 

238 ) 

239 try: 

240 if isinstance(new_value, models.BaseModel): 

241 ids = new_value.ids 

242 new_dict[new_key] = ids[0] if len(ids) == 1 else ids 

243 else: 

244 new_dict[new_key] = safe_eval(str(new_value)) 

245 except Exception: 

246 raise UserError( 

247 _( 

248 "Invalid expression, '%s' value must be a literal python list definition e.g. '[a, b]'", 

249 new_key, 

250 ) 

251 ) 

252 

253 rec.custom_parameters_dict = str(new_dict) 

254 

255 @api.depends("project_id.user_id") 

256 def _compute_consultant_upgrade_id(self): 

257 for rec in self.filtered("upgrade_team"): 

258 if not rec.consultant_upgrade_id: 258 ↛ 257line 258 didn't jump to line 257 because the condition on line 258 was always true

259 rec.consultant_upgrade_id = rec.project_id.user_id 

260 

261 @api.depends("all_request_line_ids.state", "confirm_production_sent") 

262 def _compute_date_upgrade_scheduled(self): 

263 for rec in self: 

264 requests = rec.all_request_line_ids.filtered( 

265 lambda x: x.aim == "production" and x.state not in ["cancel", "error"] 

266 ).sorted(key=lambda m: m.planned_start or datetime.min, reverse=True) 

267 rec.date_upgrade_scheduled = False 

268 rec.last_production_request_line_id = False 

269 if requests: 

270 request = requests[0] 

271 if request.confirm_production_sent: 271 ↛ 272line 271 didn't jump to line 272 because the condition on line 271 was never true

272 rec.date_upgrade_scheduled = request.planned_start 

273 rec.last_production_request_line_id = request 

274 

275 @api.depends("customer_note_ids") 

276 def _compute_customer_note_count(self): 

277 for rec in self: 

278 pending_customer_notes = rec.customer_note_ids.filtered(lambda cn: cn.status not in ["done"]) 

279 rec.customer_note_count = len( 

280 pending_customer_notes.filtered(lambda cn: not cn.post_production and cn.priority == "1") 

281 ) 

282 rec.total_customer_note_count = len(rec.customer_note_ids) 

283 rec.pending_customer_notes = len(pending_customer_notes.ids) 

284 

285 @api.depends( 

286 "request_line_ids.state", 

287 "request_line_ids.with_first_original_database", 

288 "request_line_ids.with_upgraded_database", 

289 ) 

290 def _compute_last_test_request_line(self): 

291 for rec in self.filtered("request_line_ids"): 

292 test_requests = rec.request_line_ids.filtered( 

293 lambda x: ( 

294 x.aim == "test" 

295 and x.state != "cancel" 

296 and x.with_first_original_database 

297 and x.with_upgraded_database 

298 ) 

299 ) 

300 if test_requests: 300 ↛ 301line 300 didn't jump to line 301 because the condition on line 300 was never true

301 rec.last_test_request_line_id = test_requests[-1] 

302 else: 

303 rec.last_test_request_line_id = False 

304 

305 @api.depends("upgrade_type_id.active_production_freeze") 

306 def _compute_active_production_freeze(self): 

307 for rec in self: 

308 rec.active_production_freeze = rec.upgrade_type_id.active_production_freeze 

309 

310 @api.depends("date_upgrade_scheduled", "deadline_upgrade") 

311 @api.depends_context("gantt_start_field") 

312 def _compute_gantt_date_stop(self): 

313 """Compute the Gantt end date based on the start field specified in context.""" 

314 start_field = self.env.context.get("gantt_start_field", "deadline_upgrade") 

315 for rec in self.filtered(start_field): 315 ↛ 316line 315 didn't jump to line 316 because the loop on line 315 never started

316 start_date = getattr(rec, start_field) 

317 rec.gantt_date_stop = fields.Datetime.to_datetime(start_date + timedelta(days=7)) 

318 

319 @api.onchange("upgrade_type_id", "upgrade_team", "project_id") 

320 def _suggest_upgrade_ticket_name(self): 

321 for rec in self: 

322 if rec.upgrade_team and rec.upgrade_type_id: 

323 rec.name = "%s - %s" % ( 

324 rec.upgrade_type_id.with_context(lang="es_AR").name, 

325 rec.project_id.name, 

326 ) 

327 

328 def _compute_access_url(self): 

329 records_upgrade_team = self.filtered("upgrade_team") 

330 super(HelpdeskTicket, self - records_upgrade_team)._compute_access_url() 

331 for ticket in records_upgrade_team: 

332 ticket.access_url = "/my/upgrade_ticket/%s" % ticket.id 

333 

334 @api.constrains("environment_variables_dict") 

335 def _check_environment_variables_dict(self): 

336 for rec in self: 

337 try: 

338 dict(safe_eval(rec.environment_variables_dict)) 

339 except Exception: 

340 raise UserError( 

341 _( 

342 "Invalid expression on Environment Variables Dict, it must be a literal python dictionary " 

343 "definition e.g. \"{'field': 'value'}\"" 

344 ) 

345 ) 

346 

347 @api.constrains("project_id", "main_database_id") 

348 def _check_upgrade_version_conflict(self): 

349 """ 

350 Verifies that a ticket is not created for a customer who is already on the target version 

351 """ 

352 for rec in self.filtered(lambda x: x.project_id and not x.parent_id and not x.upgrade_team): 352 ↛ 353line 352 didn't jump to line 353 because the loop on line 352 never started

353 upgrade_ticket = self.search( 

354 [ 

355 ("project_id", "=", rec.project_id.id), 

356 ("upgrade_team", "=", True), 

357 ("ticket_upgrade_status", "=", "no_upgraded"), 

358 ], 

359 limit=1, 

360 order="upgrade_target desc", 

361 ) 

362 if ( 

363 upgrade_ticket == rec 

364 and rec.main_database_id 

365 and upgrade_ticket.upgrade_target == rec.main_database_id.major_version 

366 ): 

367 raise ValidationError(_("The client is already on the target version.")) 

368 

369 def _get_estimated_production_upgrade_duration( 

370 self, measure: Literal["hours", "minutes", "seconds"] = "hours" 

371 ) -> int: 

372 """ 

373 Returns the estimated duration for the production upgrade based on the last test request. 

374 If no test request exists, returns the default appointment duration. 

375 

376 :param measure: The unit of time to return the duration in. Can be 'hours', 'minutes' or 'seconds'. 

377 :return: Estimated duration for the production upgrade in the specified unit of time. 

378 """ 

379 self.ensure_one() 

380 if self.last_test_request_line_id: 

381 time = math.ceil(self.last_test_request_line_id.cum_total_time) 

382 else: 

383 time = math.ceil(self.upgrade_type_id.appointment_type_id.appointment_duration) 

384 

385 match measure: 

386 case "hours": 

387 return time 

388 case "minutes": 

389 return time * 60 

390 case "seconds": 

391 return time * 3600 

392 case _: 

393 return time 

394 

395 # --------------------------------------------------- 

396 # Portal Upgrade Methods 

397 # --------------------------------------------------- 

398 

399 @api.model 

400 def get_portal_upgrade_info(self, partner): 

401 """ 

402 Central method to get all necessary information for the upgrade portal. 

403 Returns a dictionary with structured information about upgrade status and available options. 

404 

405 :param partner: Partner recordset to get upgrade information for 

406 :return: Dictionary with keys: stable_upgrade_type, stable_ticket, is_on_stable_version, 

407 show_upgrade_button, show_beta_tester, show_beta_tester_message, 

408 next_upgrade_version, next_upgrade_type 

409 """ 

410 stable_upgrade_type = ( 

411 self.env["saas.upgrade.type"] 

412 .sudo() 

413 .search([("stable", "=", True), ("active", "=", True)], limit=1, order="sequence") 

414 ) 

415 stable_ticket = self.sudo().search( 

416 [ 

417 ("partner_id", "=", partner.id), 

418 ("upgrade_team", "=", True), 

419 ("upgrade_type_id", "=", stable_upgrade_type.id), 

420 ], 

421 limit=1, 

422 order="id desc", 

423 ) 

424 is_on_stable_version = bool(stable_ticket and stable_ticket.ticket_upgrade_status == "upgraded") 

425 result = { 

426 "stable_upgrade_type": stable_upgrade_type, 

427 "stable_ticket": stable_ticket, 

428 "is_on_stable_version": is_on_stable_version, 

429 "show_upgrade_button": not is_on_stable_version, 

430 "show_beta_tester": False, 

431 "show_beta_tester_message": False, 

432 "next_upgrade_version": False, 

433 "next_upgrade_type": False, 

434 } 

435 if is_on_stable_version: 

436 next_upgrade_type = ( 

437 self.env["saas.upgrade.type"] 

438 .sudo() 

439 .search( 

440 [ 

441 ("active", "=", True), 

442 ("sequence", "<", stable_upgrade_type.sequence), 

443 ], 

444 limit=1, 

445 order="sequence", 

446 ) 

447 ) 

448 if next_upgrade_type: 

449 beta_ticket = self.sudo().search( 

450 [ 

451 ("partner_id", "=", partner.id), 

452 ("upgrade_team", "=", True), 

453 ("upgrade_type_id", "=", next_upgrade_type.id), 

454 ("ticket_upgrade_status", "=", "no_upgraded"), 

455 ], 

456 limit=1, 

457 order="id desc", 

458 ) 

459 result["next_upgrade_type"] = next_upgrade_type 

460 result["next_upgrade_version"] = next_upgrade_type.target 

461 result["show_beta_tester_message"] = bool(beta_ticket) 

462 result["show_beta_tester"] = not bool(beta_ticket) 

463 return result 

464 

465 # --------------------------------------------------- 

466 # upgrade ticket sharing 

467 # --------------------------------------------------- 

468 

469 def action_open_customer_notes(self) -> dict: 

470 self.ensure_one() 

471 action = self.env["ir.actions.act_window"]._for_xml_id( 

472 "saas_provider_upgrade.action_helpdesk_ticket_customer_note" 

473 ) 

474 new_context = literal_eval(action.get("context", "{}")) 

475 new_context.update({"default_ticket_id": self.id}) 

476 action["context"] = new_context 

477 action["domain"] = [["ticket_id", "=", self.id]] 

478 return action 

479 

480 def action_open_requests_logs(self) -> dict: 

481 self.ensure_one() 

482 action = self.env["ir.actions.act_window"]._for_xml_id( 

483 "saas_provider_upgrade.action_saas_upgrade_line_request_log_entry" 

484 ) 

485 new_context = literal_eval(action.get("context", "{}")) 

486 new_context.update({"default_upgrade_ticket_id": self.id}) 

487 action["context"] = new_context 

488 action["domain"] = [["upgrade_ticket_id", "=", self.id]] 

489 return action 

490 

491 def action_confirm_production(self) -> dict: 

492 self.ensure_one() 

493 upgrade_type = self.upgrade_type_id 

494 if not upgrade_type.confirm_stage_id or not upgrade_type.confirm_mail_template_id: 494 ↛ 495line 494 didn't jump to line 495 because the condition on line 494 was never true

495 action = self.env["ir.actions.act_window"]._for_xml_id("saas_provider_upgrade.action_saas_upgrade_type") 

496 action["res_id"] = upgrade_type.id 

497 action["views"] = [ 

498 ( 

499 self.env.ref("saas_provider_upgrade.view_saas_upgrade_type_form").id, 

500 "form", 

501 ) 

502 ] 

503 raise RedirectWarning( 

504 message=_( 

505 "The confirmation stage and/or mail template are not defined in the Upgrade Type. Please complete them before confirming." 

506 ), 

507 action=action, 

508 button_text=_("Go to Upgrade Type"), 

509 ) 

510 

511 self.stage_id = upgrade_type.confirm_stage_id.id 

512 self.message_post_with_source( 

513 upgrade_type.confirm_mail_template_id, 

514 message_type="comment", 

515 subtype_xmlid="mail.mt_comment", 

516 ) 

517 self.confirm_production_sent = True 

518 msg = _("Production upgrade was confirmed and customer was notified. Stage changed.") 

519 action_to_return = self.build_display_notification_action( 

520 title=_("Confirmation done"), 

521 message=msg, 

522 type="success", 

523 ) 

524 return action_to_return 

525 

526 def action_customer_portal(self) -> dict: 

527 self.ensure_one() 

528 return { 

529 "type": "ir.actions.act_url", 

530 "url": self.access_url, 

531 "target": "new", 

532 } 

533 

534 def action_schedule_upgrade_production(self): 

535 """ 

536 Action to program the upgrade to production from portal 

537 """ 

538 self.ensure_one() 

539 request_production = self.last_production_request_line_id 

540 

541 # If a production request exists, redirect to its calendar event 

542 # All production requests now have an associated event (via portal, wizard, or migration) 

543 if request_production: 

544 request_production = request_production.sudo() 

545 event_upgrade = request_production.event_upgrade_id 

546 event_token = event_upgrade.access_token 

547 url = f"/calendar/view/{event_token}" 

548 params = { 

549 "ticket_id": self.id, 

550 } 

551 if event_upgrade.partner_ids: 

552 params["partner_id"] = event_upgrade.partner_ids.id 

553 return { 

554 "type": "ir.actions.act_url", 

555 "url": f"{url}?{url_encode(params)}", 

556 "target": "new", 

557 } 

558 

559 # No production request exists - run validations before allowing scheduling 

560 action_to_return = { 

561 "type": "ir.actions.act_url", 

562 "url": f"/appointment/{self.sudo().upgrade_type_id.appointment_type_id.id}?ticket_id={self.id}", 

563 "target": "new", 

564 } 

565 # Create a dummy request with aim 'production' to trigger the ULs on_create 

566 # Raises an exception if any of the conditions are not met 

567 try: 

568 request = ( 

569 self.env["helpdesk.ticket.upgrade.request"].sudo().new({"aim": "production", "ticket_id": self.id}) 

570 ) 

571 request.run_on_create_scripts() 

572 except UserError as e: 

573 title = self.env._("Check conditions!") 

574 message = e.args[0] # e.args is a tuple 

575 action_to_return = self.build_display_notification_action(title, message, "warning") 

576 except Exception as e: 

577 body = "Error in programming the production upgrade by the user, error: \n <blockquote>%s</blockquote>" % e 

578 self.message_post(body=Markup(body)) 

579 title = self.env._("Unexpected failure!") 

580 message = self.env._( 

581 "An error occurred while processing the request, please try again. " 

582 "If the problem persists contact support." 

583 ) 

584 action_to_return = self.build_display_notification_action(title, message, "warning") 

585 return action_to_return 

586 

587 def generate_appointment_link(self) -> str: 

588 """ 

589 Genera el enlace de actualización para el ticket. 

590 """ 

591 self.ensure_one() 

592 if not self.upgrade_type_id.appointment_type_id: 

593 raise ValueError(_("There is not appointment type associated with this upgrade type.")) 

594 

595 # Create an appointment.invite associated with the ticket 

596 invite = self.env["appointment.invite"].create( 

597 { 

598 "appointment_type_ids": [(6, 0, [self.upgrade_type_id.appointment_type_id.id])], 

599 "ticket_id": self.id, 

600 } 

601 ) 

602 url = invite.redirect_url 

603 return url 

604 

605 def get_major_version_from_to_upg(self) -> tuple[str, str]: 

606 """ 

607 Retrieve the major version for main database and target version for an upgrade ticket 

608 

609 return: (old_major_version, new_major_version) 

610 """ 

611 self.ensure_one() 

612 self = self.sudo() 

613 return ( 

614 self.main_database_id.odoo_version_group_id.major_version_id.name, 

615 self.upgrade_target, 

616 ) 

617 

618 

619class HelpdeskStage(models.Model): 

620 _inherit = "helpdesk.stage" 

621 

622 show_in_upgrade_portal = fields.Boolean()