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
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-28 01:16 +0000
1# -*- coding: utf-8 -*-
3from odoo import models, fields, api, _
4from odoo.exceptions import UserError
5import logging
7_logger = logging.getLogger(__name__)
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'
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 )
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 )
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 )
64 # Optional: specific models only (for partial DB backup in future)
65 # For now, database is always full
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 )
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 )
106 @api.depends('remote_uuid')
107 def _compute_is_synced(self):
108 for record in self:
109 record.is_synced = bool(record.remote_uuid)
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'
126 def action_run_backup(self):
127 """Run backup with this profile"""
128 self.ensure_one()
130 if not self.config_id.source_registered:
131 raise UserError(_('Please register the source first.'))
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 }
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 })
159 def action_sync_with_server(self):
160 """Sync profile with LOX server - create or update remote profile"""
161 self.ensure_one()
163 if not self.config_id.source_registered:
164 raise UserError(_('Please register the source first.'))
166 api = self.env['lox.api'].create_client(self.config_id)
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 }
202 raise UserError(_('Failed to sync profile: %s') % result.get('error', 'Unknown error'))
204 def action_fetch_stats(self):
205 """Fetch profile statistics from server"""
206 self.ensure_one()
208 if not self.remote_uuid:
209 raise UserError(_('Profile is not synced with server.'))
211 if not self.config_id.source_registered:
212 raise UserError(_('Please register the source first.'))
214 api = self.env['lox.api'].create_client(self.config_id)
215 result = api.get_profile(self.remote_uuid)
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 }
236 raise UserError(_('Failed to fetch stats: %s') % result.get('error', 'Unknown error'))
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.'))
245 api = self.env['lox.api'].create_client(config)
246 result = api.get_profiles()
248 if not (result.get('success') or result.get('profiles')):
249 raise UserError(_('Failed to fetch profiles: %s') % result.get('error', 'Unknown error'))
251 profiles = result.get('profiles', result.get('data', {}).get('profiles', []))
252 created = 0
253 updated = 0
255 for profile_data in profiles:
256 remote_uuid = profile_data.get('uuid')
257 if not remote_uuid:
258 continue
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
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 }