Coverage for ingadhoc-odoo-saas-adhoc / saas_provider_upgrade / models / saas_upgrade_client_data.py: 26%

222 statements  

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

1import datetime 

2import logging 

3 

4from odoo import _, api, fields, models 

5from odoo.exceptions import UserError, ValidationError 

6from odoo.models import BaseModel 

7from odoo.tools.safe_eval import safe_eval 

8 

9_logger = logging.getLogger(__name__) 

10 

11MIN_UPGRADE_VERSION = 16 

12EPS = 1.0 

13DIVISION_RATIO = 2 

14 

15 

16class SaasUpgradeClientData(models.Model): 

17 _name = "saas.upgrade.client.data" 

18 _description = "Upgrade Client Data" 

19 

20 project_id = fields.Many2one( 

21 "project.project", 

22 ) 

23 ticket_ids = fields.One2many("helpdesk.ticket", "upgrade_client_data_id") 

24 last_ticket_id = fields.Many2one("helpdesk.ticket") 

25 service_level = fields.Selection( 

26 [ 

27 ("standard", "Standard"), 

28 ("advanced", "Advanced"), 

29 ("premium", "Premium"), 

30 ], 

31 ) 

32 project_status = fields.Selection( 

33 [ 

34 ("on_track", "Ok"), 

35 ("at_risk", "Regular"), 

36 ("off_track", "Mal"), 

37 ("undefined", "No Definido"), 

38 ], 

39 ) 

40 upgrade_nps = fields.Char() 

41 project_total_personalizations = fields.Integer( 

42 help="Total number of personalization lines.", 

43 ) 

44 project_interface_personalizations = fields.Integer( 

45 help="Number of interface personalization lines.", 

46 ) 

47 project_max_users = fields.Integer( 

48 help="Maximum users contracted by the client.", 

49 ) 

50 project_mig_count = fields.Integer( 

51 help="Total migrations performed by the client", 

52 ) 

53 project_complexity = fields.Integer( 

54 help="Project complexity.", 

55 ) 

56 supported_modules_code_count = fields.Integer( 

57 help="Number of supported module lines", 

58 ) 

59 unsupported_modules_code_count = fields.Integer( 

60 help="Number of unsupported module lines", 

61 ) 

62 companies_count = fields.Integer(help="Number of companies") 

63 db_size = fields.Integer(help="Database size in GB.") 

64 

65 # Historical data 

66 cns_qty = fields.Integer( 

67 help="Customer Notes, weighted value.", 

68 ) 

69 requests_qty = fields.Integer( 

70 help="Total requests, weighted value.", 

71 ) 

72 upgrade_tickets = fields.Integer( 

73 help="Total tickets associated with Upgrade, weighted value.", 

74 ) 

75 upgrade_tasks = fields.Integer( 

76 help="Total tasks associated with Upgrade, weighted value.", 

77 ) 

78 

79 # Classification (higher number = higher difficulty) 

80 project_classification = fields.Selection( 

81 [ 

82 ("0", "0"), 

83 ("1", "1"), 

84 ("2", "2"), 

85 ("3", "3"), 

86 ("4", "4"), 

87 ("5", "5"), 

88 ], 

89 default="0", 

90 string="Classification", 

91 help="Higher number implies higher migration difficulty", 

92 ) 

93 manually_classified = fields.Boolean( 

94 default=True, 

95 help="Used to know if the record was manually classified or by the model.", 

96 ) 

97 

98 last_cron_updated_date = fields.Date( 

99 help="Last update date from cron.", 

100 default=fields.Date.today(), 

101 ) 

102 cannot_update = fields.Boolean( 

103 default=False, 

104 help="Used to know if the record could not be updated.", 

105 ) 

106 last_error = fields.Char( 

107 help="Used to know the last error occurred.", 

108 ) 

109 

110 @api.constrains("project_id") 

111 def _check_has_project(self): 

112 for rec in self: 

113 if not rec.project_id or not rec.project_id.main_database_id: 113 ↛ 114line 113 didn't jump to line 114 because the condition on line 113 was never true

114 raise ValidationError(_("Upgrade Client Data requires project with a main_database_id.")) 

115 

116 def write(self, vals): 

117 # Mark as manually classified if modified by non-superuser 

118 if not self.env.is_superuser(): 

119 vals["manually_classified"] = True 

120 res = super(SaasUpgradeClientData, self).write(vals) 

121 return res 

122 

123 @api.model 

124 def check_project_condition(self, project_id: BaseModel) -> BaseModel: 

