[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:
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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="[
|
||||
'&',
|
||||
('date', '>=', (context_today() + relativedelta(weeks=-1,days=1,weekday=0)).strftime('%Y-%m-%d')),
|
||||
('date', '<=', (context_today() + relativedelta(weekday=6)).strftime('%Y-%m-%d')),
|
||||
]"/>
|
||||
<filter name="month" string="This Month"
|
||||
domain="[
|
||||
'&',
|
||||
('date', '>=', (context_today() + relativedelta(day=1)).strftime('%Y-%m-%d')),
|
||||
('date', '<=', (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
|
||||
|
@@ -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>
|
||||
@@ -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([
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -27,4 +27,4 @@ have real delivered quantities in sales orders.
|
||||
'data/sale_service_demo.xml',
|
||||
],
|
||||
'auto_install': True,
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user