Initial commit: Sale packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:49 +02:00
commit 14e3d26998
6469 changed files with 2479670 additions and 0 deletions

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="action_timesheet_from_invoice" model="ir.actions.act_window">
<field name="name">Timesheets</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">account.analytic.line</field>
<field name="view_mode">tree,form,graph,pivot,kanban</field>
<field name="context">{
'create': False,
'edit': False,
'delete': False,
"is_timesheet": 1,
}</field>
<field name="domain">[('timesheet_invoice_id', '=', active_id)]</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No activities found
</p>
<p>
Track your working hours by projects every day and invoice this time to your customers.
</p>
</field>
</record>
<record id="action_timesheet_from_invoice_view_tree" model="ir.actions.act_window.view">
<field name="sequence" eval="1"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="hr_timesheet.hr_timesheet_line_tree"/>
<field name="act_window_id" ref="action_timesheet_from_invoice"/>
</record>
<record id="action_timesheet_from_invoice_view_form" model="ir.actions.act_window.view">
<field name="sequence" eval="4"/>
<field name="view_mode">form</field>
<field name="view_id" ref="hr_timesheet.hr_timesheet_line_form"/>
<field name="act_window_id" ref="action_timesheet_from_invoice"/>
</record>
<record id="action_timesheet_from_invoice_view_kanban" model="ir.actions.act_window.view">
<field name="sequence" eval="5"/>
<field name="view_mode">kanban</field>
<field name="view_id" ref="hr_timesheet.view_kanban_account_analytic_line"/>
<field name="act_window_id" ref="action_timesheet_from_invoice"/>
</record>
<record id="action_timesheet_from_invoice_view_pivot" model="ir.actions.act_window.view">
<field name="sequence" eval="6"/>
<field name="view_mode">pivot</field>
<field name="view_id" ref="hr_timesheet.view_hr_timesheet_line_pivot"/>
<field name="act_window_id" ref="action_timesheet_from_invoice"/>
</record>
<record id="action_timesheet_from_invoice_view_graph" model="ir.actions.act_window.view">
<field name="sequence" eval="7"/>
<field name="view_mode">graph</field>
<field name="view_id" ref="hr_timesheet.view_hr_timesheet_line_graph"/>
<field name="act_window_id" ref="action_timesheet_from_invoice"/>
</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">
<field name="timesheet_count" invisible="1"/>
<button name="%(sale_timesheet.action_timesheet_from_invoice)d" type="action" 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>
</field>
</record>
</odoo>

View file

