odoo/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py

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
)""")