diff --git a/setup/web_widget_one2many_tree_line_duplicate/odoo/addons/web_widget_one2many_tree_line_duplicate b/setup/web_widget_one2many_tree_line_duplicate/odoo/addons/web_widget_one2many_tree_line_duplicate new file mode 120000 index 000000000..8ba1faf8d --- /dev/null +++ b/setup/web_widget_one2many_tree_line_duplicate/odoo/addons/web_widget_one2many_tree_line_duplicate @@ -0,0 +1 @@ +../../../../web_widget_one2many_tree_line_duplicate \ No newline at end of file diff --git a/setup/web_widget_one2many_tree_line_duplicate/setup.py b/setup/web_widget_one2many_tree_line_duplicate/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/web_widget_one2many_tree_line_duplicate/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) 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..4bac1287c --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/README.rst @@ -0,0 +1,99 @@ +======================================= +Web Widget One2many Tree Line Duplicate +======================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:0a96e20808687d52d22565143495374cad5121234c141e804bf8b9f06f9616d4 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/16.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-16-0/web-16-0-web_widget_one2many_tree_line_duplicate + :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/web&target_branch=16.0 + :alt: Try me on Runboat + +|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 to smash 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 + * Carlos Roca + +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..501392a33 --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/__manifest__.py @@ -0,0 +1,25 @@ +# 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": "16.0.1.0.0", + "author": "Tecnativa, Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/OCA/web", + "depends": ["web"], + "auto_install": False, + "installable": True, + "assets": { + "web.assets_backend": [ + "/web_widget_one2many_tree_line_duplicate/static/src/legacy/**/*.js", + "/web_widget_one2many_tree_line_duplicate/static/src/**/*.esm.js", + ( + "after", + "/web/static/src/views/list/list_renderer.xml", + "/web_widget_one2many_tree_line_duplicate/static/src/list/list_renderer.xml", + ), + ], + }, +} diff --git a/web_widget_one2many_tree_line_duplicate/i18n/web_widget_one2many_tree_line_duplicate.pot b/web_widget_one2many_tree_line_duplicate/i18n/web_widget_one2many_tree_line_duplicate.pot new file mode 100644 index 000000000..f0e93a927 --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/i18n/web_widget_one2many_tree_line_duplicate.pot @@ -0,0 +1,21 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_widget_one2many_tree_line_duplicate +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: web_widget_one2many_tree_line_duplicate +#. openerp-web +#: code:addons/web_widget_one2many_tree_line_duplicate/static/src/js/one2many_tree_line_duplicate.js:0 +#, python-format +msgid "Clone row " +msgstr "" 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..2ee38b271 --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* `Tecnativa `_: + + * Alexandre Díaz + * Carlos Roca 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/icon.png b/web_widget_one2many_tree_line_duplicate/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/web_widget_one2many_tree_line_duplicate/static/description/icon.png differ 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..ebcf6500b --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/static/description/index.html @@ -0,0 +1,448 @@ + + + + + + +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 Runboat

+

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 to smash 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
    • +
    • Carlos Roca
    • +
    +
  • +