@ -0,0 +1,201 @@
<?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='task_id']" position="after">
<field name="order_id" string="Sales Order" groups="sales_team.group_sale_salesman"/>
<field name="so_line" groups="sales_team.group_sale_salesman"/>
</xpath>
<xpath expr="//filter[@name='month']" position="before">
<filter name="billable_fixed" string="Billed at a Fixed Price" domain="[('timesheet_invoice_type', '=', 'billable_fixed')]"
groups="sales_team.group_sale_salesman"/>
<filter name="billable_time" string="Billed on Timesheets" domain="[('timesheet_invoice_type', '=', 'billable_time')]"
groups="sales_team.group_sale_salesman"/>
<filter name="billable_milestones" string="Billed on Milestones" domain="[('timesheet_invoice_type', '=', 'billable_milestones')]"
groups="sales_team.group_sale_salesman"/>
<filter name="billable_manual" string="Billed Manually" domain="[('timesheet_invoice_type', '=', 'billable_manual')]"
groups="sales_team.group_sale_salesman"/>
<filter name="non_billable" string="Non-Billable" domain="[('timesheet_invoice_type', '=', 'non_billable')]"
groups="sales_team.group_sale_salesman"/>
<separator/>
</xpath>
<xpath expr="//filter[@name='groupby_employee']" position="after">
<filter string="Sales Order" name="groupby_sale_order" domain="[]" context="{'group_by': 'order_id'}"
groups="sales_team.group_sale_salesman"/>
<filter string="Sales Order Item" name="groupby_sale_order_item" domain="[]" context="{'group_by': 'so_line'}"
groups="sales_team.group_sale_salesman"/>
<filter string="Invoice" name="groupby_invoice" domain="[]" context="{'group_by': 'timesheet_invoice_id'}"
groups="sales_team.group_sale_salesman"/>
<filter string="Billing Type" name="groupby_timesheet_invoice_type" domain="[]"
context="{'group_by': 'timesheet_invoice_type'}" groups="sales_team.group_sale_salesman"/>
</xpath>
</field>
</record>
<record id="hr_timesheet_line_tree_inherit" model="ir.ui.view">
<field name="name">account.analytic.line.tree.inherit</field>
<field name="model">account.analytic.line</field>
<field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_tree"/>
<field name="arch" type="xml">
<xpath expr="//tree/field[@name='name']" position="after">
<field name="commercial_partner_id" invisible="1" groups="sales_team.group_sale_salesman"/>
<field name="is_so_line_edited" invisible="1" groups="sales_team.group_sale_salesman"/>
<field name="so_line" widget="so_line_field" optional="hide" options="{'no_create': True}" context="{'create': False, 'edit': False, 'delete': False}" groups="sales_team.group_sale_salesman"/>
</xpath>
</field>
</record>
<record id="hr_timesheet_line_form_inherit" model="ir.ui.view">
<field name="name">account.analytic.line.form.inherit</field>
<field name="model">account.analytic.line</field>
<field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='task_id']" position="after">
<field name="commercial_partner_id" invisible="1" groups="sales_team.group_sale_salesman"/>
<field name="is_so_line_edited" invisible="1" groups="sales_team.group_sale_salesman"/>
<field name="so_line" widget="so_line_field" options='{"no_create": True}' context="{'create': False, 'edit': False, 'delete': False}" groups="sales_team.group_sale_salesman"/>
</xpath>
</field>
</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="Timesheets" sample="1">
<field name="date" interval="month" type="row"/>
<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="view_hr_timesheet_line_graph_employee_per_date" model="ir.ui.view">
<field name="name">account.analytic.line.graph.employee.per.date</field>
<field name="model">account.analytic.line</field>
<field name="arch" type="xml">
<graph string="Timesheet" sample="1" js_class="hr_timesheet_graphview">
<field name="date" interval="month" />
<field name="employee_id"/>
<field name="amount" type="measure" string="Timesheet Costs"/>
<field name="unit_amount" type="measure" widget="timesheet_uom"/>
</graph>
</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="context">{
"is_timesheet": 1,
}</field>
<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>
<!-- Timesheets from Sales Order Item -->
<record id="timesheet_action_from_sales_order_item" 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), ('so_line', '=', active_id)]</field>
<field name="context">{
'search_default_billable_timesheet': True,
'search_default_week': 1,
'default_so_line': active_id,
'default_is_so_line_edited': True,
"is_timesheet": 1,
}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No timesheets found. Let's create one!
</p>
<p>
Keep track of your working hours by project every day and bill your customers for that time.
</p>
</field>
</record>
<record id="timesheet_action_from_sales_order_item_tree" model="ir.actions.act_window.view">
<field name="sequence" eval="10"/>
<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_item"/>
</record>
<record id="timesheet_action_from_sales_order_item_kanban" model="ir.actions.act_window.view">
<field name="sequence" eval="20"/>
<field name="view_mode">kanban</field>
<field name="view_id" ref="hr_timesheet.view_kanban_account_analytic_line"/>
<field name="act_window_id" ref="timesheet_action_from_sales_order_item"/>
</record>
<record id="timesheet_action_from_sales_order_item_pivot" model="ir.actions.act_window.view">
<field name="sequence" eval="30"/>
<field name="view_mode">pivot</field>
<field name="view_id" ref="hr_timesheet.view_hr_timesheet_line_pivot"/>
<field name="act_window_id" ref="timesheet_action_from_sales_order_item"/>
</record>
<record id="timesheet_action_from_sales_order_item_graph" model="ir.actions.act_window.view">
<field name="sequence" eval="40"/>
<field name="view_mode">graph</field>
<field name="view_id" ref="view_hr_timesheet_line_graph_employee_per_date"/>
<field name="act_window_id" ref="timesheet_action_from_sales_order_item"/>
</record>
<record id="timesheet_action_from_sales_order_item_form" model="ir.actions.act_window.view">
<field name="sequence" eval="50"/>
<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_item"/>
</record>
<!--
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="context">{
"is_timesheet": 1,
}</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="context">{
"is_timesheet": 1,
}</field>
<field name="search_view_id" ref="hr_timesheet.hr_timesheet_line_search"/>
</record>
</odoo>

