添加功能
@@ -0,0 +1,8 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Eng Mohamed Saied
|
||||
#
|
||||
#############################################################################
|
||||
from . import models
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Eng Mohamed Saied
|
||||
#
|
||||
#############################################################################
|
||||
from . import db_backup
|
||||
@@ -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
|
||||
|
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 310 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 576 B |
|
After Width: | Height: | Size: 733 B |
|
After Width: | Height: | Size: 911 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 673 B |
|
After Width: | Height: | Size: 878 B |
|
After Width: | Height: | Size: 653 B |
|
After Width: | Height: | Size: 905 B |
|
After Width: | Height: | Size: 839 B |
|
After Width: | Height: | Size: 427 B |
|
After Width: | Height: | Size: 627 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 988 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||