summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorOdoo's Mergebot <xmo+roboto@odoo.com>2020-01-23 14:35:16 +0100
committerGitHub <noreply@github.com>2020-01-23 14:35:16 +0100
commit28dfe206dfc06cb883207910def53d6205bf21db (patch)
tree2657e19a29acec665bb2389ccf3065f040377988 /addons
parente73e0ef3c90011c6c3d68554a9becc87cdb7b3ad (diff)
parent9fc1dd538b48c5d055799ad6faa24ba5ed9e826d (diff)
[MERGE] sale_timesheet: Improve project overview
1) Add the following stat buttons on the right of the Tasks one: - (fa-tasks) Late Tasks: tasks whose deadline is exceeded (do not include tasks that don't have a deadline set) - (fa-tasks) Tasks in Overtime: tasks whose Effective hours are strictly greater than their Planned hours (if the latter is stricly greater than 0) 2) Remove the sheet view (should be like financial reports) 3) Add an 'Other Revenues' metric under the Profitability section below 'To Invoice' - It should represent all the revenues other than from timesheets linked to the analytic account of the project. - Only visible if greater than 0 4) Allow to fold/unfold the details by SO. It should be folded by default. Note: Although qweb views have a generic mechanism to manage fold and unfold, this standard behavior is not used here. It would imply refactoring the entire code preparing the template data (700+ lines of code which are hard to read). The quick and dirty way is used for now. (I wish good luck for the dev if it's one day necessary.) Task 2119567 -- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr closes odoo/odoo#40852 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
Diffstat (limited to 'addons')
-rw-r--r--addons/hr_timesheet/models/project.py3
-rw-r--r--addons/hr_timesheet/views/project_views.xml11
-rw-r--r--addons/project/views/project_views.xml1
-rw-r--r--addons/sale_timesheet/models/project_overview.py34
-rw-r--r--addons/sale_timesheet/report/project_profitability_report_analysis.py29
-rw-r--r--addons/sale_timesheet/report/project_profitability_report_analysis_views.xml1
-rw-r--r--addons/sale_timesheet/static/src/js/project_overview.js56
-rw-r--r--addons/sale_timesheet/tests/test_project_overview.py2
-rw-r--r--addons/sale_timesheet/views/hr_timesheet_templates.xml32
-rw-r--r--addons/web/models/models.py7
10 files changed, 168 insertions, 8 deletions
diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py
index ade6fc66b6b..d33d9c45722 100644
--- a/addons/hr_timesheet/models/project.py
+++ b/addons/hr_timesheet/models/project.py
@@ -88,6 +88,7 @@ class Task(models.Model):
effective_hours = fields.Float("Hours Spent", compute='_compute_effective_hours', compute_sudo=True, store=True, help="Computed using the sum of the task work done.")
total_hours_spent = fields.Float("Total Hours", compute='_compute_total_hours_spent', store=True, help="Computed as: Time Spent + Sub-tasks Hours.")
progress = fields.Float("Progress", compute='_compute_progress_hours', store=True, group_operator="avg", help="Display progress of current task.")
+ overtime = fields.Float(compute='_compute_progress_hours', store=True)
subtask_effective_hours = fields.Float("Sub-tasks Hours Spent", compute='_compute_subtask_effective_hours', store=True, help="Sum of actually spent hours on the subtask(s)")
timesheet_ids = fields.One2many('account.analytic.line', 'task_id', 'Timesheets')
@@ -108,12 +109,14 @@ class Task(models.Model):
for task in self:
if (task.planned_hours > 0.0):
task_total_hours = task.effective_hours + task.subtask_effective_hours
+ task.overtime = max(task_total_hours - task.planned_hours, 0)
if task_total_hours > task.planned_hours:
task.progress = 100
else:
task.progress = round(100.0 * task_total_hours / task.planned_hours, 2)
else:
task.progress = 0.0
+ task.overtime = 0
@api.depends('effective_hours', 'subtask_effective_hours', 'planned_hours')
def _compute_remaining_hours(self):
diff --git a/addons/hr_timesheet/views/project_views.xml b/addons/hr_timesheet/views/project_views.xml
index 39e156f157e..3df4ad3b4b0 100644
--- a/addons/hr_timesheet/views/project_views.xml
+++ b/addons/hr_timesheet/views/project_views.xml
@@ -281,6 +281,17 @@
</field>
</record>
+ <record id="project_task_view_search" model="ir.ui.view">
+ <field name="name">project.task.view.search.inherit.sale.timesheet.enterprise</field>
+ <field name="model">project.task</field>
+ <field name="inherit_id" ref="project.view_task_search_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//filter[@name='late']" position='after'>
+ <filter string="Tasks in Overtime" name="overtime" domain="[('overtime', '&gt;', 0)]"/>
+ </xpath>
+ </field>
+ </record>
+
<!-- Improve the task kanban in Project -->
<record id="project_task_view_kanban" model="ir.ui.view">
<field name="name">project.task.kanban.timer</field>
diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml
index 47573ae7024..eb7cd35a138 100644
--- a/addons/project/views/project_views.xml
+++ b/addons/project/views/project_views.xml
@@ -28,6 +28,7 @@
<filter string="Unassigned" name="unassigned" domain="[('user_id', '=', False)]"/>
<separator/>
<filter string="Starred" name="starred" domain="[('priority', 'in', [1, 2])]"/>
+ <filter string="Late Tasks" name="late" domain="[('date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"/>
<separator/>
<filter string="Unread Messages" name="message_needaction" domain="[('message_needaction', '=', True)]"/>
<separator/>
diff --git a/addons/sale_timesheet/models/project_overview.py b/addons/sale_timesheet/models/project_overview.py
index 946990be91e..e5772610e59 100644
--- a/addons/sale_timesheet/models/project_overview.py
+++ b/addons/sale_timesheet/models/project_overview.py
@@ -5,6 +5,7 @@ import itertools
import json
from odoo import fields, _, models
+from odoo.osv import expression
from odoo.tools import float_round
from odoo.tools.misc import get_lang
@@ -76,6 +77,11 @@ class Project(models.Model):
# rates from non-invoiced timesheets that are linked to canceled so
dashboard_values['rates']['canceled'] = float_round(100 * total_canceled_hours / (dashboard_total_hours or 1), precision_rounding=hour_rounding)
+ other_revenues = self.env['account.analytic.line'].read_group([
+ ('account_id', 'in', self.analytic_account_id.ids),
+ ('amount', '>=', 0),
+ ('project_id', '=', False)], ['amount'], [])[0].get('amount', 0)
+
# profitability, using profitability SQL report
profit = dict.fromkeys(['invoiced', 'to_invoice', 'cost', 'expense_cost', 'expense_amount_untaxed_invoiced', 'total'], 0.0)
profitability_raw_data = self.env['project.profitability.report'].read_group([('project_id', 'in', self.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_invoiced'], ['project_id'])
@@ -85,6 +91,7 @@ class Project(models.Model):
profit['cost'] += data.get('timesheet_cost', 0.0)
profit['expense_cost'] += data.get('expense_cost', 0.0)
profit['expense_amount_untaxed_invoiced'] += data.get('expense_amount_untaxed_invoiced', 0.0)
+ profit['other_revenues'] = other_revenues or 0
profit['total'] = sum([profit[item] for item in profit.keys()])
dashboard_values['profit'] = profit
@@ -103,7 +110,8 @@ class Project(models.Model):
employee_ids.extend(list(map(lambda x: x['employee_id'][0], aal_employee_ids)))
# Retrieve the employees for which the current user can see theirs timesheets
- employees = self.env['hr.employee'].sudo().browse(employee_ids).filtered_domain(self.env['account.analytic.line']._domain_employee_id())
+ employee_domain = expression.AND([[('company_id', 'in', self.env.companies.ids)], self.env['account.analytic.line']._domain_employee_id()])
+ employees = self.env['hr.employee'].sudo().browse(employee_ids).filtered_domain(employee_domain)
repartition_domain = [('project_id', 'in', self.ids), ('employee_id', '!=', False), ('timesheet_invoice_type', '!=', False)] # force billable type
# repartition data, without timesheet on cancelled so
repartition_data = self.env['account.analytic.line'].read_group(repartition_domain + ['|', ('so_line', '=', False), ('so_line.state', '!=', 'cancel')], ['employee_id', 'timesheet_invoice_type', 'unit_amount'], ['employee_id', 'timesheet_invoice_type'], lazy=False)
@@ -253,7 +261,7 @@ class Project(models.Model):
month_index = fields.Date.from_string(date).month
return babel.dates.get_month_names('abbreviated', locale=get_lang(self.env).code)[month_index]
- header_names = [_('Name'), _('Before')] + [_to_short_month_name(date) for date in ts_months] + [_('Total'), _('Sold'), _('Remaining')]
+ header_names = [_('Sales Order'), _('Before')] + [_to_short_month_name(date) for date in ts_months] + [_('Total'), _('Sold'), _('Remaining')]
result = []
for name in header_names:
@@ -411,6 +419,8 @@ class Project(models.Model):
# if only one project, add it in the context as default value
tasks_domain = [('project_id', 'in', self.ids)]
tasks_context = self.env.context
+ late_tasks_domain = [('project_id', 'in', self.ids), ('date_deadline', '<', fields.Date.to_string(fields.Date.today())), ('date_end', '=', False)]
+ overtime_tasks_domain = [('project_id', 'in', self.ids), ('overtime', '>', 0), ('planned_hours', '>', 0)]
# filter out all the projects that have no tasks
task_projects_ids = self.env['project.task'].read_group([('project_id', 'in', self.ids)], ['project_id'], ['project_id'])
@@ -428,6 +438,26 @@ class Project(models.Model):
context=tasks_context
)
})
+ stat_buttons.append({
+ 'name': _("Late Tasks"),
+ 'count': self.env['project.task'].search_count(late_tasks_domain),
+ 'icon': 'fa fa-tasks',
+ 'action': _to_action_data(
+ action=self.env.ref('project.action_view_task'),
+ domain=late_tasks_domain,
+ context=tasks_context,
+ ),
+ })
+ stat_buttons.append({
+ 'name': _("Tasks in Overtime"),
+ 'count': self.env['project.task'].search_count(overtime_tasks_domain),
+ 'icon': 'fa fa-tasks',
+ 'action': _to_action_data(
+ action=self.env.ref('project.action_view_task'),
+ domain=overtime_tasks_domain,
+ context=tasks_context,
+ ),
+ })
if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'):
# read all the sale orders linked to the projects' tasks
diff --git a/addons/sale_timesheet/report/project_profitability_report_analysis.py b/addons/sale_timesheet/report/project_profitability_report_analysis.py
index a8c8fa510d7..ef884e55421 100644
--- a/addons/sale_timesheet/report/project_profitability_report_analysis.py
+++ b/addons/sale_timesheet/report/project_profitability_report_analysis.py
@@ -31,6 +31,8 @@ class ProfitabilityAnalysis(models.Model):
amount_untaxed_invoiced = fields.Float("Untaxed Amount Invoiced", digits=(16, 2), readonly=True, group_operator="sum")
expense_amount_untaxed_to_invoice = fields.Float("Untaxed Amount to Re-invoice", digits=(16, 2), readonly=True, group_operator="sum")
expense_amount_untaxed_invoiced = fields.Float("Untaxed Re-invoiced Amount", digits=(16, 2), readonly=True, group_operator="sum")
+ other_revenues = fields.Float("Other Revenues", digits=(16, 2), readonly=True, group_operator="sum",
+ help="All revenues that are not from timesheets and that are linked to the analytic account of the project.")
def init(self):
tools.drop_view_if_exists(self._cr, self._table)
@@ -73,7 +75,8 @@ class ProfitabilityAnalysis(models.Model):
END AS amount_untaxed_invoiced,
COST_SUMMARY.timesheet_unit_amount AS timesheet_unit_amount,
COST_SUMMARY.timesheet_cost AS timesheet_cost,
- COST_SUMMARY.expense_cost AS expense_cost
+ COST_SUMMARY.expense_cost AS expense_cost,
+ COST_SUMMARY.other_revenues AS other_revenues
FROM project_project P
JOIN res_company C ON C.id = P.company_id
LEFT JOIN (
@@ -84,6 +87,7 @@ class ProfitabilityAnalysis(models.Model):
SUM(timesheet_unit_amount) AS timesheet_unit_amount,
SUM(timesheet_cost) AS timesheet_cost,
SUM(expense_cost) AS expense_cost,
+ SUM(other_revenues) AS other_revenues,
SUM(downpayment_invoiced) AS downpayment_invoiced
FROM (
SELECT
@@ -92,6 +96,7 @@ class ProfitabilityAnalysis(models.Model):
TS.so_line AS sale_line_id,
SUM(TS.unit_amount) AS timesheet_unit_amount,
SUM(TS.amount) AS timesheet_cost,
+ 0.0 AS other_revenues,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced
FROM account_analytic_line TS, project_project P
@@ -106,6 +111,24 @@ class ProfitabilityAnalysis(models.Model):
AAL.so_line AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
+ SUM(AAL.amount) AS other_revenues,
+ 0.0 AS expense_cost,
+ 0.0 AS downpayment_invoiced
+ FROM project_project P
+ LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
+ LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id
+ WHERE AAL.amount > 0.0 AND AAL.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
+ GROUP BY P.id, AA.id, AAL.so_line
+
+ UNION
+
+ SELECT
+ P.id AS project_id,
+ P.analytic_account_id AS analytic_account_id,
+ AAL.so_line AS sale_line_id,
+ 0.0 AS timesheet_unit_amount,
+ 0.0 AS timesheet_cost,
+ 0.0 AS other_revenues,
SUM(AAL.amount) AS expense_cost,
0.0 AS downpayment_invoiced
FROM project_project P
@@ -122,6 +145,7 @@ class ProfitabilityAnalysis(models.Model):
MY_SOLS.id AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
+ 0.0 AS other_revenues,
0.0 AS expense_cost,
CASE WHEN MY_SOLS.invoice_status = 'invoiced' THEN MY_SOLS.price_reduce ELSE 0.0 END AS downpayment_invoiced
FROM project_project P
@@ -139,6 +163,7 @@ class ProfitabilityAnalysis(models.Model):
OLIS.id AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
+ 0.0 AS other_revenues,
OLIS.price_reduce AS expense_cost,
0.0 AS downpayment_invoiced
FROM project_project P
@@ -158,6 +183,7 @@ class ProfitabilityAnalysis(models.Model):
SOL.id AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
+ 0.0 AS other_revenues,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced
FROM sale_order_line SOL
@@ -172,6 +198,7 @@ class ProfitabilityAnalysis(models.Model):
SOL.id AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
+ 0.0 AS other_revenues,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced
FROM sale_order_line SOL
diff --git a/addons/sale_timesheet/report/project_profitability_report_analysis_views.xml b/addons/sale_timesheet/report/project_profitability_report_analysis_views.xml
index d0110fa9608..abf410810c5 100644
--- a/addons/sale_timesheet/report/project_profitability_report_analysis_views.xml
+++ b/addons/sale_timesheet/report/project_profitability_report_analysis_views.xml
@@ -24,6 +24,7 @@
<field name="amount_untaxed_to_invoice" type="measure"/>
<field name="amount_untaxed_invoiced" type="measure"/>
<field name="timesheet_cost" type="measure"/>
+ <field name="other_revenues" type="measure"/>
</graph>
</field>
</record>
diff --git a/addons/sale_timesheet/static/src/js/project_overview.js b/addons/sale_timesheet/static/src/js/project_overview.js
new file mode 100644
index 00000000000..876a79633ca
--- /dev/null
+++ b/addons/sale_timesheet/static/src/js/project_overview.js
@@ -0,0 +1,56 @@
+odoo.define('sale_timesheet.project_overview', function (require) {
+ "use strict";
+
+ var qweb = require('web.qweb');
+ var viewRegistry = require('web.view_registry');
+
+ const Controller = qweb.Controller.extend({
+ events: _.extend({}, qweb.Controller.prototype.events, {
+ 'click .project_overview_foldable': '_onFoldToggle',
+ }),
+
+ _onFoldToggle(ev) {
+ const {model, resId} = ev.target.dataset;
+ const shouldOpen = ev.target.classList.contains('fa-caret-right');
+ if (model === 'sale.order' && shouldOpen) {
+ this._openSaleOrder(resId);
+ }
+ else if (model === 'sale.order.line' && shouldOpen) {
+ this._openSaleOrderLine(resId);
+ }
+ else {
+ const targetClass = `.${model.replace(/\./g, '_')}_${resId || 'None'}`;
+ this.$(targetClass).hide();
+ }
+ $(ev.target).toggleClass('fa-caret-right');
+ $(ev.target).toggleClass('fa-caret-down');
+ },
+
+ _openSaleOrder(id) {
+ id = id || 'None';
+ const targetClass = `.sale_order_${id}`;
+ this.$(`.o_timesheet_forecast_sale_order_line${targetClass}`).show();
+ this.$(`.o_timesheet_forecast_hr_employee${targetClass}`).hide();
+ this.$(`.o_timesheet_forecast_sale_order_line${targetClass} .fa`).removeClass('fa-caret-down');
+ this.$(`.o_timesheet_forecast_sale_order_line${targetClass} .fa`).addClass('fa-caret-right');
+
+ },
+
+ _openSaleOrderLine(id) {
+ id = id || 'None';
+ const targetClass = `.sale_order_line_${id}`;
+ this.$(`.o_timesheet_forecast_hr_employee${targetClass}`).show();
+ },
+ });
+
+ var ProjectOverview = qweb.View.extend({
+ withSearchBar: true,
+ searchMenuTypes: [],
+
+ config: _.extend({}, qweb.View.prototype.config, {
+ Controller: Controller,
+ }),
+ });
+
+ viewRegistry.add('project_overview', ProjectOverview);
+});
diff --git a/addons/sale_timesheet/tests/test_project_overview.py b/addons/sale_timesheet/tests/test_project_overview.py
index 845c928a5f8..df4325d29d7 100644
--- a/addons/sale_timesheet/tests/test_project_overview.py
+++ b/addons/sale_timesheet/tests/test_project_overview.py
@@ -102,5 +102,5 @@ class TestSaleProject(TestReporting):
self.assertEqual(float_compare(vals['dashboard']['profit']['invoiced'], project_invoiced, precision_rounding=rounding), 0, "The amount invoiced should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['profit']['cost'], project_timesheet_cost, precision_rounding=rounding), 0, "The amount cost should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['profit']['expense_cost'], expense.amount, precision_rounding=rounding), 0, "The amount expense-cost should be the one from the SO2 line, as we are in ordered quantity")
- self.assertEqual(float_compare(vals['dashboard']['profit']['total'], project_invoiced + project_timesheet_cost + expense.amount, precision_rounding=rounding), 0, "The total amount should be the one from the SO2 line, as we are in ordered quantity")
+ self.assertEqual(float_compare(vals['dashboard']['profit']['total'], project_invoiced + project_timesheet_cost + expense.amount, precision_rounding=rounding), 1, "The total amount should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['repartition_employee_max'], 11.0, precision_rounding=rounding), 0, "The amount of repartition-employee-max should be the one from SO2 line")
diff --git a/addons/sale_timesheet/views/hr_timesheet_templates.xml b/addons/sale_timesheet/views/hr_timesheet_templates.xml
index 1c65ecfbc07..d37043d670f 100644
--- a/addons/sale_timesheet/views/hr_timesheet_templates.xml
+++ b/addons/sale_timesheet/views/hr_timesheet_templates.xml
@@ -4,6 +4,7 @@
<template id="assets_backend" name="sale timesheet assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss" href="/sale_timesheet/static/src/scss/sale_timesheet.scss"/>
+ <script type="text/javascript" src="/sale_timesheet/static/src/js/project_overview.js"></script>
</xpath>
</template>
@@ -12,6 +13,7 @@
<field name="type">qweb</field>
<field name="model">project.project</field>
<field name="arch" type="xml">
+ <qweb js_class="project_overview">
<nav class="o_qweb_cp_buttons" t-if="actions">
<button t-foreach="actions" t-as="action"
type="action" class="btn btn-primary"
@@ -27,7 +29,7 @@
<div class="o_timesheet_plan_sale_timesheet">
<div class="o_timesheet_plan_sale_timesheet_dashboard">
- <div class="o_timesheet_plan_stat_buttons oe_button_box">
+ <div class="o_timesheet_plan_stat_buttons oe_button_box o_not_full">
<t t-foreach="stat_buttons" t-as="stat_button">
<a class="btn oe_stat_button"
type="action"
@@ -152,6 +154,12 @@
</td>
<td>To invoice</td>
</tr>
+ <tr t-if="dashboard['profit']['other_revenues'] > 0">
+ <td class="o_timesheet_plan_dashboard_cell">
+ <t t-esc="dashboard['profit']['other_revenues']" t-options='{"widget": "monetary", "display_currency": currency}'/>
+ </td>
+ <td title="All revenues that are not from timesheets and that are linked to the analytic account of the project.">Other Revenues</td>
+ </tr>
<tr>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['profit']['cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
@@ -289,15 +297,28 @@
</thead>
<tbody>
<t t-set="row_is_milestone" t-value="False"/>
+ <t t-set="current_order" t-value="False"/>
+ <t t-set="current_order_line" t-value="False"/>
<t t-foreach="timesheet_forecast_table['rows']" t-as="row">
<t t-set="row_type" t-value="row[0].get('type')"/>
<t t-if="row_type == 'sale_order_line'">
<t t-set="row_is_milestone" t-value="row[0].get('is_milestone')"/>
</t>
- <tr t-att-class="'o_timesheet_forecast_' + row_type">
+ <t t-if="row_type == 'sale_order'">
+ <t t-set="current_order" t-value="False"/>
+ <t t-set="current_order_line" t-value="False"/>
+ </t>
+ <t t-if="row_type == 'sale_order_line'">
+ <t t-set="current_order_line" t-value="False"/>
+ </t>
+ <t t-set="foldable" t-value="row_type in ('sale_order', 'sale_order_line')"/>
+ <tr t-att-class="'o_timesheet_forecast_' + row_type + ' sale_order_' + str(current_order) + ' sale_order_line_' + str(current_order_line)"
+ t-att-style="'display: none;' if row_type != 'sale_order' else ''">
<t t-foreach="row" t-as="row_value">
<td t-att-class="'o_right_bordered' if row_value_index in [5,10] else '' + ' text-center' if row_value_index != 0 else ''">
<t t-if="row_value_index == 0">
+ <span t-if="foldable" t-att-class="'fa fa-caret-right' + (' project_overview_foldable' if foldable else '')" style="cursor: pointer;"
+ t-att-data-model="row[0].get('res_model')" t-att-data-res-id="row[0].get('res_id')"/>
<t t-if="row_type == 'sale_order'">
<a type="action" t-att-data-model="row_value['res_model']" t-att-data-res-id="row_value['res_id']" t-att-class="'o_timesheet_plan_redirect' if row_value['res_id'] else ''">
<t t-esc="row_value.get('label')"/>
@@ -333,6 +354,12 @@
</td>
</t>
</tr>
+ <t t-if="row_type == 'sale_order_line'">
+ <t t-set="current_order_line" t-value="row[0].get('res_id')"/>
+ </t>
+ <t t-if="row_type == 'sale_order'">
+ <t t-set="current_order" t-value="row[0].get('res_id')"/>
+ </t>
</t>
</tbody>
</table>
@@ -344,6 +371,7 @@
</div>
</div>
</div>
+ </qweb>
</field>
</record>
diff --git a/addons/web/models/models.py b/addons/web/models/models.py
index 0d03d950b9f..3a70ab4f0c8 100644
--- a/addons/web/models/models.py
+++ b/addons/web/models/models.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import babel.dates
import pytz
+from lxml import etree
from odoo import _, api, fields, models
from odoo.osv.expression import AND
@@ -202,9 +203,11 @@ class Base(models.AbstractModel):
def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
r = super().fields_view_get(view_id, view_type, toolbar, submenu)
# avoid leaking the raw (un-rendered) template, also avoids bloating
- # the response payload for no reason
+ # the response payload for no reason. Only send the root node,
+ # to send attributes such as `js_class`.
if r['type'] == 'qweb':
- r['arch'] = '<qweb/>'
+ root = etree.fromstring(r['arch'])
+ r['arch'] = etree.tostring(etree.Element('qweb', root.attrib))
return r
@api.model