From 4f42782bc17104a9eedaf01636140783ff167096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20D=2E=20D=C3=ADaz?= Date: Fri, 22 Oct 2021 19:08:09 +0200 Subject: [PATCH 1/6] [ADD] web_widget_one2many_tree_line_duplicate --- .../README.rst | 95 ++++ .../__init__.py | 1 + .../__manifest__.py | 15 + .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 1 + .../readme/ROADMAP.rst | 1 + .../readme/USAGE.rst | 11 + .../static/description/index.html | 442 ++++++++++++++++++ .../static/src/js/basic_model.js | 277 +++++++++++ .../src/js/one2many_tree_line_duplicate.js | 163 +++++++ .../scss/one2many_tree_line_duplicate.scss | 22 + .../view/assets.xml | 21 + 12 files changed, 1052 insertions(+) create mode 100644 web_widget_one2many_tree_line_duplicate/README.rst create mode 100644 web_widget_one2many_tree_line_duplicate/__init__.py create mode 100644 web_widget_one2many_tree_line_duplicate/__manifest__.py create mode 100644 web_widget_one2many_tree_line_duplicate/readme/CONTRIBUTORS.rst create mode 100644 web_widget_one2many_tree_line_duplicate/readme/DESCRIPTION.rst create mode 100644 web_widget_one2many_tree_line_duplicate/readme/ROADMAP.rst create mode 100644 web_widget_one2many_tree_line_duplicate/readme/USAGE.rst create mode 100644 web_widget_one2many_tree_line_duplicate/static/description/index.html create mode 100644 web_widget_one2many_tree_line_duplicate/static/src/js/basic_model.js create mode 100644 web_widget_one2many_tree_line_duplicate/static/src/js/one2many_tree_line_duplicate.js create mode 100644 web_widget_one2many_tree_line_duplicate/static/src/scss/one2many_tree_line_duplicate.scss create mode 100644 web_widget_one2many_tree_line_duplicate/view/assets.xml diff --git a/web_widget_one2many_tree_line_duplicate/README.rst b/web_widget_one2many_tree_line_duplicate/README.rst new file mode 100644 index 000000000..977a4da86 --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/README.rst @@ -0,0 +1,95 @@ +======================================= +Web Widget One2many Tree Line Duplicate +======================================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fweb-lightgray.png?logo=github + :target: https://github.com/OCA/web/tree/13.0/web_widget_one2many_tree_line_duplicate + :alt: OCA/web +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/web-13-0/web-13-0-web_widget_one2many_tree_line_duplicate + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/162/13.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Allow to add a icon to clone the line. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +This module works on all one2many tree views. The cloning process doesn't trigger onchange/default calls. + +**Available Options** + +- allow_clone > Add the icon to clone the line (default: false) + +**Examples** + +.. code:: xml + + + +Known issues / Roadmap +====================== + +* Add an option to control which columns are copied + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Tecnativa + +Contributors +~~~~~~~~~~~~ + +* `Tecnativa `_: + + * Alexandre Díaz + +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/web `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/web_widget_one2many_tree_line_duplicate/__init__.py b/web_widget_one2many_tree_line_duplicate/__init__.py new file mode 100644 index 000000000..f0b902299 --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/__init__.py @@ -0,0 +1 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) diff --git a/web_widget_one2many_tree_line_duplicate/__manifest__.py b/web_widget_one2many_tree_line_duplicate/__manifest__.py new file mode 100644 index 000000000..a0aa47ddc --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2021 Tecnativa - Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +{ + "name": "Web Widget One2many Tree Line Duplicate", + "category": "web", + "version": "13.0.1.0.0", + "author": "Tecnativa, Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/OCA/web", + "depends": ["web"], + "data": ["view/assets.xml"], + "auto_install": False, + "installable": True, +} diff --git a/web_widget_one2many_tree_line_duplicate/readme/CONTRIBUTORS.rst b/web_widget_one2many_tree_line_duplicate/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..f1d7070a5 --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Tecnativa `_: + + * Alexandre Díaz diff --git a/web_widget_one2many_tree_line_duplicate/readme/DESCRIPTION.rst b/web_widget_one2many_tree_line_duplicate/readme/DESCRIPTION.rst new file mode 100644 index 000000000..a8a1fbfbf --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Allow to add a icon to clone the line. diff --git a/web_widget_one2many_tree_line_duplicate/readme/ROADMAP.rst b/web_widget_one2many_tree_line_duplicate/readme/ROADMAP.rst new file mode 100644 index 000000000..a21a75715 --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/readme/ROADMAP.rst @@ -0,0 +1 @@ +* Add an option to control which columns are copied diff --git a/web_widget_one2many_tree_line_duplicate/readme/USAGE.rst b/web_widget_one2many_tree_line_duplicate/readme/USAGE.rst new file mode 100644 index 000000000..4ab2eb200 --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/readme/USAGE.rst @@ -0,0 +1,11 @@ +This module works on all one2many tree views. The cloning process doesn't trigger onchange/default calls. + +**Available Options** + +- allow_clone > Add the icon to clone the line (default: false) + +**Examples** + +.. code:: xml + + diff --git a/web_widget_one2many_tree_line_duplicate/static/description/index.html b/web_widget_one2many_tree_line_duplicate/static/description/index.html new file mode 100644 index 000000000..91586e6ae --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/static/description/index.html @@ -0,0 +1,442 @@ + + + + + + +Web Widget One2many Tree Line Duplicate + + + +
+

