[MERGE] (hr_, sale_)timesheet: ease time tracking on employees (part I)

This commit merges various usability and business flow improvements
in time tracking application. Please refer to under-commits for more
details. Main changes are

 * simplify user experience when logging timesheets through UX, filters
   and menus improvements
 * log timesheets on employees instead of users

All tribute goes to @jem-odoo .
This commit is contained in:
Thibault Delavallée
2017-04-27 10:40:56 +02:00
21 changed files with 416 additions and 124 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ up a management by affair.
'report/hr_timesheet_report_view.xml',
'report/report_timesheet_templates.xml',
'hr_timesheet_installer.xml',
'hr_dashboard.xml',
'views/hr_views.xml',
],
'demo': [
'demo/hr_timesheet_demo.xml',
-25
View File
@@ -1,25 +0,0 @@
<odoo>
<!--Hr Department Inherit Kanban view-->
<record id="hr_department_view_kanban" model="ir.ui.view">
<field name="name">hr.department.kanban.inherit</field>
<field name="model">hr.department</field>
<field name="inherit_id" ref="hr.hr_department_view_kanban"/>
<field name="groups_id" eval="[(4,ref('hr_timesheet.group_hr_timesheet_user'))]"/>
<field name="arch" type="xml">
<data>
<xpath expr="//div[contains(@class, 'o_kanban_manage_reports')]" position="inside">
<div class="row">
<div class="col-xs-12 text-left">
<a name="%(act_hr_timesheet_report)d" type="action"
context="{ 'search_default_department_id': [active_id], 'default_department_id': active_id}">
Timesheets
</a>
</div>
</div>
</xpath>
</data>
</field>
</record>
</odoo>
+19 -5
View File
@@ -7,24 +7,38 @@ from odoo import api, fields, models
class AccountAnalyticLine(models.Model):
_inherit = 'account.analytic.line'
def _default_employee_id(self):
return self.env.user.employee_ids[0]
task_id = fields.Many2one('project.task', 'Task')
project_id = fields.Many2one('project.project', 'Project', domain=[('allow_timesheets', '=', True)])
department_id = fields.Many2one('hr.department', "Department", related='user_id.employee_ids.department_id', store=True, readonly=True)
employee_id = fields.Many2one('hr.employee', "Employee", default=_default_employee_id)
department_id = fields.Many2one('hr.department', "Department", related='employee_id.department_id', store=True, readonly=True)
@api.onchange('project_id')
def onchange_project_id(self):
self.task_id = False
@api.onchange('employee_id')
def _onchange_employee_id(self):
self.user_id = self.employee_id.user_id
@api.model
def create(self, vals):
if vals.get('project_id'):
project = self.env['project.project'].browse(vals.get('project_id'))
vals['account_id'] = project.analytic_account_id.id
vals = self._update_timesheet_values(vals)
return super(AccountAnalyticLine, self).create(vals)
@api.multi
def write(self, vals):
vals = self._update_timesheet_values(vals)
return super(AccountAnalyticLine, self).write(vals)
def _update_timesheet_values(self, vals):
if vals.get('project_id'):
project = self.env['project.project'].browse(vals.get('project_id'))
vals['account_id'] = project.analytic_account_id.id
return super(AccountAnalyticLine, self).write(vals)
if vals.get('employee_id'):
employee = self.env['hr.employee'].browse(vals['employee_id'])
vals['user_id'] = employee.user_id.id
return vals
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<menuitem id="hr_timesheet_menu_configuration" name="Configuration" parent="timesheet_menu_root"
groups="group_hr_timesheet_user" sequence="100"/>
groups="group_timesheet_manager" sequence="100"/>
<record id="hr_timesheet_config_settings_view_form" model="ir.ui.view">
<field name="name">hr.timesheet.config.settings.view.form</field>
@@ -47,7 +47,7 @@
</div>
</div>
<h2>Billing</h2>
<div class="row mt16 o_settings_container">
<div name="timesheet_billing" class="row mt16 o_settings_container" title="Sell services (e.g. project, maintenance), record time spent using Timesheets app and invoice them based on a fixed price (ordered quantity) or on the time spent (delivered quantity)">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="module_sale_timesheet"/>
@@ -57,6 +57,11 @@
<div class="text-muted">
Sell services and invoice time spent
</div>
<div class="content-group" name="msg_module_sale_timesheet">
<div class="text-warning mt16" attrs="{'invisible': [('module_sale_timesheet','=',False)]}">
Save this page and come back here to set up the feature.
</div>
</div>
</div>
</div>
</div>
+156 -31
View File
@@ -1,73 +1,155 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<menuitem id="timesheet_menu_root" name="Timesheets" sequence="55" groups="base.group_user" web_icon="hr_timesheet,static/description/icon_timesheet.png"/>
<menuitem id="menu_hr_time_tracking" name="My Timesheet" parent="timesheet_menu_root" sequence="5" />
<menuitem id="timesheet_menu_root"
name="Timesheets"
sequence="55"
groups="group_hr_timesheet_user"
web_icon="hr_timesheet,static/description/icon_timesheet.png"/>
<menuitem id="menu_hr_time_tracking"
name="Timesheet"
parent="timesheet_menu_root"
sequence="5"/>
<!--
Timesheet line Views
-->
<record id="hr_timesheet_line_tree" model="ir.ui.view">
<field name="name">account.analytic.line.tree.hr_timesheet</field>
<field name="model">account.analytic.line</field>
<field name="arch" type="xml">
<tree editable="top" string="Timesheet Activities">
<tree editable="top" string="Timesheet Activities" import="false">
<field name="date"/>
<field name="user_id" required="1" options='{"no_open": True}'/>
<field name="name"/>
<field name="project_id" required="1"/>
<field name="task_id" context="{'default_project_id': project_id}" domain="[('project_id', '=', project_id)]"/>
<field name="unit_amount" string="Duration" sum="Total time" widget="float_time"/>
<field name="unit_amount" string="Time (Hours)" sum="Total time" widget="float_time"/>
</tree>
</field>
</record>
<record id="timesheet_view_tree_user" model="ir.ui.view">
<field name="name">account.analytic.line.view.tree.with.user</field>
<field name="model">account.analytic.line</field>
<field name="inherit_id" ref="hr_timesheet_line_tree"/>
<field name="mode">primary</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='date']" position="after">
<field name="employee_id" required="1" options='{"no_open": True}'/>
<field name="user_id" invisible="1"/>
</xpath>
</field>
</record>
<record id="view_hr_timesheet_line_pivot" model="ir.ui.view">
<field name="name">account.analytic.line.pivot</field>
<field name="model">account.analytic.line</field>
<field name="arch" type="xml">
<pivot string="Timesheet">
<field name="user_id" type="row"/>
<field name="employee_id" type="row"/>
<field name="date" interval="month" type="col"/>
<field name="unit_amount" type="measure"/>
</pivot>
</field>
</record>
<record id="hr_timesheet_line_form" model="ir.ui.view">
<field name="name">account.analytic.line.form.inherit.hr_timesheet</field>
<field name="name">account.analytic.line.form</field>
<field name="model">account.analytic.line</field>
<field name="inherit_id" ref="analytic.view_account_analytic_line_form"/>
<field name="mode">primary</field>
<field name="priority">1</field>
<field name="inherit_id" eval="False"/>
<field name="arch" type="xml">
<field name="account_id" position="after">
<field name="project_id" required="1"/>
<field name="task_id" domain="[('project_id', '=', project_id)]"/>
</field>
<form string="Analytic Entry">
<sheet string="Analytic Entry">
<group>
<group>
<field name="date"/>
<field name="name"/>
<field name="project_id" required="1"/>
<field name="task_id" domain="[('project_id', '=', project_id)]"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group>
<field name="amount"/>
<field name="unit_amount"/>
<field name="currency_id" invisible="1"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="timesheet_view_form_user" model="ir.ui.view">
<field name="name">account.analytic.line.tree.with.user</field>
<field name="model">account.analytic.line</field>
<field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_form"/>
<field name="mode">primary</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='company_id']" position="before">
<field name="employee_id" required="1" options='{"no_open": True}'/>
<field name="user_id" invisible="1"/>
</xpath>
</field>
</record>
<record id="timesheet_view_calendar" model="ir.ui.view">
<field name="name">account.analytic.line.calendar</field>
<field name="model">account.analytic.line</field>
<field name="arch" type="xml">
<calendar string="Timesheets" date_start="date" color="employee_id">
<field name="name"/>
<field name="project_id"/>
</calendar>
</field>
</record>
<record id="hr_timesheet_line_search" model="ir.ui.view">
<field name="name">account.analytic.line.search</field>
<field name="model">account.analytic.line</field>
<field name="arch" type="xml">
<search string="Timesheet">
<field name="date"/>
<field name="user_id"/>
<field name="employee_id"/>
<field name="project_id"/>
<field name="task_id"/>
<field name="name"/>
<field name="department_id"/>
<filter name="mine" string="My Timesheets" domain="[('user_id', '=', uid)]"/>
<separator/>
<filter name="week" string="This Week"
domain="[
'&amp;',
('date', '>=', (context_today() + relativedelta(weeks=-1,days=1,weekday=0)).strftime('%Y-%m-%d')),
('date', '&lt;=', (context_today() + relativedelta(weekday=6)).strftime('%Y-%m-%d')),
]"/>
<filter name="month" string="This Month"
domain="[
'&amp;',
('date', '>=', (context_today() + relativedelta(day=1)).strftime('%Y-%m-%d')),
('date', '&lt;=', (context_today() + relativedelta(months=1, day=1, days=-1)).strftime('%Y-%m-%d')),
]"/>
<group expand="0" string="Group By">
<filter string="Users" domain="[]" context="{'group_by':'user_id'}"/>
<filter string="Project" domain="[]" context="{'group_by':'project_id'}"/>
<filter string="Task" domain="[]" context="{'group_by':'task_id'}"/>
<filter string="Product" domain="[]" context="{'group_by':'product_id'}"/>
<filter string="Timesheet Month" domain="[]" context="{'group_by':'date'}" help="Timesheet by Month"/>
<filter string="Employee" name="groupby_employee" domain="[]" context="{'group_by':'employee_id'}"/>
<filter string="Project" name="groupby_project" domain="[]" context="{'group_by':'project_id'}"/>
<filter string="Task" name="groupby_task" domain="[]" context="{'group_by':'task_id'}"/>
<filter string="Timesheet Month" name="groupby_date" domain="[]" context="{'group_by':'date'}" help="Timesheet by Month"/>
</group>
</search>
</field>
</record>
<!--
Menus and Actions
-->
<record id="act_hr_timesheet_line" model="ir.actions.act_window">
<field name="name">Activities</field>
<field name="name">My Timesheets</field>
<field name="res_model">account.analytic.line</field>
<field name="domain">[('project_id', '!=', False)]</field>
<field name="context">{"search_default_today":1}</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('project_id', '!=', False), ('user_id', '=', uid)]</field>
<field name="context">{"search_default_week":1}</field>
<field name="search_view_id" ref="hr_timesheet_line_search"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
@@ -80,22 +162,65 @@
</field>
</record>
<record model="ir.actions.act_window.view" id="act_hr_timesheet_line_view_tree">
<record id="act_hr_timesheet_line_view_tree" model="ir.actions.act_window.view">
<field name="view_mode">tree</field>
<field name="sequence">1</field>
<field name="view_id" ref="hr_timesheet.hr_timesheet_line_tree"/>
<field name="sequence" eval="4"/>
<field name="view_id" ref="hr_timesheet_line_tree"/>
<field name="act_window_id" ref="act_hr_timesheet_line"/>
</record>
<record model="ir.actions.act_window.view" id="act_hr_timesheet_line_view_form">
<record id="act_hr_timesheet_line_view_form" model="ir.actions.act_window.view">
<field name="view_mode">form</field>
<field name="sequence">2</field>
<field name="view_id" ref="hr_timesheet.hr_timesheet_line_form"/>
<field name="sequence" eval="5"/>
<field name="view_id" ref="hr_timesheet_line_form"/>
<field name="act_window_id" ref="act_hr_timesheet_line"/>
</record>
<menuitem id="menu_hr_working_hours" parent="menu_hr_time_tracking" action="act_hr_timesheet_line"
name="Detailed Activities"/>
<record id="timesheet_action_view_calendar" model="ir.actions.act_window.view">
<field name="sequence" eval="8"/>
<field name="view_mode">calendar</field>
<field name="view_id" ref="timesheet_view_calendar"/>
<field name="act_window_id" ref="act_hr_timesheet_line"/>
</record>
<menuitem id="timesheet_menu_activity_mine"
name="My Timesheets"
parent="menu_hr_time_tracking"
action="act_hr_timesheet_line"/>
<record id="timesheet_action_all" model="ir.actions.act_window">
<field name="name">All Timesheets</field>
<field name="res_model">account.analytic.line</field>
<field name="search_view_id" ref="hr_timesheet_line_search"/>
<field name="domain">[('project_id', '!=', False)]</field>
<field name="context">{'search_default_week':1, 'search_default_groupby_employee':1}</field>
</record>
<record id="timesheet_action_view_all_tree" model="ir.actions.act_window.view">
<field name="sequence" eval="4"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="timesheet_view_tree_user"/>
<field name="act_window_id" ref="timesheet_action_all"/>
</record>
<record id="timesheet_action_view_all_form" model="ir.actions.act_window.view">
<field name="sequence" eval="5"/>
<field name="view_mode">form</field>
<field name="view_id" ref="timesheet_view_form_user"/>
<field name="act_window_id" ref="timesheet_action_all"/>
</record>
<record id="timesheet_action_view_all_calendar" model="ir.actions.act_window.view">
<field name="sequence" eval="8"/>
<field name="view_mode">calendar</field>
<field name="view_id" ref="timesheet_view_calendar"/>
<field name="act_window_id" ref="timesheet_action_all"/>
</record>
<menuitem id="timesheet_menu_activity_all"
name="All Timesheets"
parent="menu_hr_time_tracking"
action="timesheet_action_all"/>
</data>
</odoo>
@@ -102,7 +102,8 @@
<field name="timesheet_ids" context="{'default_project_id': project_id}">
<tree editable="bottom" string="Timesheet Activities" default_order="date">
<field name="date"/>
<field name="user_id" required="1"/>
<field name="user_id" invisible="1"/>
<field name="employee_id" required="1"/>
<field name="name"/>
<field name="unit_amount" string="Duration" widget="float_time"/>
<field name="project_id" invisible="1"/>
@@ -153,7 +154,7 @@
<a name="%(act_hr_timesheet_line_by_project)d" type="action">Timesheets</a>
</div>
</xpath>
<xpath expr="//div[contains(@class, 'o_project_kanban_box')]/div[last()]" position="before">
<xpath expr="//div[contains(@class, 'o_project_kanban_box')]/div[last()]" position="after">
<div attrs="{'invisible': [('allow_timesheets', '=', False)]}" class="o_project_kanban_box">
<a t-if="record.allow_timesheets.raw_value" name="%(act_hr_timesheet_line_by_project)d" type="action">
<span class="o_label">Timesheets</span>
@@ -38,10 +38,15 @@
<field name="act_window_id" ref="act_hr_timesheet_report"/>
</record>
<menuitem id="menu_timesheets_reports"
name="Reporting"
parent="timesheet_menu_root"
groups="hr_timesheet.group_timesheet_manager"
sequence="99"/>
<menuitem id="menu_timesheets_reports" name="Reporting"
parent="timesheet_menu_root" sequence="99" groups="hr_timesheet.group_hr_timesheet_user"/>
<menuitem id="menu_hr_activity_analysis" parent="menu_timesheets_reports" action="act_hr_timesheet_report"
<menuitem id="menu_hr_activity_analysis"
parent="menu_timesheets_reports"
action="act_hr_timesheet_report"
name="Activity Analysis"/>
</data>
</odoo>
@@ -1,13 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="group_hr_timesheet_user" model="res.groups">
<field name="name">Officer</field>
<field name="category_id" ref="base.module_category_hr_timesheet"/>
<field name="implied_ids" eval="[(4, ref('hr.group_hr_user'))]"/>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
</record>
<record id="base.default_user" model="res.users">
<field name="groups_id" eval="[(4,ref('hr_timesheet.group_hr_timesheet_user'))]"/>
</record>
<data noupdate="1">
<record id="group_hr_timesheet_user" model="res.groups">
<field name="name">User</field>
<field name="category_id" ref="base.module_category_hr_timesheet"/>
<field name="implied_ids" eval="[(4, ref('hr.group_hr_user'))]"/>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
</record>
<record id="group_timesheet_manager" model="res.groups">
<field name="name">Manager</field>
<field name="category_id" ref="base.module_category_hr_timesheet"/>
<field name="implied_ids" eval="[(4, ref('group_hr_timesheet_user'))]"/>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
</record>
<record id="base.default_user" model="res.users">
<field name="groups_id" eval="[(4,ref('group_hr_timesheet_user'))]"/>
</record>
</data>
<data noupdate="0">
<!-- This rule is in noupdate mode because it can be modify by other advanced modules -->
<record id="timesheet_line_rule_user" model="ir.rule">
<field name="name">account.analytic.line.timesheet.user</field>
<field name="model_id" ref="analytic.model_account_analytic_line"/>
<field name="domain_force">[('user_id', '=', user.id), ('project_id', '!=', False)]</field>
<field name="groups" eval="[(4, ref('group_hr_timesheet_user'))]"/>
<field name="perm_create" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_read" eval="0"/>
</record>
</data>
<data noupdate="1">
<record id="timesheet_line_rule_manager" model="ir.rule">
<field name="name">account.analytic.line.timesheet.manager</field>
<field name="model_id" ref="analytic.model_account_analytic_line"/>
<field name="domain_force">[('project_id', '!=', False)]</field>
<field name="groups" eval="[(4, ref('group_timesheet_manager'))]"/>
<field name="perm_create" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_read" eval="1"/>
</record>
</data>
</odoo>
@@ -1,6 +1,8 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_account_analytic_line,analytic.account.analytic.line,analytic.model_account_analytic_line,hr_timesheet.group_hr_timesheet_user,1,1,1,1
access_hr_account_analytic_line_employee,analytic.account.analytic.line employee,analytic.model_account_analytic_line,base.group_user,1,1,1,1
access_product_product_user,product.product user,product.model_product_product,hr_timesheet.group_hr_timesheet_user,1,1,1,1
access_product_template_hr_timesheet,product.template.hr.timesheet,product.model_product_template,hr_timesheet.group_hr_timesheet_user,1,1,1,1
access_product_uom_hr_timesheet,product.uom.hr.timesheet,product.model_product_uom,hr_timesheet.group_hr_timesheet_user,1,1,1,1
access_account_analytic_line_user,analytic.account.analytic.line.timesheet.user,analytic.model_account_analytic_line,hr_timesheet.group_hr_timesheet_user,1,1,1,1
access_account_analytic_user,analytic.account.analytic.timesheet.user,analytic.model_account_analytic_account,hr_timesheet.group_hr_timesheet_user,1,1,0,0
access_product_product_user,product.product.timesheet.user,product.model_product_product,hr_timesheet.group_hr_timesheet_user,1,1,1,0
access_product_template_timesheet_user,product.template.timesheet.user,product.model_product_template,hr_timesheet.group_hr_timesheet_user,1,1,1,0
access_product_uom_hr_timesheet,product.uom.timesheet.user,product.model_product_uom,hr_timesheet.group_hr_timesheet_user,1,0,0,0
access_project_project,project.project.timesheet.user,model_project_project,hr_timesheet.group_hr_timesheet_user,1,0,0,0
access_project_task,project.task.timesheet.user,model_project_task,hr_timesheet.group_hr_timesheet_user,1,1,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_account_analytic_line access_account_analytic_line_user analytic.account.analytic.line analytic.account.analytic.line.timesheet.user analytic.model_account_analytic_line hr_timesheet.group_hr_timesheet_user 1 1 1 1
3 access_hr_account_analytic_line_employee access_account_analytic_user analytic.account.analytic.line employee analytic.account.analytic.timesheet.user analytic.model_account_analytic_line analytic.model_account_analytic_account base.group_user hr_timesheet.group_hr_timesheet_user 1 1 1 0 1 0
4 access_product_product_user product.product user product.product.timesheet.user product.model_product_product hr_timesheet.group_hr_timesheet_user 1 1 1 1 0
5 access_product_template_hr_timesheet access_product_template_timesheet_user product.template.hr.timesheet product.template.timesheet.user product.model_product_template hr_timesheet.group_hr_timesheet_user 1 1 1 1 0
6 access_product_uom_hr_timesheet product.uom.hr.timesheet product.uom.timesheet.user product.model_product_uom hr_timesheet.group_hr_timesheet_user 1 1 0 1 0 1 0
7 access_project_project project.project.timesheet.user model_project_project hr_timesheet.group_hr_timesheet_user 1 0 0 0
8 access_project_task project.task.timesheet.user model_project_task hr_timesheet.group_hr_timesheet_user 1 1 0 0
+58
View File
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="timesheet_action_from_employee" 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_line_search"/>
<field name="domain">[('project_id', '!=', False)]</field>
<field name="context">{
'search_default_month':1,
'search_default_employee_id': [active_id],
'default_employee_id': active_id
}</field>
</record>
<record id="hr_employee_view_form_inherit_timesheet" model="ir.ui.view">
<field name="name">hr.employee.form.timesheet</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='active_group']" position="before">
<!-- Empty invisible group to be filled by timesheet_grid and sale_timesheet -->
<group string="Timesheets" name="timesheet" invisible="1">
</group>
</xpath>
<xpath expr="//button[@name='toggle_active']" position="before">
<button class="oe_stat_button" type="action" name="%(timesheet_action_from_employee)d" icon="fa-calendar">
<div class="o_stat_info">
<span class="o_stat_text">Timesheets</span>
</div>
</button>
</xpath>
</field>
</record>
<record id="hr_department_view_kanban" model="ir.ui.view">
<field name="name">hr.department.kanban.inherit</field>
<field name="model">hr.department</field>
<field name="inherit_id" ref="hr.hr_department_view_kanban"/>
<field name="groups_id" eval="[(4,ref('hr_timesheet.group_hr_timesheet_user'))]"/>
<field name="arch" type="xml">
<data>
<xpath expr="//div[contains(@class, 'o_kanban_manage_reports')]" position="inside">
<div class="row">
<div class="col-xs-12 text-left">
<a name="%(act_hr_timesheet_report)d" type="action"
context="{ 'search_default_department_id': [active_id], 'default_department_id': active_id}">
Timesheets
</a>
</div>
</div>
</xpath>
</data>
</field>
</record>
</odoo>
+3 -1
View File
@@ -86,8 +86,10 @@ class Project(models.Model):
])
def _compute_task_count(self):
task_data = self.env['project.task'].read_group([('project_id', 'in', self.ids), '|', ('stage_id.fold', '=', False), ('stage_id', '=', False)], ['project_id'], ['project_id'])
result = dict((data['project_id'][0], data['project_id_count']) for data in task_data)
for project in self:
project.task_count = len(project.task_ids)
project.task_count = result.get(project.id, 0)
def _compute_task_needaction_count(self):
projects_data = self.env['project.task'].read_group([
-7
View File
@@ -246,7 +246,6 @@
<field name="alias_id"/>
<field name="alias_name"/>
<field name="alias_domain"/>
<field name="doc_count"/>
<field name="is_favorite"/>
<templates>
<t t-name="kanban-box">
@@ -291,12 +290,6 @@
<t t-esc="record.task_needaction_count.raw_value"/>
</a>
</div>
<div t-if="record.doc_count.raw_value" class="o_project_kanban_box">
<a name="attachment_tree_view" type="object">
<span class="o_value"><t t-esc="record.doc_count.value"/></span>
<span class="o_label">Documents</span>
</a>
</div>
</div>
</div>
</t>
@@ -32,7 +32,7 @@
<a name="%(action_project_issue_report_filtered)d" type="action">Issues</a>
</div>
</xpath>
<xpath expr="//div[contains(@class, 'o_project_kanban_box')]/div[last()]" position="before">
<xpath expr="//div[contains(@class, 'o_project_kanban_box')]/div[last()]" position="after">
<div attrs="{'invisible': [('use_issues', '=', False)]}" class="o_project_kanban_box">
<a name="%(act_project_project_2_project_issue_all)d" type="action">
<span class="o_value"><t t-raw="record.issue_ids.raw_value.length"/></span>
+40
View File
@@ -13,4 +13,44 @@
model="res.company"
name="_init_data_resource_calendar"
eval="[]"/>
<record id="resource_calendar_std_35h" model="resource.calendar">
<field name="name">Standard 35 Hours/Week</field>
<field name="company_id" ref="base.main_company"/>
<field name="attendance_ids"
eval="[
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12}),
(0, 0, {'name': 'Monday Evening', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12}),
(0, 0, {'name': 'Tuesday Evening', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12}),
(0, 0, {'name': 'Wednesday Evening', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 16}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12}),
(0, 0, {'name': 'Thursday Evening', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12}),
(0, 0, {'name': 'Friday Evening', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16})
]"
/>
</record>
<record id="resource_calendar_std_38h" model="resource.calendar">
<field name="name">Standard 38 Hours/Week</field>
<field name="company_id" ref="base.main_company"/>
<field name="attendance_ids"
eval="[
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12}),
(0, 0, {'name': 'Monday Evening', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16.6}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12}),
(0, 0, {'name': 'Tuesday Evening', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16.6}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12}),
(0, 0, {'name': 'Wednesday Evening', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 16.6}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12}),
(0, 0, {'name': 'Thursday Evening', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16.6}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12}),
(0, 0, {'name': 'Friday Evening', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16.6})
]"
/>
</record>
</odoo>
+2 -2
View File
@@ -23,7 +23,7 @@ class SaleConfiguration(models.TransientModel):
implied_group='product.group_uom')
group_discount_per_so_line = fields.Boolean("Discounts", implied_group='sale.group_discount_per_so_line')
group_stock_packaging = fields.Boolean("Packaging", implied_group='product.group_stock_packaging',
help="""Ability to select a package type in sales orders and
help="""Ability to select a package type in sales orders and
to force a quantity that is a multiple of the number of units per package.""")
module_sale_margin = fields.Boolean("Margins")
group_sale_layout = fields.Boolean("Sections on Sales Orders", implied_group='sale.group_sale_layout')
@@ -78,7 +78,7 @@ class SaleConfiguration(models.TransientModel):
module_delivery_ups = fields.Boolean("UPS")
module_delivery_usps = fields.Boolean("USPS")
module_timesheet_grid_sale = fields.Boolean("Timesheets")
module_sale_timesheet = fields.Boolean("Timesheets")
module_sale_ebay = fields.Boolean("eBay")
module_print_docsaway = fields.Boolean("Docsaway")
module_web_clearbit = fields.Boolean("Customer Autocomplete")
@@ -285,17 +285,17 @@
</div>
<h2>Timesheets</h2>
<div class="row mt16 o_settings_container">
<div class="col-xs-12 col-md-6 o_setting_box" title="Create new projects &amp; tasks for sold services, record time spent and get delivered quantities updated in sales orders accordingly." id="timesheet_grid_sale">
<div class="col-xs-12 col-md-6 o_setting_box" title="Sell services (e.g. project, maintenance), record time spent using Timesheets app and invoice them based on a fixed price (ordered quantity) or on the time spent (delivered quantity)" name="sale_timesheet_box">
<div class="o_setting_left_pane">
<field name="module_timesheet_grid_sale" widget="upgrade_boolean"/>
<field name="module_sale_timesheet" widget="upgrade_boolean"/>
</div>
<div class="o_setting_right_pane">
<label for="module_timesheet_grid_sale"/>
<label for="module_sale_timesheet"/>
<div class="text-muted">
Record time spent on services and invoice it
</div>
<div class="content-group" id="msg_module_timesheet_grid_sale">
<div class="text-warning mt16" attrs="{'invisible': [('module_timesheet_grid_sale','=',False)]}">
<div class="content-group" name="msg_module_sale_timesheet">
<div class="text-warning mt16" attrs="{'invisible': [('module_sale_timesheet','=',False)]}">
Save this page and come back here to set up the feature.
</div>
</div>
+1 -1
View File
@@ -27,4 +27,4 @@ have real delivered quantities in sales orders.
'data/sale_service_demo.xml',
],
'auto_install': True,
}
}
+20 -8
View File
@@ -37,16 +37,28 @@ class AccountAnalyticLine(models.Model):
if values.get('amount'):
return {}
unit_amount = values.get('unit_amount', 0.0) or self.unit_amount
user_id = values.get('user_id') or self.user_id.id or self._default_user()
user = self.env['res.users'].browse([user_id])
emp = self.env['hr.employee'].search([('user_id', '=', user_id)], limit=1)
cost = emp and emp.timesheet_cost or 0.0
uom = (emp or user).company_id.project_time_mode_id
# Nominal employee cost = 1 * company project UoM (project_time_mode_id)
# find the employee
employee_id = values.get('employee_id') or self.employee_id.id or self._default_employee_id().id
employee = self.env['hr.employee'].browse(employee_id) if employee_id else self.env['hr.employee']
if not employee:
user_id = values.get('user_id') or self.user_id.id or self._default_user()
employee = self.env['hr.employee'].search([('user_id', '=', user_id)], limit=1)
# find the analytic account
account_id = values.get('account_id') or self.account_id.id
analytic_account = self.env['account.analytic.account'].browse(account_id) if account_id else self.env['account.analytic.account']
# convert employee cost into timesheet (analytic account) currency
uom = employee.company_id.project_time_mode_id
cost = employee.timesheet_cost or 0.0
amount = -unit_amount * cost
amount_converted = employee.currency_id.compute(amount, analytic_account.currency_id)
return {
'amount': -unit_amount * cost,
'amount': amount_converted,
'product_uom_id': uom.id,
'account_id': values.get('account_id') or self.account_id.id or emp.account_id.id,
'account_id': account_id,
}
return {}
+2 -2
View File
@@ -8,5 +8,5 @@ class HrEmployee(models.Model):
_inherit = 'hr.employee'
# FIXME: this field should be in module hr_timesheet, not sale_timesheet
timesheet_cost = fields.Float('Timesheet Cost', default=0.0)
account_id = fields.Many2one('account.account', string='Account')
timesheet_cost = fields.Monetary('Timesheet Cost', currency_field='currency_id', default=0.0)
currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True)
@@ -6,6 +6,19 @@ from odoo.addons.sale.tests.test_sale_common import TestSale
class TestSaleTimesheet(TestSale):
def setUp(self):
super(TestSaleTimesheet, self).setUp()
# link hr employee to res.users of sale tests
self.employee_user = self.env['hr.employee'].create({
'name': 'Employee User',
'user_id': self.user.id,
})
self.employee_manager = self.env['hr.employee'].create({
'name': 'Employee Manager',
'user_id': self.manager.id,
})
def test_timesheet_order(self):
""" Test timesheet invoicing with 'invoice on order' timetracked products """
# intial so
@@ -26,7 +39,7 @@ class TestSaleTimesheet(TestSale):
'name': 'Test Line',
'project_id': so.project_project_id.id,
'unit_amount': 10.5,
'user_id': self.manager.id,
'employee_id': self.employee_user.id,
})
self.assertEqual(so.order_line.qty_delivered, 10.5, 'Sale Timesheet: timesheet does not increase delivered quantity on so line')
self.assertEqual(so.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so')
@@ -35,7 +48,7 @@ class TestSaleTimesheet(TestSale):
'name': 'Test Line',
'project_id': so.project_project_id.id,
'unit_amount': 39.5,
'user_id': self.user.id,
'employee_id': self.employee_user.id,
})
self.assertEqual(so.order_line.qty_delivered, 50, 'Sale Timesheet: timesheet does not increase delivered quantity on so line')
self.assertEqual(so.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so')
@@ -44,7 +57,7 @@ class TestSaleTimesheet(TestSale):
'name': 'Test Line',
'project_id': so.project_project_id.id,
'unit_amount': 10,
'user_id': self.user.id,
'employee_id': self.employee_user.id,
})
self.assertEqual(so.order_line.qty_delivered, 60, 'Sale Timesheet: timesheet does not increase delivered quantity on so line')
self.assertEqual(so.invoice_status, 'upselling', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so')
@@ -69,7 +82,7 @@ class TestSaleTimesheet(TestSale):
'name': 'Test Line',
'project_id': so.project_project_id.id,
'unit_amount': 10.5,
'user_id': self.manager.id,
'employee_id': self.employee_manager.id,
})
self.assertEqual(so.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should set the so in "to invoice" status when logged')
inv_id = so.action_invoice_create()
@@ -80,7 +93,7 @@ class TestSaleTimesheet(TestSale):
'name': 'Test Line',
'project_id': so.project_project_id.id,
'unit_amount': 39.5,
'user_id': self.user.id,
'employee_id': self.employee_user.id,
})
self.assertEqual(so.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should not modify the invoice_status of the so')
so.action_invoice_create()
@@ -90,7 +103,7 @@ class TestSaleTimesheet(TestSale):
'name': 'Test Line',
'project_id': so.project_project_id.id,
'unit_amount': 10,
'user_id': self.user.id,
'employee_id': self.employee_user.id,
})
self.assertEqual(so.invoice_status, 'to invoice', 'Sale Timesheet: supplementary timesheets do not change the status of the SO')
@@ -113,7 +126,7 @@ class TestSaleTimesheet(TestSale):
'name': 'Test Line',
'project_id': so.project_project_id.id,
'unit_amount': 16,
'user_id': self.manager.id,
'employee_id': self.employee_manager.id,
})
self.assertEqual(so.order_line.qty_delivered, 2, 'Sale: uom conversion of timesheets is wrong')
@@ -121,7 +134,7 @@ class TestSaleTimesheet(TestSale):
'name': 'Test Line',
'project_id': so.project_project_id.id,
'unit_amount': 24,
'user_id': self.user.id,
'employee_id': self.employee_user.id,
})
so.action_invoice_create()
self.assertEqual(so.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on delivery" timesheets should not modify the invoice_status of the so')
+10 -6
View File
@@ -3,13 +3,17 @@
<record id="hr_timesheet_employee_extd_form" model="ir.ui.view">
<field name="name">hr.timesheet.employee.extd_form</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="inherit_id" ref="hr_timesheet.hr_employee_view_form_inherit_timesheet"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='active_group']" position="before">
<group string="Timesheets">
<field name="timesheet_cost"/>
<field name="account_id"/>
</group>
<xpath expr="//group[@name='timesheet']" position="inside">
<label for="timesheet_cost"/>
<div>
<field name="timesheet_cost" class="oe_inline"/> per hour
<field name="currency_id" invisible="1"/>
</div>
</xpath>
<xpath expr="//group[@name='timesheet']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
</field>
</record>