View file

@ -0,0 +1,42 @@
<?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"/>
<field name="arch" type="xml">
<field name="product_tooltip" position="after">
<label for="product_tooltip" string="" attrs="{'invisible': ['|', ('type', '!=', 'service'), ('service_policy', '!=', 'ordered_prepaid')]}"/>
<div attrs="{'invisible': ['|', ('type', '!=', 'service'), ('service_policy', '!=', 'ordered_prepaid')]}" class="fst-italic text-muted">
Warn the salesperson for an upsell when work done exceeds
<field name="service_upsell_threshold" widget="percentage" class="oe_inline"/>
of hours sold. <field name="service_upsell_threshold_ratio" class="oe_inline" attrs="{'invisible': [('service_upsell_threshold_ratio', '=', False)]}"/>
</div>
</field>
</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">Services</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_detailed_type': 'service'}</field>
</record>
</odoo>

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="project_sharing_inherit_project_task_view_form" 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.project_sharing_project_task_view_form"/>
<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='timesheet_ids']/tree/field[@name='unit_amount']" position="before">
<field name="timesheet_invoice_id" invisible="1"/>
<field name="so_line"
attrs="{'column_invisible': [('parent.allow_billable', '=', False)]}"
context="{'with_remaining_hours': True, 'with_price_unit': True}" options="{'no_create': True, 'no_open': True}"
optional="hide"/>
</xpath>
<xpath expr="//field[@name='child_ids']/tree/field[@name='remaining_hours']" position="after">
<field name="remaining_hours_so" optional="hide" widget="timesheet_uom"
attrs="{'column_invisible': ['|', ('parent.allow_billable', '=', False), ('parent.allow_timesheets', '=', False)]}" />
</xpath>
<xpath expr="//field[@name='remaining_hours']" position="after">
<field name="allow_billable" invisible="1" />
<field name="remaining_hours_available" invisible="1"/>
<field name="sale_order_id" invisible="1"/>
<span id="remaining_hours_so_label" attrs="{'invisible': ['|', '|', ('allow_billable', '=', False), ('allow_timesheets', '=', False), ('remaining_hours_available', '=', False)]}" class="o_td_label float-start">
<label class="fw-bold" for="remaining_hours_so" string="Remaining Hours on SO"
attrs="{'invisible': ['|', ('encode_uom_in_days', '=', True), ('remaining_hours_so', '&lt;', 0)]}"/>
<label class="fw-bold" for="remaining_hours_so" string="Remaining Days on SO"
attrs="{'invisible': ['|', ('encode_uom_in_days', '=', False), ('remaining_hours_so', '&lt;', 0)]}"/>
<label class="fw-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="fw-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), ('allow_timesheets', '=', False), ('remaining_hours_available', '=', False)]}" decoration-danger="remaining_hours_so &lt; 0"></field>
</xpath>
</field>
</record>
<record id="project_sharing_inherit_project_task_view_tree_sale_timesheet" model="ir.ui.view">
<field name="name">project.task.tree.inherit.sale.timesheet</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="hr_timesheet.project_sharing_inherit_project_task_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='remaining_hours']" position="after">
<field name="allow_billable" invisible="1" />
<field name="allow_timesheets" invisible="1"/>
<field name="remaining_hours_so" optional="hide" widget="timesheet_uom"
attrs="{'column_invisible': ['|', ('allow_billable', '=', False), ('allow_timesheets', '=', False)]}" />
</xpath>
</field>
</record>
</odoo>

View file

