diff --git a/bi_view_editor/__manifest__.py b/bi_view_editor/__manifest__.py index dd4ecf602..ed16c4d5d 100644 --- a/bi_view_editor/__manifest__.py +++ b/bi_view_editor/__manifest__.py @@ -9,9 +9,11 @@ "license": "AGPL-3", "website": "https://github.com/OCA/reporting-engine", "category": "Productivity", - "version": "15.0.1.0.0", + "version": "16.0.1.0.0", "development_status": "Beta", - "depends": ["web"], + "depends": [ + "web", + ], "external_dependencies": { "deb": ["graphviz"], }, @@ -22,16 +24,8 @@ ], "assets": { "web.assets_backend": [ - "bi_view_editor/static/src/css/bve.css", - "bi_view_editor/static/src/js/bi_view_editor.js", - "bi_view_editor/static/src/js/bi_view_editor.JoinNodeDialog.js", - "bi_view_editor/static/src/js/bi_view_editor.ModelList.js", - "bi_view_editor/static/src/js/bi_view_editor.FieldList.js", - ], - "web.assets_qweb": [ - "bi_view_editor/static/src/xml/bi_view_editor.xml", + "bi_view_editor/static/src/components/**/*", ], }, "uninstall_hook": "uninstall_hook", - "installable": True, } diff --git a/bi_view_editor/models/bve_view.py b/bi_view_editor/models/bve_view.py index 6392a9739..aad55d2b8 100644 --- a/bi_view_editor/models/bve_view.py +++ b/bi_view_editor/models/bve_view.py @@ -60,7 +60,7 @@ class BveView(models.Model): line_ids = self._sync_lines_and_data(bve_view.data) bve_view.write({"line_ids": line_ids}) - name = fields.Char(required=True, copy=False, default="") + name = fields.Char(required=True, copy=False, translate=True) model_name = fields.Char(compute="_compute_model_name", store=True) note = fields.Text(string="Notes") state = fields.Selection( @@ -388,35 +388,6 @@ class BveView(models.Model): AsIs(from_str), ) - def action_translations(self): - self.ensure_one() - if self.state != "created": - return - self = self.sudo() - model = self.env["ir.model"].sudo().search([("model", "=", self.model_name)]) - IrTranslation = self.env["ir.translation"] - IrTranslation.translate_fields("ir.model", model.id) - for field in model.field_id: - IrTranslation.translate_fields("ir.model.fields", field.id) - return { - "name": "Translations", - "res_model": "ir.translation", - "type": "ir.actions.act_window", - "view_mode": "tree", - "view_id": self.env.ref("base.view_translation_dialog_tree").id, - "target": "current", - "flags": {"search_view": True, "action_buttons": True}, - "domain": [ - "|", - "&", - ("res_id", "in", model.field_id.ids), - ("name", "=", "ir.model.fields,field_description"), - "&", - ("res_id", "=", model.id), - ("name", "=", "ir.model,name"), - ], - } - def action_create(self): self.ensure_one() @@ -441,7 +412,10 @@ class BveView(models.Model): "name": self.name, "model": self.model_name, "state": "manual", - "field_id": [(0, 0, f) for f in bve_fields._prepare_field_vals()], + "field_id": [ + fields.Command.create(f) + for f in bve_fields._prepare_field_vals() + ], } ) ) diff --git a/bi_view_editor/models/bve_view_line.py b/bi_view_editor/models/bve_view_line.py index 4e452ac8a..bc31e2142 100644 --- a/bi_view_editor/models/bve_view_line.py +++ b/bi_view_editor/models/bve_view_line.py @@ -101,12 +101,16 @@ class BveViewLine(models.Model): "complete_name": field.complete_name, "model": line.bve_view_id.model_name, "relation": field.relation, + # FIXME: this sets the en_US value from the current language's + # translation. instead, all translations should be set with + # their corresponding value. "field_description": line.description, "ttype": field.ttype, "selection": field.selection, "size": field.size, "state": "manual", "readonly": True, + "translate": field.translate, "groups": [(6, 0, field.groups.ids)], } if vals["ttype"] == "monetary": diff --git a/bi_view_editor/models/ir_model.py b/bi_view_editor/models/ir_model.py index 16b501c64..99bc97e2a 100644 --- a/bi_view_editor/models/ir_model.py +++ b/bi_view_editor/models/ir_model.py @@ -212,7 +212,7 @@ class IrModel(models.Model): ("name", "not in", models.MAGIC_COLUMNS), ("ttype", "not in", NO_BI_TTYPES), ], - order="field_description desc", + order="field_description", ) ) fields_dict = list(map(dict_for_field, fields)) diff --git a/bi_view_editor/readme/CONTRIBUTORS.rst b/bi_view_editor/readme/CONTRIBUTORS.rst index 92632b85a..bce025526 100644 --- a/bi_view_editor/readme/CONTRIBUTORS.rst +++ b/bi_view_editor/readme/CONTRIBUTORS.rst @@ -7,3 +7,6 @@ * Antonio Esposito * Jordi Ballester Alomar * Italo LOPES +* `Coop IT Easy SC `_: + + * hugues de keyzer diff --git a/bi_view_editor/readme/ROADMAP.rst b/bi_view_editor/readme/ROADMAP.rst index 32e4e2d00..06bab991d 100644 --- a/bi_view_editor/readme/ROADMAP.rst +++ b/bi_view_editor/readme/ROADMAP.rst @@ -5,3 +5,7 @@ * Data the user has no access to (e.g. in a multi company situation) can be viewed by making a view. Would be nice if models available to select when creating a view are limited to the ones that have intersecting groups. +* As of Odoo 16, translations of the name of a BI View and of the field + descriptions do not work as expected: the translated strings are selected + (by the user's language) when the view is generated (and stored as their + ``en_US`` value) instead of when it is displayed. diff --git a/bi_view_editor/static/src/css/bve.css b/bi_view_editor/static/src/components/bi_view_editor/bi_view_editor.css similarity index 92% rename from bi_view_editor/static/src/css/bve.css rename to bi_view_editor/static/src/components/bi_view_editor/bi_view_editor.css index 83fe9ae47..e0f21214f 100644 --- a/bi_view_editor/static/src/css/bve.css +++ b/bi_view_editor/static/src/components/bi_view_editor/bi_view_editor.css @@ -91,7 +91,7 @@ } .oe_form_field_bi_editor .body .left .class-list .class.readonly { - cursor: default; + cursor: inherit; } .oe_form_field_bi_editor .body .left .class-list .class:hover { @@ -99,6 +99,11 @@ color: #fff; } +.oe_form_field_bi_editor .body .left .class-list .class.readonly:hover { + background-color: inherit; + color: inherit; +} + .oe_form_field_bi_editor .body .left .class-list .field { font-weight: normal; padding-left: 20px; @@ -134,13 +139,16 @@ border: none; background-image: none; padding: 0; - cursor: pointer; } .oe_form_field_bi_editor .body .right .field-list tbody tr:hover { background-color: #ddd; } +.oe_form_field_bi_editor .body .right .field-list tbody tr.readonly:hover { + background-color: inherit; +} + .oe_form_field_bi_editor .body .right .field-list tbody tr.join-node { background-color: #d2d2ff; text-align: center; @@ -196,3 +204,7 @@ .oe_bi_view_editor_join_node_dialog li { cursor: pointer; } + +.o_field_widget.o_field_BVEEditor { + width: 100%; +} diff --git a/bi_view_editor/static/src/components/bi_view_editor/bi_view_editor.esm.js b/bi_view_editor/static/src/components/bi_view_editor/bi_view_editor.esm.js new file mode 100644 index 000000000..a5110b456 --- /dev/null +++ b/bi_view_editor/static/src/components/bi_view_editor/bi_view_editor.esm.js @@ -0,0 +1,217 @@ +/** @odoo-module **/ + +/* Copyright 2015-2019 Onestein () + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ + +import {Component, onWillUpdateProps, useState} from "@odoo/owl"; +import {FIELD_DATA_TYPE, ModelList} from "./model_list.esm"; +import {FieldList} from "./field_list.esm"; +import {JoinNodeDialog} from "./join_node_dialog.esm"; +import {registry} from "@web/core/registry"; +import {standardFieldProps} from "@web/views/fields/standard_field_props"; +import {useService} from "@web/core/utils/hooks"; + +export class BiViewEditor extends Component { + setup() { + this.state = useState({ + models: [], + fields: [], + // This allows to access fields by their _id property in an + // efficient way. Since the fields array length is normally quite + // small, it would also be possible to use + // fields.find(element => element._id == field._id), but that + // would mean that this function would be defined in 3 places + // (deleteField(), setFieldProperty() and + // FieldList.setFieldProperty()), and using a common function + // would be quite similar to using this object. + fieldsByID: {}, + }); + this.orm = useService("orm"); + this.dialogService = useService("dialog"); + onWillUpdateProps((nextProps) => { + this.updateFields(nextProps.value); + }); + this.updateFields(this.props.value); + } + get modelIDs() { + const model_ids = {}; + for (const field of this.state.fields) { + model_ids[field.table_alias] = field.model_id; + } + return model_ids; + } + get modelData() { + const model_data = {}; + for (const field of this.state.fields) { + model_data[field.table_alias] = { + model_id: field.model_id, + model_name: field.model_name, + }; + } + return model_data; + } + _addField(field) { + field.row = typeof field.row === "undefined" ? false : field.row; + field.column = typeof field.column === "undefined" ? false : field.column; + field.measure = typeof field.measure === "undefined" ? false : field.measure; + field.list = typeof field.list === "undefined" ? true : field.list; + field._id = typeof field._id === "undefined" ? _.uniqueId("node_") : field._id; + if (field.join_node) { + field.join_left = + typeof field.join_left === "undefined" ? false : field.join_left; + } + + let i = 0; + const name = field.name; + while ( + this.state.fields.filter(function (item) { + return item.name === field.name; + }).length > 0 + ) { + field.name = name + "_" + i; + i++; + } + this.state.fields.push(field); + this.state.fieldsByID[field._id] = field; + } + deleteField(field) { + this.state.fields.splice( + this.state.fields.findIndex((element) => { + return element._id === field._id; + }), + 1 + ); + delete this.state.fieldsByID[field._id]; + this.fieldDeleted(); + } + setFieldProperty(field, property, value) { + this.state.fieldsByID[field._id][property] = value; + this.fieldUpdated(); + } + setFields(fields) { + this.state.fields = []; + this.state.fieldsByID = {}; + for (const field of fields) { + this._addField(field); + } + } + updateFields(value) { + if (value) { + this.setFields(JSON.parse(value)); + } + this.updateModels(); + } + updateModels() { + const model_ids = this.modelIDs; + this.orm + .call("ir.model", "get_models", model_ids ? [model_ids] : []) + .then((models) => { + this.state.models = models; + }); + } + clear() { + if (this.props.readonly) { + return; + } + this.setFields([]); + this.updateValue(); + } + fieldUpdated() { + this.updateValue(); + } + fieldDeleted() { + this.orm + .call("bve.view", "get_clean_list", [this.state.fields]) + .then((result) => { + this.updateFields(result); + this.updateValue(); + }); + } + getTableAlias(field) { + if (typeof field.table_alias === "undefined") { + const model_ids = this.modelIDs; + let n = 1; + while (typeof model_ids["t" + n] !== "undefined") { + n++; + } + return "t" + n; + } + return field.table_alias; + } + addFieldAndJoinNode(field, join_node) { + if (join_node.join_node === -1 || join_node.table_alias === -1) { + field.table_alias = this.getTableAlias(field); + if (join_node.join_node === -1) { + join_node.join_node = field.table_alias; + } else { + join_node.table_alias = field.table_alias; + } + this._addField(join_node); + } else { + field.table_alias = join_node.table_alias; + } + + this._addField(field); + this.updateValue(); + } + addField(field) { + const data = _.extend({}, field); + const field_data = this.state.fields; + this.orm + .call("ir.model", "get_join_nodes", [field_data, data]) + .then((result) => { + if (result.length === 1) { + this.addFieldAndJoinNode(data, result[0]); + } else if (result.length > 1) { + this.dialogService.add(JoinNodeDialog, { + choices: result, + model_data: this.modelData, + choiceSelected: (choice) => { + this.addFieldAndJoinNode(data, choice); + }, + }); + } else { + data.table_alias = this.getTableAlias(data); + this._addField(data); + this.updateValue(); + } + }); + } + fieldClicked(field) { + this.addField(field); + } + onDragOver(e) { + if (this.props.readonly) { + return; + } + const dragType = e.dataTransfer.types[0]; + if (dragType === FIELD_DATA_TYPE) { + e.preventDefault(); + e.dataTransfer.dropEffect = "copy"; + } + } + onDrop(e) { + if (this.props.readonly) { + return; + } + const dragData = e.dataTransfer.getData(FIELD_DATA_TYPE); + if (dragData) { + e.preventDefault(); + this.addField(JSON.parse(dragData)); + } + } + updateValue() { + this.props.update(JSON.stringify(this.state.fields)); + this.updateModels(); + } +} +BiViewEditor.template = "bi_view_editor.Frame"; +BiViewEditor.components = { + ModelList, + FieldList, +}; +BiViewEditor.props = { + ...standardFieldProps, +}; + +registry.category("fields").add("BVEEditor", BiViewEditor); diff --git a/bi_view_editor/static/src/components/bi_view_editor/bi_view_editor.xml b/bi_view_editor/static/src/components/bi_view_editor/bi_view_editor.xml new file mode 100644 index 000000000..1dcaea606 --- /dev/null +++ b/bi_view_editor/static/src/components/bi_view_editor/bi_view_editor.xml @@ -0,0 +1,39 @@ + + + +
+
+
+ +
+
+ +
+
+
+ + + diff --git a/bi_view_editor/static/src/components/bi_view_editor/field_list.esm.js b/bi_view_editor/static/src/components/bi_view_editor/field_list.esm.js new file mode 100644 index 000000000..f875abf75 --- /dev/null +++ b/bi_view_editor/static/src/components/bi_view_editor/field_list.esm.js @@ -0,0 +1,120 @@ +/** @odoo-module **/ + +/* Copyright 2015-2019 Onestein () + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ + +import {Component, onMounted, useRef, useState} from "@odoo/owl"; + +class FieldListItem extends Component { + delete() { + this.props.delete(this.props.field); + } + descriptionChanged(e) { + this.props.setDescription(this.props.field, e.target.value); + } +} +FieldListItem.template = "bi_view_editor.FieldListItem"; +FieldListItem.props = { + field: Object, + delete: Function, + setDescription: Function, + readonly: Boolean, +}; + +class JoinListItem extends Component {} +JoinListItem.template = "bi_view_editor.JoinListItem"; +JoinListItem.props = { + field: Object, + readonly: Boolean, +}; + +class FieldListContextMenu extends Component { + setup() { + this.main = useRef("main"); + onMounted(() => { + $(this.main.el).css({ + left: this.props.position.x + "px", + top: this.props.position.y + "px", + }); + }); + } + close() { + this.props.close(); + } + onChange(property, e) { + this.props.onChange(this.props.field, property, e.target.checked); + } +} +FieldListContextMenu.props = { + field: Object, + position: Object, + close: Function, + onChange: Function, +}; + +class FieldListFieldContextMenu extends FieldListContextMenu { + get measurable() { + const type = this.props.field.type; + return type === "float" || type === "integer" || type === "monetary"; + } +} +FieldListFieldContextMenu.template = "bi_view_editor.FieldList.FieldContextMenu"; + +class FieldListJoinContextMenu extends FieldListContextMenu {} +FieldListJoinContextMenu.template = "bi_view_editor.FieldList.JoinContextMenu"; + +export class FieldList extends Component { + setup() { + this.state = useState({ + contextMenuOpen: null, + contextMenuField: null, + contextMenuPosition: null, + }); + } + setFieldProperty(field, property, value) { + this.props.setFieldProperty(field, property, value); + // This can trigger a recreation of all the field objects. If this is + // called while the context menu is open, contextMenuField refers to a + // field that is not in the list anymore. The reference must thus be + // updated. + if (this.state.contextMenuField !== null) { + this.state.contextMenuField = + this.props.fieldsByID[this.state.contextMenuField._id]; + } + } + setFieldDescription(field, description) { + this.setFieldProperty(field, "description", description); + } + openContextMenu(which, field, e) { + if (this.props.readonly) { + return; + } + e.preventDefault(); + // Temporarily disable contextmenu for join node (until left join is implemented) + if (field.join_node) { + return; + } + this.state.contextMenuField = field; + this.state.contextMenuPosition = {x: e.x - 20, y: e.y - 20}; + this.state.contextMenuOpen = which; + } + closeContextMenu() { + this.state.contextMenuOpen = null; + this.state.contextMenuField = null; + this.state.contextMenuPosition = null; + } +} +FieldList.template = "bi_view_editor.FieldList"; +FieldList.components = { + FieldListItem, + JoinListItem, + FieldListFieldContextMenu, + FieldListJoinContextMenu, +}; +FieldList.props = { + fields: Object, + fieldsByID: Object, + deleteField: Function, + setFieldProperty: Function, + readonly: Boolean, +}; diff --git a/bi_view_editor/static/src/components/bi_view_editor/field_list.xml b/bi_view_editor/static/src/components/bi_view_editor/field_list.xml new file mode 100644 index 000000000..8a31b2774 --- /dev/null +++ b/bi_view_editor/static/src/components/bi_view_editor/field_list.xml @@ -0,0 +1,188 @@ + + + +
+ + + + + + + + + + + + + + + + + + +
NameModelOptions +
+
+ + +
+ + +
    +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
+
+ + +
    +
  • +
    + +
    +
  • +
+
+ + + + + + + + + + + + + + + + + + +
diff --git a/bi_view_editor/static/src/components/bi_view_editor/model_list.esm.js b/bi_view_editor/static/src/components/bi_view_editor/model_list.esm.js new file mode 100644 index 000000000..c58ce8edf --- /dev/null +++ b/bi_view_editor/static/src/components/bi_view_editor/model_list.esm.js @@ -0,0 +1,115 @@ +/** @odoo-module **/ + +/* Copyright 2015-2019 Onestein () + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ + +import {Component, useState} from "@odoo/owl"; +import {useService} from "@web/core/utils/hooks"; + +export const FIELD_DATA_TYPE = "application/x-odoo-bve-field"; + +class ModelListFieldItem extends Component { + clicked() { + if (this.props.readonly) { + return; + } + this.props.fieldClicked(this.props.field); + } + onDragStart(e) { + if (this.props.readonly) { + return; + } + e.dataTransfer.setData(FIELD_DATA_TYPE, JSON.stringify(this.props.field)); + } +} +ModelListFieldItem.template = "bi_view_editor.ModelListFieldItem"; +ModelListFieldItem.props = { + field: Object, + fieldClicked: Function, + readonly: Boolean, +}; + +class ModelListItem extends Component { + setup() { + this.state = useState({ + expanded: false, + fields: [], + }); + this._loaded = false; + this.orm = useService("orm"); + } + _loadFields() { + if (this._loaded) { + return; + } + this._loaded = true; + this.orm + .call("ir.model", "get_fields", [this.props.model.id]) + .then((fields) => { + this.state.fields = fields; + }); + } + clicked() { + if (this.props.readonly) { + return; + } + this.expanded = !this.expanded; + } + get matchesFilter() { + const filter = this.props.filter; + if (!filter) { + return true; + } + const model = this.props.model; + const result = + model.name.toLowerCase().indexOf(filter) !== -1 || + model.model.toLowerCase().indexOf(filter) !== -1; + if (!result) { + // Filtered-out items should be collapsed + this.expanded = false; + } + return result; + } + get expanded() { + return this.state.expanded && !this.props.readonly; + } + set expanded(expanded) { + if (expanded === this.state.expanded) { + return; + } + if (expanded) { + this._loadFields(); + } + this.state.expanded = expanded; + } +} +ModelListItem.template = "bi_view_editor.ModelListItem"; +ModelListItem.components = { + ModelListFieldItem, +}; +ModelListItem.props = { + model: Object, + filter: String, + fieldClicked: Function, + readonly: Boolean, +}; + +export class ModelList extends Component { + setup() { + this.state = useState({ + filter: "", + }); + } + filterChanged(e) { + this.state.filter = e.target.value; + } +} +ModelList.template = "bi_view_editor.ModelList"; +ModelList.components = { + ModelListItem, +}; +ModelList.props = { + models: Object, + fieldClicked: Function, + readonly: Boolean, +}; diff --git a/bi_view_editor/static/src/components/bi_view_editor/model_list.xml b/bi_view_editor/static/src/components/bi_view_editor/model_list.xml new file mode 100644 index 000000000..d4e04dfc1 --- /dev/null +++ b/bi_view_editor/static/src/components/bi_view_editor/model_list.xml @@ -0,0 +1,60 @@ + + + +
+ +
+ + + +
+
+
+ + +
+
+ +
+ + + + + +
+
+ + +
+ +
+
+
diff --git a/bi_view_editor/static/src/js/bi_view_editor.FieldList.js b/bi_view_editor/static/src/js/bi_view_editor.FieldList.js deleted file mode 100644 index 8a70c2b63..000000000 --- a/bi_view_editor/static/src/js/bi_view_editor.FieldList.js +++ /dev/null @@ -1,256 +0,0 @@ -/* Copyright 2015-2019 Onestein () - * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ - -odoo.define("bi_view_editor.FieldList", function (require) { - "use strict"; - - var core = require("web.core"); - var qweb = core.qweb; - var Widget = require("web.Widget"); - var mixins = require("web.mixins"); - - var FieldListContextMenu = Widget.extend( - _.extend({}, mixins.EventDispatcherMixin, { - start: function () { - var res = this._super.apply(this, arguments); - this.$el.mouseleave(function () { - $(this).addClass("d-none"); - }); - return res; - }, - open: function (x, y) { - this.$el.css({ - left: x + "px", - top: y + "px", - }); - this.$el.removeClass("d-none"); - return this; - }, - }) - ); - - var FieldListFieldContextMenu = FieldListContextMenu.extend({ - template: "bi_view_editor.FieldList.FieldContextMenu", - open: function (x, y, $item) { - var field = $item.data("field"); - this.$el.find(".checkbox-column").prop("checked", field.column); - this.$el.find(".checkbox-row").prop("checked", field.row); - this.$el.find(".checkbox-measure").prop("checked", field.measure); - this.$el.find(".checkbox-list").prop("checked", field.list); - - var measureable = - field.type === "float" || - field.type === "integer" || - field.type === "monetary"; - this.$el.find(".checkbox-column").attr("disabled", measureable); - this.$el.find(".checkbox-row").attr("disabled", measureable); - this.$el.find(".checkbox-measure").attr("disabled", !measureable); - this.$el.find(".checkbox-list").attr("disabled", false); - - var events = this._super(x, y, field); - this.$el.find("input").unbind("change"); - this.$el.find("input").change(function () { - var $checkbox = $(this); - var property = $checkbox.attr("data-for"); - field[property] = $checkbox.is(":checked"); - events.trigger("change", field, $item); - }); - - return events; - }, - }); - - var FieldListJoinContextMenu = FieldListContextMenu.extend({ - template: "bi_view_editor.FieldList.JoinContextMenu", - open: function (x, y, $item) { - var node = $item.data("field"); - this.$el.find(".checkbox-join-left").prop("checked", node.join_left); - - var events = this._super(x, y, node); - this.$el.find("input").unbind("change"); - this.$el.find("input").change(function () { - var $checkbox = $(this); - var property = $checkbox.attr("data-for"); - node[property] = $checkbox.is(":checked"); - events.trigger("change", node); - }); - return events; - }, - }); - - var FieldList = Widget.extend({ - template: "bi_view_editor.FieldList", - events: { - "click .delete-button": "removeClicked", - 'keyup input[name="description"]': "keyupDescription", - }, - start: function () { - var res = this._super.apply(this, arguments); - this.contextmenu = new FieldListFieldContextMenu(this); - this.contextmenu.appendTo(this.$el); - this.contextmenu.on( - "change", - this, - function (f, $item) { - $item.data("field", f); - this.refreshItem($item); - this.trigger("updated"); - }.bind(this) - ); - this.contextmenu_join = new FieldListJoinContextMenu(this); - this.contextmenu_join.appendTo(this.$el); - this.contextmenu_join.on( - "change", - this, - function (f, $item) { - $item.data("field", f); - this.refreshItem($item); - this.trigger("updated"); - }.bind(this) - ); - this.$table = this.$el.find("tbody"); - this.mode = null; - return res; - }, - setMode: function (mode) { - if (mode === "readonly") { - this.$el.find('input[type="text"]').attr("disabled", true); - this.$el.find(".delete-button").addClass("d-none"); - } else { - this.$el.find('input[type="text"]').removeAttr("disabled"); - this.$el.find(".delete-button").removeClass("d-none"); - } - this.mode = mode; - }, - get: function () { - return $.makeArray( - this.$el.find("tbody tr").map(function () { - var field = $(this).data("field"); - field.description = $(this).find('input[name="description"]').val(); - return field; - }) - ); - }, - getModelIds: function () { - var model_ids = {}; - this.$el.find("tbody tr").each(function () { - var data = $(this).data("field"); - model_ids[data.table_alias] = data.model_id; - }); - return model_ids; - }, - getModelData: function () { - var model_data = {}; - this.$el.find("tbody tr").each(function () { - var data = $(this).data("field"); - model_data[data.table_alias] = { - model_id: data.model_id, - model_name: data.model_name, - }; - }); - return model_data; - }, - add: function (field) { - var self = this; - field.row = typeof field.row === "undefined" ? false : field.row; - field.column = typeof field.column === "undefined" ? false : field.column; - field.measure = - typeof field.measure === "undefined" ? false : field.measure; - field.list = typeof field.list === "undefined" ? true : field.list; - field._id = - typeof field._id === "undefined" ? _.uniqueId("node_") : field._id; - if (field.join_node) { - field.join_left = - typeof field.join_left === "undefined" ? false : field.join_left; - } - - var i = 0; - var name = field.name; - while ( - this.get().filter(function (item) { - return item.name === field.name; - }).length > 0 - ) { - field.name = name + "_" + i; - i++; - } - - // Render table row - var $html = $( - qweb.render( - field.join_node - ? "bi_view_editor.JoinListItem" - : "bi_view_editor.FieldListItem", - { - field: field, - } - ) - ) - .data("field", field) - .contextmenu(function (e) { - var $item = $(this); - if (self.mode === "readonly") { - return; - } - e.preventDefault(); - self.openContextMenu($item, e.pageX, e.pageY); - }); - - this.$el.find("tbody").append($html); - }, - remove: function (id) { - var $item = this.$el.find('tr[data-id="' + id + '"]'); - $item.remove(); - this.trigger("removed", id); - }, - set: function (fields) { - var set_fields = fields; - if (!set_fields) { - set_fields = []; - } - this.$el.find("tbody tr").remove(); - for (var i = 0; i < set_fields.length; i++) { - this.add(set_fields[i]); - } - }, - openContextMenu: function ($item, x, y) { - var field = $item.data("field"); - var contextmenu = field.join_node - ? this.contextmenu_join - : this.contextmenu; - // Temporary disable contextmenu for join node (until left join is implemented) - if (field.join_node) { - return; - } - contextmenu.open(x - 20, y - 20, $item); - }, - refreshItem: function ($item) { - var data = $item.data("field"); - var $attributes = $item.find("span[data-for], img[data-for]"); - $.each($attributes, function () { - var $attribute = $(this); - var value = data[$attribute.attr("data-for")]; - if (value) { - $attribute.removeClass("d-none"); - } else { - $attribute.addClass("d-none"); - } - }); - }, - removeClicked: function (e) { - var $button = $(e.currentTarget); - var id = $button.attr("data-id"); - this.remove(id); - }, - keyupDescription: function () { - this.trigger("updated"); - }, - }); - - return { - FieldList: FieldList, - FieldListContextMenu: FieldListContextMenu, - FieldListFieldContextMenu: FieldListFieldContextMenu, - FieldListJoinContextMenu: FieldListJoinContextMenu, - }; -}); diff --git a/bi_view_editor/static/src/js/bi_view_editor.JoinNodeDialog.js b/bi_view_editor/static/src/js/bi_view_editor.JoinNodeDialog.js deleted file mode 100644 index 12f072e77..000000000 --- a/bi_view_editor/static/src/js/bi_view_editor.JoinNodeDialog.js +++ /dev/null @@ -1,55 +0,0 @@ -/* Copyright 2015-2019 Onestein () - * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ - -odoo.define("bi_view_editor.JoinNodeDialog", function (require) { - "use strict"; - - var Dialog = require("web.Dialog"); - var core = require("web.core"); - var qweb = core.qweb; - var _t = core._t; - - var JoinNodeDialog = Dialog.extend({ - xmlDependencies: Dialog.prototype.xmlDependencies.concat([ - "/bi_view_editor/static/src/xml/bi_view_editor.xml", - ]), - events: { - "click li": "choiceClicked", - }, - init: function (parent, options, choices, model_data) { - this.choices = choices; - // Prepare data for view - for (var i = 0; i < choices.length; i++) { - if (choices[i].join_node !== -1 && choices[i].table_alias !== -1) { - choices[i].model_name = - model_data[choices[i].table_alias].model_name; - } - choices[i].index = i; - } - - var defaults = _.defaults(options || {}, { - title: _t("Join..."), - dialogClass: "oe_act_window", - $content: qweb.render("bi_view_editor.JoinNodeDialog", { - choices: choices, - }), - buttons: [ - { - text: _t("Cancel"), - classes: "btn-default o_form_button_cancel", - close: true, - }, - ], - }); - this._super(parent, defaults); - }, - choiceClicked: function (e) { - this.trigger("chosen", { - choice: this.choices[$(e.currentTarget).attr("data-index")], - }); - this.close(); - }, - }); - - return JoinNodeDialog; -}); diff --git a/bi_view_editor/static/src/js/bi_view_editor.ModelList.js b/bi_view_editor/static/src/js/bi_view_editor.ModelList.js deleted file mode 100644 index 2883f740d..000000000 --- a/bi_view_editor/static/src/js/bi_view_editor.ModelList.js +++ /dev/null @@ -1,170 +0,0 @@ -/* Copyright 2015-2019 Onestein () - * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ - -odoo.define("bi_view_editor.ModelList", function (require) { - "use strict"; - - var Widget = require("web.Widget"); - var core = require("web.core"); - var qweb = core.qweb; - - var ModelList = Widget.extend({ - template: "bi_view_editor.ModelList", - events: { - "keyup .search-bar > input": "filterChanged", - }, - init: function (parent) { - var res = this._super(parent); - this.active_models = []; - this.cache_fields = {}; - this.current_filter = ""; - this.mode = null; - return res; - }, - setMode: function (mode) { - if (mode === "readonly") { - this.$el.find(".search-bar").attr("disabled", true); - this.$el.find(".class-list, .class").addClass("readonly"); - } else { - this.$el.find(".search-bar").attr("disabled", false); - this.$el.find(".class-list, .class").removeClass("readonly"); - } - this.mode = mode; - }, - isActive: function (id) { - return this.active_models.indexOf(id) !== -1; - }, - removeAsActive: function (id) { - var i = this.active_models.indexOf(id); - this.active_models.splice(i, 1); - }, - addAsActive: function (id) { - this.active_models.push(id); - }, - loadModels: function (model_ids) { - return this._rpc({ - model: "ir.model", - method: "get_models", - args: model_ids ? [model_ids] : [], - }); - }, - loadFields: function (model_id) { - if (!(model_id in this.cache_fields)) { - var deferred = this._rpc({ - model: "ir.model", - method: "get_fields", - args: [model_id], - }); - this.cache_fields[model_id] = deferred; - } - return this.cache_fields[model_id]; - }, - populateModels: function (models) { - var self = this; - this.$el.find(".class-list").html(""); - - _.each(models, function (model) { - var $html = $( - qweb.render("bi_view_editor.ModelListItem", { - id: model.id, - model: model.model, - name: model.name, - }) - ); - $html - .find(".class") - .data("model", model) - .click(function () { - self.modelClicked($(this)); - }); - self.$el.find(".class-list").append($html); - - if (self.isActive(model.id)) { - self.loadFields(model.id).then(function (fields) { - self.populateFields(fields, model.id); - }); - } - }); - }, - populateFields: function (fields, model_id) { - var self = this; - if (!model_id && fields.length === 0) { - return; - } - var data_model_id = model_id; - if (!data_model_id) { - data_model_id = fields[0].model_id; - } - var $model_item = this.$el.find(".class[data-id='" + data_model_id + "']"); - _.each(fields, function (field) { - var $field = $( - qweb.render("bi_view_editor.ModelListFieldItem", { - name: field.name, - description: field.description, - }) - ) - .data("field", field) - .click(function () { - self.fieldClicked($(this)); - }) - .draggable({ - revert: "invalid", - scroll: false, - helper: "clone", - appendTo: "body", - containment: "window", - }); - $model_item.after($field); - }); - }, - modelClicked: function ($el) { - if (this.mode === "readonly") { - return; - } - var model = $el.data("model"); - $el.parent().find(".field").remove(); - if (this.isActive(model.id)) { - this.removeAsActive(model.id); - } else { - this.addAsActive(model.id); - this.loadFields(model.id).then( - function (fields) { - this.populateFields(fields, model.id); - }.bind(this) - ); - } - }, - fieldClicked: function ($el) { - if (this.mode === "readonly") { - return; - } - this.trigger("field_clicked", $el.data("field")); - }, - filterChanged: function (e) { - var $input = $(e.target); - this.filter($input.val()); - }, - filter: function (value) { - this.active_models = []; - this.$el.find(".field").remove(); - var val = - typeof value === "undefined" - ? this.current_filter - : value.toLowerCase(); - this.$el.find(".class").each(function () { - var data = $(this).data("model"); - if ( - data.name.toLowerCase().indexOf(val) === -1 && - data.model.toLowerCase().indexOf(val) === -1 - ) { - $(this).addClass("d-none"); - } else { - $(this).removeClass("d-none"); - } - }); - this.current_filter = val; - }, - }); - - return ModelList; -}); diff --git a/bi_view_editor/static/src/js/bi_view_editor.js b/bi_view_editor/static/src/js/bi_view_editor.js deleted file mode 100644 index 771855538..000000000 --- a/bi_view_editor/static/src/js/bi_view_editor.js +++ /dev/null @@ -1,163 +0,0 @@ -/* Copyright 2015-2019 Onestein () - * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ - -odoo.define("bi_view_editor", function (require) { - "use strict"; - - var JoinNodeDialog = require("bi_view_editor.JoinNodeDialog"); - var ModelList = require("bi_view_editor.ModelList"); - var FieldList = require("bi_view_editor.FieldList").FieldList; - - var AbstractField = require("web.AbstractField"); - var Data = require("web.data"); - var field_registry = require("web.field_registry"); - - var BiViewEditor = AbstractField.extend({ - template: "bi_view_editor.Frame", - events: { - "click .clear-btn": "clear", - }, - start: function () { - var self = this; - var res = this._super.apply(this, arguments); - - // Init ModelList - this.model_list = new ModelList(this); - this.model_list.appendTo(this.$(".body > .left")); - this.model_list.on("field_clicked", this, function (field) { - self.addField(_.extend({}, field)); - }); - - // Init FieldList - this.field_list = new FieldList(this); - this.field_list.appendTo(this.$(".body > .right")).then( - function () { - this.field_list.on("removed", this, this.fieldListRemoved); - this.field_list.on("updated", this, this.fieldListChanged); - this.$el.find(".body > .right").droppable({ - accept: "div.class-list div.field", - drop: function (event, ui) { - self.addField(_.extend({}, ui.draggable.data("field"))); - ui.draggable.draggable("option", "revert", false); - }, - }); - - this.on("change:effective_readonly", this, function () { - this.updateMode(); - }); - this.renderValue(); - this.loadAndPopulateModelList(); - this.updateMode(); - }.bind(this) - ); - - return res; - }, - clear: function () { - if (this.mode !== "readonly") { - this.field_list.set([]); - this.loadAndPopulateModelList(); - this._setValue(this.field_list.get()); - } - }, - fieldListChanged: function () { - this._setValue(this.field_list.get()); - }, - fieldListRemoved: function () { - this._setValue(this.field_list.get()); - var model = new Data.DataSet(this, "bve.view"); - model.call("get_clean_list", [this.lastSetValue]).then( - function (result) { - this.field_list.set(JSON.parse(result)); - this._setValue(this.field_list.get()); - }.bind(this) - ); - this.loadAndPopulateModelList(); - }, - renderValue: function () { - this.field_list.set(JSON.parse(this.value)); - }, - updateMode: function () { - if (this.mode === "readonly") { - this.$el.find(".clear-btn").addClass("d-none"); - this.$el.find(".body .right").droppable("option", "disabled", true); - } else { - this.$el.find(".clear-btn").removeClass("d-none"); - this.$el.find(".body .right").droppable("option", "disabled", false); - } - this.field_list.setMode(this.mode); - this.model_list.setMode(this.mode); - }, - loadAndPopulateModelList: function () { - var model_ids = null; - if (this.field_list.get().length > 0) { - model_ids = this.field_list.getModelIds(); - } - this.model_list.loadModels(model_ids).then( - function (models) { - this.model_list.populateModels(models); - }.bind(this) - ); - }, - getTableAlias: function (field) { - if (typeof field.table_alias === "undefined") { - var model_ids = this.field_list.getModelIds(); - var n = 1; - while (typeof model_ids["t" + n] !== "undefined") { - n++; - } - return "t" + n; - } - return field.table_alias; - }, - addFieldAndJoinNode: function (field, join_node) { - if (join_node.join_node === -1 || join_node.table_alias === -1) { - field.table_alias = this.getTableAlias(field); - if (join_node.join_node === -1) { - join_node.join_node = field.table_alias; - } else { - join_node.table_alias = field.table_alias; - } - this.field_list.add(join_node); - } else { - field.table_alias = join_node.table_alias; - } - - this.field_list.add(field); - this.loadAndPopulateModelList(); - this._setValue(this.field_list.get()); - }, - addField: function (field) { - var data = _.extend({}, field); - var model = new Data.DataSet(this, "ir.model"); - var field_data = this.field_list.get(); - model.call("get_join_nodes", [field_data, data]).then( - function (result) { - if (result.length === 1) { - this.addFieldAndJoinNode(data, result[0]); - } else if (result.length > 1) { - var dialog = new JoinNodeDialog( - this, - {}, - result, - this.field_list.getModelData() - ); - dialog.open().on("chosen", this, function (e) { - this.addFieldAndJoinNode(data, e.choice); - }); - } else { - data.table_alias = this.getTableAlias(data); - this.field_list.add(data); - this.loadAndPopulateModelList(); - this._setValue(this.field_list.get()); - } - }.bind(this) - ); - }, - _parseValue: function (value) { - return JSON.stringify(value); - }, - }); - - field_registry.add("BVEEditor", BiViewEditor); -}); diff --git a/bi_view_editor/static/src/xml/bi_view_editor.xml b/bi_view_editor/static/src/xml/bi_view_editor.xml deleted file mode 100644 index c69828e55..000000000 --- a/bi_view_editor/static/src/xml/bi_view_editor.xml +++ /dev/null @@ -1,261 +0,0 @@ - - diff --git a/bi_view_editor/tests/test_bi_view.py b/bi_view_editor/tests/test_bi_view.py index c037110b6..477b027f3 100644 --- a/bi_view_editor/tests/test_bi_view.py +++ b/bi_view_editor/tests/test_bi_view.py @@ -324,7 +324,7 @@ class TestBiViewEditor(TransactionCase): self.assertTrue(line.model_id) self.assertTrue(line.model_name) self.env.cr.execute("UPDATE bve_view_line SET model_id = null") - bi_view1.invalidate_cache() + bi_view1.line_ids.invalidate_recordset() for line in bi_view1.line_ids: self.assertFalse(line.model_id) self.assertTrue(line.model_name) @@ -340,7 +340,7 @@ class TestBiViewEditor(TransactionCase): self.assertTrue(line.field_id) self.assertTrue(line.field_name) self.env.cr.execute("UPDATE bve_view_line SET field_id = null") - bi_view1.invalidate_cache() + bi_view1.line_ids.invalidate_recordset() for line in bi_view1.line_ids: self.assertFalse(line.field_id) self.assertTrue(line.field_name) @@ -359,18 +359,6 @@ class TestBiViewEditor(TransactionCase): def test_17_uninstall_hook(self): uninstall_hook(self.cr, self.env) - def test_18_action_translations(self): - self.env["res.lang"]._activate_lang("it_IT") - vals = self.bi_view1_vals - vals.update({"name": "Test View1"}) - bi_view1 = self.env["bve.view"].create(vals) - res = bi_view1.action_translations() - self.assertFalse(res) - - bi_view1.action_create() - res = bi_view1.action_translations() - self.assertTrue(res) - @odoo.tests.tagged("post_install", "-at_install") def test_19_field_selection(self): field = ( diff --git a/bi_view_editor/views/bve_view.xml b/bi_view_editor/views/bve_view.xml index 0dfcdf8c1..70149e2d2 100644 --- a/bi_view_editor/views/bve_view.xml +++ b/bi_view_editor/views/bve_view.xml @@ -1,5 +1,38 @@ + + Custom BI Views + bve.view + tree,form + +

+ Click to create a Custom Query Object. +

+

+ +

+
+
+ + + + bve.view @@ -41,6 +74,7 @@ icon="fa-align-justify" string="Create a Menu" target="new" + context="{'default_menu_id': %(bi_view_editor.menu_bi_view_editor_custom_reports)d}" /> -
-

- - - + - - - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + @@ -165,28 +176,4 @@ - - Custom BI Views - bve.view - tree,form - -

- Click to create a Custom Query Object. -

-

- -

-
-
- - diff --git a/bi_view_editor_spreadsheet_dashboard/README.rst b/bi_view_editor_spreadsheet_dashboard/README.rst new file mode 100644 index 000000000..a901a8167 --- /dev/null +++ b/bi_view_editor_spreadsheet_dashboard/README.rst @@ -0,0 +1,84 @@ +==================================== +BI View Editor Spreadsheet Dashboard +==================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:9c85b2692a951f167f03a07ceefaa3782a9d9ac830eb2598045fe5e6a548c1d3 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Freporting--engine-lightgray.png?logo=github + :target: https://github.com/OCA/reporting-engine/tree/16.0/bi_view_editor_spreadsheet_dashboard + :alt: OCA/reporting-engine +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/reporting-engine-16-0/reporting-engine-16-0-bi_view_editor_spreadsheet_dashboard + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/reporting-engine&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Glue module for BI View Editor and Spreadsheet Dashboard. + +To avoid a dependency of the ``bi_view_editor`` module on the ``spreadsheet`` +module through the ``spreadsheet_dashboard`` module, the ``bi_view_editor`` +menu items are parented to the legacy ``base.menu_board_root`` menu. In case +the ``spreadsheet_dashboard`` module is installed, this auto-installable +module moves them to the ``spreadsheet_dashboard`` menu. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Coop IT Easy SC + +Contributors +~~~~~~~~~~~~ + +* `Coop IT Easy SC `_: + + * hugues de keyzer + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/reporting-engine `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/bi_view_editor_spreadsheet_dashboard/__init__.py b/bi_view_editor_spreadsheet_dashboard/__init__.py new file mode 100644 index 000000000..bddcc21b8 --- /dev/null +++ b/bi_view_editor_spreadsheet_dashboard/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2023 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/bi_view_editor_spreadsheet_dashboard/__manifest__.py b/bi_view_editor_spreadsheet_dashboard/__manifest__.py new file mode 100644 index 000000000..9d864cf66 --- /dev/null +++ b/bi_view_editor_spreadsheet_dashboard/__manifest__.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2023 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +{ + "name": "BI View Editor Spreadsheet Dashboard", + "summary": "Glue module for BI View Editor and Spreadsheet Dashboard", + "author": "Coop IT Easy SC, Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/OCA/reporting-engine", + "category": "Hidden", + "version": "16.0.1.0.0", + "depends": [ + "bi_view_editor", + "spreadsheet_dashboard", + ], + "data": [ + "views/menus.xml", + ], + "auto_install": True, +} diff --git a/bi_view_editor_spreadsheet_dashboard/readme/CONTRIBUTORS.rst b/bi_view_editor_spreadsheet_dashboard/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..eb7c015bf --- /dev/null +++ b/bi_view_editor_spreadsheet_dashboard/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Coop IT Easy SC `_: + + * hugues de keyzer diff --git a/bi_view_editor_spreadsheet_dashboard/readme/DESCRIPTION.rst b/bi_view_editor_spreadsheet_dashboard/readme/DESCRIPTION.rst new file mode 100644 index 000000000..7aead53c6 --- /dev/null +++ b/bi_view_editor_spreadsheet_dashboard/readme/DESCRIPTION.rst @@ -0,0 +1,7 @@ +Glue module for BI View Editor and Spreadsheet Dashboard. + +To avoid a dependency of the ``bi_view_editor`` module on the ``spreadsheet`` +module through the ``spreadsheet_dashboard`` module, the ``bi_view_editor`` +menu items are parented to the legacy ``base.menu_board_root`` menu. In case +the ``spreadsheet_dashboard`` module is installed, this auto-installable +module moves them to the ``spreadsheet_dashboard`` menu. diff --git a/bi_view_editor_spreadsheet_dashboard/views/menus.xml b/bi_view_editor_spreadsheet_dashboard/views/menus.xml new file mode 100644 index 000000000..f993d16a9 --- /dev/null +++ b/bi_view_editor_spreadsheet_dashboard/views/menus.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + diff --git a/setup/bi_view_editor_spreadsheet_dashboard/odoo/addons/bi_view_editor_spreadsheet_dashboard b/setup/bi_view_editor_spreadsheet_dashboard/odoo/addons/bi_view_editor_spreadsheet_dashboard new file mode 120000 index 000000000..d16f474aa --- /dev/null +++ b/setup/bi_view_editor_spreadsheet_dashboard/odoo/addons/bi_view_editor_spreadsheet_dashboard @@ -0,0 +1 @@ +../../../../bi_view_editor_spreadsheet_dashboard \ No newline at end of file diff --git a/setup/bi_view_editor_spreadsheet_dashboard/setup.py b/setup/bi_view_editor_spreadsheet_dashboard/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/bi_view_editor_spreadsheet_dashboard/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)