214 lines
9.5 KiB
Python
214 lines
9.5 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
import time
|
||
|
from datetime import datetime
|
||
|
from dateutil.relativedelta import relativedelta
|
||
|
|
||
|
from odoo import api, fields, models
|
||
|
from odoo.tools.translate import _
|
||
|
from odoo.tools.sql import drop_view_if_exists
|
||
|
from odoo.exceptions import UserError, ValidationError
|
||
|
|
||
|
|
||
|
class HrTimesheetSheet(models.Model):
|
||
|
_name = "hr_timesheet_sheet.sheet"
|
||
|
_inherit = ['mail.thread', 'ir.needaction_mixin']
|
||
|
_table = 'hr_timesheet_sheet_sheet'
|
||
|
_order = "id desc"
|
||
|
_description = "Timesheet"
|
||
|
|
||
|
def _default_date_from(self):
|
||
|
user = self.env['res.users'].browse(self.env.uid)
|
||
|
r = user.company_id and user.company_id.timesheet_range or 'month'
|
||
|
if r == 'month':
|
||
|
return time.strftime('%Y-%m-01')
|
||
|
elif r == 'week':
|
||
|
return (datetime.today() + relativedelta(weekday=0, days=-6)).strftime('%Y-%m-%d')
|
||
|
elif r == 'year':
|
||
|
return time.strftime('%Y-01-01')
|
||
|
return fields.Date.context_today(self)
|
||
|
|
||
|
def _default_date_to(self):
|
||
|
user = self.env['res.users'].browse(self.env.uid)
|
||
|
r = user.company_id and user.company_id.timesheet_range or 'month'
|
||
|
if r == 'month':
|
||
|
return (datetime.today() + relativedelta(months=+1, day=1, days=-1)).strftime('%Y-%m-%d')
|
||
|
elif r == 'week':
|
||
|
return (datetime.today() + relativedelta(weekday=6)).strftime('%Y-%m-%d')
|
||
|
elif r == 'year':
|
||
|
return time.strftime('%Y-12-31')
|
||
|
return fields.Date.context_today(self)
|
||
|
|
||
|
def _default_employee(self):
|
||
|
emp_ids = self.env['hr.employee'].search([('user_id', '=', self.env.uid)])
|
||
|
return emp_ids and emp_ids[0] or False
|
||
|
|
||
|
name = fields.Char(string="Note", states={'confirm': [('readonly', True)], 'done': [('readonly', True)]})
|
||
|
employee_id = fields.Many2one('hr.employee', string='Employee', default=_default_employee, required=True)
|
||
|
user_id = fields.Many2one('res.users', related='employee_id.user_id', string='User', store=True, readonly=True)
|
||
|
date_from = fields.Date(string='Date From', default=_default_date_from, required=True,
|
||
|
index=True, readonly=True, states={'new': [('readonly', False)]})
|
||
|
date_to = fields.Date(string='Date To', default=_default_date_to, required=True,
|
||
|
index=True, readonly=True, states={'new': [('readonly', False)]})
|
||
|
timesheet_ids = fields.One2many('account.analytic.line', 'sheet_id',
|
||
|
string='Timesheet lines',
|
||
|
readonly=True, states={
|
||
|
'draft': [('readonly', False)],
|
||
|
'new': [('readonly', False)]})
|
||
|
# state is created in 'new', automatically goes to 'draft' when created. Then 'new' is never used again ...
|
||
|
# (=> 'new' is completely useless)
|
||
|
state = fields.Selection([
|
||
|
('new', 'New'),
|
||
|
('draft', 'Open'),
|
||
|
('confirm', 'Waiting Approval'),
|
||
|
('done', 'Approved')], default='new', track_visibility='onchange',
|
||
|
string='Status', required=True, readonly=True, index=True,
|
||
|
help=' * The \'Open\' status is used when a user is encoding a new and unconfirmed timesheet. '
|
||
|
'\n* The \'Waiting Approval\' status is used to confirm the timesheet by user. '
|
||
|
'\n* The \'Approved\' status is used when the users timesheet is accepted by his/her senior.')
|
||
|
account_ids = fields.One2many('hr_timesheet_sheet.sheet.account', 'sheet_id', string='Analytic accounts', readonly=True)
|
||
|
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env['res.company']._company_default_get())
|
||
|
department_id = fields.Many2one('hr.department', string='Department')
|
||
|
|
||
|
@api.constrains('date_to', 'date_from', 'employee_id')
|
||
|
def _check_sheet_date(self, forced_user_id=False):
|
||
|
for sheet in self:
|
||
|
new_user_id = forced_user_id or sheet.user_id and sheet.user_id.id
|
||
|
if new_user_id:
|
||
|
self.env.cr.execute('''
|
||
|
SELECT id
|
||
|
FROM hr_timesheet_sheet_sheet
|
||
|
WHERE (date_from <= %s and %s <= date_to)
|
||
|
AND user_id=%s
|
||
|
AND id <> %s''',
|
||
|
(sheet.date_to, sheet.date_from, new_user_id, sheet.id))
|
||
|
if any(self.env.cr.fetchall()):
|
||
|
raise ValidationError(_('You cannot have 2 timesheets that overlap!\nPlease use the menu \'My Current Timesheet\' to avoid this problem.'))
|
||
|
|
||
|
@api.onchange('employee_id')
|
||
|
def onchange_employee_id(self):
|
||
|
if self.employee_id:
|
||
|
self.department_id = self.employee_id.department_id
|
||
|
self.user_id = self.employee_id.user_id
|
||
|
|
||
|
def copy(self, *args, **argv):
|
||
|
raise UserError(_('You cannot duplicate a timesheet.'))
|
||
|
|
||
|
@api.model
|
||
|
def create(self, vals):
|
||
|
if 'employee_id' in vals:
|
||
|
if not self.env['hr.employee'].browse(vals['employee_id']).user_id:
|
||
|
raise UserError(_('In order to create a timesheet for this employee, you must link him/her to a user.'))
|
||
|
res = super(HrTimesheetSheet, self).create(vals)
|
||
|
res.write({'state': 'draft'})
|
||
|
return res
|
||
|
|
||
|
@api.multi
|
||
|
def write(self, vals):
|
||
|
if 'employee_id' in vals:
|
||
|
new_user_id = self.env['hr.employee'].browse(vals['employee_id']).user_id.id
|
||
|
if not new_user_id:
|
||
|
raise UserError(_('In order to create a timesheet for this employee, you must link him/her to a user.'))
|
||
|
self._check_sheet_date(forced_user_id=new_user_id)
|
||
|
return super(HrTimesheetSheet, self).write(vals)
|
||
|
|
||
|
@api.multi
|
||
|
def action_timesheet_draft(self):
|
||
|
if not self.env.user.has_group('hr_timesheet.group_hr_timesheet_user'):
|
||
|
raise UserError(_('Only an HR Officer or Manager can refuse timesheets or reset them to draft.'))
|
||
|
self.write({'state': 'draft'})
|
||
|
return True
|
||
|
|
||
|
@api.multi
|
||
|
def action_timesheet_confirm(self):
|
||
|
for sheet in self:
|
||
|
if sheet.employee_id and sheet.employee_id.parent_id and sheet.employee_id.parent_id.user_id:
|
||
|
self.message_subscribe_users(user_ids=[sheet.employee_id.parent_id.user_id.id])
|
||
|
self.write({'state': 'confirm'})
|
||
|
return True
|
||
|
|
||
|
@api.multi
|
||
|
def action_timesheet_done(self):
|
||
|
if not self.env.user.has_group('hr_timesheet.group_hr_timesheet_user'):
|
||
|
raise UserError(_('Only an HR Officer or Manager can approve timesheets.'))
|
||
|
if self.filtered(lambda sheet: sheet.state != 'confirm'):
|
||
|
raise UserError(_("Cannot approve a non-submitted timesheet."))
|
||
|
self.write({'state': 'done'})
|
||
|
|
||
|
@api.multi
|
||
|
def name_get(self):
|
||
|
# week number according to ISO 8601 Calendar
|
||
|
return [(r['id'], _('Week ') + str(datetime.strptime(r['date_from'], '%Y-%m-%d').isocalendar()[1]))
|
||
|
for r in self.read(['date_from'], load='_classic_write')]
|
||
|
|
||
|
@api.multi
|
||
|
def unlink(self):
|
||
|
sheets = self.read(['state'])
|
||
|
for sheet in sheets:
|
||
|
if sheet['state'] in ('confirm', 'done'):
|
||
|
raise UserError(_('You cannot delete a timesheet which is already confirmed.'))
|
||
|
|
||
|
analytic_timesheet_toremove = self.env['account.analytic.line']
|
||
|
for sheet in self:
|
||
|
analytic_timesheet_toremove += sheet.timesheet_ids.filtered(lambda t: not t.task_id)
|
||
|
analytic_timesheet_toremove.unlink()
|
||
|
|
||
|
return super(HrTimesheetSheet, self).unlink()
|
||
|
|
||
|
# ------------------------------------------------
|
||
|
# OpenChatter methods and notifications
|
||
|
# ------------------------------------------------
|
||
|
|
||
|
@api.multi
|
||
|
def _track_subtype(self, init_values):
|
||
|
if self:
|
||
|
record = self[0]
|
||
|
if 'state' in init_values and record.state == 'confirm':
|
||
|
return 'hr_timesheet_sheet.mt_timesheet_confirmed'
|
||
|
elif 'state' in init_values and record.state == 'done':
|
||
|
return 'hr_timesheet_sheet.mt_timesheet_approved'
|
||
|
return super(HrTimesheetSheet, self)._track_subtype(init_values)
|
||
|
|
||
|
@api.model
|
||
|
def _needaction_domain_get(self):
|
||
|
empids = self.env['hr.employee'].search([('parent_id.user_id', '=', self.env.uid)])
|
||
|
if not empids:
|
||
|
return False
|
||
|
return ['&', ('state', '=', 'confirm'), ('employee_id', 'in', empids.ids)]
|
||
|
|
||
|
|
||
|
class HrTimesheetSheetSheetAccount(models.Model):
|
||
|
_name = "hr_timesheet_sheet.sheet.account"
|
||
|
_description = "Timesheets by Period"
|
||
|
_auto = False
|
||
|
_order = 'name'
|
||
|
|
||
|
name = fields.Many2one('account.analytic.account', string='Project / Analytic Account', readonly=True)
|
||
|
sheet_id = fields.Many2one('hr_timesheet_sheet.sheet', string='Sheet', readonly=True)
|
||
|
total = fields.Float('Total Time', digits=(16, 2), readonly=True)
|
||
|
|
||
|
# still seing _depends in BaseModel, ok to leave this as is?
|
||
|
_depends = {
|
||
|
'account.analytic.line': ['account_id', 'date', 'unit_amount', 'user_id'],
|
||
|
'hr_timesheet_sheet.sheet': ['date_from', 'date_to', 'user_id'],
|
||
|
}
|
||
|
|
||
|
@api.model_cr
|
||
|
def init(self):
|
||
|
drop_view_if_exists(self._cr, 'hr_timesheet_sheet_sheet_account')
|
||
|
self._cr.execute("""create view hr_timesheet_sheet_sheet_account as (
|
||
|
select
|
||
|
min(l.id) as id,
|
||
|
l.account_id as name,
|
||
|
s.id as sheet_id,
|
||
|
sum(l.unit_amount) as total
|
||
|
from
|
||
|
account_analytic_line l
|
||
|
LEFT JOIN hr_timesheet_sheet_sheet s
|
||
|
ON (s.date_to >= l.date
|
||
|
AND s.date_from <= l.date
|
||
|
AND s.user_id = l.user_id)
|
||
|
group by l.account_id, s.id
|
||
|
)""")
|