@ -0,0 +1,200 @@
<?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">
<xpath expr="//page[@name='settings']" position="after">
<page name="billing_employee_rate" string="Invoicing" attrs="{'invisible': ['|', ('allow_billable', '=', False), ('partner_id', '=', False)]}">
<group>
<group>
<field name="display_create_order" invisible="1"/>
<field name="pricing_type" invisible="1" widget="radio"/>
<field name="timesheet_product_id" string="Default Service" invisible="1" context="{'default_detailed_type': 'service', 'default_service_policy': 'delivered_timesheet', 'default_service_type': 'timesheet'}"/>
<field name="sale_order_id" invisible="1" options="{'no_create': True, 'no_edit': True, 'delete': False, 'no_open': True}"/>
<field name="sale_line_id" groups="!sales_team.group_sale_salesman" options="{'no_create': True, 'no_edit': True, 'delete': False, 'no_open': True}"/>
<field name="sale_line_id" groups="sales_team.group_sale_salesman" options="{'no_create': True, 'no_edit': True, 'delete': False}"/>
</group>
</group>
<field name="sale_line_employee_ids">
<tree editable="bottom">
<field name="company_id" invisible="1"/>
<field name="partner_id" invisible="1"/>
<field name="employee_id" options="{'no_create': True}"/>
<field name="sale_line_id" attrs="{'required': True}" options="{'no_create': True}"/>
<field name="price_unit" widget="monetary" force_save="1" options="{'currency_field': 'currency_id'}"/>
<field name="display_cost" widget="monetary" options="{'currency_field': 'cost_currency_id'}"/>
<field name="is_cost_changed" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="cost_currency_id" invisible="1"/>
</tree>
</field>
</page>
</xpath>
</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">
<xpath expr="//div[hasclass('o_setting_box')]" position="before">
<div class="col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="company_id" invisible="1"/>
<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>
</xpath>
</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"/>
<field name="sale_order_id" invisible="1"/>
<field name="pricing_type" invisible="1"/>
</xpath>
<xpath expr="//div[hasclass('o_kanban_manage_reporting')]" position="inside">
<div role="menuitem" t-if="record.rating_active.raw_value" groups="project.group_project_manager">
<a name="action_view_all_rating" type="object">
Customer Ratings
</a>
</div>
</xpath>
</field>
</record>
<!-- We do a separate inheritance from the base view for the SO button to give the buttons a deterministic order using priorities -->
<record id="project_project_view_kanban_inherit_sale_timesheet_so_button" model="ir.ui.view">
<field name="name">project.project.kanban.inherit.sale.timesheet.so.button</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.view_project_kanban"/>
<field name="priority">30</field>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('o_kanban_manage_view')]" position="inside">
<div t-if="record.allow_billable.raw_value and record.sale_order_id.raw_value and record.pricing_type.raw_value != 'task_rate'"
role="menuitem"
groups="sales_team.group_sale_salesman_all_leads">
<a name="action_view_sos" type="object">Sales Orders</a>
</div>
</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="inherit_id" ref="hr_timesheet.view_task_form2_inherited"/>
<field name="arch" type="xml">
<xpath expr="//header" position="inside">
<field name="allow_billable" invisible="1"/>
</xpath>
<xpath expr="//field[@name='child_ids']/tree//field[@name='remaining_hours']" position="after">
<field name="remaining_hours_so" widget="timesheet_uom" optional="hide" groups="base.group_user"/>
</xpath>
<xpath expr="//field[@name='depend_on_ids']/tree//field[@name='remaining_hours']" position="after">
<field name="remaining_hours_so" widget="timesheet_uom" optional="hide" groups="base.group_user"/>
</xpath>
</field>
</record>
<record id="view_task_tree2_inherited" model="ir.ui.view">
<field name="name">project.task.tree.inherited</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="hr_timesheet.view_task_tree2_inherited" />
<field name="arch" type="xml">
<xpath expr="//field[@name='remaining_hours']" position="after">
<field name="remaining_hours_so" widget="timesheet_uom" optional="hide"/>
</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="arch" type="xml">
<xpath expr="//field[@name='timesheet_ids']/tree" position="attributes">
<!-- <field name="timesheet_ids"/> is already inside a block groups="hr_timesheet.group_hr_timesheet_user" -->
<attribute name="decoration-muted">timesheet_invoice_id != False</attribute>
</xpath>
<xpath expr="//field[@name='user_ids']" position="after">
<field name="is_project_map_empty" invisible="1" groups="hr_timesheet.group_hr_timesheet_user"/>
<field name="has_multi_sol" invisible="1" groups="hr_timesheet.group_hr_timesheet_user"/>
</xpath>
<xpath expr="//field[@name='partner_phone']" position="after">
<field name="pricing_type" invisible="1" groups="hr_timesheet.group_hr_timesheet_user"/>
</xpath>
<xpath expr="//field[@name='timesheet_ids']" position="attributes">
<!-- <field name="timesheet_ids"/> is already inside a block groups="hr_timesheet.group_hr_timesheet_user" -->
<attribute name="widget">so_line_one2many</attribute>
</xpath>
<xpath expr="//field[@name='timesheet_ids']/tree" position="inside">
<!-- <field name="timesheet_ids"/> is already inside a block groups="hr_timesheet.group_hr_timesheet_user" -->
<field name="is_so_line_edited" invisible="1" />
</xpath>
<xpath expr="//field[@name='timesheet_ids']/tree/field[@name='unit_amount']" position="before">
<!-- <field name="timesheet_ids"/> is already inside a block groups="hr_timesheet.group_hr_timesheet_user" -->
<field name="timesheet_invoice_id" invisible="1"/>
<field name="so_line" widget="so_line_field" groups="!sales_team.group_sale_salesman"
attrs="{'column_invisible': [('parent.allow_billable', '=', False)]}"
context="{'with_remaining_hours': True, 'with_price_unit': True}" options="{'no_create': True, 'no_open': True}"
domain="[('is_service', '=', True), ('order_partner_id', 'child_of', parent.commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])]"
optional="hide"/>
<field name="so_line" widget="so_line_field" groups="sales_team.group_sale_salesman"
attrs="{'column_invisible': [('parent.allow_billable', '=', False)]}"
context="{'with_remaining_hours': True, 'with_price_unit': True}" options="{'no_create': True}"
domain="[('is_service', '=', True), ('order_partner_id', 'child_of', parent.commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])]"
optional="hide"/>
</xpath>
<xpath expr="//field[@name='remaining_hours']" position="after">
<t groups="hr_timesheet.group_hr_timesheet_user">
<field name="sale_order_id" invisible="1"/>
<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)]}" class="o_td_label float-start">
<label class="fw-bold" for="remaining_hours_so" string="Remaining Hours on SO"
attrs="{'invisible': ['|', ('encode_uom_in_days', '=', True), ('remaining_hours_so', '&lt;', 0)]}"/>
<label class="fw-bold" for="remaining_hours_so" string="Remaining Days on SO"
attrs="{'invisible': ['|', ('encode_uom_in_days', '=', False), ('remaining_hours_so', '&lt;', 0)]}"/>
<label class="fw-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="fw-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)]}" decoration-danger="remaining_hours_so &lt; 0"></field>
</t>
</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="sale_project.view_sale_project_inherit_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='sale_line_id'][2]" position="attributes">
<attribute name="context">{'create': False, 'edit': False, 'delete': False, 'with_price_unit': True, 'with_remaining_hours': True}</attribute>
<!-- To do: move allow_billable field in sale_project and add attrs directly on field in master -->
<attribute name="attrs">
{'invisible': [('allow_billable', '=', False)]}
</attribute>
</xpath>
</field>
</record>
</odoo>

