3
0
Fork 0

Merge PR #2809 into 16.0

Signed-off-by pedrobaeza
16.0
OCA-git-bot 2024-06-14 08:35:58 +00:00
commit 34feddd65d
16 changed files with 1174 additions and 0 deletions

View File

@ -0,0 +1 @@
../../../../web_widget_one2many_tree_line_duplicate

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

View File

@ -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
<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 <https://github.com/OCA/web/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 <https://github.com/OCA/web/issues/new?body=module:%20web_widget_one2many_tree_line_duplicate%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Tecnativa
Contributors
~~~~~~~~~~~~
* `Tecnativa <https://www.tecnativa.com/>`_:
* 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 <https://github.com/OCA/web/tree/16.0/web_widget_one2many_tree_line_duplicate>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@ -0,0 +1 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)

View File

@ -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",
),
],
},
}

View File

@ -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 ""

View File

@ -0,0 +1,4 @@
* `Tecnativa <https://www.tecnativa.com/>`_:
* Alexandre Díaz
* Carlos Roca

View File

@ -0,0 +1 @@
Allow to add a icon to clone the line.

View File

@ -0,0 +1 @@
* Add an option to control which columns are copied

View File

@ -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
<field name="order_line" widget="section_and_note_one2many" mode="tree,kanban" options="{'allow_clone': True}" attrs="{'readonly': [('state', 'in', ('done','cancel'))]}">

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,448 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Web Widget One2many Tree Line Duplicate</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="web-widget-one2many-tree-line-duplicate">
<h1 class="title">Web Widget One2many Tree Line Duplicate</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:0a96e20808687d52d22565143495374cad5121234c141e804bf8b9f06f9616d4
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/web/tree/16.0/web_widget_one2many_tree_line_duplicate"><img alt="OCA/web" src="https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/web-16-0/web-16-0-web_widget_one2many_tree_line_duplicate"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/web&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>Allow to add a icon to clone the line.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-2">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
<p>This module works on all one2many tree views. The cloning process doesnt trigger onchange/default calls.</p>
<p><strong>Available Options</strong></p>
<ul class="simple">
<li>allow_clone &gt; Add the icon to clone the line (default: false)</li>
</ul>
<p><strong>Examples</strong></p>
<pre class="code xml literal-block">
<span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;order_line&quot;</span><span class="w"> </span><span class="na">widget=</span><span class="s">&quot;section_and_note_one2many&quot;</span><span class="w"> </span><span class="na">mode=</span><span class="s">&quot;tree,kanban&quot;</span><span class="w"> </span><span class="na">options=</span><span class="s">&quot;{'allow_clone': True}&quot;</span><span class="w"> </span><span class="na">attrs=</span><span class="s">&quot;{'readonly': [('state', 'in', ('done','cancel'))]}&quot;</span><span class="nt">&gt;</span>
</pre>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#toc-entry-2">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>Add an option to control which columns are copied</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-3">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/web/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/web/issues/new?body=module:%20web_widget_one2many_tree_line_duplicate%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-4">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-5">Authors</a></h2>
<ul class="simple">
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-6">Contributors</a></h2>
<ul class="simple">
<li><a class="reference external" href="https://www.tecnativa.com/">Tecnativa</a>:<ul>
<li>Alexandre Díaz</li>
<li>Carlos Roca</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>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.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/web/tree/16.0/web_widget_one2many_tree_line_duplicate">OCA/web</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@ -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;
},
});

View File

@ -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;
},
});
});

View File

@ -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();
},
});

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t
t-name="web_widget_one2many_tree_line_duplicate.ListRenderer"
t-inherit="web.ListRenderer"
t-inherit-mode="extension"
owl="1"
>
<xpath
expr="//th[@t-if='displayOptionalFields or activeActions.onDelete']"
position="before"
>
<th
t-if="displayDuplicateLine"
class="o_list_controller o_list_actions_header position-static"
style="width: 32px; min-width: 32px"
/>
</xpath>
</t>
<t
t-name="web_widget_one2many_tree_line_duplicate.ListRenderer.RecordRow"
t-inherit="web.ListRenderer.RecordRow"
t-inherit-mode="extension"
owl="1"
>
<xpath expr="//td[hasclass('o_list_record_remove')]" position="before">
<td
class="o_list_record_remove text-center"
t-if="displayDuplicateLine"
t-on-click.stop="() => this.onCloneIconClick(record)"
tabindex="-1"
>
<button
class="fa fa-clone"
name="duplicate"
aria-label="Duplicate row"
tabindex="-1"
/>
</td>
</xpath>
</t>
</templates>