125 """ 

126 Validates if a project meets the conditions to create upgrade client data. 

127 

128 :param project_id: Project record to validate 

129 :return: main_database_id recordset if valid, empty recordset otherwise 

130 """ 

131 return project_id.main_database_id 

132 

133 def populate(self): 

134 """ 

135 Populates the record with actual and historic data for upgrade analysis. 

136 """ 

137 self.ensure_one() 

138 self.cannot_update = False 

139 self.last_error = False 

140 self._populate_actual_data() 

141 self._populate_historic_data() 

142 

143 def _populate_actual_data(self): 

144 try: 

145 self._set_ticket_ids() 

146 self._set_service_level() 

147 self._set_project_status() 

148 self._set_upgrade_nps() 

149 self._set_project_personalizations() 

150 self._set_project_max_users() 

151 self._set_project_mig_count() 

152 self._set_project_complexity() 

153 self._set_modules_count() 

154 self._set_companies_count() 

155 self._set_db_size() 

156 except Exception as e: 

157 self.cannot_update = True 

158 self.last_error = str(e) 

159 

160 def _set_service_level(self): 

161 service_level = self.project_id.service_level 

162 if not service_level: 

163 service_level = "standard" 

164 self.service_level = service_level 

165 

166 def _set_project_status(self): 

167 undefined_status = ["on_hold", "to_define", "done", "undefined"] 

168 last_update_status = self.project_id.last_update_status 

169 self.project_status = last_update_status if last_update_status not in undefined_status else "at_risk" 

170 

171 def _set_upgrade_nps(self): 

172 project_update_type = self.env["project.update.type"].browse(1) # Customer Success type 

173 properties_definition = project_update_type.update_properties_definition 

174 property = list(filter(lambda x: x["string"] == "NPS Actua", properties_definition))[0] 

175 property_id = property["name"] 

176 selection_ids = property["selection"] 

177 project_updates = self.env["project.update"].search( 

178 [ 

179 ("project_id", "=", self.project_id.id), 

180 ("project_update_type.id", "=", project_update_type.id), 

181 ] 

182 ) 

183 

184 undefined_status = ["No aplica", "Sin medir", "No definido"] 

185 undefined_value = "Neutro" 

186 if not project_updates: 

187 self.upgrade_nps = undefined_value 

188 return 

189 

190 last_project_update = project_updates[0] 

191 

192 if not last_project_update.update_properties: 

193 self.upgrade_nps = undefined_value 

194 return 

195 

196 selection_id = last_project_update.update_properties[property_id] 

197 selection = list(filter(lambda x: x[0] == selection_id, selection_ids)) 

198 

199 if not selection: 

200 self.upgrade_nps = undefined_value 

201 return 

202 

203 selection = selection[0] 

204 upgrade_nps = selection[1] 

205 self.upgrade_nps = upgrade_nps if upgrade_nps not in undefined_status else undefined_value 

206 

207 def _set_project_personalizations(self): 

208 db = self.project_id.main_database_id 

209 db_client = None 

210 if db.state != "active": 

211 raise UserError(_("Client database is not active!")) 

212 try: 

213 db_client = db.get_client_cluster() 

214 except Exception: 

215 raise UserError(_("Cannot connect to client database!")) 

216 

217 db_client_dashboard = db_client.env["saas_client.dashboard"].create({}) 

218 personalizations_dict = safe_eval(db_client_dashboard.personalizations_amount_detail) 

219 total_personalizations = 0 

220 interface_personalizations = 0 

221 for key, value in personalizations_dict.items(): 

222 total_personalizations += value 

223 if not ("personalizations" in key or "p13n" in key): 

224 interface_personalizations += value 

225 

226 self.project_total_personalizations = total_personalizations 

227 self.project_interface_personalizations = interface_personalizations 

228 

229 def _set_project_max_users(self): 

230 db = self.project_id.main_database_id 

231 self.project_max_users = db.max_users 

232 

233 def _set_project_mig_count(self): 

234 tickets = self.env["helpdesk.ticket"].search( 

235 [("project_id", "=", self.project_id.id), ("upgrade_team", "=", True)] 

236 ) 

237 self.project_mig_count = len(tickets) 

238 

239 def _set_project_complexity(self): 

240 # Field created by Odoo Studio - may change 

241 self.project_complexity = int(self.project_id.x_studio_complejidad_de_proyecto) 

242 

243 def _set_modules_count(self): 

244 db = self.project_id.main_database_id 

245 modules = db.module_ids 

246 supported_count = 0 

247 unsupported_count = 0 

248 for module in modules: 

249 support_type = module.support_type 

