添加功能

This commit is contained in:
李鹏宇
2025-12-15 16:35:50 +08:00
parent feb038af43
commit 56414bd465
39 changed files with 452 additions and 27 deletions
+8
View File
@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Eng Mohamed Saied
#
#############################################################################
from . import models
+34
View File
@@ -0,0 +1,34 @@
{
'name': 'My Auto Backup Database',
'version': '1.0',
'category': 'Tools',
'summary': 'Manage and schedule database backups for Odoo instances',
'description': """
This module helps you to configure, schedule, and manage database backups for Odoo.
It includes features for backup generation, retention management, and report generation.
The module supports local storage and can be extended to cloud storage.
Features include:
- Configure backups for databases.
- Automate backups with a scheduler.
- Retain backups based on a defined retention policy.
- Generate detailed reports about backup status.
- Track backup progress in real-time.
""",
'depends':['base'],
'author': 'Mohamed Saied',
'data': [
'security/ir.model.access.csv',
'data/ir_cron_data.xml',
'views/db_backup.xml',
],
'images': [
'static/description/banner.gif',
'static/description/icon.png',
],
'license': 'LGPL-3',
'installable': True,
'auto_install': False,
'application': True,
}
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Schedule action for generating automatic database backup -->
<record id="ir_cron_auto_backup" model="ir.cron">
<field name="name">Backup: Automatic Database Backup</field>
<field name="model_id" ref="model_db_backup_configure"/>
<field name="state">code</field>
<field name="code">model._schedule_auto_backup()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="active">True</field>
</record>
</data>
</odoo>
+7
View File
@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Eng Mohamed Saied
#
#############################################################################
from . import db_backup
+158
View File
@@ -0,0 +1,158 @@
#############################################################################
#
# Eng Mohamed Saied
#
#############################################################################
from odoo import api, fields, models, _
from odoo.service import db
from odoo.exceptions import ValidationError, UserError
import os
import datetime
import logging
import time
_logger = logging.getLogger(__name__)
class DbBackupConfigure(models.Model):
_name = 'db.backup.configure'
_description = 'Database Backup Configuration'
name = fields.Char(string='Name', required=True)
db_name = fields.Char(string='Database Name', required=True)
master_pwd = fields.Char(string='Master Password', required=True)
backup_format = fields.Selection([('zip', 'Zip'), ('dump', 'Dump')], required=True, default='zip')
backup_destination = fields.Selection([('local', 'Local Storage')], required=True)
backup_path = fields.Char(string='Backup Path')
active = fields.Boolean(default=True)
auto_remove = fields.Boolean(string='Auto Remove Old Backups')
days_to_remove = fields.Integer(string='Days to Retain Backups')
backup_filename = fields.Char(string='Generated Filename')
generated_exception = fields.Text(string='Exception Details')
last_backup_status = fields.Selection([('success', 'Success'), ('failure', 'Failure')], string="Last Backup Status")
backup_progress = fields.Integer(string="Backup Progress (%)", default=0)
@api.constrains('db_name', 'master_pwd')
def _check_db_credentials(self):
"""Check if database credentials are valid."""
if self.db_name not in db.list_dbs():
raise ValidationError(_("Database %s does not exist.") % self.db_name)
try:
db.check_super(self.master_pwd)
except Exception as e:
raise ValidationError(_("Master Password is invalid. Error: %s") % e)
@api.constrains('backup_path')
def _check_backup_path(self):
"""Ensure the backup path is a valid directory."""
if self.backup_path and not os.path.isdir(self.backup_path):
raise ValidationError(_("Backup path '%s' is not a valid directory.") % self.backup_path)
def _generate_backup(self):
"""Generate a backup for this record with real-time progress."""
if self.backup_destination == 'local' and not self.backup_path:
raise UserError(_("Backup path is required for local storage."))
backup_time = datetime.datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S")
backup_filename = f"{self.db_name}_{backup_time}.{self.backup_format}"
self.backup_filename = backup_filename
backup_file = os.path.join(self.backup_path, backup_filename)
self.backup_progress = 0
try:
os.makedirs(self.backup_path, exist_ok=True)
total_steps = 10
for i in range(1, total_steps + 1):
time.sleep(1) # Simulate progress
self.backup_progress = int((i / total_steps) * 100)
self.backup_progress = 100
with open(backup_file, "wb") as f:
db.dump_db(self.db_name, f, self.backup_format)
if self.auto_remove:
self._remove_old_backups()
self.env['db.backup.log'].create({
'db_config_id': self.id,
'status': 'success',
'details': "Backup completed successfully.",
'file_path': backup_file,
})
self.last_backup_status = 'success'
except Exception as e:
self.backup_progress = 0
_logger.error("Backup failed for database %s: %s", self.db_name, e)
self.generated_exception = str(e)
self.env['db.backup.log'].create({
'db_config_id': self.id,
'status': 'failure',
'details': str(e),
})
self.last_backup_status = 'failure'
def _remove_old_backups(self):
"""Remove old backups based on the retention policy."""
if not self.auto_remove or not self.days_to_remove:
return
try:
now = datetime.datetime.utcnow()
for filename in os.listdir(self.backup_path):
file_path = os.path.join(self.backup_path, filename)
if os.path.isfile(file_path):
if filename.endswith(f".{self.backup_format}") and filename.startswith(self.db_name):
creation_time = datetime.datetime.fromtimestamp(os.path.getctime(file_path))
if (now - creation_time).days > self.days_to_remove:
os.remove(file_path)
except Exception as e:
_logger.error("Error removing old backups: %s", e)
def _schedule_auto_backup(self):
"""Method to trigger automatic database backup for active records."""
active_records = self.search([('active', '=', True)])
for record in active_records:
try:
record._generate_backup()
_logger.info("Backup successfully created for database: %s", record.db_name)
except Exception as e:
_logger.error("Failed to create backup for database %s: %s", record.db_name, e)
class DbBackupLog(models.Model):
_name = 'db.backup.log'
_description = 'Database Backup Log'
db_config_id = fields.Many2one('db.backup.configure', string="Configuration")
backup_date = fields.Datetime(string="Backup Date", default=fields.Datetime.now)
status = fields.Selection([('success', 'Success'), ('failure', 'Failure')], string="Status")
details = fields.Text(string="Details")
file_path = fields.Char(string="File Path")
file_size = fields.Float(string="File Size (MB)", compute="_compute_file_size")
def _compute_file_size(self):
for record in self:
if record.file_path and os.path.isfile(record.file_path):
record.file_size = os.path.getsize(record.file_path) / (1024 * 1024)
else:
record.file_size = 0
def download_backup(self):
"""Download the backup file."""
self.ensure_one()
if self.file_path and os.path.isfile(self.file_path):
return {
'type': 'ir.actions.act_url',
'url': f'/web/content?model=db.backup.log&id={self.id}&field=file_path&download=true',
'target': 'self',
}
else:
raise UserError(_("Backup file not found."))
def delete_backup(self):
"""Delete the backup file."""
self.ensure_one()
if self.file_path and os.path.isfile(self.file_path):
os.remove(self.file_path)
self.file_path = False
else:
raise UserError(_("Backup file not found or already deleted."))
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_db_backup_configure,db.backup.configure,model_db_backup_configure,,1,1,1,1
access_db_backup_log,db.backup.log,model_db_backup_log,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_db_backup_configure db.backup.configure model_db_backup_configure 1 1 1 1
3 access_db_backup_log db.backup.log model_db_backup_log 1 1 1 1
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -0,0 +1,66 @@
<div class="container" style="padding: 1rem !important; margin-bottom: 1rem !important;">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 d-flex justify-content-between" style="border-bottom: 1px solid #d5d5d5;">
<div class="my-3 d-flex align-items-center">
<div
style="background-color: #7C7BAD !important; color: #fff !important; font-weight: 600 !important; padding: 5px 15px 8px !important; margin: 0 5px !important;">
<i class="fa fa-check mr-1"></i>Community
</div>
<div
style="background-color: #875A7B !important; color: #fff !important; font-weight: 600 !important; padding: 5px 15px 8px !important; margin: 0 5px !important;">
<i class="fa fa-check mr-1"></i>Enterprise
</div>
<div
style="background-color: #008080 !important; color: #fff !important; font-weight: 600 !important; padding: 5px 15px 8px !important; margin: 0 5px !important;">
<i class="fa fa-check mr-1"></i>Odoo.sh
</div>
</div>
</div>
</div>
</div>
<div class="container" style="padding: 0rem 1.5rem 4rem !important">
<div class="row" id="screenshots">
<div class="col-md-12" style="border-bottom: 1px solid #d5d5d5 !important; margin: 2rem 0 !important">
<h2
style="font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; color: #714B67 !important; font-size: 1.5rem !important;">
<i class="fa fa-image mr-2"></i>Screenshots
</h2>
</div>
<div class="col-lg-12 my-2">
<h4 class="mt-2"
style="font-family: 'Roboto', sans-serif !important; font-weight: 600 !important; color: #282F33 !important; font-size: 1.3rem !important;">
Database Backup Configuration Menu</h4>
<p
style="font-family: 'Roboto', sans-serif !important; font-weight: 400 !important; color: #282F33 !important; font-size: 1rem !important;">
Got Setting --> Technical --> Backup Configuration to configure backups</p>
<img src="assets/screenshots/Capture1.JPG" class="img-responsive img-thumbnail border" width="100%"
height="auto" />
</div>
<div class="col-lg-12 my-3">
<h4 class="mt-3"
style="font-family: 'Roboto', sans-serif !important; font-weight: 600 !important; color: #282F33 !important; font-size: 1.3rem !important;">
Steps to Configure Database Backup</h4>
<ol
style="font-family: 'Roboto', sans-serif !important; font-weight: 400 !important; color: #282F33 !important; font-size: 1rem !important;">
<li>Enable <strong>Developer Mode</strong> in your Odoo instance.</li>
<li>Navigate to <code>Settings</code> --> <code>Technical</code> --> <code>Database Backup</code> under <strong>Configurations</strong>.</li>
<li>Click on <strong>Create</strong> to add a new database backup configuration.</li>
<li>Fill in the required fields:
<ul>
<li><strong>Host:</strong> Enter the host of your database.</li>
<li><strong>Database Name:</strong> Specify the actual database you want to back up.</li>
<li><strong>Backup Type:</strong> Choose the type of backup (e.g., full, incremental).</li>
<li><strong>Destination:</strong> Enter the directory path for the backup. Ensure the folder permissions are set correctly using <code>chown odoo:odoo</code>.</li>
</ul>
</li>
<li>Save the configuration.</li>
<li>Go to <code>Scheduled Actions</code> and locate the cron job named <strong>Backup: Automatic Database Backup</strong>.</li>
<li>Adjust the cron job settings to define how often backups should occur (e.g., daily, weekly).</li>
<li>Test the backup configuration to ensure it works as expected.</li>
</ol>
</div>
</div>
</div>
+125
View File
@@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Tree View for Database Backup Configuration -->
<record id="view_db_backup_configure_tree" model="ir.ui.view">
<field name="name">db.backup.configure.tree</field>
<field name="model">db.backup.configure</field>
<field name="arch" type="xml">
<tree string="Database Backup Configurations">
<field name="name"/>
<field name="db_name"/>
<field name="backup_format"/>
<field name="backup_destination"/>
<field name="backup_path"/>
<field name="active"/>
<field name="last_backup_status" widget="statusbar"/>
</tree>
</field>
</record>
<!-- Form View for Database Backup Configuration -->
<record id="view_db_backup_configure_form" model="ir.ui.view">
<field name="name">db.backup.configure.form</field>
<field name="model">db.backup.configure</field>
<field name="arch" type="xml">
<form string="Database Backup Configuration">
<sheet>
<group>
<field name="name"/>
<field name="db_name"/>
<field name="master_pwd" password="True"/>
<field name="backup_format"/>
<field name="backup_destination"/>
<field name="backup_path"/>
</group>
<group>
<field name="active"/>
<field name="auto_remove"/>
<field name="days_to_remove"/>
</group>
<group>
<field name="backup_filename" readonly="1"/>
<field name="generated_exception" readonly="1"/>
<field name="last_backup_status" widget="statusbar" readonly="1"/>
<field name="backup_progress" widget="progressbar" readonly="1"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- Tree View for Backup Logs -->
<record id="view_db_backup_log_tree" model="ir.ui.view">
<field name="name">db.backup.log.tree</field>
<field name="model">db.backup.log</field>
<field name="arch" type="xml">
<tree string="Backup Logs">
<field name="db_config_id"/>
<field name="backup_date"/>
<field name="status" widget="statusbar"/>
<field name="file_size" widget="float"/>
<field name="details"/>
</tree>
</field>
</record>
<!-- Form View for Backup Logs -->
<record id="view_db_backup_log_form" model="ir.ui.view">
<field name="name">db.backup.log.form</field>
<field name="model">db.backup.log</field>
<field name="arch" type="xml">
<form string="Backup Log Details">
<sheet>
<group>
<field name="db_config_id"/>
<field name="backup_date"/>
<field name="status" widget="statusbar"/>
<field name="file_path" readonly="1"/>
<field name="file_size" widget="float" readonly="1"/>
</group>
<group>
<field name="details" readonly="1"/>
</group>
<footer>
<button name="download_backup" string="Download" type="object" class="btn-primary"/>
<button name="delete_backup" string="Delete" type="object" class="btn-danger"/>
</footer>
</sheet>
</form>
</field>
</record>
<!-- Action for Database Backup Configuration -->
<record id="action_db_backup_configure" model="ir.actions.act_window">
<field name="name">Database Backup Configurations</field>
<field name="res_model">db.backup.configure</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_create">
Click to create a new database backup configuration.
</p>
</field>
</record>
<!-- Action for Backup Logs -->
<record id="action_db_backup_log" model="ir.actions.act_window">
<field name="name">Backup Logs</field>
<field name="res_model">db.backup.log</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_create">
No backup logs yet. Configure backups and start the process.
</p>
</field>
</record>
<!-- Menu Items -->
<menuitem id="menu_db_backup_configure_root" name="Database Backup" parent="base.menu_custom" sequence="10"/>
<menuitem id="menu_db_backup_configure" name="Configurations"
parent="menu_db_backup_configure_root"
action="action_db_backup_configure" sequence="10"/>
<menuitem id="menu_db_backup_logs" name="Backup Logs"
parent="menu_db_backup_configure_root"
action="action_db_backup_log" sequence="20"/>
</odoo>
@@ -94,6 +94,12 @@
<field name="shift_replacement_day"/>
<field name="total_days"/>
</xpath>
<xpath expr="//tree/field[@name='date_from']" position="attributes">
<attribute name="type">date</attribute>
</xpath>
<xpath expr="//tree/field[@name='date_to']" position="attributes">
<attribute name="type">date</attribute>
</xpath>
</field>
</record>
<record id="inherit_resource_calendar_leave_form" model="ir.ui.view">
@@ -73,6 +73,7 @@ class MeterPriceWizard(models.TransientModel):
valid.write({
'unit': unit,
})
valid.onchange_receivables()
self.env['hydropower.logs'].create({
'meter_type': 'electric',
+2 -2
View File
@@ -53,7 +53,7 @@ class TaskPushWizard(models.TransientModel):
original_amount = meter.archives_id.fixed_receivables
else:
original_amount = meter.receivables
if original_amount > 0:
self.env['property.accounts.receivable'].create({
'contract_id': meter.contract_id.id,
'lessee_id': meter.lessee_id.id if meter.lessee_id else None,
@@ -78,7 +78,7 @@ class TaskPushWizard(models.TransientModel):
remark = '新合同,暂无需抄表'
else:
remark = str(start_date) + '到' + str(end_date)
if water.water_id.fixed_receivables > 0 or water.receivables > 0:
self.env['property.accounts.receivable'].create({
'contract_id': water.contract_id.id,
'lessee_id': water.lessee_id.id if water.lessee_id else None,
@@ -72,6 +72,7 @@ class WaterPriceWizard(models.TransientModel):
valid.write({
'unit': unit,
})
valid.onchange_receivables()
self.env['hydropower.logs'].create({
'meter_type': 'water',