[MIG] web_widget_one2many_tree_line_duplicate: Migration to 16.0

pull/2809/head
Carlos Roca 2024-04-16 17:13:10 +02:00
parent 5229e9910b
commit f239c3c5d3
11 changed files with 236 additions and 290 deletions

View File

@ -7,7 +7,7 @@ Web Widget One2many Tree Line Duplicate
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:9cd7a43eab907d7d3ed3380c20cda1be40d288c01feb25dd54108e69cb350314
!! source digest: sha256:0a96e20808687d52d22565143495374cad5121234c141e804bf8b9f06f9616d4
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
@ -17,13 +17,13 @@ Web Widget One2many Tree Line Duplicate
: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
: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-13-0/web-13-0-web_widget_one2many_tree_line_duplicate
: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=13.0
:target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
@ -61,7 +61,7 @@ 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:%2013.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
`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.
@ -79,6 +79,7 @@ Contributors
* `Tecnativa <https://www.tecnativa.com/>`_:
* Alexandre Díaz
* Carlos Roca
Maintainers
~~~~~~~~~~~
@ -93,6 +94,6 @@ 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/13.0/web_widget_one2many_tree_line_duplicate>`_ project on GitHub.
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

@ -4,12 +4,22 @@
{
"name": "Web Widget One2many Tree Line Duplicate",
"category": "web",
"version": "13.0.1.0.2",
"version": "16.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,
"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

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

View File

@ -9,10 +9,11 @@
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
: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.
@ -275,7 +276,7 @@ pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
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 }
@ -301,7 +302,7 @@ span.option {
span.pre {
white-space: pre }
span.problematic {
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
@ -367,9 +368,9 @@ ul.auto-toc {
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:9cd7a43eab907d7d3ed3380c20cda1be40d288c01feb25dd54108e69cb350314
!! 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/13.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-13-0/web-13-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=13.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<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">
@ -408,7 +409,7 @@ ul.auto-toc {
<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:%2013.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<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">
@ -424,6 +425,7 @@ If you spotted it first, help us to smash it by providing a detailed and welcome
<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>
@ -431,11 +433,13 @@ If you spotted it first, help us to smash it by providing a detailed and welcome
<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>
<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/13.0/web_widget_one2many_tree_line_duplicate">OCA/web</a> project on GitHub.</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>

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

@ -1,163 +0,0 @@
/* 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($("<th>", {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($("<td>"));
}
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 = $("<button>", {
class: "fa fa-clone",
name: "clone",
"aria-label": _t("Clone row ") + (index + 1),
});
const $td = $("<td>", {class: "o_list_record_clone"}).append($icon);
$row.append($td);
}
return $row;
},
/**
* @private
* @override
*/
_getNumberOfCols: function () {
var n = this._super();
if (this.addCloneIcon) {
n++;
}
return n;
},
/**
* Trigger the clonation of the record
*
* @param {MouseEvent} ev
*/
_onCloneIconClick: function (ev) {
ev.preventDefault();
ev.stopPropagation();
var $row = $(ev.target).closest("tr");
var id = $row.data("id");
this.unselectRow().then(() => {
this.trigger_up("clone_record", {
context: ev.currentTarget.dataset.context && [
ev.currentTarget.dataset.context,
],
id: id,
});
});
},
});
FieldOne2Many.include({
custom_events: _.extend({}, FieldOne2Many.prototype.custom_events, {
clone_record: "_onCloneRecord",
}),
/**
* @param {CustomEvent} ev
*/
_onCloneRecord: function (ev) {
const data = ev.data || {};
ev.stopPropagation();
if (!this.cloningRecord && this.activeActions.create) {
this.cloningRecord = true;
this.trigger_up("edited_list", {id: this.value.id});
this._setValue(
{
operation: "CLONE", // This operation is a special case implemented only in this module
position: "bottom",
context: data.context,
id: ev.data.id,
},
{
allowWarning: data.allowWarning,
}
)
.then(() => {
this.cloningRecord = false;
})
.then(() => {
// This is necessary to propagate the changes
// to the main record
this._setValue({
operation: "TRIGGER_ONCHANGE",
});
})
.then(() => {
if (data.onSuccess) {
data.onSuccess();
}
})
.guardedCatch(() => {
this.cloningRecord = false;
});
}
},
});
}
);

View File

@ -203,60 +203,81 @@ odoo.define("web_widget_one2many_tree_line_duplicate.BasicModel", function (requ
return Promise.all(defs).then(() => _.keys(changes));
},
/**
* Modified implementation of '_performOnChange' to properly unfold the cloned
* record properties son we can interact with it as if we'd created it. We
* don't really do an onchange and the only fields that we're getting are those
* relational.
*
* @param {Object} record
* @param {String} [viewType] current viewType. If not set, we will assume
* main viewType from the record
* @returns {Promise}
*/
_pseudoOnChange: function (record, viewType) {
var self = this;
var onchangeSpec = this._buildOnchangeSpecs(record, viewType);
if (!onchangeSpec) {
return Promise.resolve();
}
var idList = record.data.id ? [record.data.id] : [];
var options = {
full: true,
};
var context = this._getContext(record, options);
var currentData = this._generateOnChangeData(record, {changesOnly: false});
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 || {};
return self
._rpc({
model: record.model,
method: "onchange",
args: [idList, currentData, [], onchangeSpec],
context: context,
})
.then(function (result) {
if (!record._changes) {
// If the _changes key does not exist anymore, it means that
// it was removed by discarding the changes after the rpc
// to onchange. So, in that case, the proper response is to
// ignore the onchange.
return;
// 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;
}
if (result.warning) {
self.trigger_up("warning", result.warning);
record._warning = true;
}
if (result.domain) {
record._domains = _.extend(record._domains, result.domain);
}
// We're only interested in relational fields
const values = _.pick(result.value, (v) => {
return typeof v === "object";
}
}
// 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,
});
return self._applyOnChange(values, record).then(function () {
return result;
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) {
@ -302,20 +323,6 @@ odoo.define("web_widget_one2many_tree_line_duplicate.BasicModel", function (requ
record_parent_id: params.clone_parent_record_id,
copy_data: params.clone_copy_data || values,
};
// 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;
}
const _this = this;
return (
this.applyDefaultValues(record.id, values, {fieldNames: fieldNames})
@ -333,19 +340,45 @@ odoo.define("web_widget_one2many_tree_line_duplicate.BasicModel", function (requ
resolve();
};
_this
._pseudoOnChange(record, targetView)
._performOnChange(record, [], {})
.then(always)
.guardedCatch(always);
});
return def;
})
.then(() => {
return this._fetchRelationalData(record);
})
.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});

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>

View File

@ -1,22 +0,0 @@
/* Copyright 2021 Tecnativa - Alexandre Díaz
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) */
.o_list_view {
.o_list_table {
.o_list_record_clone {
width: 1px; // to prevent the column to expand
}
.o_list_record_clone button {
padding: 0px;
background: none;
border-style: none;
display: table-cell;
cursor: pointer;
}
.o_list_record_clone_header {
width: 32px;
}
}
}

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template id="assets_backend" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link
rel="stylesheet"
href="/web_widget_one2many_tree_line_duplicate/static/src/scss/one2many_tree_line_duplicate.scss"
/>
</xpath>
<xpath expr="//script[1]" position="before">
<script
type="text/javascript"
src="/web_widget_one2many_tree_line_duplicate/static/src/js/basic_model.js"
/>
<script
type="text/javascript"
src="/web_widget_one2many_tree_line_duplicate/static/src/js/one2many_tree_line_duplicate.js"
/>
</xpath>
</template>
</odoo>