Coverage for models / lox_backup_profile.py: 35%

100 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-28 01:16 +0000

1# -*- coding: utf-8 -*- 

2 

3from odoo import models, fields, api, _ 

4from odoo.exceptions import UserError 

5import logging 

6 

7_logger = logging.getLogger(__name__) 

8 

9 

10class LoxBackupProfile(models.Model): 

11 """Backup profiles for quick custom backups""" 

12 _name = 'lox.backup.profile' 

13 _description = 'LOX Backup Profile' 

14 _order = 'sequence, name' 

15 

16 name = fields.Char( 

17 string='Profile Name', 

18 required=True, 

19 ) 

20 config_id = fields.Many2one( 

21 'lox.backup.config', 

22 string='Configuration', 

23 required=True, 

24 ondelete='cascade', 

25 ) 

26 active = fields.Boolean( 

27 string='Active', 

28 default=True, 

29 ) 

30 sequence = fields.Integer( 

31 string='Sequence', 

32 default=10, 

33 ) 

34 

35 # Remote profile sync 

36 remote_uuid = fields.Char( 

37 string='Remote Profile UUID', 

38 readonly=True, 

39 help='UUID of the profile on LOX server (for versioning)', 

40 ) 

41 is_synced = fields.Boolean( 

42 string='Synced with Server', 

43 compute='_compute_is_synced', 

44 store=True, 

45 ) 

46 

47 # What to backup 

48 include_database = fields.Boolean( 

49 string='Include Database', 

50 default=True, 

51 help='Include full database dump', 

52 ) 

53 include_filestore = fields.Boolean( 

54 string='Include Filestore', 

55 default=True, 

56 help='Include all attachments and files', 

57 ) 

58 include_modules = fields.Boolean( 

59 string='Include Modules Info', 

60 default=True, 

61 help='Include list of installed modules', 

62 ) 

63 

64 # Optional: specific models only (for partial DB backup in future) 

65 # For now, database is always full 

66 

67 # Custom settings 

68 custom_tags = fields.Char( 

69 string='Custom Tags', 

70 help='Additional comma-separated tags for this profile', 

71 ) 

72 retention_days = fields.Integer( 

73 string='Retention Days', 

74 default=0, 

75 help='Override retention days (0 = use config default)', 

76 ) 

77 description = fields.Text( 

78 string='Description', 

79 help='Description of what this profile backs up', 

80 ) 

81 

82 # Stats 

83 last_run = fields.Datetime( 

84 string='Last Run', 

85 readonly=True, 

86 ) 

87 run_count = fields.Integer( 

88 string='Run Count', 

89 default=0, 

90 readonly=True, 

91 ) 

92 backup_count = fields.Integer( 

93 string='Version Count', 

94 readonly=True, 

95 help='Number of backup versions on server', 

96 ) 

97 total_size = fields.Integer( 

98 string='Total Size (bytes)', 

99 readonly=True, 

100 ) 

101 total_size_display = fields.Char( 

102 string='Total Size', 

103 compute='_compute_total_size_display', 

104 ) 

105 

106 @api.depends('remote_uuid') 

107 def _compute_is_synced(self): 

108 for record in self: 

109 record.is_synced = bool(record.remote_uuid) 

110 

111 @api.depends('total_size') 

112 def _compute_total_size_display(self): 

113 for record in self: 

114 size = record.total_size 

115 if not size: 

116 record.total_size_display = '--' 

117 elif size < 1024: 

118 record.total_size_display = f'{size} B' 

119 elif size < 1024 * 1024: 

120 record.total_size_display = f'{size / 1024:.1f} KB' 

121 elif size < 1024 * 1024 * 1024: 

122 record.total_size_display = f'{size / (1024 * 1024):.1f} MB' 

123 else: 

124 record.total_size_display = f'{size / (1024 * 1024 * 1024):.2f} GB' 

125 

126 def action_run_backup(self): 

127 """Run backup with this profile""" 

128 self.ensure_one() 

129 

130 if not self.config_id.source_registered: 

131 raise UserError(_('Please register the source first.')) 

132 

133 # Open wizard with profile preselected 

134 return { 

135 'name': _('Run Backup - %s') % self.name, 

136 'type': 'ir.actions.act_window', 

137 'res_model': 'lox.backup.wizard', 

138 'view_mode': 'form', 

139 'target': 'new', 

140 'context': { 

141 'default_config_id': self.config_id.id, 

142 'default_profile_id': self.id, 

143 'default_backup_database': self.include_database, 

144 'default_backup_filestore': self.include_filestore, 

145 'default_backup_modules': self.include_modules, 

146 'default_custom_tags': self.custom_tags, 

147 'default_retention_days': self.retention_days or self.config_id.retention_days, 

148 } 

149 } 

150 

151 def _update_stats(self): 

152 """Update profile statistics after backup""" 

