[IMP] hr_timesheet: Small cleaning and linting

This commit is contained in:
Yannick Tivisse
2019-12-19 17:21:17 +01:00
parent 954e428fc9
commit 1bf2bfd2a1
16 changed files with 70 additions and 145 deletions
@@ -65,10 +65,6 @@
<field name="model_id" ref="analytic.model_account_analytic_line"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('account.group_account_invoice'))]"/>
<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>
+20 -23
View File
@@ -1,29 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- For a perfect onboarding, we want the tour of project to be consumed
only by admin when installing hr_timesheet, to avoid confusing -->
<record id="web_tour_project_consumed_by_admin" model="web_tour.tour">
<field name="name">project_tour</field>
<field name="user_id" ref='base.user_admin'/>
</record>
<!-- Set the JS widget -->
<record id="uom.product_uom_day" model="uom.uom">
<field name="timesheet_widget">float_toggle</field>
</record>
<!-- Set the JS widget -->
<record id="uom.product_uom_day" model="uom.uom">
<field name="timesheet_widget">float_toggle</field>
</record>
<record id="uom.product_uom_hour" model="uom.uom">
<field name="timesheet_widget">float_time</field>
</record>
<record id="uom.product_uom_hour" model="uom.uom">
<field name="timesheet_widget">float_time</field>
</record>
<!-- Force Analytic account creation for projects allowing timesheet (default is True) -->
<function
model="project.project"
name="_init_data_analytic_account"
eval="[]"/>
<!-- Force Analytic account creation for projects allowing timesheet (default is True) -->
<function
model="project.project"
name="_init_data_analytic_account"
eval="[]"/>
<function model="ir.config_parameter" name="set_param" eval="('hr_timesheet.timesheet_rounding', '15')"/>
<function model="ir.config_parameter" name="set_param" eval="('hr_timesheet.timesheet_min_duration', '15')"/>
</data>
<record id="ir_config_parameter_timesheet_rounding" model="ir.config_parameter">
<field name="key">hr_timesheet.timesheet_rounding</field>
<field name="value">15</field>
</record>
<record id="ir_config_parameter_timesheet_min_duration" model="ir.config_parameter">
<field name="key">hr_timesheet.timesheet_min_duration</field>
<field name="value">15</field>
</record>
</odoo>
+4 -31
View File
@@ -48,12 +48,9 @@ class AccountAnalyticLine(models.Model):
employee_id = fields.Many2one('hr.employee', "Employee", check_company=True, domain=_domain_employee_id)
department_id = fields.Many2one('hr.department', "Department", compute='_compute_department_id', store=True, compute_sudo=True)
encoding_uom_id = fields.Many2one('uom.uom', compute='_compute_encoding_uom_id')
# Timer Fields
display_timer = fields.Boolean(
compute='_compute_display_timer',
help="used to display the timer if the encoding unit is 'Hours'"
)
help="Technical field used to display the timer if the encoding unit is 'Hours'.")
def _compute_encoding_uom_id(self):
for analytic_line in self:
@@ -82,10 +79,6 @@ class AccountAnalyticLine(models.Model):
for line in self:
line.department_id = line.employee_id.department_id
# ----------------------------------------------------
# ORM overrides
# ----------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
default_user_id = self._default_user()
@@ -134,10 +127,6 @@ class AccountAnalyticLine(models.Model):
node.set('string', _('Duration (%s)') % (re.sub(r'[\(\)]', '', encoding_uom.name or '')))
return etree.tostring(doc, encoding='unicode')
# ----------------------------------------------------
# Business Methods
# ----------------------------------------------------
def _timesheet_get_portal_domain(self):
return ['|', '&',
('task_id.project_id.privacy_visibility', '=', 'portal'),
@@ -207,51 +196,37 @@ class AccountAnalyticLine(models.Model):
})
return result
# ----------------------------------------------------
# Timer Methods
# ----------------------------------------------------
def _compute_display_timer(self):
""" Check if the encoding unit is 'Hours',
if yes then the timer button is displayed
"""
uom_hour = self.env.ref('uom.product_uom_hour')
for analytic_line in self:
analytic_line.display_timer = analytic_line.encoding_uom_id.id == self.env.ref('uom.product_uom_hour').id
analytic_line.display_timer = analytic_line.encoding_uom_id == uom_hour
def action_timer_start(self):
""" Action start the timer of the current timesheet.
Start timer and search if another timer hasn't been launched.
""" Start timer and search if another timer hasn't been launched.
If yes, then stop the timer before launch this timer.
"""
self.ensure_one()
if not self.timer_start and self.display_timer:
self._stop_running_timers()
super().action_timer_start()
def _stop_running_timers(self):
""" Search if a timesheet has a timer activated and stop the timer.
Check if a timer is activated for another timesheet
if yes, then update unit_amount field and stop timer,
otherwise, do nothing.
"""
analytic_line = self.search([('timer_start', '!=', False), ('user_id', '=', self.env.uid)])
if analytic_line:
analytic_line.action_timer_stop()
def action_timer_stop(self):
""" Action stop the timer of the current timesheet.
When the timer must be stopped, we must calculate the new
unit_amount based on the timer and the previous value of
unit_amount for the current timesheet.
"""
self.ensure_one()
if self.timer_start and self.display_timer:
minutes_spent = self._get_minutes_spent()
if self.unit_amount == 0 and minutes_spent < 1:
# Check if unit_amount equals 0 and minutes_spent is less than 1 minute,
# if yes, then remove the timesheet
@@ -261,7 +236,5 @@ class AccountAnalyticLine(models.Model):
amount = self.unit_amount
else:
amount = self.unit_amount + minutes_spent * 60 / 3600
self.write({'unit_amount': amount})
super().action_timer_stop()
+6 -20
View File
@@ -20,15 +20,12 @@ class Project(models.Model):
allow_timesheet_timer = fields.Boolean('Timesheet Timer', default=False, help="Use a timer to record timesheets on tasks")
_sql_constraints = [
('timer_only_when_timesheet', "CHECK((allow_timesheets = 'f' AND allow_timesheet_timer = 'f') OR (allow_timesheets = 't'))", 'The timesheet timer can only be activated on project allowing timesheet.'),
('timer_only_when_timesheet', "CHECK((allow_timesheets = 'f' AND allow_timesheet_timer = 'f') OR (allow_timesheets = 't'))", 'The timesheet timer can only be activated on project allowing timesheets.'),
]
@api.onchange('partner_id')
def _onchange_partner_id(self):
domain = []
if self.partner_id:
domain = [('partner_id', '=', self.partner_id.id)]
return {'domain': {'analytic_account_id': domain}}
return {'domain': {'analytic_account_id': [('partner_id', '=', self.partner_id.id)] if self.partner_id else []}}
@api.onchange('analytic_account_id')
def _onchange_analytic_account(self):
@@ -74,16 +71,12 @@ class Project(models.Model):
project._create_analytic_account()
result = super(Project, self).write(values)
if 'allow_timesheet_timer' in values and not values.get('allow_timesheet_timer'):
self.env['project.task'].with_context(active_test=False).search([('project_id', 'in', self.ids)]).write({
self.with_context(active_test=False).mapped('task_ids').write({
'timer_start': False,
'timer_pause': False,
})
return result
# ---------------------------------------------------
# Business Methods
# ---------------------------------------------------
@api.model
def _init_data_analytic_account(self):
self.search([('analytic_account_id', '=', False), ('allow_timesheets', '=', True)])._create_analytic_account()
@@ -102,8 +95,9 @@ class Task(models.Model):
subtask_effective_hours = fields.Float("Sub-tasks Hours Spent", compute='_compute_subtask_effective_hours', store=True, help="Sum of actually spent hours on the subtask(s)")
timesheet_ids = fields.One2many('account.analytic.line', 'task_id', 'Timesheets')
timer_start = fields.Datetime("Timesheet Timer Start", default=None)
timer_start = fields.Datetime("Timesheet Timer Start")
timer_pause = fields.Datetime("Timesheet Timer Last Pause")
# YTI FIXME: Those field seems quite useless
timesheet_timer_first_start = fields.Datetime("Timesheet Timer First Use", readonly=True)
timesheet_timer_last_stop = fields.Datetime("Timesheet Timer Last Use", readonly=True)
display_timesheet_timer = fields.Boolean("Display Timesheet Time", compute='_compute_display_timesheet_timer')
@@ -145,10 +139,6 @@ class Task(models.Model):
for task in self:
task.display_timesheet_timer = task.allow_timesheets and task.project_id.allow_timesheet_timer and task.analytic_account_active
# ---------------------------------------------------------
# ORM
# ---------------------------------------------------------
def write(self, values):
# a timesheet must have an analytic account (and a project)
if 'project_id' in values and self and not values.get('project_id'):
@@ -173,10 +163,6 @@ class Task(models.Model):
result['arch'] = self.env['account.analytic.line']._apply_timesheet_label(result['arch'])
return result
# ---------------------------------------------------------
# Timer Methods
# ---------------------------------------------------------
def action_timer_start(self):
self.ensure_one()
if not self.timesheet_timer_first_start:
@@ -201,7 +187,7 @@ class Task(models.Model):
def _action_create_timesheet(self, time_spent):
return {
"name": _("Confirm Time Spent"),
"name": _("Validate Spent Time"),
"type": 'ir.actions.act_window',
"res_model": 'project.task.create.timesheet',
"views": [[False, "form"]],
+12 -23
View File
@@ -7,25 +7,20 @@ from odoo import models, fields, api
class TimerMixin(models.AbstractModel):
_name = 'timer.mixin'
_description = 'Timer Mixin'
# YTI Note: This mixin is supposed to be extended to
# any models
timer_start = fields.Datetime("Timer Start")
timer_pause = fields.Datetime("Timer Last Pause")
# is_timer_running field is used with timer_toggle_button widget
# to create a timer button in the view.
# If the timer field is set on False,
# then it displays a button with fa-icon-play icon.
# Otherwise, it displays a button with fa-icon-stop icon
is_timer_running = fields.Boolean(compute="_compute_timer")
@api.depends('timer_start')
def _compute_timer(self) -> None:
def _compute_timer(self):
for record in self:
record.is_timer_running = bool(record.timer_start)
def action_timer_start(self) -> None:
def action_timer_start(self):
""" Action start the timer.
Start timer and search if another timer hasn't been launched.
If yes, then stop the timer before launch this timer.
"""
@@ -45,32 +40,26 @@ class TimerMixin(models.AbstractModel):
self.write({'timer_start': False, 'timer_pause': False})
return minutes_spent
def _get_minutes_spent(self) -> float:
""" Compute the minutes spent with the timer
:return minutes spent
"""
def _get_minutes_spent(self):
start_time = self.timer_start
stop_time = fields.Datetime.now()
# timer was either running or paused
if self.timer_pause:
start_time += (stop_time - self.timer_pause)
return (stop_time - start_time).total_seconds() / 60
def action_timer_pause(self) -> None:
def action_timer_pause(self):
self.write({'timer_pause': fields.Datetime.now()})
def action_timer_resume(self) -> None:
def action_timer_resume(self):
new_start = self.timer_start + (fields.Datetime.now() - self.timer_pause)
self.write({'timer_start': new_start, 'timer_pause': False})
@api.model
def get_server_time(self):
""" Get the time of the server
The problem with the timer, it's the time can be different between server side and client side.
We need to have the time of the server and don't use the local time. Then, we have a timer beginning at 0:00
and not 23:59 or something else.
""" Returns the server time.
The timer widget needs the server time instead of the client time
to avoid time desynchronization issues like the timer beginning at 0:00
and not 23:59 and so on.
"""
return fields.Datetime.now()
@@ -36,17 +36,12 @@
<field name="domain_force">[
('user_id', '=', user.id),
('project_id', '!=', False),
'|',
'|', '|',
('project_id.privacy_visibility', '!=', 'followers'),
'|',
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
('task_id.message_partner_ids', 'in', [user.partner_id.id])
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
('task_id.message_partner_ids', 'in', [user.partner_id.id])
]</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="1"/>
</record>
<record id="timesheet_line_rule_approver" model="ir.rule">
@@ -59,10 +54,6 @@
('project_id.message_partner_ids', 'in', [user.partner_id.id])
]</field>
<field name="groups" eval="[(4, ref('hr_timesheet.group_hr_timesheet_approver'))]" />
<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>
<record id="timesheet_line_rule_manager" model="ir.rule">
@@ -70,10 +61,6 @@
<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>
+2 -2
View File
@@ -2,8 +2,8 @@ odoo.define('hr_timesheet.Timer', function (require) {
"use strict";
/**
* This class allow to facilitate to display the timer
* in unit_amount field of account.analytic.line.
* This class improves the timer display using the
* unit_amount field on account.analytic.line.
*/
class Timer {
constructor(hours, minutes, seconds) {
+2 -1
View File
@@ -19,6 +19,7 @@ var TimerFieldWidget = AbstractField.extend({
* @private
*/
_render: function () {
this._super.apply(this, arguments);
this._startTimeCounter();
},
/**
@@ -52,7 +53,7 @@ var TimerFieldWidget = AbstractField.extend({
return this._rpc({
model: 'account.analytic.line',
method: 'get_server_time',
args: [false]
args: []
});
}
});
@@ -96,7 +96,7 @@ const FieldTimesheetTime = basicFields.FieldFloatTime.extend({
const time = await this._rpc({
model: this.model,
method: 'get_server_time',
args: [false]
args: []
});
this.time = Timer.createTimer(this.recordData.unit_amount, this.recordData.timer_start, time);
return this._startTimeCounter();
@@ -23,4 +23,4 @@
&:hover > i.o-timer-stop-button {
color: darken($red, 15%) !important;
}
}
}
@@ -167,7 +167,7 @@
<t t-esc="record.name.value"/>
</span>
<div class="float-right">
<field name="is_timer_running" widget="timer_toggle_button" readonly='True' attrs="{ 'invisible': [('display_timer', '=', False)] }" />
<field name="is_timer_running" widget="timer_toggle_button" attrs="{ 'invisible': [('display_timer', '=', False)] }" />
</div>
</div>
</div>
+1 -1
View File
@@ -572,7 +572,7 @@ class SaleOrder(models.Model):
action['context'] = context
return action
def _create_invoices(self, grouped=False, final=False):
def _create_invoices(self, grouped=False, final=False, date=None):
"""
Create the invoice associated to the SO.
:param grouped: if True, invoices are grouped by SO id. If False, invoices are grouped by
+1 -3
View File
@@ -59,7 +59,7 @@ class AccountMove(models.Model):
domain = line._timesheet_domain_get_invoiced_lines(sale_line_delivery)
if date:
domain = expression.AND([domain, [('date', '<=', date)]])
timesheets = self.env['account.analytic.line'].search(domain).sudo()
timesheets = self.env['account.analytic.line'].sudo().search(domain)
timesheets.write({'timesheet_invoice_id': line.move_id.id})
@@ -73,9 +73,7 @@ class AccountMoveLine(models.Model):
:return a normalized domain
"""
return [
'&',
('so_line', 'in', sale_line_delivery.ids),
'&',
('timesheet_invoice_id', '=', False),
('project_id', '!=', False)
]
@@ -62,7 +62,7 @@ class Project(models.Model):
# hours (from timesheet) and rates (by billable type)
dashboard_domain = [('project_id', 'in', self.ids), ('timesheet_invoice_type', '!=', False), '|', ('so_line', '=', False), ('so_line.state', '!=', 'cancel')] # force billable type
dashboard_data = self.env['account.analytic.line'].sudo().read_group(dashboard_domain, ['unit_amount', 'timesheet_invoice_type'], ['timesheet_invoice_type'])
dashboard_data = self.env['account.analytic.line'].read_group(dashboard_domain, ['unit_amount', 'timesheet_invoice_type'], ['timesheet_invoice_type'])
dashboard_total_hours = sum([data['unit_amount'] for data in dashboard_data]) + total_canceled_hours
for data in dashboard_data:
billable_type = data['timesheet_invoice_type']
+9 -5
View File
@@ -65,8 +65,16 @@ class SaleOrder(models.Model):
@api.depends('timesheet_ids', 'company_id.timesheet_encode_uom_id')
def _compute_timesheet_total_duration(self):
for sale_order in self:
duration_list = []
timesheets = sale_order.timesheet_ids if self.user_has_groups('hr_timesheet.group_hr_timesheet_approver') else sale_order.timesheet_ids.filtered(lambda t: t.user_id.id == self.env.uid)
sale_order.timesheet_total_duration = sum([timesheet.unit_amount for timesheet in timesheets])
for timesheet in timesheets:
timesheet_uom = timesheet.product_uom_id or timesheet.company_id.project_time_mode_id
if timesheet_uom != sale_order.timesheet_encode_uom_id and timesheet_uom.category_id == sale_order.timesheet_encode_uom_id.category_id:
duration_list.append(timesheet_uom._compute_quantity(timesheet.unit_amount, sale_order.timesheet_encode_uom_id))
else:
duration_list.append(timesheet.unit_amount)
sale_order.timesheet_total_duration = sum(duration_list)
@api.onchange('project_id')
def _onchange_project_id(self):
@@ -144,18 +152,14 @@ class SaleOrder(models.Model):
def _create_invoices(self, grouped=False, final=False, date=None):
""" Override the _create_invoice method in sale.order model in sale module
Add new parameter in this method, to invoice sale.order with a date. This date is used in sale_make_invoice_advance_inv into this module.
:param date
:return {account.move}: the invoices created
"""
moves = super(SaleOrder, self)._create_invoices(grouped, final)
moves._link_timesheets_to_invoice(date)
return moves
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
@@ -6,18 +6,12 @@ class SaleAdvancePaymentInv(models.TransientModel):
@api.model
def _default_invoicing_timesheet_enabled(self):
product_ids = None
if self._context.get('active_id', False):
sale_order = self.env['sale.order'].browse(self._context.get('active_id'))
product_ids = sale_order.order_line.filtered(lambda sol: sol.invoice_status == 'to invoice').mapped('product_id')
elif self._context.get('active_ids', []):
sale_orders = self.env['sale.order'].browse(self._context.get('active_ids'))
order_lines = sale_orders.mapped('order_line').filtered(lambda sol: sol.invoice_status == 'to invoice')
product_ids = order_lines.mapped('product_id')
if product_ids:
return any(product._is_delivered_timesheet() for product in product_ids)
return False
if 'active_id' not in self._context and 'active_ids' not in self._context:
return False
sale_orders = self.env['sale.order'].browse(self._context.get('active_id') or self._context.get('active_ids'))
order_lines = sale_orders.mapped('order_line').filtered(lambda sol: sol.invoice_status == 'to invoice')
product_ids = order_lines.mapped('product_id').filtered(lambda p: p._is_delivered_timesheet())
return bool(product_ids)
date_invoice_timesheet = fields.Date(
string='Invoice Timesheets Up To This Date',