diff --git a/base_export_manager/README.rst b/base_export_manager/README.rst index 07454a9f0..be8bf9c81 100644 --- a/base_export_manager/README.rst +++ b/base_export_manager/README.rst @@ -44,7 +44,8 @@ To manage export profiles, you need to: * Choose a name. * Choose a model (table in the database). * Choose the fields to export. - * If you choose a related field, you can choose also up to 3 levels of subfields. + * If you choose a related field, you can choose also up to 4 levels of + subfields. * You can drag & drop to reorder the fields. To use one of those profiles, you need to: diff --git a/base_export_manager/__init__.py b/base_export_manager/__init__.py index 13d8b17b5..ba332de8d 100644 --- a/base_export_manager/__init__.py +++ b/base_export_manager/__init__.py @@ -3,3 +3,4 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import models +from .hooks import post_init_hook diff --git a/base_export_manager/__manifest__.py b/base_export_manager/__manifest__.py index 5c27c0a07..160704896 100644 --- a/base_export_manager/__manifest__.py +++ b/base_export_manager/__manifest__.py @@ -5,12 +5,11 @@ { 'name': "Manage model export profiles", 'category': 'Personalization', - 'version': '9.0.1.1.0', + 'version': '10.0.1.0.0', 'depends': [ 'web', ], 'data': [ - 'data/ir_exports_data.xml', 'views/assets.xml', 'views/ir_exports.xml', 'views/ir_model.xml', @@ -20,13 +19,12 @@ 'qweb': [ "static/src/xml/base.xml", ], - 'author': 'Antiun Ingeniería S.L., ' - 'Tecnativa, ' + 'author': 'Tecnativa, ' 'LasLabs, ' 'Ursa Information Systems, ' 'Odoo Community Association (OCA)', - 'website': 'http://www.antiun.com', + 'website': 'https://www.tecnativa.com', 'license': 'AGPL-3', - 'installable': False, + 'installable': True, 'application': False, } diff --git a/base_export_manager/data/ir_exports_data.xml b/base_export_manager/data/ir_exports_data.xml deleted file mode 100644 index c04761a89..000000000 --- a/base_export_manager/data/ir_exports_data.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/base_export_manager/hooks.py b/base_export_manager/hooks.py new file mode 100644 index 000000000..5718758cc --- /dev/null +++ b/base_export_manager/hooks.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Jairo Llopis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from openerp import api, SUPERUSER_ID + + +def post_init_hook(cr, registry): + """Loaded after installing the module. + + ``ir.exports.line.name`` was before a char field, and now it is a computed + char field with stored values. We have to inverse it to avoid database + inconsistencies. + """ + with api.Environment.manage(): + env = api.Environment(cr, SUPERUSER_ID, {}) + env["ir.exports.line"].search([ + ("field1_id", "=", False), + ("export_id", "!=", False), + ("name", "!=", False), + ])._inverse_name() diff --git a/base_export_manager/models/ir_exports.py b/base_export_manager/models/ir_exports.py index 523696085..1e48b93ec 100644 --- a/base_export_manager/models/ir_exports.py +++ b/base_export_manager/models/ir_exports.py @@ -2,8 +2,8 @@ # Copyright 2015-2016 Jairo Llopis # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp import _, api, fields, models -from openerp.exceptions import ValidationError +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError class IrExports(models.Model): diff --git a/base_export_manager/models/ir_exports_line.py b/base_export_manager/models/ir_exports_line.py index d4c268099..d9292f75e 100644 --- a/base_export_manager/models/ir_exports_line.py +++ b/base_export_manager/models/ir_exports_line.py @@ -3,8 +3,7 @@ # Copyright 2015-2016 Jairo Llopis # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp import models, fields, api, exceptions -from openerp.tools.translate import _ +from odoo import _, models, fields, api, exceptions class IrExportsLine(models.Model): @@ -30,6 +29,10 @@ class IrExportsLine(models.Model): "ir.model.fields", "Third field", domain="[('model_id', '=', model3_id)]") + field4_id = fields.Many2one( + "ir.model.fields", + "Fourth field", + domain="[('model_id', '=', model4_id)]") model1_id = fields.Many2one( "ir.model", "First model", @@ -44,6 +47,10 @@ class IrExportsLine(models.Model): "ir.model", "Third model", compute="_compute_model3_id") + model4_id = fields.Many2one( + "ir.model", + "Fourth model", + compute="_compute_model4_id") sequence = fields.Integer() label = fields.Char( compute="_compute_label") @@ -54,98 +61,117 @@ class IrExportsLine(models.Model): return self.env.context.get("default_model1_id", False) @api.multi - @api.depends("field1_id", "field2_id", "field3_id") + @api.depends("field1_id", "field2_id", "field3_id", "field4_id") def _compute_name(self): """Get the name from the selected fields.""" - for s in self: - s.name = "/".join((s.field_n(num).name - for num in range(1, 4) - if s.field_n(num))) + for one in self: + name = "/".join((one.field_n(num).name for num in range(1, 5) + if one.field_n(num))) + if name != one.name: + one.name = name @api.multi @api.depends("field1_id") def _compute_model2_id(self): """Get the related model for the second field.""" - ir_model = self.env["ir.model"] - for s in self: - s.model2_id = ( - s.field1_id.ttype and - "2" in s.field1_id.ttype and - ir_model.search([("model", "=", s.field1_id.relation)])) + IrModel = self.env["ir.model"] + for one in self: + one.model2_id = ( + one.field1_id.ttype and + "2" in one.field1_id.ttype and + IrModel.search([("model", "=", one.field1_id.relation)])) @api.multi @api.depends("field2_id") def _compute_model3_id(self): """Get the related model for the third field.""" - ir_model = self.env["ir.model"] - for s in self: - s.model3_id = ( - s.field2_id.ttype and - "2" in s.field2_id.ttype and - ir_model.search([("model", "=", s.field2_id.relation)])) + IrModel = self.env["ir.model"] + for one in self: + one.model3_id = ( + one.field2_id.ttype and + "2" in one.field2_id.ttype and + IrModel.search([("model", "=", one.field2_id.relation)])) + + @api.multi + @api.depends("field3_id") + def _compute_model4_id(self): + """Get the related model for the third field.""" + IrModel = self.env["ir.model"] + for one in self: + one.model4_id = ( + one.field3_id.ttype and + "2" in one.field3_id.ttype and + IrModel.search([("model", "=", one.field3_id.relation)])) @api.multi @api.depends('name') def _compute_label(self): """Column label in a user-friendly format and language.""" - translations = self.env["ir.translation"] - for s in self: + for one in self: parts = list() - for num in range(1, 4): - field = s.field_n(num) + for num in range(1, 5): + field = one.field_n(num) if not field: break - # Translate label if possible - parts.append( - translations.search([ - ("type", "=", "field"), - ("lang", "=", self.env.context.get("lang")), - ("name", "=", "%s,%s" % (s.model_n(num).model, - field.name)), - ]).value or - field.display_name) - s.label = ("%s (%s)" % ("/".join(parts), s.name) - if parts and s.name else False) + try: + parts.append( + one.env[one.model_n(num).model]._fields[field.name] + .get_description(one.env)["string"]) + except KeyError: + # No human-readable string available, so empty this + return + one.label = ("%s (%s)" % ("/".join(parts), one.name) + if parts and one.name else False) @api.multi def _inverse_name(self): """Get the fields from the name.""" - for s in self: - # Field names can have up to only 3 indentation levels - parts = s.name.split("/", 2) - - for num in range(1, 4): - try: - # Fail in excessive subfield level - field_name = parts[num - 1] - except IndexError: - # Remove subfield on failure - s[s.field_n(num, True)] = False - else: - model = s.model_n(num) - s[s.field_n(num, True)] = self._get_field_id( - model, field_name) + for one in self: + # Field names can have up to only 4 indentation levels + parts = one.name.split("/") + if len(parts) > 4: + raise exceptions.ValidationError( + _("It's not allowed to have more than 4 levels depth: " + "%s") % one.name) + for num in range(1, 5): + if num > len(parts): + # Empty subfield in this case + one[one.field_n(num, True)] = False + continue + field_name = parts[num - 1] + model = one.model_n(num) + # You could get to failing constraint while populating the + # fields, so we skip the uniqueness check and manually check + # the full constraint after the loop + one.with_context(skip_check=True)[one.field_n(num, True)] = ( + one._get_field_id(model, field_name)) + one._check_name() @api.multi - @api.constrains("field1_id", "field2_id", "field3_id") + @api.constrains("field1_id", "field2_id", "field3_id", "field4_id") def _check_name(self): - for rec_id in self: - if not rec_id.label: + for one in self: + if not one.label: raise exceptions.ValidationError( - _("Field '%s' does not exist") % rec_id.name) - lines = self.search([('export_id', '=', rec_id.export_id.id), - ('name', '=', rec_id.name)]) - if len(lines) > 1: - raise exceptions.ValidationError( - _("Field '%s' already exists") % rec_id.name) + _("Field '%s' does not exist") % one.name) + if not one.env.context.get('skip_check'): + lines = one.search([('export_id', '=', one.export_id.id), + ('name', '=', one.name)]) + if len(lines) > 1: + raise exceptions.ValidationError( + _("Field '%s' already exists") % one.name) - @api.model - def _install_base_export_manager(self): - """Populate ``field*_id`` fields.""" - self.search([("export_id", "=", False)]).unlink() - self.search([("field1_id", "=", False), - ("name", "!=", False)])._inverse_name() + @api.multi + @api.onchange('name') + def _onchange_name(self): + if self.name: + self._inverse_name() + else: + self.field1_id = False + self.field2_id = False + self.field3_id = False + self.field4_id = False @api.model def _get_field_id(self, model, name): @@ -157,9 +183,13 @@ class IrExportsLine(models.Model): :param str name: Technical name of the field, like ``child_ids``. """ - return self.env["ir.model.fields"].search( + field = self.env["ir.model.fields"].search( [("name", "=", name), ("model_id", "=", model.id)]) + if not field.exists(): + raise exceptions.ValidationError( + _("Field '%s' not found in model '%s'") % (name, model.model)) + return field @api.multi def field_n(self, n, only_name=False): diff --git a/base_export_manager/models/ir_model_access.py b/base_export_manager/models/ir_model_access.py index 24fed7261..7dfc16358 100644 --- a/base_export_manager/models/ir_model_access.py +++ b/base_export_manager/models/ir_model_access.py @@ -3,7 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) -from openerp import models, fields +from odoo import models, fields class IrModelAccess(models.Model): diff --git a/base_export_manager/models/res_users.py b/base_export_manager/models/res_users.py index 759bf266a..4db9dd660 100644 --- a/base_export_manager/models/res_users.py +++ b/base_export_manager/models/res_users.py @@ -3,7 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) -from openerp import api, models +from odoo import api, models class ResUsers(models.Model): diff --git a/base_export_manager/static/src/js/base_export_manager.js b/base_export_manager/static/src/js/base_export_manager.js index a5a642e9b..b31e5df74 100644 --- a/base_export_manager/static/src/js/base_export_manager.js +++ b/base_export_manager/static/src/js/base_export_manager.js @@ -5,58 +5,13 @@ odoo.define('base_export_manager.base_export_manager', function(require) { 'use strict'; - var jQuery = require('$'); - var DataExport = require('web.DataExport'); var core = require('web.core'); var Model = require('web.DataModel'); var ListView = require('web.ListView'); var Sidebar = require('web.Sidebar'); var _t = core._t; - var Session = require('web.Session'); - var session = require('web.session'); - - - DataExport.include({ - do_load_export_field: function(field_list) { - var export_node = this.$el.find("#fields_list"); - _(field_list).each(function (field) { - export_node.append(new Option(field.label + ' (' + field.name + ')', field.name)); - }); - }, - add_field: function(field_id, string) { - var field_list = this.$el.find('#fields_list'); - if (this.$el.find("#fields_list option[value='" + field_id + "']") && - !this.$el.find("#fields_list option[value='" + field_id + "']").length) - { - field_list.append(new Option(string + ' (' + field_id + ')', field_id)); - } - }, - }); - - - Session.include({ - get_export_models: function() { - if (!this.uid) { - return $.when().resolve(false); - } - var Users = new Model('res.users'); - var export_models = Users.call('fetch_export_models', []); - return export_models; - }, - }); ListView.include({ - view_loading: function(fvg) { - this._super(fvg); - this.is_export_manager(); - }, - is_export_manager: function () { - var self = this; - $.when(self.session.get_export_models()).then(function - (export_models) { - self.export_models=export_models; - }); - }, /** * Instantiate and render the sidebar. * Sets this.sidebar @@ -66,26 +21,27 @@ odoo.define('base_export_manager.base_export_manager', function(require) { **/ render_sidebar: function($node) { var self = this; - self.render_export_enable = jQuery.inArray( this.model, self.export_models ); - if (!this.sidebar && this.options.sidebar) { - this.sidebar = new Sidebar(this, {editable: this.is_action_enabled('edit')}); - if (this.fields_view.toolbar) { - this.sidebar.add_toolbar(this.fields_view.toolbar); - } - this.sidebar.add_items('other', _.compact([ - self.render_export_enable >= 0 && {label: _t("Export"), callback: this.on_sidebar_export}, - this.fields_view.fields.active && {label: _t("Archive"), callback: this.do_archive_selected}, - this.fields_view.fields.active && {label: _t("Unarchive"), callback: this.do_unarchive_selected}, - this.is_action_enabled('delete') && {label: _t('Delete'), callback: this.do_delete_selected} - ])); - - $node = $node || this.options.$sidebar; - this.sidebar.appendTo($node); - - // Hide the sidebar by default (it will be shown as soon as a record is selected) - this.sidebar.do_hide(); - } + this._super($node); + var Users = new Model('res.users'); + Users.call('fetch_export_models', []).done(function(export_models){ + self.export_models = export_models; + self.render_export_enable = jQuery.inArray( self.model, self.export_models ); + if(self.sidebar && self.sidebar.items && self.sidebar.items.other){ + var items_data = []; + _.each(self.sidebar.items.other,function(rec){ + if(rec.label != _t("Export")){ + items_data.push(rec); + } + }); + self.sidebar.items.other = items_data; + self.sidebar.add_items('other', _.compact([ + self.render_export_enable >= 0 && { + label: _t("Export"), + callback: self.on_sidebar_export + }, + ])); + } + }); }, }); - }); diff --git a/base_export_manager/static/src/xml/base.xml b/base_export_manager/static/src/xml/base.xml index b9b51f28a..b26077a69 100644 --- a/base_export_manager/static/src/xml/base.xml +++ b/base_export_manager/static/src/xml/base.xml @@ -1,8 +1,8 @@ - - + + () diff --git a/base_export_manager/tests/__init__.py b/base_export_manager/tests/__init__.py index 242cd85cd..5a46641eb 100644 --- a/base_export_manager/tests/__init__.py +++ b/base_export_manager/tests/__init__.py @@ -3,4 +3,5 @@ # Copyright 2015 Antiun Ingeniería S.L. - Jairo Llopis # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from . import test_ir_exports, test_ir_exports_line +from . import test_ir_exports +from . import test_ir_exports_line diff --git a/base_export_manager/tests/test_ir_exports.py b/base_export_manager/tests/test_ir_exports.py index 983c995ee..f5c9041ea 100644 --- a/base_export_manager/tests/test_ir_exports.py +++ b/base_export_manager/tests/test_ir_exports.py @@ -2,8 +2,8 @@ # © 2015 Antiun Ingeniería S.L. - Jairo Llopis # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp.exceptions import ValidationError -from openerp.tests.common import TransactionCase +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase class TestIrExportsCase(TransactionCase): diff --git a/base_export_manager/tests/test_ir_exports_line.py b/base_export_manager/tests/test_ir_exports_line.py index acfb3cc3a..00a2afb61 100644 --- a/base_export_manager/tests/test_ir_exports_line.py +++ b/base_export_manager/tests/test_ir_exports_line.py @@ -1,18 +1,26 @@ # -*- coding: utf-8 -*- # Copyright 2015 Antiun Ingenieria S.L. - Javier Iniesta +# Copyright 2016 Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp.tests.common import TransactionCase -from openerp.exceptions import ValidationError +from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError class TestIrExportsLineCase(TransactionCase): - def setUp(self): super(TestIrExportsLineCase, self).setUp() m_ir_exports = self.env['ir.exports'] self.export = m_ir_exports.create({'name': 'Partner Test', 'resource': 'res.partner'}) + self.partner_model = self.env['ir.model'].search( + [('model', '=', 'res.partner')]) + self.field_parent_id = self.env['ir.model.fields'].search( + [('name', '=', 'parent_id'), + ('model_id', '=', self.partner_model.id)]) + self.field_name = self.env['ir.model.fields'].search( + [('name', '=', 'name'), + ('model_id', '=', self.partner_model.id)]) def test_check_name(self): m_ir_exports_line = self.env['ir.exports.line'] @@ -34,3 +42,46 @@ class TestIrExportsLineCase(TransactionCase): with self.assertRaises(ValidationError): m_ir_exports_line.create({'name': '', 'export_id': self.export.id}) + + def test_model_default_by_context(self): + """Fields inherit the model_id by context.""" + line = self.env["ir.exports.line"].with_context( + default_model1_id=self.export.model_id.id).create({ + "name": "name", + "export_id": self.export.id, + }) + self.assertEqual(line.model1_id, self.export.model_id) + + def test_inverse_name(self): + line = self.env['ir.exports.line'].create({ + 'export_id': self.export.id, + 'name': 'parent_id/parent_id/parent_id/name', + }) + self.assertEqual(line.model1_id, self.partner_model) + self.assertEqual(line.model2_id, self.partner_model) + self.assertEqual(line.field1_id, self.field_parent_id) + self.assertEqual(line.field2_id, self.field_parent_id) + self.assertEqual(line.field3_id, self.field_parent_id) + self.assertEqual(line.field4_id, self.field_name) + + def test_compute_name(self): + line = self.env['ir.exports.line'].create({ + 'export_id': self.export.id, + 'field1_id': self.field_parent_id.id, + 'field2_id': self.field_parent_id.id, + 'field3_id': self.field_parent_id.id, + 'field4_id': self.field_name.id, + }) + self.assertEqual(line.name, 'parent_id/parent_id/parent_id/name') + + def test_write_name_same_root(self): + self.env['ir.exports.line'].create({ + 'export_id': self.export.id, + 'name': 'parent_id', + }) + line = self.env['ir.exports.line'].create({ + 'export_id': self.export.id, + 'name': 'name', + }) + # This should end without errors + line.name = 'parent_id/name' diff --git a/base_export_manager/views/ir_exports.xml b/base_export_manager/views/ir_exports.xml index 106948f7e..c252d47cd 100644 --- a/base_export_manager/views/ir_exports.xml +++ b/base_export_manager/views/ir_exports.xml @@ -47,33 +47,25 @@ + + options="{'no_open': True, 'no_create': True}"/> + attrs="{'readonly': [('model2_id', '=', False)]}" + options="{'no_open': True, 'no_create': True}"/> + attrs="{'readonly': [('model3_id', '=', False)]}" + options="{'no_open': True, 'no_create': True}"/> +