153 self.ensure_one() 

154 self.write({ 

155 'last_run': fields.Datetime.now(), 

156 'run_count': self.run_count + 1, 

157 }) 

158 

159 def action_sync_with_server(self): 

160 """Sync profile with LOX server - create or update remote profile""" 

161 self.ensure_one() 

162 

163 if not self.config_id.source_registered: 

164 raise UserError(_('Please register the source first.')) 

165 

166 api = self.env['lox.api'].create_client(self.config_id) 

167 

168 if self.remote_uuid: 

169 # Update existing profile 

170 result = api.update_profile(self.remote_uuid, { 

171 'name': self.name, 

172 'description': self.description or '', 

173 'is_active': self.active, 

174 }) 

175 if result.get('success') or result.get('uuid'): 

176 return { 

177 'type': 'ir.actions.client', 

178 'tag': 'display_notification', 

179 'params': { 

180 'title': _('Profile Synced'), 

181 'message': _('Profile updated on server.'), 

182 'type': 'success', 

183 } 

184 } 

185 else: 

186 # Create new profile 

187 result = api.create_profile(self.name, { 

188 'description': self.description or '', 

189 }) 

190 if result.get('uuid'): 

191 self.write({'remote_uuid': result['uuid']}) 

192 return { 

193 'type': 'ir.actions.client', 

194 'tag': 'display_notification', 

195 'params': { 

196 'title': _('Profile Created'), 

197 'message': _('Profile created on server: %s') % result['uuid'][:8], 

198 'type': 'success', 

199 } 

200 } 

201 

202 raise UserError(_('Failed to sync profile: %s') % result.get('error', 'Unknown error')) 

203 

204 def action_fetch_stats(self): 

205 """Fetch profile statistics from server""" 

206 self.ensure_one() 

207 

208 if not self.remote_uuid: 

209 raise UserError(_('Profile is not synced with server.')) 

210 

211 if not self.config_id.source_registered: 

212 raise UserError(_('Please register the source first.')) 

213 

214 api = self.env['lox.api'].create_client(self.config_id) 

215 result = api.get_profile(self.remote_uuid) 

216 

217 if result.get('uuid') or result.get('success'): 

218 data = result.get('data', result) 

219 self.write({ 

220 'backup_count': data.get('backup_count', 0), 

221 'total_size': data.get('total_size_bytes', 0), 

222 }) 

223 return { 

224 'type': 'ir.actions.client', 

225 'tag': 'display_notification', 

226 'params': { 

227 'title': _('Stats Updated'), 

228 'message': _('%d versions, %s total') % ( 

229 data.get('backup_count', 0), 

230 self.total_size_display 

231 ), 

232 'type': 'success', 

233 } 

234 } 

235 

236 raise UserError(_('Failed to fetch stats: %s') % result.get('error', 'Unknown error')) 

237 

238 @api.model 

239 def action_sync_all_from_server(self): 

240 """Sync all profiles from server - import remote profiles as local""" 

241 config = self.env['lox.backup.config'].search([], limit=1) 

242 if not config or not config.source_registered: 

243 raise UserError(_('Please configure and register the source first.')) 

244 

245 api = self.env['lox.api'].create_client(config) 

246 result = api.get_profiles() 

247 

248 if not (result.get('success') or result.get('profiles')): 

249 raise UserError(_('Failed to fetch profiles: %s') % result.get('error', 'Unknown error')) 

250 

251 profiles = result.get('profiles', result.get('data', {}).get('profiles', [])) 

252 created = 0 

253 updated = 0 

254 

255 for profile_data in profiles: 

256 remote_uuid = profile_data.get('uuid') 

257 if not remote_uuid: 

258 continue 

259 

260 # Check if profile already exists locally 

261 existing = self.search([('remote_uuid', '=', remote_uuid)], limit=1) 

262 if existing: 

263 existing.write({ 

264 'name': profile_data.get('name', existing.name), 

265 'backup_count': profile_data.get('backup_count', 0), 

266 'total_size': profile_data.get('total_size_bytes', 0), 

267 }) 

268 updated += 1 

269 else: 

270 # Create new local profile 

271 self.create({ 

272 'name': profile_data.get('name', 'Imported Profile'), 

273 'config_id': config.id, 

274 'remote_uuid': remote_uuid, 

275 'backup_count': profile_data.get('backup_count', 0), 

276 'total_size': profile_data.get('total_size_bytes', 0), 

277 'include_database': True, 

278 'include_filestore': True, 

279 }) 

280 created += 1 

281 

282 return { 

283 'type': 'ir.actions.client', 

284 'tag': 'display_notification', 

285 'params': { 

286 'title': _('Profiles Synced'), 

287 'message': _('Created: %d, Updated: %d') % (created, updated), 

288 'type': 'success', 

289 } 

290 }