View file

@ -0,0 +1,53 @@
<?xml version="1.0"?>
<odoo>
<template id="project_update_default_description" inherit_id="project.project_update_default_description">
<!--As this template is rendered in an html field, the spaces may be interpreted as nbsp while editing. -->
<xpath expr="//div[@name='milestone']" position="before">
<br/>
<div t-if="show_sold">
<h3 style="font-weight: bolder"><u>Sold</u></h3>
<table class="table table-bordered table-striped">
<tbody>
<thead>
<td style="font-weight: bolder">Service</td>
<td style="font-weight: bolder">Sold</td>
<td style="font-weight: bolder">Effective</td>
<td style="font-weight: bolder">Remaining</td>
</thead>
<tr t-foreach="services['data']" t-as="service">
<t t-set="is_unit" t-value="service['is_unit']"/>
<td t-attf-class="#{ 'fst-italic' if is_unit else ''}"><t t-esc="service['name']"/></td>
<td t-attf-class="#{ 'fst-italic' if is_unit else ''}" style="text-align: right; vertical-align: middle;"><t t-esc="format_value(service['sold_value'], service['is_hour'])"/> <t t-esc="service['unit']"/></td>
<td t-attf-class="#{ 'fst-italic' if is_unit else ''}" style="text-align: right; vertical-align: middle;"><t t-esc="format_value(service['effective_value'], service['is_hour'])"/> <t t-esc="service['unit']"/></td>
<td t-attf-class="#{ 'fst-italic' if is_unit else ''}" style="text-align: right; vertical-align: middle;"><t t-esc="format_value(service['remaining_value'], service['is_hour'])"/> <t t-esc="service['unit']"/></td>
</tr>
<tfoot>
<td style="font-weight: bolder; text-align: right">Total</td>
<td style="font-weight: bolder; text-align: right; vertical-align: middle;"><t t-esc="format_value(services['total_sold'], services['is_hour'])"/> <t t-esc="services['company_unit_name']"/></td>
<td style="font-weight: bolder; text-align: right; vertical-align: middle;"><t t-esc="format_value(services['total_effective'], services['is_hour'])"/> <t t-esc="services['company_unit_name']"/></td>
<td style="font-weight: bolder; text-align: right; vertical-align: middle;"><t t-esc="format_value(services['total_remaining'], services['is_hour'])"/> <t t-esc="services['company_unit_name']"/></td>
</tfoot>
</tbody>
</table>
<br/>
</div>
<div name="profitability" t-if="show_profitability">
<t t-if="project.analytic_account_id and project.allow_billable and user.has_group('project.group_project_manager')" name="costs">
<h3 style="font-weight: bolder"><u>Profitability</u></h3>
The cost of the project is now at <t t-esc="profitability['costs_formatted']"/>, for a revenue of <t t-esc="profitability['revenues_formatted']"/>, leading to a
<span>
<font t-if="profitability['margin'] &gt; 0" style="color: rgb(0, 128, 0)">
<b><t t-esc="profitability['margin_formatted']"/></b>
</font>
<font t-elif="profitability['margin'] &lt; 0" style="color: rgb(128, 0, 0)">
<b><t t-esc="profitability['margin_formatted']"/></b>
</font>
<t t-else="" t-esc="profitability['margin_formatted']"/>
</span> margin (<t t-esc="profitability['margin_percentage']"/>%).
</t>
</div>
</xpath>
</template>
</odoo>