250 line_qty = module.x_code_qty 

251 supported_count += line_qty if support_type == "supported" else 0 

252 unsupported_count += line_qty if support_type == "unsupported" else 0 

253 

254 self.supported_modules_code_count = supported_count 

255 self.unsupported_modules_code_count = unsupported_count 

256 

257 def _set_companies_count(self): 

258 db = self.project_id.main_database_id 

259 companies_product_template_id = 202 # Keep track of this ID 

260 count = db.active_subscription_id.order_line.filtered( 

261 lambda x: x.product_template_id.id == companies_product_template_id 

262 ).product_uom_qty 

263 self.companies_count = int(count) 

264 

265 def _set_db_size(self): 

266 self.db_size = self.project_id.main_database_id.pg_storage_size 

267 

268 def _populate_historic_data(self): 

269 try: 

270 self._set_cns_qty() 

271 self._set_requests_qty() 

272 self._set_upgrade_tickets_and_tasks() 

273 except Exception as e: 

274 self.cannot_update = True 

275 self.last_error = str(e) 

276 

277 def _set_ticket_ids(self): 

278 tickets = self.env["helpdesk.ticket"].search( 

279 [ 

280 ("project_id", "=", self.project_id.id), 

281 ("upgrade_team", "=", True), 

282 ], 

283 order="id", 

284 ) 

285 

286 if not tickets: 

287 return 

288 

289 last_ticket_id = tickets[-1] 

290 to_set_tickets = [] 

291 for ticket in tickets: 

292 major_version = float(ticket.upgrade_type_id.target_odoo_version_group_id.major_version) 

293 if major_version < MIN_UPGRADE_VERSION or ticket.ticket_upgrade_status == "upgraded": 

294 continue 

295 

296 to_set_tickets += [(4, ticket.id, 0)] 

297 

298 self.write({"ticket_ids": to_set_tickets, "last_ticket_id": last_ticket_id}) 

299 return to_set_tickets 

300 

301 def _set_cns_qty(self): 

302 eps = EPS 

303 qty = 0 

304 for ticket in self.ticket_ids: 

305 cns = ticket.customer_note_ids 

306 qty += int(eps * len(cns)) 

307 eps /= DIVISION_RATIO 

308 

309 self.cns_qty = qty 

310 

311 def _set_requests_qty(self): 

312 eps = EPS 

313 qty = 0 

314 for ticket in self.ticket_ids: 

315 requests = ticket.request_line_ids 

316 qty += int(eps * len(requests)) 

317 eps /= DIVISION_RATIO 

318 

319 self.requests_qty = qty 

320 

321 def _set_upgrade_tickets_and_tasks(self): 

322 eps = EPS 

323 ticket_qty = task_qty = 0 

324 for ticket in self.ticket_ids: 

325 upgrade_tickets = ticket.child_ids 

326 upgrade_tasks = ticket.task_ids 

327 ticket_qty += int(eps * len(upgrade_tickets)) 

328 task_qty += int(eps * len(upgrade_tasks)) 

329 eps /= DIVISION_RATIO 

330 

331 self.upgrade_tickets = ticket_qty 

332 self.upgrade_tasks = task_qty 

333 

334 @api.model 

335 def _cron_create_client_data_records(self): 

336 """ 

337 Cron that creates the client data records by project. 

338 It process all the project, and if one of them does not have a 'data' record associated, it creates it 

339 """ 

340 data_project_ids = self.search([]).mapped("project_id") 

341 project_ids = self.env["project.project"].search([("id", "not in", data_project_ids.ids)]) 

342 new_data = [{"project_id": project.id} for project in project_ids if self.check_project_condition(project)] 

343 if new_data: 

344 self.create(new_data) 

345 _logger.info(f"[ADV] create Client Data records: New records {len(new_data)}") 

346 

347 @api.model 

348 def _cron_update_client_data_records(self): 

349 """ 

350 Cron that updates the client data records by a batch_size. 

351 """ 

352 all_client_data = self.search([("last_cron_updated_date", "<", datetime.date.today())]) 

353 if not all_client_data: 

354 return 

355 _logger.info("[ADV] Updating Client Data records: %s" % all_client_data.ids) 

356 total_len = len(all_client_data) 

357 self.env["ir.cron"]._commit_progress(remaining=total_len) 

358 for data in all_client_data: 

359 try: 

360 data.populate() 

361 data.last_cron_updated_date = datetime.date.today() 

362 self.env["ir.cron"]._commit_progress(processed=1) 

363 except Exception: 

364 self.env.cr.rollback()