summaryrefslogtreecommitdiff
path: root/addons/sale_timesheet/views
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/sale_timesheet/views
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/sale_timesheet/views')
-rw-r--r--addons/sale_timesheet/views/account_invoice_views.xml26
-rw-r--r--addons/sale_timesheet/views/hr_timesheet_templates.xml494
-rw-r--r--addons/sale_timesheet/views/hr_timesheet_views.xml139
-rw-r--r--addons/sale_timesheet/views/product_views.xml41
-rw-r--r--addons/sale_timesheet/views/project_task_views.xml209
-rw-r--r--addons/sale_timesheet/views/report_invoice.xml10
-rw-r--r--addons/sale_timesheet/views/res_config_settings_views.xml33
-rw-r--r--addons/sale_timesheet/views/sale_order_views.xml32
-rw-r--r--addons/sale_timesheet/views/sale_timesheet_portal_templates.xml161
9 files changed, 1145 insertions, 0 deletions
diff --git a/addons/sale_timesheet/views/account_invoice_views.xml b/addons/sale_timesheet/views/account_invoice_views.xml
new file mode 100644
index 00000000..9bd3969e
--- /dev/null
+++ b/addons/sale_timesheet/views/account_invoice_views.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+ <record id="action_timesheet_from_invoice" model="ir.actions.act_window">
+ <field name="name">Timesheet</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">account.analytic.line</field>
+ <field name="view_mode">tree,form,graph</field>
+ <field name="context">{}</field>
+ <field name="domain">[('timesheet_invoice_id', '=', active_id)]</field>
+ </record>
+
+ <record id="account_invoice_view_form_inherit_sale_timesheet" model="ir.ui.view">
+ <field name="name">account.invoice.form.inherit.timesheet</field>
+ <field name="model">account.move</field>
+ <field name="inherit_id" ref="account.view_move_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//div[@name='button_box']" position="inside">
+ <button name="%(sale_timesheet.action_timesheet_from_invoice)d" type="action" class="oe_stat_button" icon="fa-calendar" attrs="{'invisible':[('timesheet_count','=', 0)]}">
+ <field name="timesheet_count" widget="statinfo" string="Timesheets"/>
+ </button>
+ </xpath>
+ </field>
+ </record>
+
+</odoo>
diff --git a/addons/sale_timesheet/views/hr_timesheet_templates.xml b/addons/sale_timesheet/views/hr_timesheet_templates.xml
new file mode 100644
index 00000000..1715f3c2
--- /dev/null
+++ b/addons/sale_timesheet/views/hr_timesheet_templates.xml
@@ -0,0 +1,494 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+ <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>
+ <script type="text/javascript" src="/sale_timesheet/static/src/js/sale_project_kanban_controller.js"/>
+ </xpath>
+ </template>
+
+ <record id="timesheet_plan" model="ir.ui.view">
+ <field name="name">Timesheet Plan</field>
+ <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"
+ t-att-name="action['action_id']"
+ t-att-data-context="action.get('context')"
+ >
+ <t t-esc="action['label']"/>
+ </button>
+ </nav>
+ <div class="o_form_view o_form_readonly o_project_plan">
+ <div class="o_form_sheet_bg">
+ <div class="o_form_sheet o_timesheet_plan_content">
+ <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 o_not_full">
+ <t t-foreach="stat_buttons" t-as="stat_button">
+ <a class="btn oe_stat_button"
+ type="action"
+ t-att="stat_button['action']"
+ >
+ <div t-attf-class="fa fa-fw o_button_icon #{stat_button['icon']}" role="img" aria-label="Statistics" title="Statistics"></div>
+ <div class="o_field_widget o_stat_info o_readonly_modifier" t-att-title="stat_button['name']">
+ <t t-if="not isinstance(stat_button['name'], list)">
+ <span class="o_stat_value" t-if="'count' in stat_button">
+ <t t-esc="stat_button['count']"/>
+ </span>
+ <span class="o_stat_text">
+ <t t-esc="stat_button['name']"/>
+ </span>
+ </t>
+ <t t-if="isinstance(stat_button['name'], list)">
+ <div class="oe_inline">
+ <span class="o_stat_value mr-1">
+ <t t-esc="stat_button.get('count')"/>
+ </span>
+ <span class="o_stat_value">
+ <t t-esc="stat_button['name'][0]"/>
+ </span>
+ </div>
+ <span class="o_stat_text">
+ <t t-esc="stat_button['name'][1]"/>
+ </span>
+ </t>
+ </div>
+ </a>
+ </t>
+ </div>
+
+ <div class="o_title">
+ <h2 t-if="is_uom_day">Recorded Days and Profitability</h2>
+ <h2 t-else="">Recorded Hours and Profitability</h2>
+ </div>
+
+ <t t-set="display_cost" t-value="dashboard['profit']['expense_cost'] != 0.0"/>
+ <div class="o_profitability_wrapper">
+ <div class="o_profitability_section">
+ <div>
+ <table class="table">
+ <tbody>
+ <th>
+ <a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain)"
+ data-context='{"pivot_row_groupby": ["date:month"],"pivot_column_groupby": ["timesheet_invoice_type"], "pivot_measures": ["unit_amount"]}'
+ data-views='[[0, "pivot"], [0, "list"]]' tabindex="-1">
+ <span t-if="is_uom_day">Days recorded</span>
+ <span t-else="">Hours recorded</span>
+ </a>
+ </th>
+ <tr>
+ <td class="o_timesheet_plan_dashboard_cell">
+ <t t-if="is_uom_day" t-esc="dashboard['time']['billable_time']" t-options="{'widget': 'timesheet_uom'}"/>
+ <t t-else="" t-esc="dashboard['time']['billable_time']" t-options="{'widget': 'float_time'}"/>
+ </td>
+ <td class="o_timesheet_plan_dashboard_cell">
+ (<t t-esc="dashboard['rates']['billable_time']"/> %)
+ </td>
+ <td title="Includes the time logged into tasks for which you invoice based on timesheets on tasks.">
+ Billed on Timesheets
+ </td>
+ </tr>
+ <tr>
+ <td class="o_timesheet_plan_dashboard_cell">
+ <t t-if="is_uom_day" t-esc="dashboard['time']['billable_fixed']" t-options="{'widget': 'timesheet_uom'}"/>
+ <t t-else="" t-esc="dashboard['time']['billable_fixed']" t-options="{'widget': 'float_time'}"/>
+ </td>
+ <td class="o_timesheet_plan_dashboard_cell">
+ (<t t-esc="dashboard['rates']['billable_fixed']"/> %)
+ </td>
+ <td title="Includes the time logged into tasks for which you invoice based on ordered quantities or on milestones.">
+ Billed at a Fixed price
+ </td>
+ </tr>
+ <tr t-if="dashboard['time']['non_billable_project'] != 0">
+ <td class="o_timesheet_plan_dashboard_cell">
+ <t t-if="is_uom_day" t-esc="dashboard['time']['non_billable_project']" t-options="{'widget': 'timesheet_uom'}"/>
+ <t t-else="" t-esc="dashboard['time']['non_billable_project']" t-options="{'widget': 'float_time'}"/>
+ </td>
+ <td class="o_timesheet_plan_dashboard_cell">
+ (<t t-esc="dashboard['rates']['non_billable_project']"/> %)
+ </td>
+ <td title="Includes the time logged from the Timesheet module that is linked to a project, but not to a task.">
+ No task found
+ </td>
+ </tr>
+ <tr>
+ <td class="o_timesheet_plan_dashboard_cell">
+ <t t-if="is_uom_day" t-esc="dashboard['time']['non_billable']" t-options="{'widget': 'timesheet_uom'}"/>
+ <t t-else="" t-esc="dashboard['time']['non_billable']" t-options="{'widget': 'float_time'}"/>
+ </td>
+ <td class="o_timesheet_plan_dashboard_cell">
+ (<t t-esc="dashboard['rates']['non_billable']"/> %)
+ </td>
+ <td>
+ <a type="action"
+ data-model="project.task"
+ data-views='[[false, "list"], [false, "form"]]'
+ t-att-data-domain="json.dumps([['project_id', 'in', projects.ids], ['sale_line_id', '=', False]])"
+ >
+ <span class="btn-link"
+ style="font-weight:normal;"
+ title="Includes the time logged into a task which is not linked to any Sales Order.">
+ Non Billable Tasks
+ </span>
+ </a>
+ </td>
+ </tr>
+ <tr t-if="dashboard['time']['non_billable_timesheet'] > 0">
+ <td class="o_timesheet_plan_dashboard_cell">
+ <t t-if="is_uom_day" t-esc="dashboard['time']['non_billable_timesheet']" t-options="{'widget': 'timesheet_uom'}"/>
+ <t t-else="" t-esc="dashboard['time']['non_billable_timesheet']" t-options="{'widget': 'float_time'}"/>
+ </td>
+ <td class="o_timesheet_plan_dashboard_cell">
+ (<t t-esc="dashboard['rates']['non_billable_timesheet']"/> %)
+ </td>
+ <td>
+ <a type="action"
+ data-model="account.analytic.line"
+ data-views='[[false, "list"], [false, "form"]]'
+ t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','non_billable_timesheet')])"
+ >
+ <span class="btn-link"
+ style="font-weight:normal;">
+ Non Billable Timesheets
+ </span>
+ </a>
+ </td>
+ </tr>
+ <tr t-if="dashboard['time']['canceled'] > 0">
+ <td class="o_timesheet_plan_dashboard_cell">
+ <t t-if="is_uom_day" t-esc="dashboard['time']['canceled']" t-options="{'widget': 'timesheet_uom'}"/>
+ <t t-else="" t-esc="dashboard['time']['canceled']" t-options="{'widget': 'float_time'}"/>
+ </td>
+ <td class="o_timesheet_plan_dashboard_cell">
+ (<t t-esc="dashboard['rates']['canceled']"/> %)
+ </td>
+ <td title="Includes the time logged into a task which is linked to a cancelled Sales Order.">
+ Cancelled
+ </td>
+ </tr>
+ <tr>
+ <td class="o_timesheet_plan_dashboard_total">
+ <b>
+ <t t-if="is_uom_day" t-esc="dashboard['time']['total']" t-options="{'widget': 'timesheet_uom'}"/>
+ <t t-else="" t-esc="dashboard['time']['total']" t-options="{'widget': 'float_time'}"/>
+ </b>
+ </td>
+ <td><b>Total</b></td>
+ <td></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <div class="o_profitability_section">
+ <div>
+ <table class="table">
+ <tbody>
+ <th>
+ <a type="action" data-model="project.profitability.report" t-att-data-domain="json.dumps(profitability_domain)" data-context="{'group_by_no_leaf':1, 'group_by':[], 'sale_show_order_product_name': 1}" data-views='[[0, "pivot"], [0, "graph"]]' tabindex="-1">Profitability</a>
+ </th>
+ <tr>
+ <td class="o_timesheet_plan_dashboard_cell">
+ <t t-esc="dashboard['profit']['invoiced']" t-options='{"widget": "monetary", "display_currency": currency}'/>
+ </td>
+ <td title="Revenues linked to Timesheets already invoiced.">
+ Invoiced
+ </td>
+ </tr>
+ <tr>
+ <td class="o_timesheet_plan_dashboard_cell">
+ <t t-esc="dashboard['profit']['to_invoice']" t-options='{"widget": "monetary", "display_currency": currency}'/>
+ </td>
+ <td title="Revenues linked to Timesheets not yet invoiced.">
+ 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}'/>
+ </td>
+ <td title="This cost is based on the &quot;Timesheet cost&quot; set in the HR Settings of your employees.">
+ Timesheet costs
+ </td>
+ </tr>
+ <tr t-if="display_cost">
+ <td class="o_timesheet_plan_dashboard_cell">
+ <t t-esc="dashboard['profit']['expense_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
+ </td>
+ <td title="Any cost linked to the Analytic Account of the Project.">
+ Other costs
+ </td>
+ </tr>
+ <tr t-if="display_cost &amp; (dashboard['profit']['expense_amount_untaxed_invoiced'] != 0)">
+ <td class="o_timesheet_plan_dashboard_cell">
+ <t t-esc="dashboard['profit']['expense_amount_untaxed_invoiced']" t-options='{"widget": "monetary", "display_currency": currency}'/>
+ </td>
+ <td title="Costs from expenses that were reinvoiced to your customer (provided that the Analytic Account of the Project was set on the Expense).">
+ Re-invoiced costs
+ </td>
+ </tr>
+ <tr>
+ <td class="o_timesheet_plan_dashboard_total">
+ <b>
+ <t t-esc="dashboard['profit']['total']" t-options='{"widget": "monetary", "display_currency": currency}'/>
+ </b>
+ </td>
+ <td><b>Total</b></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="o_title">
+ <h2>Time by people</h2>
+ </div>
+
+ <div class="o_timesheet_plan_sale_timesheet_people_time">
+ <t t-if="not repartition_employee">
+ <p>There are no timesheets for now.</p>
+ </t>
+ <t t-if="repartition_employee">
+ <div class="table-responsive">
+ <table class="table">
+ <thead>
+ <tr>
+ <th></th>
+ <th>Employee</th>
+ <th t-if="is_uom_day" class="text-nowrap text-right pr-5">Days Spent</th>
+ <th t-else="" class="text-nowrap text-right pr-5">Hours Spent</th>
+ <td>
+ <div class="float-right o_timesheet_plan_badge">
+ <span class="badge badge-pill o_progress_billable_time">
+ <a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','billable_time')])" tabindex="-1">Billed on Timesheets</a>
+ </span>
+ <span class="badge badge-pill o_progress_billable_fixed">
+ <a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','billable_fixed')])" tabindex="-1">Billed at a Fixed price</a>
+ </span>
+ <span t-if="dashboard['time']['non_billable_project'] != 0" class="badge badge-pill o_progress_non_billable_project">
+ <a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','non_billable_project')])" tabindex="-1">No task found</a>
+ </span>
+ <span t-if="dashboard['time']['non_billable_timesheet'] != 0" class="badge badge-pill o_progress_non_billable_timesheet">
+ <a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','non_billable_timesheet')])" tabindex="-1">Non billable timesheets</a>
+ </span>
+ <span class="badge badge-pill o_progress_non_billable">
+ <a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','non_billable')])" tabindex="-1">Non billable tasks</a>
+ </span>
+ <!-- only show the canceled pill if there were timesheets on canceled so -->
+ <t t-if="sum([employee.get('canceled', 0.0) for employee in repartition_employee.values()]) > 0">
+ <span class="badge badge-pill o_progress_canceled">
+ <a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('so_line.state', '=', 'cancel')])" tabindex="-1">Cancelled</a>
+ </span>
+ </t>
+ </div>
+ </td>
+ </tr>
+ </thead>
+ <tbody>
+ <t t-foreach="repartition_employee" t-as="employee_id">
+ <t t-set="employee" t-value="repartition_employee[employee_id]"/>
+ <tr>
+ <td style="width: 3%; vertical-align: middle;">
+ <img class="img rounded-circle mr-2 mb-2" t-attf-src="/web/image?model=hr.employee&amp;field=image_128&amp;id=#{employee['employee_id']}" t-att-title="employee['employee_name']" t-att-alt="employee['employee_name']" width="25" height="25"/>
+ </td>
+ <td style="width: 15%; vertical-align: middle;" >
+ <a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain)" t-att-data-context="json.dumps({'search_default_employee_id': employee_id})" data-views="[[0, &quot;list&quot;]]" tabindex="-1">
+ <t t-esc="employee['employee_name']"/>
+ </a>
+ </td>
+ <td class="text-right pr-5" style="width: 10%; vertical-align: middle;">
+ <t t-if="is_uom_day" t-esc="employee['total']" t-options="{'widget': 'timesheet_uom'}"/>
+ <t t-else="" t-esc="employee['total']" t-options="{'widget': 'float_time'}"/>
+ </td>
+ <td style="vertical-align:middle">
+ <div class="border rounded">
+ <div t-if="repartition_employee_max" class="progress" t-attf-style="width: {{max(0, employee['total'] / repartition_employee_max * 100)}}%; margin-bottom: 0em;">
+
+ <t t-set="total" t-value="employee['total'] or 1.0" />
+ <t t-call="sale_timesheet.progressbar">
+ <t t-set="label">Billed on Timesheets</t>
+ <t t-set="key" t-translation="off">billable_time</t>
+ </t>
+ <t t-call="sale_timesheet.progressbar">
+ <t t-set="label">Billed at a Fixed price</t>
+ <t t-set="key" t-translation="off">billable_fixed</t>
+ </t>
+ <t t-call="sale_timesheet.progressbar">
+ <t t-set="label">No task found</t>
+ <t t-set="key" t-translation="off">non_billable_project</t>
+ </t>
+ <t t-call="sale_timesheet.progressbar">
+ <t t-set="label">Non billable timesheets</t>
+ <t t-set="key" t-translation="off">non_billable_timesheet</t>
+ </t>
+ <t t-call="sale_timesheet.progressbar">
+ <t t-set="label">Non billable tasks</t>
+ <t t-set="key" t-translation="off">non_billable</t>
+ </t>
+ <t t-call="sale_timesheet.progressbar">
+ <t t-set="label">Cancelled</t>
+ <t t-set="key" t-translation="off">canceled</t>
+ </t>
+ </div>
+ </div>
+ </td>
+ </tr>
+ </t>
+ </tbody>
+ </table>
+ </div>
+ </t>
+ </div>
+
+ <div class="o_title">
+ <h2>Timesheets</h2>
+ </div>
+
+ <!-- NOTE: this template to display a table works whatever the length of the rows, as project_timesheet_forecast_sale extends the table to add forecasts -->
+ <div class="o_project_plan_project_timesheet_forecast">
+ <t t-if="timesheet_forecast_table and timesheet_forecast_table['rows']">
+ <div class="table-responsive">
+ <table class="table">
+ <thead>
+ <tr>
+ <th></th>
+ <th colspan="5" id="table_plan_title" class="o_right_bordered"><h3>Timesheets</h3></th>
+ <th colspan="2" id="table_plan_total"></th>
+ </tr>
+ <tr>
+ <t t-foreach="timesheet_forecast_table['header']" t-as="header_val">
+ <th t-att-class="'o_right_bordered' if header_val_index in [5,10] else ''">
+ <span t-att-title="header_val['tooltip']"><t t-esc="header_val['label']"/></span>
+ </th>
+ </t>
+ </tr>
+ </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>
+ <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[0].get('has_children')"/>
+ <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 not in ('sale_order', 'sale_order_line') 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-down' if row_type == 'sale_order' else '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'">
+ <t t-if="env.user.has_group('sales_team.group_sale_salesman')">
+ <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')"/>
+ </a>
+ </t>
+ <t t-else="">
+ <t t-esc="row_value.get('label')"/>
+ </t>
+ <span t-if="row_value.get('canceled')" class="badge badge-pill o_canceled_tag">
+ Cancelled
+ </span>
+ </t>
+ <t t-if="row_type != 'sale_order'">
+ <t t-if="not row_is_milestone">
+ <span><t t-esc="row_value.get('label')"/></span>
+ </t>
+ <t t-if="row_is_milestone">
+ <span><i><t t-esc="row_value.get('label')"/></i></span>
+ </t>
+ </t>
+ </t>
+ <t t-if="row_value_index != 0">
+ <t t-if="row_value_index &lt; len(row)-2">
+ <t t-if="row_is_milestone">
+ <i t-att-class="'text-muted' if not row_value else ''">
+ <t t-if="is_uom_day" t-esc="row_value" t-options="{'widget': 'timesheet_uom'}"/>
+ <t t-else="" t-esc="row_value" t-options="{'widget': 'float_time'}"/>
+ </i>
+ </t>
+ <t t-else="">
+ <span t-att-class="'text-muted' if not row_value else ''">
+ <t t-if="is_uom_day" t-esc="row_value" t-options="{'widget': 'timesheet_uom'}"/>
+ <t t-else="" t-esc="row_value" t-options="{'widget': 'float_time'}"/>
+ </span>
+ </t>
+ </t>
+ <t t-else="">
+ <t t-if="not row_is_milestone and not row[0].get('type') == 'hr_employee'">
+ <span t-att-class="'text-muted' if not row_value else ''">
+ <t t-if="is_uom_day" t-esc="row_value" t-options="{'widget': 'timesheet_uom'}"/>
+ <t t-else="" t-esc="row_value" t-options="{'widget': 'float_time'}"/>
+ </span>
+ </t>
+ </t>
+ </t>
+ </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>
+ </div>
+ </t>
+ </div>
+
+ </div>
+ </div>
+ </div>
+ </div>
+ </qweb>
+ </field>
+ </record>
+
+ <template id="progressbar" name="project overview progressbar segments">
+ <t t-set="amount" t-value="employee[key]"/>
+ <t t-if="amount &gt; 0">
+ <t t-set="title">
+ <t t-esc="label"/>: <t t-if="is_uom_day" t-esc="amount" t-options="{'widget': 'timesheet_uom'}"/><t t-else="" t-esc="amount" t-options="{'widget': 'float_time'}"/>
+ </t>
+ <a t-attf-class="progress-bar o_progress_{{key}}"
+ t-attf-style="width: {{amount / total * 100}}%"
+ type="action" data-model="account.analytic.line"
+ t-att-data-domain="employee['__domain_' + key]"
+ >
+ <span t-att-title="title" style="font-size: 0px; width: 100%; height: 100%;">
+ <t t-esc="label" />
+ </span>
+ </a>
+ </t>
+ </template>
+
+</odoo>
diff --git a/addons/sale_timesheet/views/hr_timesheet_views.xml b/addons/sale_timesheet/views/hr_timesheet_views.xml
new file mode 100644
index 00000000..782d2c07
--- /dev/null
+++ b/addons/sale_timesheet/views/hr_timesheet_views.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+ <record id="timesheet_view_search" model="ir.ui.view">
+ <field name="name">account.analytic.line.search</field>
+ <field name="model">account.analytic.line</field>
+ <field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_search"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='department_id']" position="after">
+ <field name="timesheet_invoice_type"/>
+ </xpath>
+ <xpath expr="//filter[@name='month']" position="before">
+ <filter name="billable_timesheet" string="Billable" domain="[('so_line', '!=', False), ('non_allow_billable', '=', False)]"/>
+ <filter name="non_billable_timesheet" string="Non Billable" domain="['|', ('so_line', '=', False), ('non_allow_billable', '=', True)]"/>
+ <separator/>
+ <filter name="billable_time" string="Billed on Timesheets" domain="[('timesheet_invoice_type', '=', 'billable_time')]"/>
+ <filter name="billable_fixed" string="Billed at a Fixed Price" domain="[('timesheet_invoice_type', '=', 'billable_fixed')]"/>
+ <filter name="non_billable" string="Non Billable Tasks" domain="[('timesheet_invoice_type', '=', 'non_billable')]"/>
+ <separator/>
+ </xpath>
+ <xpath expr="//filter[@name='groupby_department']" position="before">
+ <filter string="Billable Type" name="groupby_timesheet_invoice_type" domain="[]" context="{'group_by': 'timesheet_invoice_type'}"/>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="timesheet_view_pivot_revenue" model="ir.ui.view">
+ <field name="name">account.analytic.line.pivot.revenue</field>
+ <field name="model">account.analytic.line</field>
+ <field name="arch" type="xml">
+ <pivot string="Timesheet" sample="1">
+ <field name="employee_id" type="row"/>
+ <field name="date" interval="month" type="col"/>
+ <field name="unit_amount" type="measure"/>
+ </pivot>
+ </field>
+ </record>
+
+ <record id="timesheet_view_tree_sale" model="ir.ui.view">
+ <field name="name">account.analytic.line.view.tree.with.allow.billable</field>
+ <field name="model">account.analytic.line</field>
+ <field name="inherit_id" ref="hr_timesheet.timesheet_view_tree_user"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='user_id']" position="after">
+ <field name="non_allow_billable" attrs="{'invisible': [('non_allow_billable', '=', False)]}" optional="hide"/>
+ </xpath>
+ </field>
+ </record>
+
+ <!--
+ Timesheet from Sales Order
+ -->
+ <record id="timesheet_action_from_sales_order" model="ir.actions.act_window">
+ <field name="name">Timesheets</field>
+ <field name="res_model">account.analytic.line</field>
+ <field name="search_view_id" ref="hr_timesheet.hr_timesheet_line_search"/>
+ <field name="domain">[('project_id', '!=', False)]</field>
+ </record>
+
+ <record id="timesheet_action_from_sales_order_tree" model="ir.actions.act_window.view">
+ <field name="sequence" eval="4"/>
+ <field name="view_mode">tree</field>
+ <field name="view_id" ref="hr_timesheet.timesheet_view_tree_user"/>
+ <field name="act_window_id" ref="timesheet_action_from_sales_order"/>
+ </record>
+
+ <record id="timesheet_action_from_sales_order_form" model="ir.actions.act_window.view">
+ <field name="sequence" eval="5"/>
+ <field name="view_mode">form</field>
+ <field name="view_id" ref="hr_timesheet.timesheet_view_form_user"/>
+ <field name="act_window_id" ref="timesheet_action_from_sales_order"/>
+ </record>
+
+ <!--
+ Reporting
+ -->
+ <record id="timesheet_action_billing_report" model="ir.actions.act_window">
+ <field name="name">Timesheets By Billing Type</field>
+ <field name="res_model">account.analytic.line</field>
+ <field name="view_mode">pivot,graph</field>
+ <field name="domain">[('project_id', '!=', False)]</field>
+ <field name="context">{
+ 'search_default_groupby_date': 1,
+ }</field>
+ <field name="search_view_id" ref="hr_timesheet.hr_timesheet_line_search"/>
+ </record>
+
+ <record id="view_hr_timesheet_line_pivot_billing_rate" model="ir.ui.view">
+ <field name="name">account.analytic.line.pivot.billing.rate</field>
+ <field name="model">account.analytic.line</field>
+ <field name="arch" type="xml">
+ <pivot string="Timesheet" sample="1">
+ <field name="timesheet_invoice_type" type="col"/>
+ <field name="unit_amount" type="measure" widget="timesheet_uom"/>
+ <field name="amount" string="Timesheet Costs"/>
+ </pivot>
+ </field>
+ </record>
+
+ <record id="timesheet_action_view_report_by_billing_rate_pivot" model="ir.actions.act_window.view">
+ <field name="sequence" eval="5"/>
+ <field name="view_mode">pivot</field>
+ <field name="view_id" ref="view_hr_timesheet_line_pivot_billing_rate"/>
+ <field name="act_window_id" ref="timesheet_action_billing_report"/>
+ </record>
+
+ <record id="timesheet_action_view_report_by_billing_rate_graph" model="ir.actions.act_window.view">
+ <field name="sequence" eval="6"/>
+ <field name="view_mode">graph</field>
+ <field name="view_id" ref="hr_timesheet.view_hr_timesheet_line_graph"/>
+ <field name="act_window_id" ref="timesheet_action_billing_report"/>
+ </record>
+
+ <menuitem id="menu_timesheet_billing_analysis"
+ parent="hr_timesheet.menu_timesheets_reports_timesheet"
+ action="timesheet_action_billing_report"
+ name="By Billing Type"
+ sequence="40"/>
+
+ <!--
+ Plan
+ -->
+ <record id="timesheet_action_plan_pivot" model="ir.actions.act_window">
+ <field name="name">Timesheet</field>
+ <field name="res_model">account.analytic.line</field>
+ <field name="view_mode">pivot,tree,form</field>
+ <field name="domain">[('project_id', '!=', False)]</field>
+ <field name="search_view_id" ref="hr_timesheet.hr_timesheet_line_search"/>
+ </record>
+
+ <record id="timesheet_action_from_plan" model="ir.actions.act_window">
+ <field name="name">Timesheet</field>
+ <field name="res_model">account.analytic.line</field>
+ <field name="view_mode">tree,form</field>
+ <field name="domain">[('project_id', '!=', False)]</field>
+ <field name="search_view_id" ref="hr_timesheet.hr_timesheet_line_search"/>
+ </record>
+
+</odoo>
diff --git a/addons/sale_timesheet/views/product_views.xml b/addons/sale_timesheet/views/product_views.xml
new file mode 100644
index 00000000..d238c6ea
--- /dev/null
+++ b/addons/sale_timesheet/views/product_views.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="view_product_timesheet_form" model="ir.ui.view">
+ <field name="name">product.template.timesheet.form</field>
+ <field name="model">product.template</field>
+ <field name="inherit_id" ref="sale.product_template_form_view_invoice_policy"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='invoice_policy']" position="attributes">
+ <attribute name="invisible">False</attribute>
+ <attribute name="attrs">{'invisible': [('type','==','service')]}</attribute>
+ </xpath>
+ <xpath expr="//field[@name='service_type']" position="after">
+ <field name="service_policy" widget="radio" attrs="{'invisible': [('type','!=','service')]}"/>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="product_template_view_search_sale_timesheet" model="ir.ui.view">
+ <field name="name">product.template.search.timesheet</field>
+ <field name="model">product.template</field>
+ <field name="inherit_id" ref="product.product_template_search_view"/>
+ <field name="mode">primary</field>
+ <field name="arch" type="xml">
+ <xpath expr="//filter[@name='consumable']" position="after">
+ <separator/>
+ <filter string="Time-based services" name="product_time_based" domain="[('type', '=', 'service'), ('invoice_policy', '=', 'delivery'), ('service_type', '=', 'timesheet')]"/>
+ <filter string="Fixed price services" name="product_service_fixed" domain="[('type', '=', 'service'), ('invoice_policy', '=', 'order'), ('service_type', '=', 'timesheet')]"/>
+ <filter string="Milestone services" name="product_service_milestone" domain="[('type', '=', 'service'), ('invoice_policy', '=', 'delivery'), ('service_type', '=', 'manual')]"/>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="product_template_action_default_services" model="ir.actions.act_window">
+ <field name="name">Products</field>
+ <field name="res_model">product.template</field>
+ <field name="view_mode">tree,form</field>
+ <field name="search_view_id" ref="sale_timesheet.product_template_view_search_sale_timesheet"/>
+ <field name="context">{'search_default_services': 1, 'default_type': 'service'}</field>
+ </record>
+
+</odoo>
diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml
new file mode 100644
index 00000000..7291b612
--- /dev/null
+++ b/addons/sale_timesheet/views/project_task_views.xml
@@ -0,0 +1,209 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+ <record id="project_project_view_form" model="ir.ui.view">
+ <field name="name">project.project.form.inherit</field>
+ <field name="model">project.project</field>
+ <field name="inherit_id" ref="hr_timesheet.project_invoice_form"/>
+ <field name="arch" type="xml">
+ <div class="oe_button_box" position="inside">
+ <button string="Project Overview" class="oe_stat_button" type="object" name="action_view_timesheet" icon="fa-puzzle-piece" attrs="{'invisible': [('allow_billable', '=', False)]}"/>
+ </div>
+ <xpath expr="//button[@name='action_view_so']" position="attributes">
+ <attribute name="attrs">{'invisible': ['|', '|', ('allow_billable', '=', False), ('sale_order_id', '=', False), ('bill_type', '!=', 'customer_project')]}</attribute>
+ </xpath>
+ <xpath expr="//header" position="inside">
+ <button name="action_make_billable" string="Create Sales Order" type="object" attrs="{'invisible': [('display_create_order', '=', False)]}" groups="sales_team.group_sale_salesman"/>
+ </xpath>
+ <xpath expr="//page[@name='settings']" position="after">
+ <page name="billing_employee_rate" string="Invoicing" attrs="{'invisible': [('allow_billable', '=', False)]}">
+ <group>
+ <group>
+ <field name="display_create_order" invisible="1"/>
+ <field name="bill_type" widget="radio"/>
+ <field name="pricing_type" attrs="{'invisible': ['|', ('allow_billable', '=', False), ('bill_type', '!=', 'customer_project')], 'required': ['&amp;', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" widget="radio"/>
+ <field name="timesheet_product_id" string="Service" attrs="{'invisible': ['|', '|', ('allow_timesheets', '=', False), ('sale_order_id', '!=', False), ('bill_type', '!=', 'customer_task')], 'required': ['&amp;', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" context="{'default_type': 'service', 'default_service_policy': 'delivered_timesheet', 'default_service_type': 'timesheet'}"/>
+ <field name="sale_order_id" attrs="{'invisible': [('bill_type', '!=', 'customer_project')], 'readonly': [('sale_order_id', '!=', False)]}" force_save="1" options="{'no_create': True, 'no_edit': True, 'delete': False, 'no_open': True}"/>
+ <field name="sale_line_id" string="Default Sales Order Item" attrs="{'invisible': [('bill_type', '!=', 'customer_project')]}" options="{'no_create': True, 'no_edit': True, 'delete': False, 'no_open': True}"/>
+ </group>
+ </group>
+ <field name="sale_line_employee_ids" attrs="{'invisible': ['|', ('bill_type', '!=', 'customer_project'), ('pricing_type', '!=', 'employee_rate')]}">
+ <tree editable="top">
+ <field name="company_id" invisible="1"/>
+ <field name="project_id" invisible="1"/>
+ <field name="employee_id" options="{'no_create': True}"/>
+ <field name="timesheet_product_id" attrs="{'column_invisible': [('parent.sale_order_id', '!=', False)]}" invisible="1"/>
+ <field name="sale_line_id" attrs="{'required': True}" options="{'no_create': True}" domain="[('order_id','=',parent.sale_order_id), ('is_service', '=', True)]"/>
+ <field name="price_unit" widget="monetary" force_save="1" options="{'currency_field': 'currency_id'}"/>
+ <field name="currency_id" invisible="1"/>
+ </tree>
+ </field>
+ </page>
+ </xpath>
+ <xpath expr="//div[@id='timesheet_settings']/.." position="after">
+ <div class="row mt16 o_settings_container">
+ <div class="col-lg-6 o_setting_box" id="allow_billable_container">
+ <div class="o_setting_left_pane">
+ <field name="allow_billable"/>
+ </div>
+ <div class="o_setting_right_pane">
+ <label for="allow_billable"/>
+ <div class="text-muted" id="allow_billable_setting">
+ Invoice your time and material to customers
+ </div>
+ </div>
+ </div>
+ </div>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="project_project_view_form_salesman" model="ir.ui.view">
+ <field name="name">project.project.form.inherit.salesman</field>
+ <field name="model">project.project</field>
+ <field name="inherit_id" ref="sale_timesheet.project_project_view_form"/>
+ <field name="groups_id" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
+ <field name="arch" type="xml">
+ <field name="sale_order_id" position="attributes">
+ <attribute name="options">{'no_create': True, 'no_edit': True, 'delete': False}</attribute>
+ </field>
+ <field name="sale_line_id" position="attributes">
+ <attribute name="options">{'no_create': True, 'no_edit': True, 'delete': False}</attribute>
+ </field>
+ </field>
+ </record>
+
+ <record id="project_project_view_form_simplified_inherit" model="ir.ui.view">
+ <field name="name">project.project.view.form.simplified.inherit</field>
+ <field name="model">project.project</field>
+ <field name="inherit_id" ref="hr_timesheet.project_project_view_form_simplified_inherit_timesheet"/>
+ <field name="arch" type="xml">
+ <field name="allow_timesheets" position="after">
+ <field name="company_id" invisible="1"/>
+ <field name="allow_billable"/>
+ </field>
+ </field>
+ </record>
+
+ <record id="project_project_view_kanban_inherit_sale_timesheet" model="ir.ui.view">
+ <field name="name">project.project.kanban.inherit.sale.timesheet</field>
+ <field name="model">project.project</field>
+ <field name="inherit_id" ref="hr_timesheet.view_project_kanban_inherited"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='allow_timesheets']" position="after">
+ <field name="allow_billable"/>
+ <field name="warning_employee_rate" invisible="1"/>
+ </xpath>
+ <xpath expr="//div[hasclass('o_primary')]//span" position="inside">
+ <t t-if="1 == 0">
+ <i class="ml-2 fa fa-exclamation-triangle text-danger small" role="img" title="Some of the employees who are recording time on this project are not linked to any Sales Order Item. This means that their time will be considered as non-billable." aria-label="Some of the employees who are recording time on this project are not linked to any ales Order Item. This means that their time will be considered as non-billable."/>
+ </t>
+ </xpath>
+ <xpath expr="//a[@name='action_view_account_analytic_line']" position="attributes">
+ <attribute name="t-if">record.analytic_account_id.raw_value and !record.allow_timesheets.raw_value and record.allow_billable.raw_value</attribute>
+ </xpath>
+ <xpath expr="//a[@t-if='record.allow_timesheets.raw_value']" position="replace">
+ <a t-if="record.allow_timesheets.raw_value and record.allow_billable.raw_value" name="action_view_timesheet" type="object" class="o_project_kanban_box o_project_timesheet_box" groups="project.group_project_manager">
+ <div>
+ <span class="o_label">Overview</span>
+ </div>
+ </a>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="view_sale_service_inherit_form2" model="ir.ui.view">
+ <field name="name">sale.service.form.view.inherit</field>
+ <field name="model">project.task</field>
+ <field name="groups_id" eval="[(4, ref('base.group_user'))]"/>
+ <field name="inherit_id" ref="project.view_task_form2"/>
+ <field name="arch" type="xml">
+ <xpath expr="//header" position='inside'>
+ <field name="allow_billable" invisible="1"/>
+ <field name="display_create_order" invisible="1"/>
+ <button name="action_make_billable" string="Create Sales Order" type="object" attrs="{'invisible': [('display_create_order', '=', False)]}" groups="sales_team.group_sale_salesman" invisible="1"/>
+ </xpath>
+ <xpath expr="//field[@name='email_cc']" position="after">
+ <field name="analytic_account_id" groups="base.group_no_one"/>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="project_task_view_form_inherit_sale_timesheet" model="ir.ui.view">
+ <field name="name">project.task.form.inherit.timesheet</field>
+ <field name="model">project.task</field>
+ <field name="inherit_id" ref="project.view_task_form2"/>
+ <field name="groups_id" eval="[(6,0, (ref('hr_timesheet.group_hr_timesheet_user'),))]"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='timesheet_ids']/tree" position="attributes">
+ <attribute name="decoration-muted">timesheet_invoice_id != False</attribute>
+ </xpath>
+ <xpath expr="//field[@name='user_id']" position="after">
+ <field name="is_project_map_empty" invisible="1"/>
+ <field name="has_multi_sol" invisible="1"/>
+ </xpath>
+ <xpath expr="//field[@name='partner_phone']" position="after">
+ <field name="bill_type" invisible="1"/>
+ <field name="pricing_type" invisible="1"/>
+ <field name="timesheet_product_id" invisible="1"/>
+ <field name="non_allow_billable" attrs="{'invisible': ['|', '|', '|', ('allow_billable', '=', False), ('allow_timesheets', '=', False), ('pricing_type', '!=', 'employee_rate'), ('bill_type', '=', 'customer_task')]}" invisible="1"/>
+ </xpath>
+ <xpath expr="//field[@name='timesheet_ids']/tree/field[@name='unit_amount']" position="before">
+ <field name="timesheet_invoice_id" invisible="1"/>
+ <field name="so_line" readonly="1" attrs="{'column_invisible': [('parent.allow_billable', '=', False)]}" context="{'with_remaining_hours': True}" optional="hide"/>
+ </xpath>
+ <xpath expr="//field[@name='timesheet_ids']/tree" position="inside">
+ <field name="non_allow_billable" attrs="{'column_invisible': ['|', '&amp;', '&amp;', '|', ('parent.bill_type', '!=', 'customer_project'), ('parent.pricing_type', '!=', 'employee_rate'), ('parent.timesheet_product_id', '=', False), ('parent.sale_line_id', '=', False), '&amp;', ('parent.bill_type', '=', 'customer_project'), ('parent.pricing_type', '=', 'employee_rate')]}" invisible="1"/>
+ </xpath>
+ <xpath expr="//field[@name='remaining_hours']" position="after">
+ <field name="remaining_hours_available" invisible="1"/>
+ <span id="remaining_hours_so_label" attrs="{'invisible': ['|', '|', '|', '|', ('allow_billable', '=', False), ('sale_order_id', '=', False), ('partner_id', '=', False), ('sale_line_id', '=', False), ('remaining_hours_available', '=', False)]}">
+ <label class="font-weight-bold" for="remaining_hours_so" string="Remaining Hours on SO"
+ attrs="{'invisible': ['|', ('encode_uom_in_days', '=', True), ('remaining_hours_so', '&lt;', 0)]}"/>
+ <label class="font-weight-bold" for="remaining_hours_so" string="Remaining Days on SO"
+ attrs="{'invisible': ['|', ('encode_uom_in_days', '=', False), ('remaining_hours_so', '&lt;', 0)]}"/>
+ <label class="font-weight-bold text-danger" for="remaining_hours_so" string="Remaining Hours on SO"
+ attrs="{'invisible': ['|', ('encode_uom_in_days', '=', True), ('remaining_hours_so', '&gt;=', 0)]}"/>
+ <label class="font-weight-bold text-danger" for="remaining_hours_so" string="Remaining Days on SO"
+ attrs="{'invisible': ['|', ('encode_uom_in_days', '=', False), ('remaining_hours_so', '&gt;=', 0)]}"/>
+ </span>
+ <field name="remaining_hours_so" nolabel="1" widget="timesheet_uom" attrs="{'invisible': ['|', '|', '|', '|', ('allow_billable', '=', False), ('sale_order_id', '=', False), ('partner_id', '=', False), ('sale_line_id', '=', False), ('remaining_hours_available', '=', False)]}"></field>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="view_task_form2_inherit_sale_timesheet" model="ir.ui.view">
+ <field name="name">view.task.form2.inherit</field>
+ <field name="model">project.task</field>
+ <field name="inherit_id" ref="project.view_task_form2"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='sale_line_id']" position="attributes">
+ <attribute name="context">{'with_remaining_hours': True}</attribute>
+ <attribute name="attrs">
+ {'invisible': ['|', ('allow_billable', '=', False), ('partner_id', '=', False)]}
+ </attribute>
+ </xpath>
+ <xpath expr="//field[@name='sale_order_id']" position="attributes">
+ <attribute name="invisible">1</attribute>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="quick_create_task_form_sale_timesheet" model="ir.ui.view">
+ <field name="name">project.task.form.inherit.timesheet</field>
+ <field name="model">project.task</field>
+ <field name="inherit_id" ref="project.quick_create_task_form"/>
+ <field name="arch" type="xml">
+ <field name="project_id" position="after">
+ <field name="timesheet_product_id" invisible="1"/>
+ </field>
+ </field>
+ </record>
+
+ <record id="project_timesheet_action_client_timesheet_plan" model="ir.actions.act_window">
+ <field name="name">Overview</field>
+ <field name="res_model">project.project</field>
+ <field name="view_mode">qweb</field>
+ </record>
+
+</odoo>
diff --git a/addons/sale_timesheet/views/report_invoice.xml b/addons/sale_timesheet/views/report_invoice.xml
new file mode 100644
index 00000000..dbf89fc2
--- /dev/null
+++ b/addons/sale_timesheet/views/report_invoice.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+ <template id="report_invoice_document" inherit_id="account.report_invoice_document">
+ <xpath expr="//div[hasclass('page')]/h2" position="after">
+ <a t-if="report_type == 'html' and o.move_type == 'out_invoice' and o.state in ('draft', 'posted') and o.timesheet_count > 0" target="_blank" t-att-href="'/my/timesheets?search_in=invoice_id&amp;search=%s' % o.id">View Timesheets</a>
+ </xpath>
+ </template>
+ </data>
+</odoo>
diff --git a/addons/sale_timesheet/views/res_config_settings_views.xml b/addons/sale_timesheet/views/res_config_settings_views.xml
new file mode 100644
index 00000000..ffefb17b
--- /dev/null
+++ b/addons/sale_timesheet/views/res_config_settings_views.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+
+ <record id="res_config_settings_view_form" model="ir.ui.view">
+ <field name="name">res.config.settings.view.form.inherit.sale.timesheet</field>
+ <field name="model">res.config.settings</field>
+ <field name="priority" eval="1"/>
+ <field name="inherit_id" ref="hr_timesheet.res_config_settings_view_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//div[@name='section_leaves']" position="before">
+ <h2>Billing</h2>
+ <div name="timesheet_billing" class="row mt16 o_settings_container">
+ <div class="col-12 col-lg-6 o_setting_box" id="time_billing_setting">
+ <div class="o_setting_right_pane">
+ <span class="o_form_label">Time Billing</span>
+ <div class="text-muted">
+ Sell services and invoice time spent
+ </div>
+ <div class="content-group" name="msg_module_sale_timesheet">
+ <div class="mt8">
+ <div>
+ <button name="%(sale_timesheet.product_template_action_default_services)d" string="Configure your services" type="action" class="btn-link" icon="fa-arrow-right"/>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </xpath>
+ </field>
+ </record>
+
+</odoo>
diff --git a/addons/sale_timesheet/views/sale_order_views.xml b/addons/sale_timesheet/views/sale_order_views.xml
new file mode 100644
index 00000000..261e207a
--- /dev/null
+++ b/addons/sale_timesheet/views/sale_order_views.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="view_order_form_inherit_sale_timesheet" model="ir.ui.view">
+ <field name="name">sale.order.form.sale.timesheet</field>
+ <field name="model">sale.order</field>
+ <field name="inherit_id" ref="sale_project.view_order_form_inherit_sale_project"/>
+ <field name="arch" type="xml">
+ <data>
+ <xpath expr="//button[@name='action_view_project_ids']" position="attributes">
+ <attribute name="string">Project Overview</attribute>
+ </xpath>
+ <xpath expr="//button[@name='action_view_invoice']" position="before">
+ <field name="timesheet_count" invisible="1" />
+ <button type="object"
+ name="action_view_timesheet"
+ class="oe_stat_button"
+ icon="fa-clock-o"
+ attrs="{'invisible': [('timesheet_count', '=', 0)]}"
+ groups="hr_timesheet.group_hr_timesheet_user">
+ <div class="o_field_widget o_stat_info">
+ <span class="o_stat_value">
+ <field name="timesheet_total_duration" class="mr4" widget="statinfo" nolabel="1"/>
+ <field name="timesheet_encode_uom_id" options="{'no_open' : True}"/>
+ </span>
+ <span class="o_stat_text">Recorded</span>
+ </div>
+ </button>
+ </xpath>
+ </data>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml b/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml
new file mode 100644
index 00000000..76d6d52f
--- /dev/null
+++ b/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+ <template id="assets_frontend" name="report timesheet assets" inherit_id="web.assets_frontend">
+ <xpath expr="." position="inside">
+ <link rel="stylesheet" type="text/scss" href="/sale_timesheet/static/src/scss/sale_timesheet_portal.scss"/>
+ </xpath>
+ </template>
+
+ <!-- TODO: [XBO] Remove me in master -->
+ <template id="portal_invoice_page_inherit_timesheet" inherit_id="account.portal_invoice_page">
+ <xpath expr="//t[@t-call='portal.portal_record_sidebar']//div[hasclass('o_download_pdf')]" position="after">
+ <t t-if="1 == 0">
+ <li t-if="timesheets" class="list-group-item flex-grow-1" >
+ <a href="#accordion">Timesheets</a>
+ </li>
+ </t>
+ </xpath>
+
+ <xpath expr="//div[@id='invoice_content']//div[hasclass('o_portal_html_view')]" position="after">
+ <t t-if="1 == 0">
+ <div t-if="timesheets" class="container">
+ <div id="accordion" class="o_timesheet_accordion mt-4">
+ <div class="card mb-0">
+ <div class="card-header">
+ <h5 class="mb0">
+ <a class="card-title" data-toggle="collapse" href="#collapseTimesheet">
+ Timesheets
+ </a>
+ </h5>
+ </div>
+ <div id="collapseTimesheet" class="card-body show" data-parent="#accordion">
+ <t t-set="nr_tasks" t-value="len(timesheets.mapped('task_id'))"/>
+ <t t-set="nr_projects" t-value="len(timesheets.mapped('project_id'))"/>
+ <table class="table table-sm">
+ <thead>
+ <tr>
+ <th>Date</th>
+ <th>Employee</th>
+ <th t-if="nr_projects &gt; 1">Project</th>
+ <th t-if="nr_tasks &gt; 0">Task</th>
+ <th>Description</th>
+ <th t-if="timesheets[0]._is_timesheet_encode_uom_day()" class="text-right">Duration (days)</th>
+ <th t-else="" class="text-right">Duration (hours)</th>
+ </tr>
+ </thead>
+ <tr t-foreach="timesheets" t-as="timesheet">
+ <td><t t-esc="timesheet.date" t-options='{"widget": "date"}'/></td>
+ <td><t t-esc="timesheet.employee_id.name"/></td>
+ <td t-if="nr_projects &gt; 1"><span t-field="timesheet.project_id"/></td>
+ <td t-if="nr_tasks &gt; 0"><span t-field="timesheet.task_id"/></td>
+ <td><t t-esc="timesheet.name"/></td>
+ <td class="text-right">
+ <span t-if="timesheet._is_timesheet_encode_uom_day()" t-esc="timesheet._get_timesheet_time_day()" t-options='{"widget": "timesheet_uom"}'/>
+ <span t-else="" t-field="timesheet.unit_amount" t-options='{"widget": "float_time"}'/>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+ </t>
+ </xpath>
+ </template>
+
+ <template id="sale_order_portal_content_inherit" inherit_id="sale.sale_order_portal_content">
+ <xpath expr="//td[@id='product_name']" position="inside">
+ <a t-if="timesheets and len(timesheets.filtered(lambda t: t.so_line == line)) > 0" t-att-href="'/my/timesheets?search_in=sol_id&amp;search=%s' % line.id">View Timesheets</a>
+ </xpath>
+ </template>
+
+ <!-- TODO: [XBO] Remove me in master -->
+ <template id="sale_order_portal_template_inherit" inherit_id="sale.sale_order_portal_template">
+ <xpath expr="//t[@t-call='portal.portal_record_sidebar']//div[hasclass('o_download_pdf')]" position="after">
+ <t t-if="1 == 0">
+ <li t-if="timesheets" class="list-group-item flex-grow-1" >
+ <a href="#accordion">Timesheets</a>
+ </li>
+ </t>
+ </xpath>
+
+ <xpath expr="//div[@id='sale_order_communication']" position="before">
+ <t t-if="1 == 0">
+ <div t-if="timesheets" class="container">
+ <div id="accordion" class="o_timesheet_accordion mt-4">
+ <div class="card mb-0">
+ <div class="card-header">
+ <h5 class="mb0">
+ <a class="card-title" data-toggle="collapse" href="#collapseTimesheet">
+ Timesheets
+ </a>
+ </h5>
+ </div>
+ <div id="collapseTimesheet" class="card-body show" data-parent="#accordion">
+ <t t-set="nr_tasks" t-value="len(timesheets.mapped('task_id'))"/>
+ <t t-set="nr_projects" t-value="len(timesheets.mapped('project_id'))"/>
+ <table class="table table-sm">
+ <thead>
+ <tr>
+ <th>Date</th>
+ <th>Employee</th>
+ <th t-if="nr_projects &gt; 1">Project</th>
+ <th t-if="nr_tasks &gt; 0">Task</th>
+ <th>Description</th>
+ <th t-if="timesheets[0]._is_timesheet_encode_uom_day()" class="text-right">Duration (days)</th>
+ <th t-else="" class="text-right">Duration (hours)</th>
+ </tr>
+ </thead>
+ <tr t-foreach="timesheets" t-as="timesheet">
+ <td><t t-esc="timesheet.date" t-options='{"widget": "date"}'/></td>
+ <td><t t-esc="timesheet.employee_id.name"/></td>
+ <td t-if="nr_projects &gt; 1"><span t-field="timesheet.project_id"/></td>
+ <td t-if="nr_tasks &gt; 0"><span t-field="timesheet.task_id"/></td>
+ <td><t t-esc="timesheet.name"/></td>
+ <td class="text-right">
+ <span t-if="timesheet._is_timesheet_encode_uom_day()" t-esc="timesheet._get_timesheet_time_day()" t-options='{"widget": "timesheet_uom"}'/>
+ <span t-else="" t-field="timesheet.unit_amount" t-options='{"widget": "float_time"}'/>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+ </t>
+ </xpath>
+ </template>
+
+ <template id="portal_my_timesheets_inherit" inherit_id="hr_timesheet.portal_my_timesheets">
+ <xpath expr="//thead/tr[contains(@t-attf-class, 'thead-light')]" position="inside">
+ <t t-elif="groupby == 'sol'">
+ <t t-set="sol" t-value="timesheets[0].so_line"/>
+ <th colspan="5">
+ <t t-if="sol">
+ <em class="font-weight-normal text-muted">Timesheets for sales order item:</em>
+ <span t-field="sol.display_name"/>
+ <t t-if="sol.remaining_hours_available">
+ <span class="text-muted font-weight-normal">(<span t-field="sol.product_uom_qty" t-options='{"widget": "float_time"}'></span> <span t-field="sol.product_uom.display_name"></span> Ordered, <span t-field="sol.remaining_hours" t-options='{"widget": "float_time"}'></span> <span t-field="sol.product_uom.display_name"></span> Remaining)</span>
+ </t>
+ </t>
+ </th>
+ <th colspan="1" class="text-right text-muted font-weight-normal">
+ <t t-if="is_uom_day">
+ Total: <span t-esc="timesheets[0]._convert_hours_to_days(hours_spent)" t-options='{"widget": "timesheet_uom"}'/>
+ </t>
+ <t t-else="">
+ Total: <span t-esc="hours_spent" t-options='{"widget": "float_time"}'/>
+ </t>
+ </th>
+ </t>
+ </xpath>
+ <xpath expr="//thead/tr/th[@t-if='is_uom_day']" position="before">
+ <th t-if="not groupby == 'sol'">Sale Order Item</th>
+ </xpath>
+ <xpath expr="//tbody//td[hasclass('text-right')]" position="before">
+ <td t-if="not groupby == 'sol'"><span t-field="timesheet.so_line" t-att-title="timesheet.so_line.display_name"></span></td>
+ </xpath>
+ </template>
+
+</odoo>