View file

@ -0,0 +1,44 @@
<?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 class="col-12 col-lg-6 o_setting_box" name="invoice_policy">
<div class="o_setting_left_pane">
<field name="invoice_policy" widget="upgrade_boolean"/>
</div>
<div class="o_setting_right_pane">
<label for="invoice_policy"/>
<div class="text-muted">
Timesheets taken into account when invoicing your time
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>

View file

@ -0,0 +1,29 @@
<?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_task']" position="after">
<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>

View file

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="sale_order_portal_content_inherit" inherit_id="sale.sale_order_portal_template">
<xpath expr="//li[.//a[@id='print_invoice_report']]" position="after">
<li t-if="sale_order.timesheet_count > 0 and sale_order.state in ('sale', 'done')" class="list-group-item flex-grow-1">
<div class="btn-toolbar flex-sm-nowrap justify-content-center">
<div class="btn-group">
<a t-att-href="'/my/timesheets?search_in=so&amp;search=%s' % sale_order.name">View Timesheets</a>
</div>
</div>
</li>
</xpath>
</template>
<template id="portal_my_timesheets_inherit" inherit_id="hr_timesheet.portal_my_timesheets">
<xpath expr="//t[@t-foreach='grouped_timesheets']/tbody/tr[hasclass('table-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">
<span t-field="sol.display_name"/>
<t t-if="sol.remaining_hours_available">
<span class="text-muted fw-normal">
<t t-if="is_uom_day">
(<span t-esc="timesheets._timesheet_convert_sol_uom(sol, 'uom.product_uom_day')" t-options='{"widget": "timesheet_uom"}'></span> Days Ordered, <span t-esc="timesheets._convert_hours_to_days(sol.remaining_hours)" t-options='{"widget": "timesheet_uom"}'></span> Days Remaining)
</t>
<t t-else="">
(<span t-esc="timesheets._timesheet_convert_sol_uom(sol, 'uom.product_uom_hour')" t-options='{"widget": "float_time"}'></span> Hours Ordered, <span t-esc="sol.remaining_hours" t-options='{"widget": "float_time"}'></span> Hours Remaining)
</t>
</span>
</t>
</t>
<t t-else="">
No Sales Order Item
</t>
</th>
<th colspan="1" class="text-end text-muted fw-normal">
<t t-if="is_uom_day">
Total: <span t-esc="timesheets._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>
<t t-elif="groupby == 'so'">
<t t-set="so" t-value="timesheets[0].order_id"/>
<th colspan="6">
<t t-if="so">
<span t-field="so.display_name"/>
</t>
<t t-else="">
No Sales Order
</t>
</th>
<th colspan="1" class="text-end text-muted">
<t t-if="is_uom_day">
Total: <span t-esc="timesheets._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>
<t t-elif="groupby == 'invoice'">
<t t-set="invoice" t-value="timesheets.timesheet_invoice_id"/>
<th colspan="6">
<t t-if="invoice">
<span t-field="invoice.display_name"/>
</t>
<t t-else="">
No Invoice
</t>
</th>
<th colspan="1" class="text-end text-muted">
<t t-if="is_uom_day">
Total: <span t-esc="timesheets._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'">Sales Order Item</th>
</xpath>
<xpath expr="//tbody//td[hasclass('text-end')]" 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>
<template id="portal_invoice_page_inherit" inherit_id="account.portal_invoice_page">
<xpath expr="//t[@t-set='entries']/ul/li[.//a[@id='print_invoice_report']]" position="after">
<li class="list-group-item flex-grow-1">
<div class="btn-toolbar flex-sm-nowrap justify-content-center">
<div class="btn-group mb-1">
<a t-if="invoice.move_type == 'out_invoice' and invoice.state in ('draft', 'posted') and invoice.timesheet_count > 0"
target="_blank" t-att-href="'/my/timesheets?search_in=invoice&amp;search=%s' % invoice.name">View Timesheets</a>
</div>
</div>
</li>
</xpath>
</template>
<template id="portal_my_task_inherit" inherit_id="project.portal_my_task">
<xpath expr="//div[@name='portal_my_task_second_column']" position="inside">
<t t-if="task.project_id.allow_billable">
<div t-if="task.sale_order_id"><strong>Sales Order:</strong>
<span t-if="so_accessible"><a t-attf-href="/my/orders/{{ task.sale_order_id.id }}" t-field="task.sale_order_id"></a></span>
<span t-else="" t-field="task.sale_order_id"></span>
</div>
<div t-if="invoices_accessible"><strong>Invoices:</strong>
<span t-foreach="task.sale_order_id.invoice_ids" t-as="invoice_line">
<t t-if="invoice_line.id in invoices_accessible">
<a t-attf-href="/my/invoices/{{ invoice_line.id }}">
<i t-if="invoice_line.state == 'draft'">Draft Invoice</i>
<t t-else="" t-esc="invoice_line.name"/></a>
<span t-if="not invoice_line_last">,</span>
</t>
<t t-else=""><span t-esc="invoice_line.name"></span><span t-if="not invoice_line_last">,</span></t>
</span>
</div>
<div t-if="task.sale_line_id.untaxed_amount_invoiced > 0"><strong>Invoiced:</strong>
<span t-field="task.sale_line_id.untaxed_amount_invoiced"/>
</div>
<div t-if="task.sale_line_id.untaxed_amount_to_invoice > 0"><strong>To invoice:</strong>
<span t-field="task.sale_line_id.untaxed_amount_to_invoice"/>
</div>
</t>
</xpath>
</template>
<template id="portal_timesheet_table_inherit" inherit_id="hr_timesheet.portal_timesheet_table">
<xpath expr="//div[@name='planned_time']" position="after">
<span t-if="task.allow_billable and task.sale_line_id and task.sale_line_id.remaining_hours_available" t-attf-class="{{task.remaining_hours_so &lt; 0 and 'text-danger' or ''}}">
<div t-if="is_uom_day">Remaining Days on SO: <span t-esc="timesheets._convert_hours_to_days(task.remaining_hours_so)" t-options='{"widget": "timesheet_uom"}'/></div>
<div t-else="">Remaining Hours on SO: <span t-esc="task.remaining_hours_so" t-options='{"widget": "float_time"}'/></div>
</span>
</xpath>
</template>
</odoo>