Coverage for ingadhoc-odoo-saas / saas_client / models / ir_module_module.py: 19%

93 statements  

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

1import logging 

2from unittest.mock import patch 

3 

4from odoo import api, models 

5from odoo.fields import Domain 

6from odoo.modules import Manifest 

7from odoo.tools import config, parse_version 

8 

9_logger = logging.getLogger(__name__) 

10 

11 

12class IrModuleModule(models.Model): 

13 _inherit = "ir.module.module" 

14 

15 def button_upgrade(self): 

16 """ 

17 Override to prevent schema conflicts during fixdb operations. 

18 

19 When fixdb_mode context is active, temporarily patches the translation function 

20 to return untranslated strings, preventing database access to models with fields 

21 that don't exist yet in the schema during module upgrades. 

22 """ 

23 if not self.env.context.get("fixdb_mode"): 

24 return super().button_upgrade() 

25 

26 with patch("odoo.addons.base.models.ir_module._", side_effect=lambda x, **kw: x): 

27 return super().button_upgrade() 

28 

29 def button_install(self): 

30 """ 

31 Override to prevent schema conflicts during fixdb operations. 

32 

33 When fixdb_mode context is active, temporarily patches the translation function 

34 to return untranslated strings, preventing database access to models with fields 

35 that don't exist yet in the schema during module installations. 

36 """ 

37 if not self.env.context.get("fixdb_mode"): 

38 return super().button_install() 

39 

40 with patch("odoo.addons.base.models.ir_module._", side_effect=lambda x, **kw: x): 

41 return super().button_install() 

42 

43 @api.model 

44 def _get_not_installable_modules(self): 

45 states = ["installed", "to upgrade", "to remove", "to install"] 

46 domain = Domain([("state", "in", states)]) 

47 if self.search_count([("name", "=", "base_import_module"), ("state", "=", "installed")]): 

48 domain &= Domain([("imported", "=", False)]) 

49 modules = self.search(domain) 

50 

51 not_installable_modules = self.browse() 

52 for module in modules: 

53 manifest = Manifest.for_addon(module.name) 

54 modules_install_disabled = config.get("module_change_install.modules_disabled") or "" 

55 modules_install_disabled = [x.strip() for x in modules_install_disabled.split(",")] 

56 # Non-installability due to modules_install_disabled manifests slightly differently: 

57 # the system doesn't fail because we allow Odoo to load, but it warns that something is wrong 

58 if not manifest or not manifest["installable"] or module.name in modules_install_disabled: 

59 not_installable_modules += module 

60 

61 return not_installable_modules 

62 

63 @api.model 

64 def _get_to_upgrade_modules(self): 

65 to_upgrade_modules = self.search([("state", "=", "installed")]) 

66 to_upgrade_modules = to_upgrade_modules.filtered( 

67 lambda x: parse_version(x.installed_version) != parse_version(x.latest_version), 

68 ) 

69 return to_upgrade_modules 

70 

71 @api.model 

72 def _get_not_installed_autoinstall_modules(self): 

73 uninstalled_modules = self.search( 

74 [ 

75 ("state", "=", "uninstalled"), 

76 ("auto_install", "=", True), 

77 ("country_ids", "=", False), 

78 ] 

79 ) 

80 satisfied_states = frozenset(("installed", "to install", "to upgrade")) 

81 

82 def all_dependencies_satisfied(m): 

83 states = set(d.state for d in m.dependencies_id) 

84 return states.issubset(satisfied_states) 

85 

86 return uninstalled_modules.filtered(all_dependencies_satisfied) 

87 

88 @api.model 

89 def get_update_status_details(self): 

90 not_installable = self._get_not_installable_modules() 

91 update_required = self._get_to_upgrade_modules() 

92 to_upgrade_modules = self.search([("state", "=", "to upgrade")]) 

93 to_install_modules = self.search([("state", "=", "to install")]) 

94 to_remove_modules = self.search([("state", "=", "to remove")]) 

95 not_installed_autoinstall_modules = self._get_not_installed_autoinstall_modules() 

96 

97 if not_installable: 

98 update_state = "not_installable" 

99 elif update_required: 

100 update_state = "update_required" 

101 elif to_upgrade_modules: 

102 update_state = "on_to_upgrade" 

103 elif to_install_modules: 

104 update_state = "on_to_install" 

105 elif to_remove_modules: 

106 update_state = "on_to_remove" 

107 elif not_installed_autoinstall_modules: 

108 update_state = "uninstalled_auto_install" 

109 else: 

110 update_state = "ok" 

111 

112 details = { 

113 "not_installable": not_installable.mapped("name") if not_installable else [], 

114 "update_required": update_required.mapped("name") if update_required else [], 

115 "to_upgrade_modules": to_upgrade_modules.mapped("name") if to_upgrade_modules else [], 

116 "to_install_modules": to_install_modules.mapped("name") if to_install_modules else [], 

117 "to_remove_modules": to_remove_modules.mapped("name") if to_remove_modules else [], 

118 "not_installed_autoinstall_modules": not_installed_autoinstall_modules.mapped("name") 

119 if not_installed_autoinstall_modules 

120 else [], 

121 } 

122 return {"state": update_state, "details": details} 

123 

124 @api.model 

125 def update_list(self): 

126 """ 

127 Override to mark modules as uninstallable when they are no longer available. 

128 

129 After updating the module list, compares modules in the database with available manifests 

130 and marks as uninstallable those that are no longer present in the addons paths. 

131 Only affects uninstalled modules to avoid breaking active installations. 

132 """ 

133 res = super().update_list() 

134 

135 known_uninstalled_mods = self.with_context(lang=None).search([("state", "=", "uninstalled")]) 

136 available_manifests = {manifest.name for manifest in Manifest.all_addon_manifests()} 

137 modules_to_mark = known_uninstalled_mods.filtered(lambda mod: mod.name not in available_manifests) 

138 

139 if modules_to_mark: 

140 _logger.info( 

141 "Marking modules no longer available as uninstallable: %s", 

142 modules_to_mark.mapped("name"), 

143 ) 

144 modules_to_mark.write({"state": "uninstallable"}) 

145 

146 return res 

147 

148 @api.model 

149 def fix_modules(self): 

150 """ 

151 Execute fixdb operations: update list, upgrade modules, install auto_install modules. 

152 Can be called from CLI (fixdb command) or remotely via odooly. 

153 """ 

154 # Update module list 

155 self.update_list() 

156 

157 # Upgrade modules that need upgrading 

158 to_upgrade_modules = self._get_to_upgrade_modules() 

159 if to_upgrade_modules: 

160 _logger.info("Upgrading modules: %s", to_upgrade_modules.mapped("name")) 

161 to_upgrade_modules.with_context(fixdb_mode=True).button_upgrade() 

162 

163 # Install auto_install modules with all dependencies satisfied 

164 to_install_modules = self._get_not_installed_autoinstall_modules() 

165 if to_install_modules: 

166 _logger.info( 

167 "Installing auto_install modules with dependencies satisfied: %s", to_install_modules.mapped("name") 

168 ) 

169 to_install_modules.with_context(fixdb_mode=True).button_install() 

170 

171 # Run upgrade wizard to apply changes 

172 self.env["base.module.upgrade"].sudo().upgrade_module() 

173 

174 return True