Skip to content
Commits on Source (3)
# Eclipse files
.settings
.project
.pydevproject
.externalToolBuilders
# Python build file.
*.py[co]
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
# Unit test / coverage reports
.coverage
.tox
coverage.xml
nosetests.xml
setup.py.bak
sonar-project.properties
language: python
sudo: false
cache:
apt: true
directories:
- $HOME/.cache/pip
python:
- "2.7"
addons:
apt:
packages:
- expect-dev # provides unbuffer utility
- python-lxml # because pip installation is slow
- python-simplejson
- python-serial
- python-yaml
- python-ldap
env:
global:
- VERSION="10.0" TESTS="0" LINT_CHECK="0" TRANSIFEX="0"
- PHANTOMJS_VERSION="latest"
- WEBSITE_REPO="1"
matrix:
#- LINT_CHECK="1"
- TESTS="1" ODOO_REPO="odoo/odoo"
- TESTS="1" ODOO_REPO="OCA/OCB"
virtualenv:
system_site_packages: true
install:
- git clone --depth=1 https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools
- export PATH=${HOME}/maintainer-quality-tools/travis:${PATH}
- travis_install_nightly
script:
- travis_run_tests
after_success:
- travis_after_tests_success
[![Build Status](https://travis-ci.org/ikus060/odoo-addons.svg?branch=8.0)](https://travis-ci.org/ikus060/odoo-addons)
# odoo-addons
Collection of odoo addons developped by Patrik Dufresne Service Logiciel inc.
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import models
import wizard
\ No newline at end of file
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Project Timesheets',
'version': '1.1',
'category': 'Human Resources',
'sequence': 80,
'summary': 'Timesheets, Activities',
'description': """TODO""",
'website': 'http://www.patrikdufresne.com',
'depends': [
'hr_timesheet',
'project',
],
'data': [
'security/ir.model.access.csv',
'security/hr_timesheet_project_sheet_security.xml',
'data/hr_timesheet_project_sheet_data.xml',
'views/hr_analytic_timesheet.xml',
'views/hr_timesheet_project_sheet_templates.xml',
'views/hr_timesheet_project_sheet_views.xml',
],
'installable': True,
'auto_install': False,
'qweb': ['static/src/xml/timesheet.xml', ],
}
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="ir_actions_server_timesheet_sheet" model="ir.actions.server">
<field name="sequence" eval="5"/>
<field name="state">code</field>
<field name="type">ir.actions.server</field>
<field name="model_id" ref="model_hr_timesheet_current_open"/>
<field name="code">action = model.open_timesheet()</field>
<field name="condition">True</field>
<field name="name">My Timesheet</field>
</record>
<!-- Timesheet sheet related subtypes for messaging / Chatter -->
<record id="mt_timesheet_confirmed" model="mail.message.subtype">
<field name="name">Waiting Approval</field>
<field name="res_model">hr_timesheet_project_sheet.sheet</field>
<field name="default" eval="True"/>
<field name="description">waiting approval</field>
</record>
<record id="mt_timesheet_approved" model="mail.message.subtype">
<field name="name">Approved</field>
<field name="res_model">hr_timesheet_project_sheet.sheet</field>
<field name="default" eval="True"/>
<field name="description">Timesheet approved</field>
</record>
<!-- Department (Parent) related subtypes for messaging / Chatter -->
<record id="mt_department_timesheet_confirmed" model="mail.message.subtype">
<field name="name">Timesheets to Approve</field>
<field name="res_model">hr.department</field>
<field name="default" eval="False"/>
<field name="parent_id" eval="ref('mt_timesheet_confirmed')"/>
<field name="relation_field">department_id</field>
<field name="sequence" eval="5"/>
</record>
<record id="mt_department_timesheet_approved" model="mail.message.subtype">
<field name="name">Timesheets Approved</field>
<field name="res_model">hr.department</field>
<field name="default" eval="False"/>
<field name="parent_id" eval="ref('mt_timesheet_approved')"/>
<field name="relation_field">department_id</field>
<field name="sequence" eval="5"/>
</record>
</data>
</odoo>
This diff is collapsed.
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * hr_timesheet_project_sheet
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-09-07 08:56+0000\n"
"PO-Revision-Date: 2016-02-16 04:39+0000\n"
"Last-Translator: Martin Trigaux\n"
"Language-Team: French (Canada) (http://www.transifex.com/odoo/odoo-9/"
"language/fr_CA/)\n"
"Language: fr_CA\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#. module: hr_timesheet_project_sheet
#: model:ir.ui.view,arch_db:hr_timesheet_project_sheet.hr_timesheet_project_sheet_form
msgid "Approve"
msgstr "Approbation"
#. module: hr_timesheet_project_sheet
#: model:ir.model,name:hr_timesheet_project_sheet.model_res_company
msgid "Companies"
msgstr "Sociétés"
#. module: hr_timesheet_project_sheet
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_current_open_create_uid
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_project_sheet_sheet_create_uid
msgid "Created by"
msgstr "Créé par"
#. module: hr_timesheet_project_sheet
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_current_open_create_date
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_project_sheet_sheet_create_date
msgid "Created on"
msgstr "Créé le"
#. module: hr_timesheet_project_sheet
#: model:ir.ui.view,arch_db:hr_timesheet_project_sheet.hr_timesheet_project_sheet_form
msgid "Details"
msgstr "Détails"
#. module: hr_timesheet_project_sheet
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_current_open_display_name
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_project_sheet_sheet_account_display_name
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_project_sheet_sheet_display_name
msgid "Display Name"
msgstr "Nom affiché"
#. module: hr_timesheet_project_sheet
#: model:ir.ui.view,arch_db:hr_timesheet_project_sheet.view_hr_timesheet_project_sheet_filter
msgid "Group By"
msgstr "Grouper par"
#. module: hr_timesheet_project_sheet
#: model:ir.ui.view,arch_db:hr_timesheet_project_sheet.hr_timesheet_project_sheet_form
msgid "Hours"
msgstr "Heures"
#. module: hr_timesheet_project_sheet
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_current_open_id
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_project_sheet_sheet_account_id
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_project_sheet_project_sheet_id
msgid "ID"
msgstr "Identifiant"
#. module: hr_timesheet_project_sheet
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_current_open___last_update
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_project_sheet_sheet___last_update
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_project_sheet_sheet_account___last_update
msgid "Last Modified on"
msgstr "Dernière modification le"
#. module: hr_timesheet_project_sheet
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_current_open_write_uid
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_project_sheet_sheet_write_uid
msgid "Last Updated by"
msgstr "Dernière mise à jour par"
#. module: hr_timesheet_project_sheet
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_current_open_write_date
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_project_sheet_sheet_write_date
msgid "Last Updated on"
msgstr "Dernière mise à jour le"
#. module: hr_timesheet_project_sheet
#: selection:res.company,timesheet_range:0
msgid "Month"
msgstr "Mois"
#. module: hr_timesheet_project_sheet
#: selection:hr_timesheet_project_sheet.sheet,state:0
msgid "New"
msgstr "Nouveau"
#. module: hr_timesheet_project_sheet
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_project_sheet_sheet_name
msgid "Note"
msgstr "Note"
#. module: hr_timesheet_project_sheet
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_project_sheet_sheet_state
msgid "Status"
msgstr "Statut"
#. module: hr_timesheet_project_sheet
#: model:ir.ui.view,arch_db:hr_timesheet_project_sheet.hr_timesheet_project_sheet_form
msgid "Summary"
msgstr "Résumé"
#. module: hr_timesheet_project_sheet
#. openerp-web
#: code:addons/hr_timesheet_project_sheet/static/src/xml/timesheet.xml:14
#: code:addons/hr_timesheet_project_sheet/static/src/xml/timesheet.xml:36
#: model:ir.ui.view,arch_db:hr_timesheet_project_sheet.hr_timesheet_account_form
#: model:ir.ui.view,arch_db:hr_timesheet_project_sheet.hr_timesheet_account_tree
#, python-format
msgid "Total"
msgstr "Total"
#. module: hr_timesheet_project_sheet
#: model:ir.ui.view,arch_db:hr_timesheet_project_sheet.view_hr_timesheet_project_sheet_filter
msgid "Unread Messages"
msgstr "Messages non-lus"
#. module: hr_timesheet_project_sheet
#: model:ir.model.fields,field_description:hr_timesheet_project_sheet.field_hr_timesheet_project_sheet_sheet_user_id
msgid "User"
msgstr "Utilisateur"
This diff is collapsed.
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import account_analytic_line
import hr_timesheet_project_sheet
# -*- coding: utf-8 -*-
from __future__ import division
from datetime import timedelta
import math
from odoo import api, fields, models, _
from odoo import models, fields, api, exceptions, _
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_compare
def float_time_convert(float_val):
hours = math.floor(abs(float_val))
mins = abs(float_val) - hours
mins = round(mins * 60)
if mins >= 60.0:
hours = hours + 1
mins = 0.0
return '%02d:%02d' % (hours, mins)
class AccountAnalyticLine(models.Model):
_inherit = "account.analytic.line"
@api.model
def _default_project(self):
return self.env.context.get('project_id', None)
project_sheet_id_computed = fields.Many2one('hr_timesheet_project_sheet.sheet', string='Sheet', compute='_compute_sheet', index=True,
ondelete='cascade', search='_search_sheet')
project_sheet_id = fields.Many2one('hr_timesheet_project_sheet.sheet', compute='_compute_sheet', string='Sheet', store=True)
project_id = fields.Many2one(default=_default_project)
employee_category = fields.Many2one(comodel_name='hr.employee.category', string="Employee Category")
time_start = fields.Float(string='Begin Hour')
time_stop = fields.Float(string='End Hour')
@api.depends('date', 'user_id', 'project_id', 'project_sheet_id_computed.date_to', 'project_sheet_id_computed.date_from', 'project_sheet_id_computed.project_id')
def _compute_sheet(self):
"""Links the timesheet line to the corresponding sheet
"""
for ts_line in self:
if not ts_line.project_id:
continue
sheets = self.env['hr_timesheet_project_sheet.sheet'].search(
[('date_to', '>=', ts_line.date), ('date_from', '<=', ts_line.date),
('project_id.id', '=', ts_line.project_id.id),
('state', 'in', ['draft', 'new'])])
if sheets:
# [0] because only one sheet possible for a project between 2 dates
ts_line.project_sheet_id_computed = sheets[0]
ts_line.project_sheet_id = sheets[0]
def _search_sheet(self, operator, value):
assert operator == 'in'
ids = []
for ts in self.env['hr_timesheet_project_sheet.sheet'].browse(value):
self._cr.execute("""
SELECT l.id
FROM account_analytic_line l
WHERE %(date_to)s >= l.date
AND %(date_from)s <= l.date
AND %(project_id)s = l.project_id
GROUP BY l.id""", {'date_from': ts.date_from,
'date_to': ts.date_to,
'project_id': ts.project_id.id, })
ids.extend([row[0] for row in self._cr.fetchall()])
return [('id', 'in', ids)]
@api.multi
def write(self, values):
self._check_state()
return super(AccountAnalyticLine, self).write(values)
@api.multi
def unlink(self):
self._check_state()
return super(AccountAnalyticLine, self).unlink()
def _check_state(self):
for line in self:
if line.project_sheet_id and line.project_sheet_id.state not in ('draft', 'new'):
raise UserError(_('You cannot modify an entry in a confirmed timesheet.'))
return True
@api.one
@api.constrains('time_start', 'time_stop', 'unit_amount')
def _check_time_start_stop(self):
start = timedelta(hours=self.time_start)
stop = timedelta(hours=self.time_stop)
if stop < start:
raise exceptions.ValidationError(
_('The beginning hour (%s) must '
'precede the ending hour (%s).') %
(float_time_convert(self.time_start),
float_time_convert(self.time_stop))
)
hours = (stop - start).seconds / 3600
if (hours and
float_compare(hours, self.unit_amount, precision_digits=4)):
raise exceptions.ValidationError(
_('The duration (%s) must be equal to the difference '
'between the hours (%s).') %
(float_time_convert(self.unit_amount),
float_time_convert(hours))
)
# check if lines overlap
others = self.search([
('id', '!=', self.id),
('user_id', '=', self.user_id.id),
('date', '=', self.date),
('time_start', '<', self.time_stop),
('time_stop', '>', self.time_start),
])
if others:
message = _("Lines can't overlap:\n")
message += '\n'.join(['%s - %s' %
(float_time_convert(line.time_start),
float_time_convert(line.time_stop))
for line
in (self + others).sorted(
lambda l: l.time_start
)])
raise exceptions.ValidationError(message)
@api.onchange('time_start', 'time_stop')
def onchange_hours_start_stop(self):
start = timedelta(hours=self.time_start)
stop = timedelta(hours=self.time_stop)
if stop < start:
return
self.unit_amount = (stop - start).seconds / 3600
#@api.onchange('employee_category')
#def onchange_place(self):
# res = {}
# if self.employee_category:
# res['domain'] = {'user_id': [('employee_ids.category_ids', 'in', self.employee_category)]}
# return res
# -*- coding: utf-8 -*-
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_project_sheet.sheet"
_inherit = ['mail.thread', 'ir.needaction_mixin']
_table = 'hr_timesheet_project_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="Event Name", states={'confirm': [('readonly', True)], 'done': [('readonly', True)]}, help="The event name, You may include reference to PO number in this field.")
project_id = fields.Many2one('project.project', string='Project', required=True, readonly=True, states={'new': [('readonly', False)]})
date_from = fields.Date(string='Date From', required=True,
index=True, readonly=True, states={'new': [('readonly', False)]})
date_to = fields.Date(string='Date To', required=True,
index=True, readonly=True, states={'new': [('readonly', False)]})
timesheet_ids = fields.One2many('account.analytic.line', 'project_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_project_sheet.sheet.account', 'project_sheet_id', string='Analytic accounts', readonly=True)
company_id = fields.Many2one('res.company', string='Company')
# Get project_id fields.
user_id = fields.Many2one('res.users', string='Responsible', required=False, default=lambda self: self.env.user)
contact_id = fields.Many2one('res.partner', string='Remote Contact', store=True, readonly=False)
location = fields.Char(string="Location", help="Short description describing the location of the event.", store=True, readonly=False)
#@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_project_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_project_sheet.mt_timesheet_confirmed'
elif 'state' in init_values and record.state == 'done':
return 'hr_timesheet_project_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_project_sheet.sheet.account"
_description = "Timesheets by Period"
_auto = False
_order = 'name'
name = fields.Many2one('account.analytic.account', string='Project / Analytic Account', readonly=True)
project_sheet_id = fields.Many2one('hr_timesheet_project_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', 'project_id'],
'hr_timesheet_project_sheet.sheet': ['date_from', 'date_to', 'project_id'],
}
@api.model_cr
def init(self):
drop_view_if_exists(self._cr, 'hr_timesheet_project_sheet_sheet_account')
self._cr.execute("""create view hr_timesheet_project_sheet_sheet_account as (
select
min(l.id) as id,
l.account_id as name,
s.id as project_sheet_id,
sum(l.unit_amount) as total
from
account_analytic_line l
LEFT JOIN hr_timesheet_project_sheet_sheet s
ON (s.date_to >= l.date
AND s.date_from <= l.date
AND s.project_id = l.project_id)
group by l.account_id, s.id
)""")
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record model="ir.rule" id="timesheet_comp_rule">
<field name="name">Timesheet multi-company</field>
<field name="model_id" search="[('model','=','hr_timesheet_project_sheet.sheet')]" model="ir.model"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
</record>
</data>
</odoo>
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_timesheet_project_sheet_sheet_user,hr_timesheet_project_sheet.sheet.user,model_hr_timesheet_project_sheet_sheet,base.group_user,1,1,1,1
access_hr_timesheet_project_sheet_sheet_system_employee,hr_timesheet_project_sheet.sheet.system.employee,model_hr_timesheet_project_sheet_sheet,base.group_user,1,1,1,0
access_hr_timesheet_project_sheet_sheet_account,hr_timesheet_project_sheet.sheet.account,model_hr_timesheet_project_sheet_sheet_account,hr_timesheet.group_hr_timesheet_user,1,1,1,1
odoo.define('hr_timesheet_project_sheet.sheet', function (require) {
"use strict";
var core = require('web.core');
var data = require('web.data');
var form_common = require('web.form_common');
var formats = require('web.formats');
var Model = require('web.DataModel');
var time = require('web.time');
var utils = require('web.utils');
var QWeb = core.qweb;
var _t = core._t;
var WeeklyTimesheet = form_common.FormWidget.extend(form_common.ReinitializeWidgetMixin, {
events: {
"click .oe_timesheet_weekly_account a": "go_to",
},
ignore_fields: function() {
return ['line_id'];
},
init: function() {
this._super.apply(this, arguments);
this.set({
sheets: [],
date_from: false,
date_to: false,
});
this.field_manager.on("field_changed:timesheet_ids", this, this.query_sheets);
this.field_manager.on("field_changed:date_from", this, function() {
this.set({"date_from": time.str_to_date(this.field_manager.get_field_value("date_from"))});
});
this.field_manager.on("field_changed:date_to", this, function() {
this.set({"date_to": time.str_to_date(this.field_manager.get_field_value("date_to"))});
});
this.field_manager.on("field_changed:project_id", this, function() {
this.set({"project_id": this.field_manager.get_field_value("project_id")});
});
this.on("change:sheets", this, this.update_sheets);
this.res_o2m_drop = new utils.DropMisordered();
this.render_drop = new utils.DropMisordered();
this.description_line = _t("/");
},
go_to: function(event) {
var id = JSON.parse($(event.target).data("id"));
this.do_action({
type: 'ir.actions.act_window',
res_model: "res.users",
res_id: id,
views: [[false, 'form']],
});
},
query_sheets: function() {
if (this.updating) {
return;
}
this.querying = true;
var commands = this.field_manager.get_field_value("timesheet_ids");
var self = this;
this.res_o2m_drop.add(new Model(this.view.model).call("resolve_2many_commands",
["timesheet_ids", commands, [], new data.CompoundContext()]))
.done(function(result) {
self.set({sheets: result});
self.querying = false;
});
},
update_sheets: function() {
if(this.querying) {
return;
}
this.updating = true;
var commands = [form_common.commands.delete_all()];
_.each(this.get("sheets"), function (_data) {
var data = _.clone(_data);
if(data.id) {
commands.push(form_common.commands.link_to(data.id));
commands.push(form_common.commands.update(data.id, data));
} else {
commands.push(form_common.commands.create(data));
}
});
var self = this;
this.field_manager.set_values({'timesheet_ids': commands}).done(function() {
self.updating = false;
});
},
initialize_field: function() {
form_common.ReinitializeWidgetMixin.initialize_field.call(this);
this.on("change:sheets", this, this.initialize_content);
this.on("change:date_to", this, this.initialize_content);
this.on("change:date_from", this, this.initialize_content);
this.on("change:project_id", this, this.initialize_content);
},
initialize_content: function() {
if(this.setting) {
return;
}
// don't render anything until we have date_to and date_from
if (!this.get("date_to") || !this.get("date_from")) {
return;
}
// it's important to use those vars to avoid race conditions
var dates;
var employees;
var employees_by_categories;
var timesheet_lines;
var category_names;
var employee_names;
var default_get;
var self = this;
return this.render_drop.add(new Model("account.analytic.line").call("default_get", [
['account_id','general_account_id','journal_id','date','name','user_id','product_id','product_uom_id','amount','unit_amount','project_id', 'employee_category'],
new data.CompoundContext({'project_id': self.get('project_id')})
]).then(function(result) {
default_get = result;
// calculating dates
dates = [];
var start = self.get("date_from");
var end = self.get("date_to");
while (start <= end) {
dates.push(start);
var m_start = moment(start).add(1, 'days');
start = m_start.toDate();
}
timesheet_lines = _(self.get('sheets')).chain()
.map(function(el) {
// much simpler to use only the id in all cases
if (typeof(el.user_id) === "object")
el.user_id = el.user_id[0];
if (typeof(el.employee_category) === 'object')
el.employee_category = el.employee_category[0];
return el;
}).value();
// group by employee
employees = _.groupBy(timesheet_lines, function(el) {
return el.user_id;
});
// group by employee and employee_category
employees_by_categories = _.groupBy(timesheet_lines, function(el) {
return [el.user_id, el.employee_category];
});
employees = _(employees_by_categories).chain().map(function(lines, user_id_category_id) {
var user_id = lines[0].user_id;
var employees_defaults = _.extend({}, default_get, (employees[user_id] || {}).value || {});
// group by days
user_id = (user_id === "false")? false : Number(user_id);
var index = _.groupBy(lines, "date");
var days = _.map(dates, function(date) {
var day = {day: date, lines: index[time.date_to_str(date)] || []};
// add line where we will insert/remove hours
var to_add = _.find(day.lines, function(line) { return line.name === self.description_line; });
if (to_add) {
day.lines = _.without(day.lines, to_add);
day.lines.unshift(to_add);
} else {
day.lines.unshift(_.extend(_.clone(employees_defaults), {
name: self.description_line,
unit_amount: 0,
date: time.date_to_str(date),
user_id: user_id,
employee_category: lines[0].employee_category,
}));
}
return day;
});
var partner_id = undefined;
if(lines[0].partner_id){
if(parseInt(lines[0].partner_id, 10) == lines[0].partner_id){
partner_id = lines[0].partner_id;
} else {
partner_id = lines[0].partner_id[0];
}
}
return {user_id_category_id: user_id_category_id, user_id: user_id, days: days, employees_defaults: employees_defaults, employee_category: lines[0].employee_category, partner_id: partner_id};
}).value();
// we need the name_get of the employee
return new Model("res.users").call("name_get", [_.pluck(employees, "user_id"),
new data.CompoundContext()]).then(function(result) {
employee_names = {};
_.each(result, function(el) {
employee_names[el[0]] = el[1];
});
// we need the name_get of the categories
return new Model('hr.employee.category').call('name_get', [_(employees).chain().pluck('employee_category').filter(function(el) { return el; }).value(),
new data.CompoundContext()]).then(function(result) {
category_names = {};
_.each(result, function(el) {
category_names[el[0]] = el[1];
});
employees = _.sortBy(employees, function(el) {
return category_names[el.employee_category];
});
});
});
})).then(function(result) {
// we put all the gathered data in self, then we render
self.dates = dates;
self.employees = employees;
self.employee_names = employee_names;
self.category_names = category_names;
self.default_get = default_get;
//real rendering
self.display_data();
});
},
destroy_content: function() {
if (this.dfm) {
this.dfm.destroy();
this.dfm = undefined;
}
},
is_valid_value:function(value){
this.view.do_notify_change();
var split_value = value.split(":");
var valid_value = true;
if (split_value.length > 2) {
return false;
}
_.detect(split_value,function(num){
if(isNaN(num)) {
valid_value = false;
}
});
return valid_value;
},
display_data: function() {
var self = this;
self.$el.html(QWeb.render("hr_timesheet_project_sheet.WeeklyTimesheet", {widget: self}));
_.each(self.employees, function(employee) {
_.each(_.range(employee.days.length), function(day_count) {
if (!self.get('effective_readonly')) {
self.get_box(employee, day_count).val(self.sum_box(employee, day_count, true)).change(function() {
var num = $(this).val();
if (self.is_valid_value(num) && num !== 0) {
num = Number(self.parse_client(num));
}
if (isNaN(num)) {
$(this).val(self.sum_box(employee, day_count, true));
} else {
employee.days[day_count].lines[0].unit_amount += num - self.sum_box(employee, day_count);
var product = (employee.days[day_count].lines[0].product_id instanceof Array) ? employee.days[day_count].lines[0].product_id[0] : employee.days[day_count].lines[0].product_id;
var journal = (employee.days[day_count].lines[0].journal_id instanceof Array) ? employee.days[day_count].lines[0].journal_id[0] : employee.days[day_count].lines[0].journal_id;
if(!isNaN($(this).val())){
$(this).val(self.sum_box(employee, day_count, true));
}
self.display_totals();
self.sync();
}
});
} else {
self.get_box(employee, day_count).html(self.sum_box(employee, day_count, true));
}
});
});
self.display_totals();
if(!this.get('effective_readonly')) {
this.init_add_employee();
}
},
init_add_employee: function() {
if (this.dfm) {
this.dfm.destroy();
}
var self = this;
this.$(".oe_timesheet_weekly_add_row").show();
this.dfm = new form_common.DefaultFieldManager(this);
this.dfm.extend_field_desc({
category: {
relation: 'hr.employee.category',
},
employee: {
relation: "res.users",
},
});
var FieldMany2One = core.form_widget_registry.get('many2one');
self.category_m2o = new FieldMany2One(self.dfm, {
attrs: {
name: 'category',
type: 'many2one',
modifiers: '{"required": true}',
},
});
this.employee_m2o = new FieldMany2One(this.dfm, {
attrs: {
name: "employee",
type: "many2one",
domain: [
['id', 'not in', _.pluck(this.employees, "user_id")],
],
modifiers: '{"required": false}',
},
});
this.employee_m2o.prependTo(this.$(".o_add_timesheet_line > div")).then(function() {
self.employee_m2o.$el.addClass('oe_edit_only');
});
self.category_m2o.prependTo(this.$('.o_add_timesheet_line > div')).then(function() {
self.category_m2o.$el.addClass('oe_edit_only');
});
// TODO Need to filter employee based on category.
this.$(".oe_timesheet_button_add").click(function() {
var category_id = self.category_m2o.get_value();
var id = self.employee_m2o.get_value();
if (category_id === false) {
self.dfm.set({display_invalid_fields: true});
return;
}
var ops = self.generate_o2m_value();
ops.push(_.extend({}, self.default_get, {
name: self.description_line,
unit_amount: 0,
date: time.date_to_str(self.dates[0]),
user_id: id,
employee_category: category_id,
}));
self.set({sheets: ops});
self.destroy_content();
});
},
get_box: function(employee, day_count) {
return this.$('[data-employee-category="' + employee.user_id_category_id + '"][data-day-count="' + day_count + '"]');
},
get_employee_box : function(employee) {
return this.$('[data-employee-category-employee="' + employee.user_id_category_id + '"]');
} ,
sum_box: function(employee, day_count, show_value_in_hour) {
var line_total = 0;
_.each(employee.days[day_count].lines, function(line) {
line_total += line.unit_amount;
});
return (show_value_in_hour && line_total !== 0)?this.format_client(line_total):line_total;
},
display_totals: function() {
var self = this;
var day_tots = _.map(_.range(self.dates.length), function() { return 0; });
var super_tot = 0;
_.each(self.employees, function(employee) {
var acc_tot = 0;
_.each(_.range(self.dates.length), function(day_count) {
var sum = self.sum_box(employee, day_count);
acc_tot += sum;
day_tots[day_count] += sum;
super_tot += sum;
});
self.$('[data-employee-category-total="' + employee.user_id_category_id + '"]').html(self.format_client(acc_tot));
});
_.each(_.range(self.dates.length), function(day_count) {
self.$('[data-day-total="' + day_count + '"]').html(self.format_client(day_tots[day_count]));
});
this.$('.oe_timesheet_weekly_supertotal').html(self.format_client(super_tot));
},
sync: function() {
this.setting = true;
this.set({sheets: this.generate_o2m_value()});
this.setting = false;
},
//converts hour value to float
parse_client: function(value) {
return formats.parse_value(value, { type:"float_time" });
},
//converts float value to hour
format_client:function(value){
return formats.format_value(value, { type:"float_time" });
},
generate_o2m_value: function() {
var ops = [];
var ignored_fields = this.ignore_fields();
_.each(this.employees, function(employee) {
_.each(employee.days, function(day) {
_.each(day.lines, function(line) {
if (line.unit_amount !== 0) {
var tmp = _.clone(line);
_.each(line, function(v, k) {
if (v instanceof Array) {
tmp[k] = v[0];
}
});
// we remove line_id as the reference to the _inherits field will no longer exists
tmp = _.omit(tmp, ignored_fields);
ops.push(tmp);
}
});
});
});
return ops;
},
});
core.form_custom_registry.add('weekly_timesheet', WeeklyTimesheet);
});
.o_web_client .oe_timesheet_weekly {
overflow-x: auto;
> table {
width: 100%;
> tbody > tr {
> th {
text-align: center;
font-size: 10px;
background-color: @odoo-brand-lightsecondary;
}
> td {
text-align: right;
&.oe_timesheet_weekly_account, &.oe_timesheet_weekly_account_task {
white-space: nowrap;
text-align: left;
}
input.oe_timesheet_weekly_input {
display: inline-block;
width: 40px;
text-align: right;
}
.oe_timesheet_button_add {
margin-left: 8px;
}
.o_with_button + .oe_timesheet_button_add {
margin-left: 45px;
}
}
.oe_timesheet_weekly_today {
background-color: lighten(@odoo-brand-primary, 40%);
}
.oe_timesheet_total {
font-weight: bold;
background-color: @odoo-brand-lightsecondary;
}
.o_add_timesheet_line {
width: 1%;
background-color: @odoo-brand-lightsecondary;
> div {
.o-flex-display();
.o-align-items(center);
.o_form_field_many2one {
min-width: 200px;
margin: 0;
}
.o_external_button {
background-color: @odoo-brand-lightsecondary;
padding-top: 0;
padding-bottom: 0;
}
.oe_timesheet_button_add {
.o-flex(0, 0, auto);
margin-left: 8px;
}
.o_with_button + .oe_timesheet_button_add {
margin-left: 45px;
}
}
}
}
}
}
.openerp .oe_timesheet_weekly {
> table > tbody > tr {
> th {
color: #069;
}
.o_add_timesheet_line {
width: auto;
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="hr_timesheet_project_sheet.WeeklyTimesheet">
<div class="oe_timesheet_weekly">
<table class="table table-condensed table-responsive">
<tr>
<th class="oe_timesheet_first_col" colspan="3"/>
<t t-foreach="widget.dates" t-as="date">
<th t-att-class="'oe_timesheet_weekly_date_head' + (moment().format('DD-MM-YYYY') === moment(date).format('DD-MM-YYYY') ? ' oe_timesheet_weekly_today' : '')">
<t t-esc="moment(date).format('ddd')"/><br/>
<t t-esc="moment(date).format('MMM DD')"/>
</th>
</t>
<th class="oe_timesheet_weekly_date_head">Total</th>
</tr>
<tr t-foreach="widget.employees" t-as="employee">
<td class="oe_timesheet_weekly_account_category" colspan="1"><a href="javascript:void(0)" t-att-data-id="JSON.stringify(employee.employee_category)"><t t-esc="widget.category_names[employee.employee_category]"/></a></td>
<td class="oe_timesheet_weekly_account" colspan="1">
<a href="javascript:void(0)" t-att-data-id="JSON.stringify(employee.user_id)" t-att-data-employee-category-employee="employee.user_id_category_id" ><t t-esc="widget.employee_names[employee.user_id]"/></a>
</td>
<td></td>
<t t-set="day_count" t-value="0"/>
<t t-foreach="employee.days" t-as="day">
<td t-att-class="moment().format('DD-MM-YYYY') === moment(day.day).format('DD-MM-YYYY') ? 'oe_timesheet_weekly_today' : ''">
<input t-if="!widget.get('effective_readonly')" class="oe_timesheet_weekly_input" t-att-data-employee-category="employee.user_id_category_id"
t-att-data-day-count="day_count" type="text"/>
<span t-if="widget.get('effective_readonly')" t-att-data-employee-category="employee.user_id_category_id"
t-att-data-day-count="day_count" class="oe_timesheet_weekly_box"/>
<t t-set="day_count" t-value="day_count + 1"/>
</td>
</t>
<td t-att-data-employee-category-total="employee.user_id_category_id" class="oe_timesheet_total"/>
</tr>
<tr>
<td class="o_add_timesheet_line" colspan="2">
<div>
<button t-if="!widget.get('effective_readonly')" class="btn btn-sm btn-primary oe_edit_only oe_timesheet_button_add">Add a Line</button>
</div>
</td>
<td class="oe_timesheet_total">
Total
</td>
<t t-set="day_count" t-value="0"/>
<t t-foreach="widget.dates" t-as="date">
<td class="oe_timesheet_total">
<span class="oe_timesheet_weekly_box" t-att-data-day-total="day_count"/>
<t t-set="day_count" t-value="day_count + 1"/>
</td>
</t>
<td class="oe_timesheet_weekly_supertotal oe_timesheet_total"/>
</tr>
</table>
<div t-if="widget.employees.length == 0">
<div class="oe_view_nocontent oe_edit_only">
<p class="oe_view_nocontent_create">Click to add employee.</p>
<p>You will be able to register working hours and activities.</p>
</div>
</div>
</div>
</t>
</templates>
# -*- coding: utf-8 -*-
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
from . import test_timesheet_begin_end
# -*- coding: utf-8 -*-
# Copyright 2015 Camptocamp SA - Guewen Baconnier
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
from odoo import fields, exceptions
from odoo.tests import common
class TestBeginEnd(common.TransactionCase):
def setUp(self):
super(TestBeginEnd, self).setUp()
self.timesheet_line_model = self.env['account.analytic.line']
self.analytic = self.env.ref('analytic.analytic_administratif')
self.user = self.env.ref('base.user_root')
self.base_line = {
'name': 'test',
'date': fields.Date.today(),
'time_start': 10.,
'time_stop': 12.,
'user_id': self.user.id,
'unit_amount': 2.,
'account_id': self.analytic.id,
'amount': -60.,
}
def test_onchange(self):
line = self.timesheet_line_model.new({
'name': 'test',
'time_start': 10.,
'time_stop': 12.,
})
line.onchange_hours_start_stop()
self.assertEquals(line.unit_amount, 2)
def test_check_begin_before_end(self):
line = self.base_line.copy()
line.update({
'time_start': 12.,
'time_stop': 10.,
})
with self.assertRaises(exceptions.ValidationError):
self.timesheet_line_model.create(line)
def test_check_wrong_duration(self):
message_re = (r"The duration \(\d\d:\d\d\) must be equal to the "
r"difference between the hours \(\d\d:\d\d\)\.")
line = self.base_line.copy()
line.update({
'time_start': 10.,
'time_stop': 12.,
'unit_amount': 5.,
})
with self.assertRaisesRegexp(exceptions.ValidationError, message_re):
self.timesheet_line_model.create(line)
def test_check_overlap(self):
line1 = self.base_line.copy()
line1.update({'time_start': 10., 'time_stop': 12., 'unit_amount': 2.})
line2 = self.base_line.copy()
line2.update({'time_start': 12., 'time_stop': 14., 'unit_amount': 2.})
self.timesheet_line_model.create(line1)
self.timesheet_line_model.create(line2)
message_re = r"overlap"
line3 = self.base_line.copy()
line3.update({'time_start': 9., 'time_stop': 11, 'unit_amount': 2.})
with self.assertRaisesRegexp(exceptions.ValidationError, message_re):
self.timesheet_line_model.create(line3)
line3.update({'time_start': 13., 'time_stop': 15, 'unit_amount': 2.})
with self.assertRaisesRegexp(exceptions.ValidationError, message_re):
self.timesheet_line_model.create(line3)
line3.update({'time_start': 8., 'time_stop': 15, 'unit_amount': 7.})
with self.assertRaisesRegexp(exceptions.ValidationError, message_re):
self.timesheet_line_model.create(line3)
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="hr_timesheet_line_tree" model="ir.ui.view">
<field name="name">hr.analytic.timesheet.tree</field>
<field name="model">account.analytic.line</field>
<field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_tree"/>
<field name="arch" type="xml">
<field name="unit_amount" position="before">
<field name="time_start" widget="float_time"/>
<field name="time_stop" widget="float_time"/>
</field>
</field>
</record>
</odoo>