Coverage for ingadhoc-odoo-saas-adhoc / saas_provider_upgrade / models / helpdesk_ticket_upgrade_request.py: 42%
966 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-09 18:15 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-09 18:15 +0000
1import base64
2import logging
3from ast import literal_eval
4from collections.abc import Callable
5from contextlib import contextmanager
6from datetime import (
7 datetime as dt,
8 timedelta,
9)
10from functools import partial
11from textwrap import dedent
12from typing import TYPE_CHECKING, Any, Literal, Optional
14import pandas as pd
15from markupsafe import Markup
16from odoo import _, api, fields, models
17from odoo.exceptions import UserError, ValidationError
18from odoo.tools.safe_eval import datetime, dateutil, json, pytz, safe_eval, time, wrap_module
20from .. import UpgradeScript
21from ..constants import MAP_NEXT_STATES
23if TYPE_CHECKING:
24 from odooly import Client
26 from saas_provider_upgrade.models.saas_database import SaasDatabase
27 from saas_provider_upgrade.models.saas_upgrade_line import SaasUpgradeLine
29_logger = logging.getLogger(__name__)
31SHELL_COMMAND_TEMPLATE = """
32request = env['helpdesk.ticket.upgrade.request'].browse({})
33eval_context = dict()
34with request.with_context(no_new_client={}, no_close_cursor=True)._get_eval_context(eval_context):
35 pass
36for key, value in eval_context.items():
37 globals()[key] = value
38"""
41class HelpdeskTicketUpgradeRequest(models.Model):
42 _name = "helpdesk.ticket.upgrade.request"
43 _inherit = ["mail.thread", "mail.activity.mixin", "saas.provider.upgrade.util"]
44 _description = "Helpdesk Ticket Upgrade Request"
45 _rec_name = "id"
47 _upgrade_request_states = [
48 ("on_create", "On Create"),
49 ("draft", "Draft"),
50 ("validated", "Validated"),
51 ("requested", "Requested"),
52 ("odoo_done", "Odoo Done"),
53 ("restored", "Restored"),
54 ("upgrading", "Upgrading"),
55 ("upgraded", "Upgraded"),
56 ("running_tests", "Running Tests"),
57 ("done", "Done"),
58 ("cancel", "Cancelled"),
59 ]
61 @api.model
62 def _get_stop_at_states(self):
63 """
64 Returns valid states for stop_at field.
65 Excludes initial states (on_create, draft) and cancelled state.
66 """
67 excluded = ["on_create", "draft", "cancel"]
68 return [(key, label) for key, label in self._upgrade_request_states if key not in excluded]
70 description = fields.Char()
71 original_database_id = fields.Many2one(
72 "saas.database",
73 string="Old Database",
74 domain=[("state", "=", "active")],
75 tracking=True,
76 )
77 original_database_state = fields.Selection(
78 string="Old Database State",
79 related="original_database_id.state",
80 store=True,
81 tracking=False,
82 )
83 original_main_hostname = fields.Char(
84 string="Old Main Hostname",
85 related="original_database_id.main_hostname",
86 )
87 upgraded_database_id = fields.Many2one(
88 "saas.database",
89 string="New Database",
90 tracking=True,
91 )
92 upgraded_database_state = fields.Selection(
93 string="New Database State",
94 related="upgraded_database_id.state",
95 store=True,
96 tracking=False,
97 )
98 upgraded_main_hostname = fields.Char(
99 string="New Main Hostname",
100 related="upgraded_database_id.main_hostname",
101 )
102 state = fields.Selection(
103 _upgrade_request_states,
104 required=True,
105 readonly=True,
106 default="on_create",
107 tracking=True,
108 )
109 ticket_id = fields.Many2one(
110 "helpdesk.ticket",
111 required=True,
112 readonly=True,
113 ondelete="cascade",
114 string="Upgrade",
115 )
116 upgrade_type_id = fields.Many2one(
117 "saas.upgrade.type",
118 string="Upgrade Type",
119 readonly=True,
120 )
121 upgrade_target = fields.Char(
122 related="upgrade_type_id.target",
123 )
124 analytic_account_id = fields.Many2one(
125 related="ticket_id.main_database_id.analytic_account_id",
126 )
127 hide_from_portal = fields.Boolean(
128 "Hide from Portal",
129 help="If enabled, this request will not be visible in the customer portal.",
130 default=False,
131 tracking=True,
132 )
133 odoo_request_id = fields.Char(
134 readonly=True,
135 )
136 odoo_request_token = fields.Char()
137 odoo_host_uri = fields.Char(
138 readonly=True,
139 )
140 script_ids = fields.Many2many(
141 "saas.upgrade.line",
142 compute="_compute_scripts",
143 string="Upgrade Scripts",
144 )
145 deactivated_script_ids = fields.Many2many(
146 "saas.upgrade.line",
147 "deactivated_upgrade_line_request_rel",
148 "request_id",
149 "upgrade_line_id",
150 string="Deactivated Upgrade Lines",
151 help="Upgrade lines that were deactivated in this request.",
152 )
153 automatic = fields.Boolean(
154 tracking=True,
155 help="Indicates whether this request was created automatically.",
156 )
157 planned_start = fields.Datetime(
158 readonly=True,
159 default=fields.Datetime.now,
160 )
161 end_date = fields.Datetime(
162 readonly=True,
163 )
164 total_time = fields.Float(
165 compute="_compute_total_time",
166 store=True,
167 help="Total time of the request, excluding error periods.",
168 )
169 cum_total_time = fields.Float(
170 compute="_compute_total_time",
171 store=True,
172 string="Accumulated Total Time",
173 help="Cumulative total time of the request and its predecessors.",
174 )
175 real_total_time = fields.Float(
176 compute="_compute_real_total_time",
177 store=True,
178 help="Total elapsed time from start to end, including error periods.",
179 )
180 cum_real_total_time = fields.Float(
181 compute="_compute_real_total_time",
182 store=True,
183 string="Accumulated Real Total Time",
184 help="Cumulative real total time of the request and its predecessors.",
185 )
186 aim = fields.Selection(
187 [("test", "Test"), ("production", "Production")],
188 tracking=True,
189 default="test",
190 help="If 'Test' is selected, a copy of the production database will be used "
191 "without shutting it down. If 'Production' is selected, the production "
192 "database will be switched and the current one will be used as a backup.",
193 )
194 odoo_aim = fields.Selection(
195 [("test", "Test"), ("production", "Production")],
196 default="production",
197 help="The type of upgrade requested from Odoo. Default is 'Production' to prevent "
198 "Odoo from running neutralization scripts.",
199 )
200 stop_at = fields.Selection(
201 selection=_get_stop_at_states,
202 help="When set, the automatic request will stop at this state and notify the assigned technician. "
203 "Only applicable for automatic requests.",
204 tracking=True,
205 )
206 stop_at_user_id = fields.Many2one(
207 "res.users",
208 string="Stop At User",
209 help="User who set the stop_at value and will be notified when the request reaches that state.",
210 tracking=True,
211 )
212 upgrade_line_request_run_ids = fields.One2many(
213 "saas.upgrade.line.request.run",
214 "request_id",
215 string="Upgrade Line Runs",
216 tracking=False,
217 )
218 upgrade_line_request_log_entry_ids = fields.One2many(
219 "saas.upgrade.line.request.log.entry",
220 "request_id",
221 string="Upgrade Line Log Entries",
222 tracking=False,
223 )
224 request_info = fields.Text(
225 default="{}",
226 help="Internal JSON data used to store additional request metadata.",
227 )
228 event_upgrade_id = fields.Many2one(
229 "calendar.event",
230 string="Upgrade Event",
231 help="Calendar event linked to this upgrade request.",
232 )
233 status = fields.Selection(
234 [
235 ("on_hold", "On Hold"),
236 ("running", "Running"),
237 ("error", "Error"),
238 ],
239 default="on_hold",
240 help="Current execution status of the upgrade request.",
241 )
242 upgrade_alerts = fields.Json(
243 compute="_compute_upgrade_alerts",
244 help="Contains system alerts or validation warnings related to the upgrade.",
245 )
246 with_error_log = fields.Boolean(
247 help="Indicates whether this request contains logged errors.",
248 )
249 time_running = fields.Json(
250 compute="_compute_total_time",
251 store=True,
252 help="Tracks total time the request spent running by state.",
253 )
254 time_on_hold = fields.Json(
255 compute="_compute_total_time",
256 store=True,
257 help="Tracks total time the request was on hold by state.",
258 )
259 with_original_database = fields.Boolean(
260 compute="_compute_with_database",
261 store=True,
262 help="Indicates whether there is an active related original database.",
263 )
264 with_upgraded_database = fields.Boolean(
265 compute="_compute_with_database",
266 store=True,
267 help="Indicates whether there is an active related upgraded database.",
268 )
269 with_first_original_database = fields.Boolean(
270 compute="_compute_with_database",
271 store=True,
272 help="Indicates whether there is an active related first original database.",
273 )
274 upgrade_line_versions_used = fields.Json(
275 help="Tracks which upgrade line versions were used in this request.",
276 )
277 active_production_freeze = fields.Boolean(
278 related="ticket_id.active_production_freeze",
279 help="Indicates whether the production environment is currently frozen.",
280 )
281 request_completion = fields.Float(
282 compute="_compute_request_completion",
283 recursive=True,
284 help="Percentage of completion of the upgrade request.",
285 )
286 odoo_upgrade_log_attachment_id = fields.Many2one(
287 "ir.attachment",
288 string="Odoo Upgrade Log",
289 ondelete="set null",
290 help="Attachment containing raw logs from the Odoo upgrade process.",
291 )
292 upgrade_log_attachment_id = fields.Many2one(
293 "ir.attachment",
294 string="Upgrade Log",
295 ondelete="set null",
296 help="Attachment containing raw logs of the upgrade process.",
297 )
298 upgrade_report_html = fields.Html(
299 string="Upgrade Report",
300 help="Detailed HTML report of the executed upgrade.",
301 )
302 on_by_pass = fields.Boolean(
303 compute="_compute_on_by_pass",
304 store=True,
305 precompute=True,
306 help="Indicates whether this request bypasses standard upgrade validation steps.",
307 )
308 confirm_production_sent = fields.Boolean(
309 related="ticket_id.confirm_production_sent",
310 help="Indicates whether the production confirmation has been sent.",
311 )
312 deadline_upgrade = fields.Date(
313 related="ticket_id.deadline_upgrade",
314 store=True,
315 help="Deadline for completing the upgrade.",
316 )
317 shell_command = fields.Text(
318 compute="_compute_shell_command",
319 help="Shell command to access the upgraded database environment via Odoo shell.",
320 )
321 first_original_database_id = fields.Many2one(
322 "saas.database",
323 string="First Original Database",
324 compute="_compute_first_original_database",
325 store=True,
326 readonly=True,
327 recursive=True,
328 help="Reference to the first original database in a series of upgrade requests.",
329 )
330 next_request_ids = fields.One2many(
331 "helpdesk.ticket.upgrade.request",
332 "prev_request_id",
333 string="Next Upgrade Requests",
334 )
335 next_request_id = fields.Many2one(
336 "helpdesk.ticket.upgrade.request",
337 string="Next Upgrade Request",
338 compute="_compute_next_request_id",
339 inverse="_inverse_next_request_id",
340 store=True,
341 recursive=True,
342 index=True,
343 help="Reference to the next upgrade request in sequence.",
344 )
345 prev_request_id = fields.Many2one(
346 "helpdesk.ticket.upgrade.request",
347 string="Previous Upgrade Request",
348 index=True,
349 help="Reference to the previous upgrade request in sequence.",
350 )
351 prod_backup_id = fields.Many2one(
352 "saas.cnpg.snapshot",
353 string="Production Backup",
354 help="Reference to the production backup taken before starting the upgrade.",
355 )
356 int_backup_id = fields.Many2one(
357 "saas.cnpg.snapshot",
358 string="Intermediate Backup",
359 help="Reference to the intermediate backup taken during the upgrade process.",
360 )
362 @api.model
363 def default_get(self, fields_list):
364 defaults = super().default_get(fields_list)
365 # only if the context has a default_state we set the state otherwise let the original default
366 default_state = self.env.context.get("default_state", False)
367 if default_state and "state" in fields_list:
368 defaults["state"] = default_state
369 return defaults
371 @api.onchange("stop_at")
372 def _onchange_stop_at(self):
373 """Update stop_at_user_id when stop_at is modified from UI."""
374 if self.stop_at:
375 self.stop_at_user_id = self.env.user
376 else:
377 self.stop_at_user_id = False
379 @api.depends("aim")
380 @api.depends_context("force_by_pass")
381 def _compute_on_by_pass(self):
382 for rec in self:
383 force_by_pass = self.env.context.get("force_by_pass", False)
384 rec.on_by_pass = force_by_pass if force_by_pass else rec.aim == "production"
386 @api.depends("status", "state")
387 def _compute_upgrade_alerts(self):
388 for rec in self:
389 upgrade_alerts = {}
390 if rec.state in ["done", "cancel"]:
391 rec.upgrade_alerts = upgrade_alerts
392 continue
394 has_prev_request = bool(rec.prev_request_id)
395 if has_prev_request and rec.prev_request_id.state != "done":
396 message = _("Previous request #%s is not completed.") % rec.prev_request_id.id
397 action_text = _("Go to previous request")
398 action = rec.prev_request_id.get_formview_action()
399 level = "info"
400 else:
401 if rec.status == "error":
402 action = False
403 action_text = False
404 last_error_log = rec.upgrade_line_request_log_entry_ids.filtered(
405 lambda log: log.type == "error" and not log.solved
406 )[:1]
407 if rec.with_error_log and last_error_log:
408 action_text = _("Check the log errors here")
409 action = last_error_log._get_records_action(name=_("Error Log"))
410 message = _("This request has an error!")
411 level = "danger"
412 elif rec.status == "running" and rec.automatic:
413 message = _("This request is being processed.")
414 action_text = False
415 action = False
416 level = "warning"
417 elif rec.status == "on_hold" and rec.automatic:
418 message = _("This request is waiting to be processed.")
419 action_text = False
420 action = False
421 level = "info"
422 else:
423 rec.upgrade_alerts = upgrade_alerts
424 continue
426 upgrade_alerts["request_status"] = {
427 "message": message,
428 "action_text": action_text,
429 "action": action,
430 "level": level,
431 }
432 rec.upgrade_alerts = upgrade_alerts
434 @api.depends(
435 "state",
436 "ticket_id.custom_script_ids",
437 "upgrade_type_id.upgrade_line_ids",
438 "active_production_freeze",
439 "deactivated_script_ids",
440 )
441 def _compute_scripts(self):
442 state_ul_type_map = {
443 "on_create": "0_on_create",
444 "draft": "1_evaluation",
445 "validated": "2_pre",
446 "restored": "3_pre-adhoc",
447 "upgraded": "4_post",
448 "running_tests": "5_test",
449 "done": "6_after_done",
450 }
452 ul_model = self.env["saas.upgrade.line"]
453 for rec in self:
454 ul_type = state_ul_type_map.get(rec.state)
455 if not ul_type: 455 ↛ 456line 455 didn't jump to line 456 because the condition on line 455 was never true
456 rec.script_ids = ul_model
457 continue
458 original_database_version = rec.original_database_id.odoo_version_group_id
459 domain = fields.Domain(
460 [
461 ("type", "=", ul_type),
462 ("state", "=", "approved"),
463 ("run_type", "in", [rec.aim, False]),
464 ("from_major_version_ids", "in", original_database_version.ids + [False]),
465 ]
466 )
468 if (
469 rec.active_production_freeze
470 and rec.aim == "production"
471 and not ul_model._production_freeze_ignore(ul_type)
472 ):
473 # We use the upgrade_lines in the map, then in the run function we filter by version.
474 # The map is a flat mapping of upgrade_line_id -> version.
475 try:
476 upgrade_line_versions_to_use = rec._get_upgrade_line_versions_to_use()
477 freezed_upgrade_line_ids = (
478 upgrade_line_versions_to_use and [int(id) for id in upgrade_line_versions_to_use.keys()] or []
479 )
480 except Exception as e:
481 _logger.exception("Error obtaining freezed upgrade lines for request %s: %s", rec.id, str(e))
482 freezed_upgrade_line_ids = []
484 # If 'active_production_freeze' is True and request aim is 'production' and ul_type is not ignored in freeze
485 # we restrict the 'script_ids' to run
486 domain &= fields.Domain.OR(
487 [
488 [("id", "in", freezed_upgrade_line_ids)],
489 [("run_type", "=", "production")],
490 ]
491 )
493 # Check for installed modules in the original database
494 installed_modules = set(rec.original_database_id.module_ids.mapped("name"))
496 def _script_matches_modules(script: "SaasUpgradeLine") -> bool:
497 if not script.module_ids: 497 ↛ 499line 497 didn't jump to line 499 because the condition on line 497 was always true
498 return True
499 script_module_names = set(script.module_ids.mapped("name"))
500 match script.module_check_logic:
501 case "all":
502 return script_module_names.issubset(installed_modules)
503 case "any":
504 return bool(script_module_names & installed_modules)
505 return True
507 custom_scripts = rec.ticket_id.custom_script_ids.filtered(
508 lambda ul: not ul.upgrade_type_ids or rec.upgrade_type_id in ul.upgrade_type_ids
509 )
510 upgrade_type_scripts = rec.upgrade_type_id.upgrade_line_ids.filtered(lambda ul: not ul.upgrade_ticket_ids)
511 scripts = (
512 (custom_scripts + upgrade_type_scripts)
513 .filtered(domain)
514 .filtered(lambda ul: _script_matches_modules(ul))
515 )
516 rec.script_ids = (scripts - rec.deactivated_script_ids).sorted()
518 @api.depends("original_database_id.state", "upgraded_database_id.state", "first_original_database_id.state")
519 def _compute_with_database(self):
520 valid_states = {"active", "inactive", "processing", "editing"}
521 for rec in self:
522 rec.with_original_database = rec.original_database_id.state in valid_states
523 rec.with_upgraded_database = rec.upgraded_database_id.state in valid_states
524 rec.with_first_original_database = rec.first_original_database_id.state in valid_states
526 @api.depends("end_date", "planned_start", "prev_request_id", "prev_request_id.end_date")
527 def _compute_real_total_time(self):
528 for rec in self:
529 if rec.end_date and rec.planned_start: 529 ↛ 530line 529 didn't jump to line 530 because the condition on line 529 was never true
530 rec.real_total_time = ((rec.end_date - rec.planned_start).total_seconds()) / 3600.0
531 prev_requests = rec._get_previous_requests()
532 rec.cum_real_total_time = rec.real_total_time + sum(prev_requests.mapped("real_total_time"))
534 @api.depends("state", "prev_request_id", "prev_request_id.end_date")
535 def _compute_total_time(self):
536 states = [x[0] for x in self._fields["state"].selection]
537 for rec in self.filtered(lambda r: not r.end_date):
538 # Singular total time logic
539 state_index = states.index(rec.state)
540 next_states = states[state_index:]
541 time_on_hold: dict = rec.time_on_hold or {}
542 time_running: dict = rec.time_running or {}
543 time_on_hold_keys = set(time_on_hold.keys()) if time_on_hold else set()
544 time_running_keys = set(time_running.keys()) if time_running else set()
545 for key in time_on_hold_keys | time_running_keys: 545 ↛ 546line 545 didn't jump to line 546 because the loop on line 545 never started
546 if key not in next_states:
547 continue
548 time_on_hold[key] = 0
549 time_running[key] = 0
551 rec.time_on_hold = time_on_hold
552 rec.time_running = time_running
553 time = 0
554 if rec.time_on_hold: 554 ↛ 555line 554 didn't jump to line 555 because the condition on line 554 was never true
555 time += sum(rec.time_on_hold.values())
556 if rec.time_running: 556 ↛ 557line 556 didn't jump to line 557 because the condition on line 556 was never true
557 time += sum(rec.time_running.values())
558 rec.total_time = time / 3600.0
560 # Accumulated total time logic
561 prev_requests = rec._get_previous_requests()
562 rec.cum_total_time = rec.total_time + sum(prev_requests.mapped("total_time"))
564 @api.depends("state", "prev_request_id", "prev_request_id.request_completion")
565 def _compute_request_completion(self):
566 states = [x[0] for x in self._upgrade_request_states if x[0] not in ["on_create", "cancel"]]
567 states_len = len(states)
568 for rec in self:
569 if rec.state in ["on_create", "cancel"]:
570 rec.request_completion = 0.0
571 continue
572 rec_len = states.index(rec.state)
573 prev_requests = rec._get_previous_requests()
574 current_completion = round(rec_len / (states_len - 1), 2)
575 total_prev_completion = sum(prev_requests.mapped("request_completion"))
576 rec.request_completion = (current_completion + total_prev_completion) / (len(prev_requests) + 1)
578 @api.depends("with_upgraded_database")
579 def _compute_shell_command(self):
580 for rec in self:
581 no_new_client = not rec.with_upgraded_database
582 rec.shell_command = dedent(SHELL_COMMAND_TEMPLATE.format(rec.id, no_new_client)).strip()
584 @api.depends("prev_request_id", "prev_request_id.first_original_database_id", "original_database_id")
585 def _compute_first_original_database(self):
586 for rec in self:
587 if rec.prev_request_id:
588 rec.first_original_database_id = rec.prev_request_id.first_original_database_id
589 else:
590 rec.first_original_database_id = rec.original_database_id
592 @api.depends("next_request_ids")
593 def _compute_next_request_id(self):
594 for rec in self:
595 rec.next_request_id = rec.next_request_ids[:1]
597 def _inverse_next_request_id(self):
598 for rec in self:
599 if rec.next_request_id:
600 rec.next_request_id.prev_request_id = rec
602 @api.constrains("ticket_id", "original_database_id", "upgrade_type_id")
603 def _check_databases_version(self):
604 for rec in self:
605 original_database_version = rec.original_database_id.odoo_version_group_id
606 target_version = rec.upgrade_type_id.target_odoo_version_group_id
607 if original_database_version and (original_database_version.sequence <= target_version.sequence):
608 # Sequence is used descending, so we check if the original version is lower than the target version
609 # 18.0 -> sequence 1, 17.0 -> sequence 2
610 raise ValidationError(
611 _("The original database version must be lower than the upgraded database version.")
612 )
614 @api.ondelete(at_uninstall=False)
615 def _check_aim_before_deletion(self):
616 if self.env.context.get("bypass_delete", False):
617 return
618 for rec in self:
619 if rec.aim == "production":
620 raise UserError(
621 _("Cannot delete 'Production' type requests. If necessary, cancel this one and schedule a new one.")
622 )
624 @api.model_create_multi
625 def create(self, vals_list):
626 from_create_request = self.env.context.get("from_create_request", False)
627 if not from_create_request: 627 ↛ 628line 627 didn't jump to line 628 because the condition on line 627 was never true
628 raise UserError(
629 _("Upgrade requests can only be created via the designated creation method '_create_request'.")
630 )
632 res = super().create(vals_list)
633 for rec in res:
634 rec.run_on_create_scripts()
635 if rec.state == "on_create":
636 rec._next_state()
637 return res
639 def write(self, vals):
640 state = vals.get("state", False)
641 vals_upgraded_database_id = vals.get("upgraded_database_id", False)
642 vals_original_database_id = vals.get("original_database_id", False)
643 automatic = vals.get("automatic", False)
644 for rec in self.sudo():
645 runs = self.env["saas.upgrade.line.request.run"]
646 if rec.aim == "production" and state == "cancel" and rec.state != "draft": 646 ↛ 647line 646 didn't jump to line 647 because the condition on line 646 was never true
647 raise UserError(_("Only upgrade requests in draft can be cancelled."))
648 if vals_original_database_id and rec.original_database_id.id != vals_original_database_id: 648 ↛ 649line 648 didn't jump to line 649 because the condition on line 648 was never true
649 runs = rec.upgrade_line_request_run_ids
650 elif vals_upgraded_database_id and rec.upgraded_database_id.id != vals_upgraded_database_id: 650 ↛ 651line 650 didn't jump to line 651 because the condition on line 650 was never true
651 rec._delete_log_attachment("upgrade_log_attachment_id")
652 runs |= rec.upgrade_line_request_run_ids.filtered(
653 lambda u: u.upgrade_line_id.type in ["3_pre-adhoc", "4_post", "5_test", "6_after_done"]
654 )
655 if rec.automatic and "automatic" in vals and not automatic:
656 runs |= rec.upgrade_line_request_run_ids.filtered(lambda r: r.state in ["pending", "waiting"])
657 runs.write({"request_id": False, "state": False})
658 runs.mapped("bg_job_id").cancel(self.env._("Upgrade request is no longer valid."))
659 res = super().write(vals)
660 for rec in self.sudo():
661 # if we cancel request execute the method to completen the process
662 if state == "cancel" and rec.state == "cancel":
663 rec._process_cancel_upgrade_request()
664 if not self.env.context.get("cancel_cascade"):
665 related_requests = rec._get_previous_requests() | rec._get_next_requests()
666 related_requests.with_context(cancel_cascade=True).state = "cancel"
667 return res
669 def _create_log_attachment(
670 self, field_name: str, file_content: str | bytes, filename: str, description: str
671 ) -> None:
672 """
673 Create or update log file attachment in the chatter and link it to the given field.
675 :param field_name: Name of the Many2one field to link (e.g. 'upgrade_log_attachment_id')
676 :param file_content: Base64 encoded file content (str or bytes)
677 :param filename: Name of the attachment file
678 :param description: Description for the attachment
679 """
680 self.ensure_one()
681 existing = self[field_name]
682 if existing:
683 existing.write({"datas": file_content, "name": filename})
684 else:
685 attachment = self.env["ir.attachment"].create(
686 {
687 "name": filename,
688 "datas": file_content,
689 "res_model": self._name,
690 "res_id": self.id,
691 "description": description,
692 "type": "binary",
693 }
694 )
695 self[field_name] = attachment
697 def _delete_log_attachment(self, field_name: str) -> None:
698 """
699 Delete log file attachment from the chatter and clear the field.
701 :param field_name: Name of the Many2one field to clear (e.g. 'upgrade_log_attachment_id')
702 """
703 self.ensure_one()
704 if self[field_name]:
705 self[field_name].unlink()
707 def web_read(self, specification):
708 fields_to_read = list(specification) or ["id"]
709 if "script_ids" in fields_to_read and "context" in specification["script_ids"]:
710 specification["script_ids"]["context"].update({"ticket_request_id": self._ids[:1]})
711 if "deactivated_script_ids" in fields_to_read and "context" in specification["deactivated_script_ids"]:
712 specification["deactivated_script_ids"]["context"].update(
713 {"ticket_request_id": self._ids[:1], "deactivated_scripts": True}
714 )
715 return super().web_read(specification)
717 def action_login_with_user(self):
718 database = self.env.context.get("database")
719 if database:
720 return self[database].action_login_with_user()
722 def action_choose_user_login(self):
723 database = self.env.context.get("database")
724 if database:
725 return self[database].action_choose_user_login()
727 @api.model
728 def action_log_in(self) -> dict:
729 database_id = self.env["saas.database"].browse(self.env.context.get("database_id"))
730 if not database_id:
731 title = self.env._("Access Error")
732 message = self.env._(
733 "An error occurred while trying to log into the database, please try again. "
734 "If the problem persists, contact support."
735 )
736 action_to_return = self.build_display_notification_action(title, message, "warning")
737 return action_to_return
738 user_list = (
739 self.env["saas.database.user_list"]
740 .sudo()
741 .with_context(portal=True)
742 .create({"database_id": database_id.active_subscription_id.main_database_id.id})
743 )
744 user = user_list.user_ids.filtered(lambda x: x.saas_provider_uuid == self.env.user.saas_uuid)
745 return {
746 "type": "ir.actions.act_url",
747 "target": "new",
748 "url": database_id._get_login_url(user.login),
749 }
751 @api.model
752 def action_log_in_as(self) -> dict:
753 database_id = self.env["saas.database"].browse(self.env.context.get("database_id"))
754 if not database_id:
755 title = self.env._("Access Error")
756 message = self.env._(
757 "An error occurred while trying to log into the database, please try again. "
758 "If the problem persists, contact support."
759 )
760 action_to_return = self.build_display_notification_action(title, message, "warning")
761 return action_to_return
763 return {
764 "type": "ir.actions.act_url",
765 "target": "new",
766 "url": database_id.get_portal_url(suffix="/users"),
767 }
769 def action_open_upgrade_lines(self):
770 self.ensure_one()
771 action = self.env["ir.actions.act_window"]._for_xml_id("saas_provider_upgrade.action_saas_upgrade_line")
772 action["domain"] = [["id", "in", self.script_ids.ids]]
773 action["context"] = {"ticket_request_id": self.id}
774 return action
776 def action_open_running_status(self):
777 self.ensure_one()
778 action = self.env["ir.actions.act_window"]._for_xml_id(
779 "saas_provider_upgrade.action_saas_upgrade_line_request_run"
780 )
781 action["domain"] = [["request_id", "=", self.id]]
782 action["context"] = {
783 "search_default_applicable": True,
784 "search_default_group_by_type": True,
785 "default_request_id": self.id,
786 }
787 action["views"] = [(False, "list"), (False, "form")]
788 return action
790 def action_cancel(self):
791 self.ensure_one()
792 if self.state == "cancel":
793 return
794 self.state = "cancel"
796 def action_open_customer_notes(self) -> dict:
797 self.ensure_one()
798 action = self.env["ir.actions.act_window"]._for_xml_id(
799 "saas_provider_upgrade.action_helpdesk_ticket_customer_note"
800 )
801 action["domain"] = [["ticket_id", "=", self.ticket_id.id], ["upgrade_type_id", "=", self.upgrade_type_id.id]]
802 return action
804 def action_set_request(self):
805 self.ensure_one()
806 upgrade_line_id = self.env.context.get("upgrade_line_id", False)
807 action = None
808 if upgrade_line_id:
809 upgrade_line = self.env["saas.upgrade.line"].browse(upgrade_line_id)
810 try:
811 old_client, new_client = self._get_databases_and_clients(attempts=1, sleep=1, timeout=5)[2:]
812 except Exception:
813 raise UserError(
814 _("Cannot obtain client databases. Ensure both databases are active and without errors.")
815 )
817 if not upgrade_line.with_context(no_raise_on_filter_error=True, ticket_request_id=self.id)._applies(
818 old_client, new_client
819 ):
820 raise UserError(_("This script does not apply to this request."))
822 action: dict = self.env["ir.actions.actions"]._for_xml_id("saas_provider_upgrade.action_saas_upgrade_line")
823 form_view = [(self.env.ref("saas_provider_upgrade.view_saas_upgrade_line_form").id, "form")]
824 if "views" in action:
825 action["views"] = form_view + [(state, view) for state, view in action["views"] if view != "form"]
826 else:
827 action["views"] = form_view
828 action["res_id"] = upgrade_line_id
829 new_context: dict = literal_eval(action.get("context", "{}"))
830 new_context.update({"ticket_request_id": self.id})
831 action["context"] = new_context
832 return action
834 def action_activate(self):
835 self.ensure_one()
836 request = self
837 automatic = not request.automatic
838 request.automatic = automatic
839 request._on_hold()
840 if not request.automatic:
841 # Cancel pending tasks if we pause the process
842 if request.original_database_id:
843 request.original_database_id.action_cancel_task()
844 if request.upgraded_database_id:
845 request.upgraded_database_id.action_cancel_task()
847 # Sync the change to the whole chain
848 chained_requests = (request._get_previous_requests() | request._get_next_requests()).filtered(
849 lambda r: r.state not in ["done", "cancel"]
850 )
851 for request in chained_requests:
852 if request.automatic == automatic:
853 continue
854 request.automatic = automatic
855 request._on_hold()
857 def action_open_request(self) -> dict:
858 self.ensure_one()
859 return {
860 "type": "ir.actions.act_window",
861 "name": "Database Upgrade Request",
862 "res_model": self._name,
863 "view_mode": "form",
864 "res_id": self.id,
865 }
867 def action_open_chained_requests(self) -> dict:
868 self.ensure_one()
869 request = self
870 # Go to the first request
871 while request.prev_request_id:
872 request = request.prev_request_id
874 # Collect all requests in the chain
875 chained_requests = request | request._get_next_requests()
876 return {
877 "name": _("Chained Requests"),
878 "type": "ir.actions.act_window",
879 "res_model": self._name,
880 "view_mode": "list,form",
881 "views": [
882 (self.env.ref("saas_provider_upgrade.view_helpdesk_ticket_upgrade_chained_requests_list").id, "list"),
883 (False, "form"),
884 ],
885 "domain": [("id", "in", chained_requests.ids)],
886 }
888 def action_open_form_or_chained(self) -> dict:
889 """
890 Action to open either the current request form or the chained requests view
891 """
892 self.ensure_one()
893 if not self.prev_request_id and not self.next_request_id:
894 return self.action_open_request()
895 return self.action_open_chained_requests()
897 def action_toggle_portal_visibility(self):
898 """
899 Toggle the hide_from_portal field value.
900 Allows quick toggling of portal visibility directly from the list view.
901 """
902 for rec in self:
903 rec.hide_from_portal = not rec.hide_from_portal
904 return True
906 def action_rollback(self):
907 """
908 Rollback the upgrade request using 'upgrade-rollback' task.
909 """
910 self.ensure_one()
911 if self.aim != "production":
912 raise UserError(self.env._("Only 'Production' aim requests can be rolled back."))
914 main_database = self.ticket_id.main_database_id
915 if main_database.state != "inactive" or not main_database.is_production:
916 raise UserError(
917 self.env._("Main database must be inactive and not a production copy to perform a rollback.")
918 )
920 main_database._schedule_task("upgrade-rollback", helpdesk_ticket_upgrade_request_id=self.id)
922 @api.model
923 def _create_request(
924 self,
925 ticket_id: int,
926 aim: Literal["test", "production"] = "test",
927 odoo_aim: Literal["test", "production"] = "production",
928 date_start: bool = False,
929 automatic: bool = False,
930 hide_from_portal: bool = False,
931 event_upgrade_id: int = False,
932 bypass: bool = False,
933 stop_at: str | bool = False,
934 ) -> "HelpdeskTicketUpgradeRequest":
935 """
936 This method is used to generate upgrade requests from wherever it is required by sending the parameters.
938 :param ticket_id: ID of the ticket
939 :param aim: Upgrade aim (test or production)
940 :param odoo_aim: Odoo upgrade aim (test or production)
941 :param date_start: Start/planned date
942 :param automatic: If the request is automatic
943 :param hide_from_portal: If the request should be hidden from portal
944 :param event_upgrade_id: If there is an associated calendar event
945 :param bypass: If we bypass the validations
946 :param stop_at: State at which to stop the automatic request
947 :return: The last created request
948 """
949 ticket = self.env["helpdesk.ticket"].browse(ticket_id)
950 if ticket.ticket_upgrade_status == "upgraded": 950 ↛ 951line 950 didn't jump to line 951 because the condition on line 950 was never true
951 return self.env["helpdesk.ticket.upgrade.request"]
952 from_version = ticket.main_database_id.odoo_version_group_id
953 target_version = ticket.upgrade_type_id.target_odoo_version_group_id
954 steps = self.env["saas.upgrade.type"].calculate_upgrade_steps(from_version, target_version)
955 prev_request = self.env["helpdesk.ticket.upgrade.request"]
956 for step in steps:
957 create_vals = {
958 "ticket_id": ticket_id,
959 "upgrade_type_id": step.id,
960 "aim": aim,
961 "odoo_aim": odoo_aim,
962 "automatic": automatic,
963 "hide_from_portal": hide_from_portal,
964 "event_upgrade_id": event_upgrade_id,
965 "prev_request_id": prev_request.id,
966 }
967 if not prev_request.id:
968 create_vals["planned_start"] = date_start or fields.Datetime.now()
969 create_vals["stop_at"] = stop_at
970 if stop_at:
971 create_vals["stop_at_user_id"] = self.env.user.id
972 current_request = (
973 self.env["helpdesk.ticket.upgrade.request"]
974 .sudo()
975 .with_context(force_by_pass=bypass, from_create_request=True)
976 .create(create_vals)
977 )
978 prev_request = current_request
980 conditions = [
981 (
982 aim == "production" and ticket.upgrade_pull_request_ids.filtered(lambda p: p.state == "open"),
983 _("IMPORTANT the ticket has unmerged PRs, remember to merge them before upgrading to production."),
984 ),
985 ]
986 body = "<br/>".join([message for condition, message in conditions if condition])
987 if body: 987 ↛ 988line 987 didn't jump to line 988 because the condition on line 987 was never true
988 ticket.message_post(
989 body=Markup(body),
990 partner_ids=ticket.assigned_technician_id.partner_id.ids,
991 )
992 return current_request
994 def run_on_create_scripts(self):
995 """
996 Runs the on_create scripts when the request is created.
997 If we get a result we raise an error and the request is not created.
998 """
999 self.ensure_one()
1000 if self.state == "on_create":
1001 if self.on_by_pass:
1002 script_ids = self.script_ids.filtered(lambda s: not s.allow_bypass)
1003 if self.aim == "production" and self.env.user.has_group( 1003 ↛ 1006line 1003 didn't jump to line 1006 because the condition on line 1003 was never true
1004 "saas_provider_upgrade.group_upgrade_advanced_user"
1005 ):
1006 script_ids = self.env["saas.upgrade.line"]
1007 else:
1008 script_ids = self.script_ids
1009 for script in script_ids:
1010 script.run(self)
1012 def process_automatic_requests(self):
1013 """
1014 Process automatic requests, checking the state, a condition, and running the corresponding method
1015 """
1016 state_method_map: dict[str, Callable[[HelpdeskTicketUpgradeRequest], Any]] = {
1017 "draft": lambda r: r.evaluate(),
1018 "validated": lambda r: r.request_to_odoo(),
1019 "requested": lambda r: r.check_odoo_upgrade_state(message_post=False),
1020 "odoo_done": lambda r: r.restore(),
1021 "restored": lambda r: r.run_pre_adhoc(),
1022 "upgrading": lambda r: r.run_upgrade(),
1023 "upgraded": lambda r: r.finish_upgrade(),
1024 "running_tests": lambda r: r.run_upgrade_tests(),
1025 "done": lambda r: r.run_after_done(),
1026 }
1028 for rec in self.sorted(key="aim", reverse=True):
1029 try:
1030 method = state_method_map.get(rec.state)
1031 method(rec)
1032 except Exception as e:
1033 self.env.cr.rollback()
1034 _logger.exception("Error processing upgrade request %s in state %s: %s", rec.id, rec.state, str(e))
1035 rec._set_error_and_notify(e)
1037 def evaluate(self):
1038 """
1039 Generates the old database from the production database and runs the Evaluation scripts on it
1040 """
1041 self.ensure_one()
1042 upgrade_type = self.upgrade_type_id
1043 main_database = self.ticket_id.project_id.main_database_id or self.ticket_id.main_database_id
1044 self = self.with_context(lang="es_419")
1045 if not main_database:
1046 self._error()
1047 raise UserError(self.env._("Could not upgrade database, no main database found"))
1049 aim = self.aim
1050 if not self.prev_request_id and (not self.original_database_id or self.original_database_id.state == "deleted"):
1051 if aim == "production":
1052 if main_database.state == "active":
1053 expected_time = self.ticket_id._get_estimated_production_upgrade_duration(measure="minutes")
1054 # Here we directly call the method triggered by a task to do it synchronously, since we need this to finish before continuing with the rest.
1055 # This implies having to send the inactive_mode and eta values via task_kwargs, so that it can be retrieved and then the database state changed.
1056 task_kwargs = main_database.task_kwargs or {}
1057 task_kwargs.update({"inactive_mode": "upgrade", "eta": int(time.time()) + expected_time * 60})
1058 main_database.task_kwargs = task_kwargs
1059 main_database._inactivate()
1060 main_database.task_kwargs = {}
1061 # Notify the start of the upgrade request
1062 user = self.ticket_id.assigned_technician_id or self.ticket_id.user_id
1063 hours, minutes = divmod(expected_time, 60)
1064 minutes = str(minutes).zfill(2)
1065 self.urgent_communication(
1066 self.env._(
1067 "The upgrade request for %s has started.\n Responsible: %s.\n Estimated time for the upgrade: %s:%s hs"
1068 )
1069 % (self.ticket_id.name, user.name, hours, minutes)
1070 )
1071 if main_database.state != "inactive":
1072 return None
1074 original_database_type = upgrade_type.original_database_type_id
1075 context = {"bypass_check_max_dbs": self.on_by_pass}
1076 # If the database has 'disable_duplicate' we need to restore the last backup,
1077 # we cannot restore the productive database
1078 # TODO remove IF after boggio migracion
1079 if main_database.disable_duplicate and aim == "test":
1080 backup_list = (
1081 self.env["saas.backup_list"]
1082 .with_context(
1083 analytic_account_id=main_database.analytic_account_id.id,
1084 environment=main_database.environment_id,
1085 odoo_version=main_database.odoo_version_id,
1086 database_type=original_database_type,
1087 )
1088 .create({})
1089 )
1090 if backup_list:
1091 last_backup = backup_list.line_ids.sorted("date", reverse=True)[0]
1092 self.original_database_id = last_backup.with_context(**context).action_restore()["res_id"]
1093 else:
1094 raise UserError(
1095 _(
1096 "Could not generate old database, no backups found! Remember this database has 'disable_duplicate'."
1097 )
1098 )
1099 else:
1100 if aim == "production":
1101 # In production do not force neutralize
1102 context["force_neutralize"] = False
1103 default = {"database_type_id": original_database_type.id}
1104 self.original_database_id = main_database.with_context(**context)._action_duplicate(default)
1105 task_kwargs = self.original_database_id.task_kwargs or {}
1106 task_kwargs.update(
1107 {
1108 "helpdesk_ticket_upgrade_request_id": self.id,
1109 "state_on_success": "editing",
1110 }
1111 )
1112 self.original_database_id.task_kwargs = task_kwargs
1114 if self.original_database_id.state in ["active", "inactive", "editing"]:
1115 if not self.prev_request_id:
1116 # Only index installed modules if this is the first request in the chain
1117 error_msgs = self.first_original_database_id._get_installed_modules()
1118 if error_msgs:
1119 error = _(
1120 "Cannot get installed modules from the original database: <blockquote>%s</blockquote>"
1121 ) % ("<br/>".join(error_msgs.mapped("body")))
1122 self.message_post(body=Markup(error))
1123 if aim == "production":
1124 # Inactivate the main database in the first request of a production upgrade
1125 self.ticket_id.main_database_id._schedule_task(
1126 "upgrade-prod-backup",
1127 helpdesk_ticket_upgrade_request_id=self.id,
1128 state_on_success="inactive",
1129 )
1130 self._running()
1131 return self._chain_scripts_execution(self.script_ids, callback="_next_state")
1133 def request_to_odoo(self):
1134 """
1135 Runs the Pre-Odoo scripts and send the old database to Odoo for upgrade
1136 """
1137 self.ensure_one()
1138 self._running()
1139 if self.aim == "production" and not self.prev_request_id and not self.original_database_id.protected:
1140 # Before sending to odoo we protect the original database
1141 self.original_database_id.write(
1142 {
1143 "protect_reason": "Original database for upgrade request %s" % self.id,
1144 "protect_till_date": self.original_database_id.drop_date,
1145 }
1146 )
1147 self._delete_log_attachment("odoo_upgrade_log_attachment_id")
1148 return self._chain_scripts_execution(self.script_ids, callback="_request_to_odoo_callback")
1150 def _request_to_odoo_callback(self):
1151 """
1152 Callback used when we finish the pre-odoo scripts to send the request to odoo
1153 """
1154 # Reset the request key every send a request to odoo
1155 self.odoo_request_token = False
1156 self.original_database_id._schedule_task("odooupgrade", helpdesk_ticket_upgrade_request_id=self.id)
1158 def check_odoo_upgrade_state(self, log_lines_qty: int = 50, message_post: bool = True):
1159 """
1160 Checks the upgrade state on the Odoo upgrade
1162 :param log_lines_qty: Number of log lines to retrieve in case of failure
1163 :param message_post: Whether to post a message in the ticket or raise an error
1164 """
1165 self._running()
1166 response = self._get_odoo_upgrade_request_status()
1167 status = response.get("status")
1168 log_lines: str = self._get_odoo_upgrade_request_logs()
1169 match status:
1170 case "failed" | "cancelled":
1171 log_list: list = log_lines.split("\n")[-log_lines_qty:] # Take the last "log_lines_qty" from the log
1172 log: str = "<br/>".join(log_list)
1173 if not message_post:
1174 raise UserError(log)
1175 self._error()
1176 body = self.create_message(_("Odoo Upgrade failed!"), log)
1177 case "done":
1178 self._next_state()
1179 body = _("Upgrade done!")
1180 case _:
1181 self._on_hold()
1182 body = _("Upgrade status: %s") % status
1184 # Save complete logs as attachment
1185 if status in ["failed", "cancelled", "done"]:
1186 encoded_log = base64.b64encode(log_lines.encode("utf-8"))
1187 log_filename = f"odoo_log_{self.odoo_request_id}.txt"
1188 self._create_log_attachment(
1189 "odoo_upgrade_log_attachment_id",
1190 encoded_log,
1191 log_filename,
1192 "Odoo Upgrade Log",
1193 )
1195 if message_post:
1196 self.message_post(body=body)
1198 def restore(self):
1199 """
1200 Restores the new database from Odoo
1201 """
1202 self.ensure_one()
1203 self._running()
1204 response = self._get_odoo_upgrade_request_status()
1205 if response.get("status") not in ["done"] or response.get("archived"):
1206 raise UserError(
1207 _(
1208 "Cannot proceed because the request is no longer in Odoo Done or has been archived. It must be sent to Odoo again."
1209 )
1210 )
1211 # Create the upgraded database record, if it doesn't exist
1212 if not self.upgraded_database_id:
1213 analytic_account = (
1214 self.ticket_id.main_database_id
1215 and self.ticket_id.main_database_id.analytic_account_id
1216 or self.ticket_id.project_id.account_id
1217 )
1218 up_type = self.upgrade_type_id
1219 version = self.upgrade_type_id.target_odoo_version_group_id._get_latest_version(False)
1220 version = up_type.new_version_id or version
1221 environment = up_type.new_environment_id or False
1222 if self.on_by_pass:
1223 analytic_account = analytic_account.with_context(bypass_check_max_dbs=True)
1225 is_intermediate = bool(self.next_request_id)
1226 database_type = (
1227 up_type.upgraded_database_type_id if not is_intermediate else up_type.intermediate_database_type_id
1228 )
1229 self.upgraded_database_id = analytic_account.create_database(
1230 database_type=database_type,
1231 environment=environment,
1232 odoo_version=version,
1233 )
1234 # We use isdigit() to include all PRs that do not depend on a version
1235 ticket_pulls = self.ticket_id.upgrade_pull_request_ids.filtered(
1236 lambda p: not p.head_branch.replace(".", "").isdigit() or p.head_branch == up_type.target
1237 )
1238 self.upgraded_database_id.pull_ids = (up_type.pull_ids | ticket_pulls).filtered(
1239 lambda r: r.state not in ["dirty"]
1240 )
1241 # Schedule the restore from Odoo
1242 state_on_success = "editing" if not is_intermediate else "active"
1243 self.upgraded_database_id._schedule_task(
1244 "odoorestore", helpdesk_ticket_upgrade_request_id=self.id, state_on_success=state_on_success
1245 )
1246 else:
1247 if self.upgraded_database_id.state in ["editing", "active"]:
1248 self._next_state()
1249 else:
1250 raise UserError(self.env._("Database is not in a valid state for restoration."))
1252 def run_pre_adhoc(self):
1253 """
1254 Runs Pre-Adhoc scripts
1255 """
1256 self.ensure_one()
1257 self._running()
1258 return self._chain_scripts_execution(self.script_ids, callback="_next_state")
1260 def run_upgrade(self):
1261 """
1262 Starts the upgrade process on the database
1263 """
1264 self.ensure_one()
1265 if self.upgrade_log_attachment_id:
1266 raise UserError(
1267 _(
1268 "Cannot run upgrade process again, the request already has a log file. Please check the logs and restore the database if needed."
1269 )
1270 )
1271 self._running()
1272 # When the tasks finishes, it will call _next_state() or _error()
1273 state_on_success = "editing" if not self.next_request_id else self.upgraded_database_id.state
1274 self.upgraded_database_id._schedule_task(
1275 "obaupgrade", helpdesk_ticket_upgrade_request_id=self.id, state_on_success=state_on_success
1276 )
1278 def finish_upgrade(self):
1279 """
1280 Runs the Post-Adhoc scripts, fix the database and finish the upgrade
1281 """
1282 self.ensure_one()
1283 self._running()
1284 return self._chain_scripts_execution(self.script_ids, callback="_finish_upgrade_callback")
1286 def _finish_upgrade_callback(self):
1287 """
1288 Callback used when we finish the post-adhoc scripts to fix the database and finish the upgrade
1289 """
1290 if self.upgraded_database_id.environment_id.bucket_config_id:
1291 self.upgraded_database_id._ensure_bucket()
1292 if not self.next_request_id:
1293 self.upgraded_database_id._schedule_task(
1294 "postupgrade",
1295 helpdesk_ticket_upgrade_request_id=self.id,
1296 state_on_success=self.upgraded_database_id.state,
1297 )
1298 else:
1299 self._next_state()
1301 def run_upgrade_tests(self):
1302 """
1303 Runs the Tests scripts.
1304 """
1305 self.ensure_one()
1306 self._running()
1307 return self._chain_scripts_execution(self.script_ids, callback="_next_state")
1309 def run_after_done(self):
1310 """
1311 Runs After-Done scripts
1312 """
1313 self.ensure_one()
1314 self._running()
1315 callback = "_run_after_done_callback"
1316 if self.aim == "production" and not self.next_request_id:
1317 main_database = self.ticket_id.main_database_id
1318 # The 'after-done' scripts are executed in the last step of the task, on the new production database
1319 match main_database.state:
1320 case "deleted":
1321 self.upgraded_database_id._action_duplicate_as_prod(self, callback=callback)
1322 return
1323 case "inactive":
1324 main_database._schedule_task(
1325 "upgrade-done",
1326 state_on_success="deleted",
1327 helpdesk_ticket_upgrade_request_id=self.id,
1328 callback=callback,
1329 )
1330 return
1331 case _:
1332 _logger.warning(
1333 "The upgrade request %s is in an inconsistent state, the main database is in state '%s' when it should be 'inactive' or 'deleted'.",
1334 self.id,
1335 main_database.state,
1336 )
1337 pass
1339 self._chain_scripts_execution(self.script_ids, callback=callback)
1341 def _run_after_done_callback(self):
1342 """
1343 Callback used when we finish the after-done scripts to finish the request
1344 """
1345 if not self.end_date:
1346 self.write({"end_date": fields.Datetime.now()})
1348 self.automatic = False
1349 self._on_hold() # _on_hold propagates to the next requests
1350 original_database = self.original_database_id
1351 upgraded_database = self.upgraded_database_id
1352 next_request = self.next_request_id
1353 if original_database.state == "editing":
1354 original_database.apply_update()
1355 if upgraded_database.state == "editing" and not next_request:
1356 upgraded_database.apply_update()
1358 if next_request:
1359 upgraded_database._schedule_task(
1360 "upgrade-int-backup",
1361 helpdesk_ticket_upgrade_request_id=self.id,
1362 state_on_success="active",
1363 )
1364 next_request.write(
1365 {
1366 "original_database_id": upgraded_database.id,
1367 "planned_start": fields.Datetime.now(),
1368 }
1369 )
1371 def run_after_error(self):
1372 """
1373 Runs the After-Error scripts
1374 """
1375 self.ensure_one()
1376 # Disable automatic so that cron doesn't try to run it again
1377 self.automatic = False
1378 self.upgrade_type_id.run_after_error_script(self)
1380 def _chain_scripts_execution(self, scripts: "SaasUpgradeLine", callback: str):
1381 """
1382 Given a request and a list of scripts, create the runs to execute them in order
1384 :param scripts: List of scripts to create the runs for
1385 :param callback: Method to call when the chain is finished
1386 """
1387 first_run_id = self.env["saas.upgrade.line.request.run"]
1388 last_run_id = self.env["saas.upgrade.line.request.run"]
1389 scripts = scripts.with_context(ticket_request_id=self.id) # To compute run_info_ids
1390 # Resolve clients once to avoid repeated get_client_cluster() calls per script
1391 try:
1392 old_client, new_client = self._get_databases_and_clients(timeout=5)[2:]
1393 except Exception:
1394 raise UserError(
1395 self.env._(
1396 "Cannot obtain client databases. Ensure databases are active. We cannot validate the scripts applicability."
1397 )
1398 )
1399 for script in scripts.filtered(lambda s: not s.run_info_id or s.run_info_id.state != "done"):
1400 script._create_run_info_id()
1401 if not script._applies(old_client, new_client):
1402 script.run_info_id.set_not_applicable()
1403 customer_notes = self.ticket_id.customer_note_ids.filtered(lambda c: c.upgrade_line_id == script)
1404 if customer_notes:
1405 customer_notes.write({"status": "done"})
1406 continue
1408 script.run_info_id.write(
1409 {
1410 "state": "waiting",
1411 }
1412 )
1413 if last_run_id:
1414 last_run_id.next_run_id = script.run_info_id.id
1415 if not first_run_id:
1416 first_run_id = script.run_info_id
1417 last_run_id = script.run_info_id
1419 if not first_run_id:
1420 getattr(self, callback)() # If no runs were created, we call the callback directly
1421 else:
1422 if last_run_id.upgrade_line_id.execution_mode == "odoo_shell":
1423 # If the last run is a job, we need to create a dummy run to hold the callback
1424 # because sometimes callbacks also call jobs, and we can't have a job calling another job
1425 dummy_run = self.env["saas.upgrade.line.request.run"].create(
1426 {
1427 "request_id": self.id,
1428 "state": "waiting",
1429 }
1430 )
1431 last_run_id.next_run_id = dummy_run.id
1432 last_run_id = dummy_run
1433 last_run_id.callback = callback # We set the callback to the last run
1434 first_run_id.enqueue(in_chain=True) # We enqueue the first run to start the chain
1436 def _process_cancel_upgrade_request(self):
1437 """
1438 This process the cancel of the upgrade request to send the notification to
1439 the customer and cancel event of upgrade if exists
1440 """
1441 self.ensure_one()
1442 user_has_portal = self.env.user.has_group("base.group_portal")
1443 if self.aim == "production":
1444 self = self.sudo()
1445 if user_has_portal: 1445 ↛ 1446line 1445 didn't jump to line 1446 because the condition on line 1445 was never true
1446 partners_to_notify = (
1447 self.ticket_id.user_id.partner_id | self.ticket_id.assigned_technician_id.partner_id
1448 )
1449 self.ticket_id.message_post(
1450 body=_("The customer has canceled the upgrade from the portal."),
1451 partner_ids=partners_to_notify.ids,
1452 )
1453 if self.event_upgrade_id: 1453 ↛ 1458line 1453 didn't jump to line 1458 because the condition on line 1453 was always true
1454 self.event_upgrade_id.action_cancel_meeting(
1455 partner_ids=self.event_upgrade_id.attendee_ids.mapped("partner_id").ids
1456 )
1457 # Reset the flag to allow approving the new scheduler date.
1458 self.ticket_id.confirm_production_sent = False
1459 self.automatic = False
1460 runs = self.upgrade_line_request_run_ids.filtered(lambda r: r.state in ["running", "pending", "waiting"])
1461 if runs:
1462 runs._cancel()
1463 return True
1465 def _is_no_new_client(self):
1466 """
1467 Checks if the new client is available in the context, to avoid unnecessary calls to get_client_cluster
1468 which can be costly if the client is not available
1469 :return: True if no new client is needed
1470 """
1471 self.ensure_one()
1472 states = [x[0] for x in self._fields["state"].selection]
1473 return self.env.context.get(
1474 "no_new_client", True if states.index(self.state) <= states.index("restored") else False
1475 )
1477 def _get_databases_and_clients(
1478 self, attempts: int = 3, sleep: int = 3, timeout: int = 0
1479 ) -> tuple[
1480 "SaasDatabase" | Literal[None],
1481 "SaasDatabase" | Literal[None],
1482 "Client" | Literal[False],
1483 "Client" | Literal[False],
1484 ]:
1485 """
1486 Get the databases and clients for the request, with some optimizations to avoid unnecessary calls
1487 returns original_database, upgraded_database, original_client, upgraded_client
1488 :return: tuple of (original_database, upgraded_database, original_client, upgraded_client)
1489 """
1490 self.ensure_one()
1491 no_new_client = self._is_no_new_client()
1492 no_database = self.env.context.get("no_database", False)
1493 old_database = self.original_database_id.sudo() if not no_database else None
1494 new_database = self.upgraded_database_id.sudo() if not no_database else None
1495 old_client = (
1496 (old_database and not no_database)
1497 and old_database.get_client_cluster(attempts=attempts, sleep=sleep, timeout=timeout)
1498 or False
1499 )
1500 new_client = (
1501 (new_database and not (no_database or no_new_client))
1502 and new_database.get_client_cluster(attempts=attempts, sleep=sleep, timeout=timeout)
1503 or False
1504 )
1505 return old_database, new_database, old_client, new_client
1507 @contextmanager
1508 def _get_eval_context(self, eval_context: dict, upgrade_line: Optional["SaasUpgradeLine"] = None): # noqa: C901
1509 """
1510 Global context for the evaluation of the scripts
1512 :param eval_context: dict to update with the eval context
1513 :param upgrade_line: Optional upgrade line to include local context
1514 """
1515 no_close_cursor = self.env.context.get("no_close_cursor", False)
1516 old_database, new_database, old_client, new_client = self._get_databases_and_clients()
1517 old_cr = old_database._get_cursor() if old_database and old_database.active else None
1518 new_cr = new_database._get_cursor() if new_database and new_database.active else None
1520 parameters = dict(safe_eval(self.ticket_id.custom_parameters_dict or "{}")) or {}
1522 def run_sql(database, query, params=None, fetchall=False, do_not_raise=False):
1523 if self.upgrade_type_id.upgraded_database_type_id == database.database_type_id:
1524 cursor = new_cr
1525 elif self.upgrade_type_id.original_database_type_id == database.database_type_id:
1526 cursor = old_cr
1527 else:
1528 cursor = database._get_cursor()
1529 try:
1530 res = database._run_sql(cursor, query, params, fetchall=fetchall)
1531 return res
1532 except Exception as e:
1533 if do_not_raise:
1534 return False
1535 raise e
1537 def run_private_method(database, model, method, ids, args=None, kw=None, context=None, get_result=True):
1538 client = database.get_client_cluster()
1539 return client.db.saas_run_private_method(
1540 database.instance_password, model, method, ids, args, kw, context, get_result
1541 )
1543 def run_openupgradelib_method(database, method, args=None, kw=None):
1544 client = database.get_client_cluster()
1545 return client.db.saas_run_openupgradelib_method(database.instance_password, method, args, kw)
1547 def run_upgrade_util_method(database, method, args=None, kw=None, exception=True):
1548 client = database.get_client_cluster()
1549 return client.db.saas_run_upgrade_util_method(database.instance_password, method, args, kw, exception)
1551 def run_multiple_method(database, model, method, ids_and_args=None, kw=None, context=None):
1552 client = database.get_client_cluster()
1553 return client.db.saas_run_multiple_method(
1554 database.instance_password, model, method, ids_and_args, kw, context
1555 )
1557 def ensure_parameter(parameter):
1558 if parameter not in parameters:
1559 raise UserError(_('No parameter found for "%s"') % parameter)
1560 res = parameters.get(parameter)
1561 if res is None:
1562 raise UserError(_('Parameter value not defined for "%s"') % parameter)
1563 return res
1565 def add_unordered_list(input_list):
1566 html_list = "<ul>\n"
1567 for item in input_list:
1568 if isinstance(item, list):
1569 html_list += f"<li>{add_unordered_list(item)}</li>\n"
1570 else:
1571 html_list += f"<li>{item}</li>\n"
1572 html_list += "</ul>"
1573 return html_list
1575 def add_table(list_of_lists):
1576 headers = list_of_lists[0]
1577 data = list_of_lists[1:]
1578 df = pd.DataFrame(data, columns=headers)
1579 html_table = df.to_html(index=False, justify="center", escape=False, border=0)
1580 html_table = html_table.replace("<table ", '<table border="1" ')
1581 html_table = html_table.replace("<tr>", '<tr style="border: 1px solid black;">')
1582 html_table = html_table.replace("<td>", '<td align="center" style="border: 1px solid black;">')
1583 html_table = html_table.replace("<th>", '<th align="center" style="border: 1px solid black;">')
1584 return html_table
1586 def get_request_info(request):
1587 return request._get_request_info()
1589 def update_request_info(request, dict):
1590 return request._update_request_info(dict)
1592 eval_context.update(
1593 {
1594 # Libs
1595 "base64": wrap_module(__import__("base64"), ["b64decode", "b64encode"]),
1596 "re": wrap_module(__import__("re"), ["findall", "search", "match"]),
1597 "requests": wrap_module(__import__("requests"), ["get", "post"]),
1598 "time": time,
1599 "datetime": datetime,
1600 "timezone": pytz.timezone,
1601 "dateutil": dateutil,
1602 "json": json,
1603 # Objects
1604 "request": self,
1605 "parameters": parameters,
1606 "context": dict(self.env.context), # copy context to prevent side-effects of eval
1607 "old_client": old_client,
1608 "new_client": new_client,
1609 "old_database": old_database,
1610 "new_database": new_database,
1611 "today": datetime.datetime.today(),
1612 "user": self.env.user,
1613 # Functions
1614 "run_sql": run_sql,
1615 "run_private_method": run_private_method,
1616 "run_openupgradelib_method": run_openupgradelib_method,
1617 "run_upgrade_util_method": run_upgrade_util_method,
1618 "run_multiple_method": run_multiple_method,
1619 "ensure_parameter": ensure_parameter,
1620 "add_unordered_list": add_unordered_list,
1621 "add_table": add_table,
1622 "get_request_info": get_request_info,
1623 "update_request_info": update_request_info,
1624 "safe_eval": safe_eval,
1625 "urgent_communication": self.urgent_communication,
1626 }
1627 )
1628 if upgrade_line:
1629 eval_context.update(self._get_local_eval_context(upgrade_line))
1630 try:
1631 yield
1632 except Exception as e:
1633 _logger.error("Error occurred while processing request: %s", e)
1634 raise UserError(_("Error occurred while processing request: %s") % e)
1635 finally:
1636 if not no_close_cursor: 1636 ↛ exitline 1636 didn't return from function '_get_eval_context' because the condition on line 1636 was always true
1637 if old_cr: 1637 ↛ 1638line 1637 didn't jump to line 1638 because the condition on line 1637 was never true
1638 old_cr.close()
1639 if new_cr: 1639 ↛ 1640line 1639 didn't jump to line 1640 because the condition on line 1639 was never true
1640 new_cr.close()
1642 def _get_local_eval_context(self, upgrade_line: "SaasUpgradeLine"): # noqa: C901
1643 """
1644 Local context by Upgrade Line
1646 :param upgrade_line: Upgrade Line record
1647 """
1648 upgrade_line.ensure_one()
1649 ticket = self.ticket_id
1650 RequestLog = self.env["saas.upgrade.line.request.log"]
1651 ClientConfigLine = self.env["saas.upgrade.client.config.line"]
1653 return {
1654 "log_message": partial(RequestLog.log_message, upgrade_line, self),
1655 "get_config_selected_values": partial(ClientConfigLine.get_config_selected_values, upgrade_line, ticket),
1656 "add_config_boolean": partial(ClientConfigLine.add_config_boolean, upgrade_line, ticket),
1657 "add_config_selection": partial(ClientConfigLine.add_config_selection, upgrade_line, ticket),
1658 "add_config_selection_multi": partial(ClientConfigLine.add_config_selection_multi, upgrade_line, ticket),
1659 "add_config_matrix": partial(ClientConfigLine.add_config_matrix, upgrade_line, ticket),
1660 }
1662 def _get_odoo_upgrade_request_logs(self) -> str:
1663 """
1664 Returns the logs of the upgrade request in Odoo
1665 """
1666 self.ensure_one()
1667 UpgradeScript.set_servers_info(self.odoo_host_uri)
1668 return UpgradeScript.get_logs(self.odoo_request_token)
1670 def _get_odoo_upgrade_request_status(self) -> dict[str, Any]:
1671 """
1672 Returns the status of the upgrade request in Odoo
1673 """
1674 self.ensure_one()
1675 if not self.odoo_request_token:
1676 raise UserError(_("No request key found. Send the database to Odoo!"))
1677 UpgradeScript.set_servers_info(self.odoo_host_uri)
1678 return UpgradeScript.send_json_request("upgrade/request/status", {"token": self.odoo_request_token})
1680 def _set_error_and_notify(self, e: Exception | Markup | str) -> None:
1681 """
1682 Generic method to handle cron errors
1684 :param e: The error
1685 """
1686 self._error()
1687 title = _("Error in state: %s!") % self.state
1688 message = self.create_message(title, str(e))
1689 self.message_post(body=message)
1691 def _next_state(self):
1692 """
1693 From the current state, we look up the next state in the MAP_NEXT_STATES map and update it.
1694 If stop_at is set and matches the next state, the request will be stopped and the assigned
1695 technician will be notified.
1696 """
1697 self.ensure_one()
1698 next_state = self._get_next_state(self.state)
1699 vals = {"state": next_state}
1701 # Check if we should stop at this state
1702 if self.stop_at and self.stop_at == next_state and self.automatic:
1703 user_to_notify = self.stop_at_user_id or self.ticket_id.assigned_technician_id
1704 # Reset stop_at field and disable automatic execution so the cron does not keep processing it
1705 vals["stop_at"] = False
1706 vals["stop_at_user_id"] = False
1707 vals["automatic"] = False
1708 self.write(vals)
1709 self._on_hold()
1710 if user_to_notify: 1710 ↛ exitline 1710 didn't return from function '_next_state' because the condition on line 1710 was always true
1711 state_label = dict(self._get_stop_at_states()).get(next_state)
1712 message = Markup(
1713 _(
1714 "The upgrade request has reached the stop state: <strong>{}</strong>. "
1715 "The automatic execution has been paused."
1716 ).format(state_label)
1717 )
1718 self.message_post(
1719 body=message,
1720 partner_ids=user_to_notify.partner_id.ids,
1721 message_type="notification",
1722 subtype_xmlid="mail.mt_note",
1723 )
1724 else:
1725 self.write(vals)
1726 self._on_hold()
1728 @api.model
1729 def _get_next_state(self, state: str) -> str:
1730 """
1731 Returns the next state based on the given state using the MAP_NEXT_STATES map.
1733 :param state: Some state of the request
1734 """
1735 if state not in MAP_NEXT_STATES.keys(): 1735 ↛ 1736line 1735 didn't jump to line 1736 because the condition on line 1735 was never true
1736 raise UserError(_("Error! Can't use _next_state() with %s state!") % state)
1738 next_state = MAP_NEXT_STATES[state]
1739 return next_state
1741 @api.model
1742 def _get_prev_state(self, state: str) -> str:
1743 """
1744 Returns the previous state based on the given state using the MAP_NEXT_STATES map.
1746 :param state: Some state of the request
1747 """
1748 if state not in MAP_NEXT_STATES.values(): 1748 ↛ 1749line 1748 didn't jump to line 1749 because the condition on line 1748 was never true
1749 raise UserError(_("Error! Can't use _get_prev_state() with %s state!") % state)
1751 prev_state = next(k for k, v in MAP_NEXT_STATES.items() if v == state)
1752 return prev_state
1754 def _on_hold(self):
1755 """
1756 Set request on 'on_hold'
1757 """
1758 res = dict()
1759 # Only count time if we are not in 'draft' and we are automatic
1760 if self.state != "draft" and self.automatic:
1761 res = self._get_status_time("running") # Always search for "time_running" and reset the counter
1762 res.update(self._set_init_time(fields.Datetime.now()))
1763 # we clear with_error_log when putting the request on hold, even if it was previously in error state
1764 res.update({"status": "on_hold", "with_error_log": False})
1765 self.write(res)
1766 self._get_next_requests().write({"status": "on_hold"})
1768 def _running(self):
1769 """
1770 Set request on 'running'
1771 """
1772 res = self._get_status_time("on_hold") # Always search for "time_on_hold" and reset the counter
1773 res.update(self._set_init_time(fields.Datetime.now()))
1774 res.update({"status": "running"})
1775 self.write(res)
1776 self._get_next_requests().write({"status": "running"})
1778 def _error(self):
1779 """
1780 Set request on 'error'
1781 """
1782 res = self._get_status_time("running") # Always search for "time_running" and reset the counter
1783 res.update({"status": "error"})
1784 res.update(self._set_init_time(False))
1785 self.write(res)
1786 self._get_next_requests().write({"status": "error"})
1788 def _get_status_time(self, status: Literal["on_hold", "running"]) -> dict:
1789 """
1790 Updates the time of the request in the actual state
1791 We take the time from two fields, time_running and time_on_hold
1792 Fields structure:
1793 {
1794 <state>: <seconds>,
1795 }
1797 :param status: The status to look for the time (running or on_hold)
1798 :return: A dict with the updated time field to write
1799 """
1800 now = fields.Datetime.now()
1801 res = dict()
1802 if not self.end_date and (init_time := self._get_init_time()):
1803 key = f"time_{status}"
1804 value = getattr(self, key) or {}
1805 # We must get the previous state to the current one in case of 'running'
1806 # since the time represents the previous stage, not the current one
1807 state = self._get_prev_state(self.state) if status == "running" else self.state
1808 actual_time = value.get(state, 0)
1809 value.update({state: actual_time + (now - init_time).seconds})
1810 res[key] = value
1811 return res
1813 def _get_request_info(self) -> dict:
1814 """
1815 Evals the request_info field and returns it as a dict
1817 :return: dict with the request_info field
1818 """
1819 request_info = safe_eval(self.request_info or "{}", {"datetime": datetime})
1820 return request_info
1822 def _update_request_info(self, dict: dict) -> dict:
1823 """
1824 Updates the request_info field with the given dict
1826 :param dict: dictionary to update the request_info field
1827 :return: dict with the updated request_info field
1828 """
1829 request_info = self._get_request_info()
1830 request_info.update(dict)
1831 self.request_info = str(request_info)
1832 return {"request_info": str(request_info)}
1834 def _set_init_time(self, val: dt) -> dict:
1835 """
1836 Uses the request_info field to store the initial time of an stage of the request
1838 :param val: datetime object that represents the initial time
1839 :return: dict with the updated request_info field
1840 """
1841 return self._update_request_info({"init_time": val})
1843 def _get_init_time(self) -> dt:
1844 """
1845 Uses the request_info field to get the initial time of an stage of the request
1846 """
1847 request_info = self._get_request_info()
1848 init_time = request_info.get("init_time", False)
1849 return init_time
1851 def _get_previous_requests(self) -> "HelpdeskTicketUpgradeRequest":
1852 """
1853 Returns all previous requests linked to this one
1855 :return: A recordset of all previous requests
1856 """
1857 previous_requests = self.env["helpdesk.ticket.upgrade.request"]
1858 current_request = self
1859 while current_request.prev_request_id:
1860 previous_request = current_request.prev_request_id
1861 previous_requests |= previous_request
1862 current_request = previous_request
1863 return previous_requests
1865 def _get_next_requests(self) -> "HelpdeskTicketUpgradeRequest":
1866 """
1867 Returns all next requests linked to this one
1869 :return: A recordset of all next requests
1870 """
1871 next_requests = self.env["helpdesk.ticket.upgrade.request"]
1872 current_request = self
1873 while current_request.next_request_id:
1874 next_request = current_request.next_request_id
1875 next_requests |= next_request
1876 current_request = next_request
1877 return next_requests
1879 def _get_upgrade_line_versions_to_use(self) -> dict:
1880 """
1881 Returns all upgrade lines versions to use for the request
1883 :return: A dict of all upgrade lines versions to use
1884 """
1885 last_test_requests = self.ticket_id.request_line_ids.filtered(lambda x: x.aim == "test" and x.state == "done")
1886 last_test_request = last_test_requests[-1] if last_test_requests else None
1887 if not last_test_request:
1888 return {}
1890 # Walk backwards through the chain and keep only the request for the
1891 # same upgrade type as the current request (same upgrade jump)
1892 chain_requests = last_test_request._get_previous_requests() | last_test_request
1893 request = chain_requests.filtered(lambda r: r.upgrade_type_id == self.upgrade_type_id)
1894 if not request: 1894 ↛ 1895line 1894 didn't jump to line 1895 because the condition on line 1894 was never true
1895 raise UserError(
1896 self.env._("No previous test request found for upgrade type %s. Cannot determine upgrade lines to use.")
1897 )
1899 request.ensure_one()
1900 return request.upgrade_line_versions_used or {}
1902 def reactivate_databases_from_portal(self):
1903 """
1904 Method to reactivate the databases from the portal
1905 """
1906 self.ensure_one()
1907 if self.upgraded_database_id.state == "inactive":
1908 self.upgraded_database_id.sudo()._schedule_task("reactivate")
1909 if self.original_database_id.state == "inactive":
1910 self.original_database_id.sudo()._schedule_task("reactivate")
1911 return self.build_display_notification_action(
1912 title=_("Activation of databases scheduled"),
1913 message=_(
1914 "The databases will been activated successfully in a few minutes. This reactivation lasts 24 hours."
1915 ),
1916 )
1918 @api.model
1919 def _cron_automatic_requests(self):
1920 requests = self.search(
1921 [
1922 ("automatic", "=", True),
1923 ("status", "=", "on_hold"),
1924 ("state", "!=", "cancel"),
1925 ("planned_start", "<=", "now"),
1926 "|",
1927 ("aim", "!=", "production"),
1928 ("ticket_id.confirm_production_sent", "=", True),
1929 ],
1930 order="planned_start asc",
1931 )
1932 _logger.info("Running requests %s" % requests.ids)
1933 requests.process_automatic_requests()
1935 @api.model
1936 def _cron_automatic_error_requests(self, limit: int = 5):
1937 requests = self.search(
1938 [
1939 ("automatic", "=", True),
1940 ("status", "=", "error"),
1941 ],
1942 limit=limit,
1943 order="aim asc, planned_start asc",
1944 )
1945 _logger.info("Running After Error script for requests %s" % requests.ids)
1946 for rec in requests:
1947 try:
1948 rec.run_after_error()
1949 except Exception as e:
1950 title = _("Error running After Error script!")
1951 message = self.create_message(title, e)
1953 # Post to chatter and notify assigned technician if available
1954 partner_ids = []
1955 if rec.ticket_id.assigned_technician_id:
1956 partner_ids = [rec.ticket_id.assigned_technician_id.partner_id.id]
1958 rec.message_post(body=message, partner_ids=partner_ids)
1960 @api.model
1961 def _cron_monitor_production_requests(self, min_hours: float = 1.0):
1962 now = fields.Datetime.now()
1963 requests = self.search(
1964 [
1965 ("automatic", "=", True),
1966 ("aim", "=", "production"),
1967 ("planned_start", "<=", now),
1968 ("ticket_id.confirm_production_sent", "=", True),
1969 ]
1970 )
1971 _logger.info("Monitoring production requests %s" % requests.ids)
1972 min_seconds = min_hours * 3600
1973 for rec in requests:
1974 request_info = rec._get_request_info()
1975 already_monitored = request_info.get("already_monitored", False)
1976 if already_monitored:
1977 continue
1978 time_passed = (now - rec.planned_start).total_seconds()
1979 expected_time = max(min_seconds, rec.ticket_id.last_test_request_line_id.total_time * 3600)
1980 if expected_time and time_passed > expected_time:
1981 rec._update_request_info({"already_monitored": True})
1982 rec.urgent_communication(
1983 self.with_context(lang="es_419").env._(
1984 "The upgrade for %s is taking longer than expected. Actual state: %s"
1985 )
1986 % (rec.ticket_id.name, rec.state)
1987 )
1989 @api.ondelete(at_uninstall=False)
1990 def _unlink_from_parent_request(self):
1991 """
1992 Unlink the whole chain when deleting the last request in the chain.
1993 Prevent deleting a request that has a next request.
1994 """
1995 bypass_delete = self.env.context.get("bypass_delete", False)
1996 if bypass_delete:
1997 return
1999 # Check if any request has a next request that is NOT being deleted in this batch
2000 requests_being_deleted = self.ids
2001 for rec in self:
2002 if rec.next_request_id and rec.next_request_id.id not in requests_being_deleted:
2003 raise UserError(
2004 self.env._(
2005 "You cannot delete an upgrade request that has a next request (ID %s). "
2006 "Delete the whole chain instead from the last request."
2007 )
2008 % rec.id
2009 )
2011 to_delete = self.env["helpdesk.ticket.upgrade.request"]
2012 for rec in self:
2013 to_delete |= rec._get_previous_requests()
2015 to_delete = to_delete - self
2016 if to_delete:
2017 to_delete.with_context(bypass_delete=True).unlink()
2019 @api.autovacuum
2020 def _gc_upgrade_requests(self):
2021 """
2022 Autovacuum to permanently delete upgrade requests according to business rules:
2023 - Test requests: canceled ones AND ones from databases that are upgraded to new version
2024 AND ones from canceled tickets
2025 - Production requests: canceled ones AND ones from tickets that are already upgraded
2026 - Requests from canceled tickets
2027 """
2028 cutoff_date = fields.Datetime.now() - timedelta(days=1)
2030 # Test requests to delete
2031 test_requests_to_delete = self.search(
2032 [
2033 ("aim", "=", "test"),
2034 ("create_date", "<", cutoff_date),
2035 ("state", "=", "cancel"),
2036 ("ticket_id.ticket_upgrade_status", "=", "upgraded"),
2037 ("ticket_id.stage_id.fold", "=", True),
2038 ("next_request_id", "=", False),
2039 ]
2040 )
2042 # Production requests to delete
2043 production_requests_to_delete = self.search(
2044 [
2045 ("aim", "=", "production"),
2046 ("create_date", "<", cutoff_date),
2047 ("state", "=", "cancel"),
2048 ("ticket_id.ticket_upgrade_status", "=", "upgraded"),
2049 ("next_request_id", "=", False),
2050 ]
2051 )
2053 # Requests from canceled tickets
2054 canceled_ticket_requests = self.search(
2055 [
2056 ("create_date", "<", cutoff_date),
2057 ("ticket_id.ticket_upgrade_status", "=", "no_upgraded"),
2058 ("ticket_id.stage_id.fold", "=", True),
2059 ("next_request_id", "=", False),
2060 ]
2061 )
2063 total_requests_to_delete = test_requests_to_delete | production_requests_to_delete | canceled_ticket_requests
2065 if total_requests_to_delete:
2066 _logger.info(
2067 "Autovacuum: Deleting %d upgrade requests (test: %d, production: %d, canceled tickets: %d)",
2068 len(total_requests_to_delete),
2069 len(test_requests_to_delete),
2070 len(production_requests_to_delete),
2071 len(canceled_ticket_requests),
2072 )
2073 total_requests_to_delete.with_context(bypass_delete=True).unlink()
2074 _logger.info("Autovacuum: Successfully deleted upgrade requests")
2075 else:
2076 _logger.info("Autovacuum: No upgrade requests to delete")