+
+
+

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/basic_relational_model/basic_relational_model.esm.js b/web_widget_one2many_tree_line_duplicate/static/src/basic_relational_model/basic_relational_model.esm.js new file mode 100644 index 000000000..a014408ae --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/static/src/basic_relational_model/basic_relational_model.esm.js @@ -0,0 +1,22 @@ +/** @odoo-module **/ +/* Copyright 2024 Tecnativa - Carlos Roca + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ + +import {StaticList} from "@web/views/basic_relational_model"; +import {patch} from "@web/core/utils/patch"; + +patch(StaticList.prototype, "web_widget_one2many_tree_line_duplicate.StaticList", { + async cloneRecord(recordId, params) { + const operation = { + context: [params.context], + operation: "CLONE", + position: "bottom", + id: recordId, + }; + await this.model.__bm__.save(this.__bm_handle__, {savePoint: true}); + this.model.__bm__.freezeOrder(this.__bm_handle__); + await this.__syncParent(operation); + const newRecord = this.records[this.records.length - 1]; + return newRecord; + }, +}); diff --git a/web_widget_one2many_tree_line_duplicate/static/src/legacy/basic_model/basic_model.js b/web_widget_one2many_tree_line_duplicate/static/src/legacy/basic_model/basic_model.js new file mode 100644 index 000000000..c7297558c --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/static/src/legacy/basic_model/basic_model.js @@ -0,0 +1,453 @@ +/* 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"); + const rpc = require("web.rpc"); + + 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._cloneX2Many.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} + */ + _cloneX2Many: function (record, fieldName, command, options) { + const localID = + (record._changes && record._changes[fieldName]) || + record.data[fieldName]; + const list = this.localData[localID]; + const context = _.extend({}, this._getContext(list)); + const position = (command && command.position) || "bottom"; + const viewType = (options && options.viewType) || record.viewType; + const fieldInfo = record.fieldsInfo[viewType][fieldName]; + const record_command = this.get(command.id); + // Trigger addFieldsInfo on every possible view in the field to avoid + // errors when loading such views from the cloned line + const loaded_views = Object.keys(list.fieldsInfo); + const field_views = Object.keys(fieldInfo.views); + const to_load_views = field_views.filter( + (value) => !loaded_views.includes(value) + ); + _.each(to_load_views, (name) => { + this.addFieldsInfo(localID, { + fields: fieldInfo.views[name].fields, + fieldInfo: fieldInfo.views[name].fieldsInfo[name], + viewType: name, + }); + }); + // Only load fields available in the views. Otherwise we could get into + // problems when some process try to get their states. + var whitelisted_fields = []; + _.each(_.allKeys(record_command.fieldsInfo), function (view) { + _.each(_.allKeys(record_command.fieldsInfo[view]), function (field) { + if (!whitelisted_fields.includes(field)) { + whitelisted_fields.push(field); + } + }); + }); + + const params = { + context: context, + fields: list.fields, + fieldsInfo: list.fieldsInfo, + parentID: list.id, + position: position, + allowWarning: options && options.allowWarning, + viewType: viewType, + views: fieldInfo.views, + clone_parent_record_id: command.id, + }; + + let read_data = Promise.resolve(); + if (this.isNew(command.id)) { + // We need the 'copy_data' of the original parent record + if (!_.isEmpty(record_command.clone_data)) { + params.clone_copy_data = record_command.clone_copy_data; + } + } else { + // Record state only has loaded data and only for the fields defined in the views. + // For this reason we need ensure copy all the model fields. + read_data = rpc.query({ + model: list.model, + method: "copy_data", + args: [record_command.res_id], + kwargs: {context: record.getContext()}, + }); + } + + return read_data.then((result) => { + const clone_values = _.defaults( + {}, + this._getValuesToClone(record_command, params), + _.pick(result, whitelisted_fields) + ); + return this._makeCloneRecord(list.model, params, clone_values) + .then((id) => { + const ids = [id]; + list._changes = list._changes || []; + list._changes.push({ + operation: "ADD", + id: id, + position: position, + isNew: true, + }); + const local_record = this.localData[id]; + list._cache[local_record.res_id] = id; + if (list.orderedResIDs) { + const index = + list.offset + (position !== "top" ? list.limit : 0); + list.orderedResIDs.splice(index, 0, local_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(() => { + const x2ManysDef = this._fetchX2ManysBatched(list); + const 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)); + }, + + applyDefaultValues: function (recordID, values, options) { + options = options || {}; + var record = this.localData[recordID]; + var viewType = options.viewType || record.viewType; + var fieldNames = + options.fieldNames || Object.keys(record.fieldsInfo[viewType]); + record._changes = record._changes || {}; + + // Ignore values for non requested fields (for instance, fields that are + // not in the view) + values = _.pick(values, fieldNames); + + // Fill default values for missing fields + for (var i = 0; i < fieldNames.length; i++) { + var fieldName = fieldNames[i]; + if (!(fieldName in values) && !(fieldName in record._changes)) { + var field = record.fields[fieldName]; + if ( + field.type === "float" || + field.type === "integer" || + field.type === "monetary" + ) { + values[fieldName] = 0; + } else if ( + field.type === "one2many" || + field.type === "many2many" + ) { + values[fieldName] = []; + } else { + values[fieldName] = null; + } + } + } + + // Parse each value and create dataPoints for relational fields + var defs = []; + for (var fieldName in values) { + var field = record.fields[fieldName]; + record.data[fieldName] = null; + if (field.type === "many2one" && values[fieldName]) { + var dp = this._makeDataPoint({ + context: record.context, + data: {id: values[fieldName]}, + modelName: field.relation, + parentID: record.id, + }); + record._changes[fieldName] = dp.id; + } else if (field.type === "reference" && values[fieldName]) { + var ref = values[fieldName].split(","); + var dp = this._makeDataPoint({ + context: record.context, + data: {id: parseInt(ref[1])}, + modelName: ref[0], + parentID: record.id, + }); + defs.push(this._fetchNameGet(dp)); + record._changes[fieldName] = dp.id; + } else if (field.type === "one2many" || field.type === "many2many") { + defs.push( + this._processX2ManyCommands( + record, + fieldName, + values[fieldName], + options + ) + ); + } else { + record._changes[fieldName] = this._parseServerValue( + field, + values[fieldName] + ); + } + } + + return Promise.all(defs); + }, + + _makeCloneRecord: function (modelName, params, values) { + const targetView = params.viewType; + let fields = params.fields; + const fieldsInfo = params.fieldsInfo; + + // Get available fields + for (const view_type in params.views) { + fields = _.defaults({}, fields, params.views[view_type].fields); + } + let fieldNames = Object.keys(fields); + // 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, + }); + // Extend dataPoint with clone info + record.clone_data = { + record_parent_id: params.clone_parent_record_id, + copy_data: params.clone_copy_data || values, + }; + const _this = this; + return ( + this.applyDefaultValues(record.id, values, {fieldNames: fieldNames}) + // This will ensure we refresh the proper properties + .then(() => { + var def = new Promise(function (resolve, reject) { + var always = function () { + if (record._warning) { + if (params.allowWarning) { + delete record._warning; + } else { + reject(); + } + } + resolve(); + }; + _this + ._performOnChange(record, [], {}) + .then(always) + .guardedCatch(always); + }); + return def; + }) + .then(() => { + // Inject always_reload to many2one fieldsInfo + for (var key of Object.keys(record.fieldsInfo[targetView])) { + if (record.fields[key].type === "many2one") { + const old_reload_value = + record.fieldsInfo[targetView][key].options && + record.fieldsInfo[targetView][key].options + .always_reload; + record.fieldsInfo[targetView][key].options = { + ...record.fieldsInfo[targetView][key].options, + always_reload: true, + old_reload_value, + }; + } + } + return this._postprocess(record); + }) + .then(() => { + // Recover always_reload state before injection to many2one fieldsInfo + for (var key of Object.keys(record.fieldsInfo[targetView])) { + if ( + record.fieldsInfo[targetView][key].options && + "old_reload_value" in + record.fieldsInfo[targetView][key].options + ) { + const old_reload_value = + record.fieldsInfo[targetView][key].options + .old_reload_value; + record.fieldsInfo[targetView][ + key + ].options.always_reload = old_reload_value; + } + } + // 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 + * @param {Object} params + * @returns {Object} + */ + _getValuesToClone: function (line_state, params) { + 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 = params.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") { + values_to_clone[field_name] = [ + [ + 6, + 0, + _.map(value.data || [], (item) => { + return item.data.id; + }), + ], + ]; + } else if (field_info.type === "one2many") { + values_to_clone[field_name] = _.map(value.data || [], (item) => { + return [ + 0, + 0, + this._getValuesToClone(item, {fields: value.fields}), + ]; + }); + } 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; + }, + + _generateChanges: function (record) { + let res = this._super.apply(this, arguments); + if (!_.isEmpty(record.clone_data)) { + // If a cloned record, ensure that all fields are written (and not only the view fields) + res = _.extend({}, record.clone_data.copy_data, res); + } + return res; + }, + }); +}); diff --git a/web_widget_one2many_tree_line_duplicate/static/src/list/list_renderer.esm.js b/web_widget_one2many_tree_line_duplicate/static/src/list/list_renderer.esm.js new file mode 100644 index 000000000..f0b8be640 --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/static/src/list/list_renderer.esm.js @@ -0,0 +1,38 @@ +/** @odoo-module **/ +/* Copyright 2024 Tecnativa - Carlos Roca + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ + +import {ListRenderer} from "@web/views/list/list_renderer"; +import {patch} from "@web/core/utils/patch"; + +patch(ListRenderer.prototype, "web_widget_one2many_tree_line_duplicate.ListRenderer", { + setup() { + this._super(...arguments); + const parent = this.__owl__.parent.parent; + this.displayDuplicateLine = + parent && + parent.props && + parent.props.fieldInfo && + parent.props.fieldInfo.options && + parent.props.fieldInfo.options.allow_clone; + }, + get nbCols() { + var nbCols = this._super(...arguments); + if (this.displayDuplicateLine) { + nbCols++; + } + return nbCols; + }, + async onCloneIconClick(record) { + const editedRecord = this.props.list.editedRecord; + if (editedRecord && editedRecord !== record) { + const unselected = await this.props.list.unselectRecord(true); + if (!unselected) { + return; + } + } + const context = this.props.list.model.root.context; + await this.props.list.cloneRecord(record.__bm_handle__, {context}); + this.props.list.model.notify(); + }, +}); diff --git a/web_widget_one2many_tree_line_duplicate/static/src/list/list_renderer.xml b/web_widget_one2many_tree_line_duplicate/static/src/list/list_renderer.xml new file mode 100644 index 000000000..e1aa685e9 --- /dev/null +++ b/web_widget_one2many_tree_line_duplicate/static/src/list/list_renderer.xml @@ -0,0 +1,43 @@ + + + + + + + + + + +