[MIG] auto_backup: Migrate to v9
* Add self.ensure_ones * Add test coveragepull/2712/head
parent
4b828622a9
commit
98d4864407
|
@ -70,15 +70,24 @@ manually execute the selected processes.
|
||||||
|
|
||||||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
||||||
:alt: Try me on Runbot
|
:alt: Try me on Runbot
|
||||||
:target: https://runbot.odoo-community.org/runbot/149/8.0
|
:target: https://runbot.odoo-community.org/runbot/149/10.0
|
||||||
|
|
||||||
|
Known issues / Roadmap
|
||||||
|
======================
|
||||||
|
|
||||||
|
* On larger databases, it is possible that backups will die due to Odoo server
|
||||||
|
settings. In order to circumvent this without frivolously changing settings,
|
||||||
|
you need to run the backup from outside of the main Odoo instance. How to do
|
||||||
|
this is outlined in `this blog post
|
||||||
|
<https://blog.laslabs.com/2016/10/running-python-scripts-within-odoos-environment/>`_.
|
||||||
|
|
||||||
Bug Tracker
|
Bug Tracker
|
||||||
===========
|
===========
|
||||||
|
|
||||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-tools/issues>`_.
|
Bugs are tracked on `GitHub Issues
|
||||||
In case of trouble, please check there if your issue has already been reported.
|
<https://github.com/OCA/server-tools/issues>`_. In case of trouble, please
|
||||||
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback
|
check there if your issue has already been reported. If you spotted it first,
|
||||||
`here <https://github.com/OCA/server-tools/issues/new?body=module:%20auto_backup%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
help us smashing it by providing a detailed and welcomed feedback.
|
||||||
|
|
||||||
Credits
|
Credits
|
||||||
=======
|
=======
|
||||||
|
@ -89,6 +98,7 @@ Contributors
|
||||||
* Yenthe Van Ginneken <yenthe.vanginneken@vanroey.be>
|
* Yenthe Van Ginneken <yenthe.vanginneken@vanroey.be>
|
||||||
* Alessio Gerace <alessio.gerace@agilebg.com>
|
* Alessio Gerace <alessio.gerace@agilebg.com>
|
||||||
* Jairo Llopis <yajo.sk8@gmail.com>
|
* Jairo Llopis <yajo.sk8@gmail.com>
|
||||||
|
* Dave Lasley <dave@laslabs.com>
|
||||||
|
|
||||||
Maintainer
|
Maintainer
|
||||||
----------
|
----------
|
||||||
|
|
|
@ -7,19 +7,24 @@
|
||||||
{
|
{
|
||||||
"name": "Database Auto-Backup",
|
"name": "Database Auto-Backup",
|
||||||
"summary": "Backups database",
|
"summary": "Backups database",
|
||||||
"version": "8.0.1.0.1",
|
"version": "10.0.1.0.0",
|
||||||
"author": (
|
"author": (
|
||||||
"VanRoey.be - Yenthe Van Ginneken, Agile Business Group,"
|
"Yenthe Van Ginneken, "
|
||||||
" Grupo ESOC Ingenier??a de Servicios,"
|
"Agile Business Group, "
|
||||||
" Odoo Community Association (OCA)"
|
"Grupo ESOC Ingenier??a de Servicios, "
|
||||||
|
"LasLabs, "
|
||||||
|
"Odoo Community Association (OCA)"
|
||||||
),
|
),
|
||||||
'license': "AGPL-3",
|
'license': "AGPL-3",
|
||||||
"website": "http://www.vanroey.be/applications/bedrijfsbeheer/odoo",
|
"website": "http://www.vanroey.be/applications/bedrijfsbeheer/odoo",
|
||||||
"category": "Tools",
|
"category": "Tools",
|
||||||
"depends": ['email_template'],
|
"depends": [
|
||||||
"demo": [],
|
'base_setup',
|
||||||
|
'mail',
|
||||||
|
],
|
||||||
"data": [
|
"data": [
|
||||||
"data/backup_data.yml",
|
"data/ir_cron.xml",
|
||||||
|
"data/mail_message_subtype.xml",
|
||||||
"security/ir.model.access.csv",
|
"security/ir.model.access.csv",
|
||||||
"view/db_backup_view.xml",
|
"view/db_backup_view.xml",
|
||||||
],
|
],
|
|
@ -1,28 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# ?? 2016 Grupo ESOC Ingenier??a de Servicios, S.L.U. - Jairo Llopis
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
||||||
|
|
||||||
# Cron job
|
|
||||||
- !record {model: ir.cron, id: ir_cron_backupscheduler0}:
|
|
||||||
name: Backup scheduler
|
|
||||||
user_id: base.user_root
|
|
||||||
interval_number: 1
|
|
||||||
interval_type: days
|
|
||||||
numbercall: -1
|
|
||||||
nextcall: !eval
|
|
||||||
(datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d 02:00:00")
|
|
||||||
model: db.backup
|
|
||||||
function: action_backup_all
|
|
||||||
|
|
||||||
# New message subtypes
|
|
||||||
- !record {model: mail.message.subtype, id: success}:
|
|
||||||
name: Backup successful
|
|
||||||
res_model: db.backup
|
|
||||||
default: False
|
|
||||||
description: Database backup succeeded.
|
|
||||||
|
|
||||||
- !record {model: mail.message.subtype, id: failure}:
|
|
||||||
name: Backup failed
|
|
||||||
res_model: db.backup
|
|
||||||
default: True
|
|
||||||
description: Database backup failed.
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<odoo noupdate="1">
|
||||||
|
|
||||||
|
<record id="ir_cron_backup_scheduler_0" model="ir.cron">
|
||||||
|
<field name="name">Backup Scheduler</field>
|
||||||
|
<field name="user_id" ref="base.user_root" />
|
||||||
|
<field name="interval_number">1</field>
|
||||||
|
<field name="interval_type">days</field>
|
||||||
|
<field name="numbercall">-1</field>
|
||||||
|
<field name="nextcall"
|
||||||
|
eval="(datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d 02:00:00')"
|
||||||
|
/>
|
||||||
|
<field name="model">db.backup</field>
|
||||||
|
<field name="function">action_backup_all</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<odoo noupdate="1">
|
||||||
|
|
||||||
|
<record id="mail_message_subtype_success" model="mail.message.subtype">
|
||||||
|
<field name="name">Backup Successful</field>
|
||||||
|
<field name="description">Database backup succeeded.</field>
|
||||||
|
<field name="res_model">db.backup</field>
|
||||||
|
<field name="default" eval="False" />
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mail_message_subtype_failure" model="mail.message.subtype">
|
||||||
|
<field name="name">Backup Failed</field>
|
||||||
|
<field name="description">Database backup failed.</field>
|
||||||
|
<field name="res_model">db.backup</field>
|
||||||
|
<field name="default" eval="True" />
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
|
@ -10,13 +10,13 @@ import traceback
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from glob import iglob
|
from glob import iglob
|
||||||
from openerp import exceptions, models, fields, api, _, tools
|
from odoo import exceptions, models, fields, api, _, tools
|
||||||
from openerp.service import db
|
from odoo.service import db
|
||||||
import logging
|
import logging
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
try:
|
try:
|
||||||
import pysftp
|
import pysftp
|
||||||
except ImportError:
|
except ImportError: # pragma: no cover
|
||||||
_logger.debug('Cannot import pysftp')
|
_logger.debug('Cannot import pysftp')
|
||||||
|
|
||||||
|
|
||||||
|
@ -107,8 +107,8 @@ class DbBackup(models.Model):
|
||||||
rec.name = "sftp://%s@%s:%d%s" % (
|
rec.name = "sftp://%s@%s:%d%s" % (
|
||||||
rec.sftp_user, rec.sftp_host, rec.sftp_port, rec.folder)
|
rec.sftp_user, rec.sftp_host, rec.sftp_port, rec.folder)
|
||||||
|
|
||||||
@api.constrains("folder", "method")
|
|
||||||
@api.multi
|
@api.multi
|
||||||
|
@api.constrains("folder", "method")
|
||||||
def _check_folder(self):
|
def _check_folder(self):
|
||||||
"""Do not use the filestore or you will backup your backups."""
|
"""Do not use the filestore or you will backup your backups."""
|
||||||
for s in self:
|
for s in self:
|
||||||
|
@ -235,6 +235,7 @@ class DbBackup(models.Model):
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def cleanup_log(self):
|
def cleanup_log(self):
|
||||||
"""Log a possible cleanup failure."""
|
"""Log a possible cleanup failure."""
|
||||||
|
self.ensure_one()
|
||||||
try:
|
try:
|
||||||
_logger.info("Starting cleanup process after database backup: %s",
|
_logger.info("Starting cleanup process after database backup: %s",
|
||||||
self.name)
|
self.name)
|
||||||
|
@ -263,6 +264,7 @@ class DbBackup(models.Model):
|
||||||
@api.multi
|
@api.multi
|
||||||
def sftp_connection(self):
|
def sftp_connection(self):
|
||||||
"""Return a new SFTP connection with found parameters."""
|
"""Return a new SFTP connection with found parameters."""
|
||||||
|
self.ensure_one()
|
||||||
params = {
|
params = {
|
||||||
"host": self.sftp_host,
|
"host": self.sftp_host,
|
||||||
"username": self.sftp_user,
|
"username": self.sftp_user,
|
||||||
|
|
|
@ -4,4 +4,4 @@
|
||||||
# ?? 2016 Grupo ESOC Ingenier??a de Servicios, S.L.U. - Jairo Llopis
|
# ?? 2016 Grupo ESOC Ingenier??a de Servicios, S.L.U. - Jairo Llopis
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
from . import test_auto_backup
|
from . import test_db_backup
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# ?? 2015 Agile Business Group <http://www.agilebg.com>
|
|
||||||
# ?? 2015 Alessio Gerace <alesiso.gerace@agilebg.com>
|
|
||||||
# ?? 2016 Grupo ESOC Ingenier??a de Servicios, S.L.U. - Jairo Llopis
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
||||||
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
from openerp.tests import common
|
|
||||||
|
|
||||||
|
|
||||||
class TestsAutoBackup(common.TransactionCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestsAutoBackup, self).setUp()
|
|
||||||
self.abk = self.env["db.backup"].create(
|
|
||||||
{
|
|
||||||
'name': u'T??st backup',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_local(self):
|
|
||||||
"""A local database is backed up."""
|
|
||||||
filename = self.abk.filename(datetime.now())
|
|
||||||
self.abk.action_backup()
|
|
||||||
generated_backup = [f for f in os.listdir(self.abk.folder)
|
|
||||||
if f >= filename]
|
|
||||||
self.assertEqual(len(generated_backup), 1)
|
|
|
@ -0,0 +1,232 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# ?? 2015 Agile Business Group <http://www.agilebg.com>
|
||||||
|
# ?? 2015 Alessio Gerace <alesiso.gerace@agilebg.com>
|
||||||
|
# ?? 2016 Grupo ESOC Ingenier??a de Servicios, S.L.U. - Jairo Llopis
|
||||||
|
# Copyright 2016 LasLabs Inc.
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
|
import os
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from odoo.tests import common
|
||||||
|
from odoo import exceptions, tools
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pysftp
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
model = 'odoo.addons.auto_backup.models.db_backup'
|
||||||
|
|
||||||
|
|
||||||
|
class TestConnectionException(pysftp.ConnectionException):
|
||||||
|
def __init__(self):
|
||||||
|
super(TestConnectionException, self).__init__('test', 'test')
|
||||||
|
|
||||||
|
|
||||||
|
class TestDbBackup(common.TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestDbBackup, self).setUp()
|
||||||
|
self.Model = self.env["db.backup"]
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def mock_assets(self):
|
||||||
|
""" It provides mocked core assets """
|
||||||
|
self.path_join_val = '/this/is/a/path'
|
||||||
|
with mock.patch('%s.db' % model) as db:
|
||||||
|
with mock.patch('%s.os' % model) as os:
|
||||||
|
with mock.patch('%s.shutil' % model) as shutil:
|
||||||
|
os.path.join.return_value = self.path_join_val
|
||||||
|
yield {
|
||||||
|
'db': db,
|
||||||
|
'os': os,
|
||||||
|
'shutil': shutil,
|
||||||
|
}
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def patch_filtered_sftp(self, record, mocks=None):
|
||||||
|
""" It patches filtered record and provides a mock """
|
||||||
|
if mocks is None:
|
||||||
|
mocks = ['sftp_connection']
|
||||||
|
mocks = {m: mock.DEFAULT for m in mocks}
|
||||||
|
with mock.patch.object(record, 'filtered') as filtered:
|
||||||
|
with mock.patch.object(record, 'backup_log'):
|
||||||
|
with mock.patch.multiple(record, **mocks):
|
||||||
|
filtered.side_effect = [], [record]
|
||||||
|
yield filtered
|
||||||
|
|
||||||
|
def new_record(self, method='sftp'):
|
||||||
|
vals = {
|
||||||
|
'name': u'T??st backup',
|
||||||
|
'method': method,
|
||||||
|
}
|
||||||
|
if method == 'sftp':
|
||||||
|
vals.update({
|
||||||
|
'sftp_host': 'test_host',
|
||||||
|
'sftp_port': '222',
|
||||||
|
'sftp_user': 'tuser',
|
||||||
|
'sftp_password': 'password',
|
||||||
|
'folder': '/folder/',
|
||||||
|
})
|
||||||
|
self.vals = vals
|
||||||
|
return self.Model.create(vals)
|
||||||
|
|
||||||
|
def test_compute_name_sftp(self):
|
||||||
|
""" It should create proper SFTP URI """
|
||||||
|
rec_id = self.new_record()
|
||||||
|
self.assertEqual(
|
||||||
|
'sftp://%(user)s@%(host)s:%(port)s%(folder)s' % {
|
||||||
|
'user': self.vals['sftp_user'],
|
||||||
|
'host': self.vals['sftp_host'],
|
||||||
|
'port': self.vals['sftp_port'],
|
||||||
|
'folder': self.vals['folder'],
|
||||||
|
},
|
||||||
|
rec_id.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_check_folder(self):
|
||||||
|
""" It should not allow recursive backups """
|
||||||
|
rec_id = self.new_record('local')
|
||||||
|
with self.assertRaises(exceptions.ValidationError):
|
||||||
|
rec_id.write({
|
||||||
|
'folder': '%s/another/path' % tools.config.filestore(
|
||||||
|
self.env.cr.dbname
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
@mock.patch('%s._' % model)
|
||||||
|
def test_action_sftp_test_connection_success(self, _):
|
||||||
|
""" It should raise connection succeeded warning """
|
||||||
|
rec_id = self.new_record()
|
||||||
|
with mock.patch.object(rec_id, 'sftp_connection'):
|
||||||
|
with self.assertRaises(exceptions.Warning):
|
||||||
|
rec_id.action_sftp_test_connection()
|
||||||
|
_.assert_called_once_with("Connection Test Succeeded!")
|
||||||
|
|
||||||
|
@mock.patch('%s._' % model)
|
||||||
|
def test_action_sftp_test_connection_fail(self, _):
|
||||||
|
""" It should raise connection fail warning """
|
||||||
|
rec_id = self.new_record()
|
||||||
|
with mock.patch.object(rec_id, 'sftp_connection') as conn:
|
||||||
|
conn().__enter__.side_effect = TestConnectionException
|
||||||
|
with self.assertRaises(exceptions.Warning):
|
||||||
|
rec_id.action_sftp_test_connection()
|
||||||
|
_.assert_called_once_with("Connection Test Failed!")
|
||||||
|
|
||||||
|
def test_action_backup_local(self):
|
||||||
|
""" It should backup local database """
|
||||||
|
rec_id = self.new_record('local')
|
||||||
|
filename = rec_id.filename(datetime.now())
|
||||||
|
rec_id.action_backup()
|
||||||
|
generated_backup = [f for f in os.listdir(rec_id.folder)
|
||||||
|
if f >= filename]
|
||||||
|
self.assertEqual(1, len(generated_backup))
|
||||||
|
|
||||||
|
def test_action_backup_sftp_mkdirs(self):
|
||||||
|
""" It should create remote dirs """
|
||||||
|
rec_id = self.new_record()
|
||||||
|
with self.mock_assets():
|
||||||
|
with self.patch_filtered_sftp(rec_id):
|
||||||
|
conn = rec_id.sftp_connection().__enter__()
|
||||||
|
rec_id.action_backup()
|
||||||
|
conn.makedirs.assert_called_once_with(rec_id.folder)
|
||||||
|
|
||||||
|
def test_action_backup_sftp_mkdirs_conn_exception(self):
|
||||||
|
""" It should guard from ConnectionException on remote.mkdirs """
|
||||||
|
rec_id = self.new_record()
|
||||||
|
with self.mock_assets():
|
||||||
|
with self.patch_filtered_sftp(rec_id):
|
||||||
|
conn = rec_id.sftp_connection().__enter__()
|
||||||
|
conn.makedirs.side_effect = TestConnectionException
|
||||||
|
rec_id.action_backup()
|
||||||
|
# No error was raised, test pass
|
||||||
|
self.assertTrue(True)
|
||||||
|
|
||||||
|
def test_action_backup_sftp_remote_open(self):
|
||||||
|
""" It should open remote file w/ proper args """
|
||||||
|
rec_id = self.new_record()
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
with self.patch_filtered_sftp(rec_id):
|
||||||
|
conn = rec_id.sftp_connection().__enter__()
|
||||||
|
rec_id.action_backup()
|
||||||
|
conn.open.assert_called_once_with(
|
||||||
|
assets['os'].path.join(),
|
||||||
|
'wb'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_action_backup_sftp_remote_open(self):
|
||||||
|
""" It should open remote file w/ proper args """
|
||||||
|
rec_id = self.new_record()
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
with self.patch_filtered_sftp(rec_id):
|
||||||
|
conn = rec_id.sftp_connection().__enter__()
|
||||||
|
rec_id.action_backup()
|
||||||
|
conn.open.assert_called_once_with(
|
||||||
|
assets['os'].path.join(),
|
||||||
|
'wb'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_action_backup_all_search(self):
|
||||||
|
""" It should search all records """
|
||||||
|
rec_id = self.new_record()
|
||||||
|
with mock.patch.object(rec_id, 'search'):
|
||||||
|
rec_id.action_backup_all()
|
||||||
|
rec_id.search.assert_called_once_with([])
|
||||||
|
|
||||||
|
def test_action_backup_all_return(self):
|
||||||
|
""" It should return result of backup operation """
|
||||||
|
rec_id = self.new_record()
|
||||||
|
with mock.patch.object(rec_id, 'search'):
|
||||||
|
res = rec_id.action_backup_all()
|
||||||
|
self.assertEqual(
|
||||||
|
rec_id.search().action_backup(), res
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('%s.pysftp' % model)
|
||||||
|
def test_sftp_connection_init_passwd(self, pysftp):
|
||||||
|
""" It should initiate SFTP connection w/ proper args and pass """
|
||||||
|
rec_id = self.new_record()
|
||||||
|
rec_id.sftp_connection()
|
||||||
|
pysftp.Connection.assert_called_once_with(
|
||||||
|
host=rec_id.sftp_host,
|
||||||
|
username=rec_id.sftp_user,
|
||||||
|
port=rec_id.sftp_port,
|
||||||
|
password=rec_id.sftp_password,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('%s.pysftp' % model)
|
||||||
|
def test_sftp_connection_init_key(self, pysftp):
|
||||||
|
""" It should initiate SFTP connection w/ proper args and key """
|
||||||
|
rec_id = self.new_record()
|
||||||
|
rec_id.write({
|
||||||
|
'sftp_private_key': 'pkey',
|
||||||
|
'sftp_password': 'pkeypass',
|
||||||
|
})
|
||||||
|
rec_id.sftp_connection()
|
||||||
|
pysftp.Connection.assert_called_once_with(
|
||||||
|
host=rec_id.sftp_host,
|
||||||
|
username=rec_id.sftp_user,
|
||||||
|
port=rec_id.sftp_port,
|
||||||
|
private_key=rec_id.sftp_private_key,
|
||||||
|
private_key_pass=rec_id.sftp_password,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('%s.pysftp' % model)
|
||||||
|
def test_sftp_connection_return(self, pysftp):
|
||||||
|
""" It should return new sftp connection """
|
||||||
|
rec_id = self.new_record()
|
||||||
|
res = rec_id.sftp_connection()
|
||||||
|
self.assertEqual(
|
||||||
|
pysftp.Connection(), res,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_filename(self):
|
||||||
|
""" It should not error and should return a .dump.zip file str """
|
||||||
|
now = datetime.now()
|
||||||
|
res = self.Model.filename(now)
|
||||||
|
self.assertTrue(res.endswith(".dump.zip"))
|
|
@ -1,6 +1,5 @@
|
||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<openerp>
|
<odoo>
|
||||||
<data>
|
|
||||||
|
|
||||||
<record model="ir.ui.view" id="view_backup_conf_form">
|
<record model="ir.ui.view" id="view_backup_conf_form">
|
||||||
<field name="name">Automated Backups</field>
|
<field name="name">Automated Backups</field>
|
||||||
|
@ -80,7 +79,7 @@
|
||||||
res_model="db.backup"/>
|
res_model="db.backup"/>
|
||||||
|
|
||||||
<menuitem
|
<menuitem
|
||||||
parent="base.menu_config"
|
parent="base_setup.menu_config"
|
||||||
action="action_backup_conf_form"
|
action="action_backup_conf_form"
|
||||||
id="backup_conf_menu"/>
|
id="backup_conf_menu"/>
|
||||||
|
|
||||||
|
@ -104,5 +103,5 @@
|
||||||
<field name="model">db.backup</field>
|
<field name="model">db.backup</field>
|
||||||
<field name="key2">client_action_multi</field>
|
<field name="key2">client_action_multi</field>
|
||||||
</record>
|
</record>
|
||||||
</data>
|
|
||||||
</openerp>
|
</odoo>
|
||||||
|
|
Loading…
Reference in New Issue