Web Widget One2many Tree Line Duplicate

+ + +

Beta License: AGPL-3 OCA/web Translate me on Weblate Try me on Runbot

+

Allow to add a icon to clone the line.

+

Table of contents

+ +
+

Usage

+

This module works on all one2many tree views. The cloning process doesn’t trigger onchange/default calls.

+

Available Options

+
    +
  • allow_clone > Add the icon to clone the line (default: false)
  • +
+

Examples

+
+<field name="order_line" widget="section_and_note_one2many" mode="tree,kanban" options="{'allow_clone': True}" attrs="{'readonly': [('state', 'in', ('done','cancel'))]}">
+
+
+
+

Known issues / Roadmap

+
    +
  • Add an option to control which columns are copied
  • +
+
+
+

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 smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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/web project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/web_widget_one2many_tree_line_duplicate/static/src/js/basic_model.js b/web_widget_one2many_tree_line_duplicate/static/src/js/basic_model.js new file mode 100644 index 000000000..4ee844743 --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/static/src/js/basic_model.js @@ -0,0 +1,277 @@ +/* Copyright 2021 Tecnativa - Alexandre Díaz + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) */ + +odoo.define("web_widget_one2many_tree_line_duplicate.BasicModel", function(require) { + "use strict"; + + const BasicModel = require("web.BasicModel"); + + function dateToServer(date) { + return date + .clone() + .utc() + .locale("en") + .format("YYYY-MM-DD HH:mm:ss"); + } + + BasicModel.include({ + /** + * @override + */ + _applyChange: function(recordID, changes) { + // The normal way is to have only one change with the 'CLONE' operation + // but to ensure that "omitOnchange" is used we check that almost one change + // is a 'CLONE' operation. + // TODO: This is done in this way to don't override other "big" methods + const has_clone_oper = !_.chain(changes) + .values() + .filter({operation: "CLONE"}) + .isEmpty() + .value(); + if (has_clone_oper) { + return this._applyChangeOmitOnchange.apply(this, arguments); + } + return this._super.apply(this, arguments); + }, + + /** + * Force use "omitOnchange" when 'CLONE' operation are performed + * + * @override + */ + _applyX2ManyChange: function(record, fieldName, command) { + if (command.operation === "CLONE") { + return this._applyX2ManyChangeOmitOnchange.apply(this, arguments); + } + return this._super.apply(this, arguments); + }, + + /** + * Modified implementation of '_applyX2ManyChange' to allow create + * without trigger onchanges + * + * @param {String} recordID + * @param {Object} changes + * @param {Object} options + * @returns {Promise} + */ + _applyX2ManyChangeOmitOnchange: function(record, fieldName, command, options) { + var localID = + (record._changes && record._changes[fieldName]) || + record.data[fieldName]; + var list = this.localData[localID]; + var position = (command && command.position) || "top"; + var viewType = (options && options.viewType) || record.viewType; + var fieldInfo = record.fieldsInfo[viewType][fieldName]; + var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; + var params = { + fields: list.fields, + fieldsInfo: list.fieldsInfo, + parentID: list.id, + position: position, + viewType: view ? view.type : fieldInfo.viewType, + allowWarning: options && options.allowWarning, + }; + + if ( + command.position === "bottom" && + list.orderedResIDs && + list.orderedResIDs.length >= list.limit + ) { + list.tempLimitIncrement = (list.tempLimitIncrement || 0) + 1; + list.limit += 1; + } + + const record_state = this.get(command.id); + const clone_values = this._getValuesToClone(record_state); + return this._makeDefaultRecordOmitOnchange(list.model, params, clone_values) + .then(id => { + var ids = [id]; + list._changes = list._changes || []; + list._changes.push({ + operation: "ADD", + id: id, + position: position, + isNew: true, + }); + var record = this.localData[id]; + list._cache[record.res_id] = id; + if (list.orderedResIDs) { + var index = list.offset + (position !== "top" ? list.limit : 0); + list.orderedResIDs.splice(index, 0, record.res_id); + // List could be a copy of the original one + this.localData[list.id].orderedResIDs = list.orderedResIDs; + } + return ids; + }) + .then(ids => { + this._readUngroupedList(list).then(() => { + var x2ManysDef = this._fetchX2ManysBatched(list); + var referencesDef = this._fetchReferencesBatched(list); + return Promise.all([x2ManysDef, referencesDef]).then(() => ids); + }); + }); + }, + + /** + * Modified implementation of '_applyChange' to allow changes + * without trigger onchanges + * + * @param {String} recordID + * @param {Object} changes + * @param {Object} options + * @returns {Promise} + */ + _applyChangeOmitOnchange: function(recordID, changes, options) { + var record = this.localData[recordID]; + var field = false; + var defs = []; + options = options || {}; + record._changes = record._changes || {}; + if (!options.doNotSetDirty) { + record._isDirty = true; + } + // Apply changes to local data + for (var fieldName in changes) { + field = record.fields[fieldName]; + if ( + field && + (field.type === "one2many" || field.type === "many2many") + ) { + defs.push( + this._applyX2ManyChange( + record, + fieldName, + changes[fieldName], + options + ) + ); + } else if ( + field && + (field.type === "many2one" || field.type === "reference") + ) { + defs.push( + this._applyX2OneChange(record, fieldName, changes[fieldName]) + ); + } else { + record._changes[fieldName] = changes[fieldName]; + } + } + return Promise.all(defs).then(() => _.keys(changes)); + }, + + /** + * Modified implementation of '_makeDefaultRecord' to allow create + * without trigger onchanges/default values + * + * @param {String} modelName + * @param {Object} params + * @param {Object} values + * @returns {Promise} + */ + _makeDefaultRecordOmitOnchange: function(modelName, params, values) { + const targetView = params.viewType; + let fields = params.fields; + const fieldsInfo = params.fieldsInfo; + let fieldNames = Object.keys(fieldsInfo[targetView]); + // Fields that are present in the originating view, that need to be initialized + // Hence preventing their value to crash when getting back to the originating view + const parentRecord = + params.parentID && this.localData[params.parentID].type === "list" + ? this.localData[params.parentID] + : null; + if (parentRecord && parentRecord.viewType in parentRecord.fieldsInfo) { + const originView = parentRecord.viewType; + fieldNames = _.union( + fieldNames, + Object.keys(parentRecord.fieldsInfo[originView]) + ); + fieldsInfo[targetView] = _.defaults( + {}, + fieldsInfo[targetView], + parentRecord.fieldsInfo[originView] + ); + fields = _.defaults({}, fields, parentRecord.fields); + } + const record = this._makeDataPoint({ + modelName: modelName, + fields: fields, + fieldsInfo: fieldsInfo, + context: params.context, + parentID: params.parentID, + res_ids: params.res_ids, + viewType: targetView, + }); + + // We want to overwrite the default value of the handle field (if any), + // in order for new lines to be added at the correct position. + // -> This is a rare case where the defaul_get from the server + // will be ignored by the view for a certain field (usually "sequence"). + + var overrideDefaultFields = this._computeOverrideDefaultFields( + params.parentID, + params.position + ); + + if (overrideDefaultFields) { + values[overrideDefaultFields.field] = overrideDefaultFields.value; + } + + return this.applyDefaultValues(record.id, values, {fieldNames: fieldNames}) + .then(() => { + return this._fetchRelationalData(record); + }) + .then(() => { + return this._postprocess(record); + }) + .then(() => { + // Save initial changes, so they can be restored later, + // if we need to discard. + this.save(record.id, {savePoint: true}); + return record.id; + }); + }, + + /** + * Get the values formatted to clone + * + * @param {Object} line_state + * @returns {Object} + */ + _getValuesToClone: function(line_state) { + const values_to_clone = {}; + const line_data = line_state.data; + for (const field_name in line_data) { + if (field_name === "id") { + continue; + } + const value = line_data[field_name]; + const field_info = line_state.fields[field_name]; + if (!field_info) { + continue; + } + if (field_info.type !== "boolean" && !value) { + values_to_clone[field_name] = value; + } else if (field_info.type === "many2one") { + const rec_id = value.data && value.data.id; + values_to_clone[field_name] = rec_id || false; + } else if ( + field_info.type === "many2many" || + field_info.type === "one2many" + ) { + values_to_clone[field_name] = _.map(value.data || [], item => { + return item.data.id; + }); + } else if ( + field_info.type === "date" || + field_info.type === "datetime" + ) { + values_to_clone[field_name] = dateToServer(value); + } else { + values_to_clone[field_name] = value; + } + } + return values_to_clone; + }, + }); +}); diff --git a/web_widget_one2many_tree_line_duplicate/static/src/js/one2many_tree_line_duplicate.js b/web_widget_one2many_tree_line_duplicate/static/src/js/one2many_tree_line_duplicate.js new file mode 100644 index 000000000..88d355283 --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/static/src/js/one2many_tree_line_duplicate.js @@ -0,0 +1,163 @@ +/* Copyright 2021 Tecnativa - Alexandre Díaz + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) */ + +odoo.define( + "web_widget_one2many_tree_line_duplicate.One2manyTreeLineDuplicate", + function(require) { + "use strict"; + + const core = require("web.core"); + const FieldOne2Many = require("web.relational_fields").FieldOne2Many; + const ListRenderer = require("web.ListRenderer"); + + const _t = core._t; + + ListRenderer.include({ + events: _.extend({}, ListRenderer.prototype.events, { + "click tr .o_list_record_clone": "_onCloneIconClick", + }), + + /** + * @override + */ + init: function(parent) { + this._super.apply(this, arguments); + let allow_clone = + parent.attrs && + parent.attrs.options && + parent.attrs.options.allow_clone; + allow_clone = typeof allow_clone === "undefined" ? false : allow_clone; + this.addCloneIcon = + allow_clone && + !parent.isReadonly && + parent.activeActions && + parent.activeActions.create; + }, + + /** + * @private + * @override + */ + _renderHeader: function() { + var $thead = this._super.apply(this, arguments); + if (this.addCloneIcon) { + $thead + .find("tr") + .append($("", {class: "o_list_record_clone_header"})); + } + return $thead; + }, + + /** + * @override + * @private + */ + _renderFooter: function() { + const $footer = this._super.apply(this, arguments); + if (this.addCloneIcon) { + $footer.find("tr").append($("")); + } + return $footer; + }, + + /** + * Inject the icon for clone action + * + * @private + * @override + */ + _renderRow: function(record, index) { + const $row = this._super.apply(this, arguments); + if (this.addCloneIcon) { + const $icon = $("