remove conflict
This commit is contained in:
commit
81454e4494
|
@ -0,0 +1 @@
|
|||
.pyc
|
|
@ -6,4 +6,3 @@ from . import project
|
|||
from . import project_overview
|
||||
from . import analytic
|
||||
from . import product
|
||||
#from . import sale
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo import api, exceptions, fields, models, _
|
||||
from odoo.exceptions import UserError, AccessError, ValidationError
|
||||
from odoo.osv import expression
|
||||
|
||||
|
@ -11,6 +11,30 @@ class AccountAnalyticLine(models.Model):
|
|||
start_time = fields.Float('Start Time', digits=(16, 2))
|
||||
end_time = fields.Float('End Time', digits=(16, 2))
|
||||
|
||||
|
||||
_sql_constraints = [
|
||||
('check_start_time_lower_than_24', 'CHECK(start_time <= 24)', 'You cannot have a start hour greater than 24'),
|
||||
('check_start_time_positive', 'CHECK(start_time >= 0)', 'Start hour must be a positive number'),
|
||||
('check_end_time_lower_than_24', 'CHECK(end_time <= 24)', 'You cannot have a end hour greater than 24'),
|
||||
('check_end_time_positive', 'CHECK(end_time >= 0)', 'End hour must be a positive number'),
|
||||
]
|
||||
|
||||
@api.constrains('start_time', 'end_time')
|
||||
def _check_validity_start_time_end_time(self):
|
||||
for rec in self:
|
||||
if rec.start_time and rec.end_time:
|
||||
if rec.end_time < rec.start_time:
|
||||
raise exceptions.ValidationError(_('End time cannot be earlier than Start time'))
|
||||
|
||||
@api.onchange('start_time', 'end_time')
|
||||
def _onchange_start_end_time(self):
|
||||
if self.start_time > 0 and self.end_time > 0:
|
||||
res = self.end_time - self.start_time
|
||||
if res <= 0:
|
||||
raise ValidationError(_("End time cannot be earlier than Start time"))
|
||||
self.unit_amount = res
|
||||
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if vals.get('unit_amount') == 0.0:
|
||||
|
|
|
@ -24,6 +24,8 @@ class Lead(models.Model):
|
|||
start_date = fields.Date(string='Start Date')
|
||||
close_date = fields.Date(string='Close Date')
|
||||
admin_user = fields.Boolean(compute='get_admin_login')
|
||||
#user_id = fields.Many2one('res.users', string='Salesperson', index=True, tracking=True, default=lambda self: self.env.user)
|
||||
#set_readonly = fields.Boolean('Set Readonly', compute='_domain_check_user_group')
|
||||
|
||||
_sql_constraints = [
|
||||
('phone_uniq', 'unique(phone)', "Phone No already exists !"),
|
||||
|
@ -39,3 +41,9 @@ class Lead(models.Model):
|
|||
else:
|
||||
record.admin_user = False
|
||||
|
||||
"""def _domain_check_user_group(self):
|
||||
print(self.user_has_groups('sales_team.group_sale_salesman'), self.user_has_groups('sales_team.group_sale_salesman_all_leads'), self.user_has_groups('group_sale_manager'))
|
||||
if self.user_has_groups('sales_team.group_sale_salesman') and \
|
||||
(not self.user_has_groups('sales_team.group_sale_salesman_all_leads') or not self.user_has_groups('group_sale_manager')):
|
||||
self.set_readonly = True
|
||||
return domain"""
|
||||
|
|
|
@ -27,8 +27,8 @@
|
|||
</field>
|
||||
</record>
|
||||
|
||||
<record id="timesheet_view_tree_user_inherit1" model="ir.ui.view">
|
||||
<field name="name">account.analytic.line.tree.hr_timesheet_inherit1</field>
|
||||
<!--<record id="timesheet_view_tree_user_inherit1" model="ir.ui.view">
|
||||
<field name="name">account.analytic.line.tree.hr_timesheet_user_inherit1</field>
|
||||
<field name="model">account.analytic.line</field>
|
||||
<field name="inherit_id" ref="hr_timesheet.timesheet_view_tree_user"/>
|
||||
<field name="arch" type="xml">
|
||||
|
@ -37,6 +37,6 @@
|
|||
<field name="end_time"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</record>-->
|
||||
|
||||
</odoo>
|
||||
|
|
|
@ -32,6 +32,11 @@
|
|||
}"
|
||||
/>
|
||||
</xpath>
|
||||
<!--<xpath expr="//field[@name='user_id']" position="replace">
|
||||
<field name="set_readonly" invisible="1" />
|
||||
<field name="user_id" domain="[('share', '=', False)]" attrs="{'readonly': [('set_readonly','!=',True)]}"
|
||||
context="{'default_sales_team_id': team_id}" widget="many2one_avatar_user"/>
|
||||
</xpath>-->
|
||||
<xpath expr="//group[@name='Misc']" position="after">
|
||||
<group colspan="2" col="4">
|
||||
<field name="lead_no"/>
|
||||
|
|
0
cor_custom/wizard/__pycache__/crm_opportunity_to_quotation.cpython-36.pyc
Normal file → Executable file
0
cor_custom/wizard/__pycache__/crm_opportunity_to_quotation.cpython-36.pyc
Normal file → Executable file
0
cor_custom/wizard/__pycache__/project_create_sale_order.cpython-36.pyc
Normal file → Executable file
0
cor_custom/wizard/__pycache__/project_create_sale_order.cpython-36.pyc
Normal file → Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from . import controllers
|
||||
from . import wizard
|
||||
from . import report
|
|
@ -0,0 +1,36 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': "Planning",
|
||||
'summary': """Manage your employees' schedule""",
|
||||
'description': """
|
||||
Schedule your teams and employees with shift.
|
||||
""",
|
||||
'category': 'Human Resources/Planning',
|
||||
'version': '1.0',
|
||||
'depends': ['hr', 'web_gantt'],
|
||||
'data': [
|
||||
'security/planning_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'wizard/planning_send_views.xml',
|
||||
'views/assets.xml',
|
||||
'views/hr_views.xml',
|
||||
'report/planning_report_views.xml',
|
||||
'views/planning_template_views.xml',
|
||||
'views/planning_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/planning_templates.xml',
|
||||
'data/planning_cron.xml',
|
||||
'data/mail_data.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/planning_demo.xml',
|
||||
],
|
||||
'application': True,
|
||||
'license': 'OEEL-1',
|
||||
'qweb': [
|
||||
'static/src/xml/planning_gantt.xml',
|
||||
'static/src/xml/field_colorpicker.xml',
|
||||
]
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
|
@ -0,0 +1,127 @@
|
|||
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licens
|
||||
|
||||
from odoo import http, fields, _
|
||||
from odoo.http import request
|
||||
from odoo.osv import expression
|
||||
|
||||
import pytz
|
||||
from werkzeug.utils import redirect
|
||||
import babel
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from odoo import tools
|
||||
|
||||
|
||||
class ShiftController(http.Controller):
|
||||
|
||||
@http.route(['/planning/<string:planning_token>/<string:employee_token>'], type='http', auth="public", website=True)
|
||||
def planning(self, planning_token, employee_token, message=False, **kwargs):
|
||||
""" Displays an employee's calendar and the current list of open shifts """
|
||||
employee_sudo = request.env['hr.employee'].sudo().search([('employee_token', '=', employee_token)], limit=1)
|
||||
if not employee_sudo:
|
||||
return request.not_found()
|
||||
|
||||
planning_sudo = request.env['planning.planning'].sudo().search([('access_token', '=', planning_token)], limit=1)
|
||||
if not planning_sudo:
|
||||
return request.not_found()
|
||||
|
||||
employee_tz = pytz.timezone(employee_sudo.tz or 'UTC')
|
||||
employee_fullcalendar_data = []
|
||||
open_slots = []
|
||||
planning_slots = []
|
||||
|
||||
# domain of slots to display
|
||||
domain = planning_sudo._get_domain_slots()[planning_sudo.id]
|
||||
if planning_sudo.include_unassigned:
|
||||
domain = expression.AND([domain, ['|', ('employee_id', '=', employee_sudo.id), ('employee_id', '=', False)]])
|
||||
else:
|
||||
domain = expression.AND([domain, [('employee_id', '=', employee_sudo.id)]])
|
||||
|
||||
planning_slots = request.env['planning.slot'].sudo().search(domain)
|
||||
|
||||
# filter and format slots
|
||||
for slot in planning_slots:
|
||||
if slot.employee_id:
|
||||
employee_fullcalendar_data.append({
|
||||
'title': '%s%s' % (slot.role_id.name, u' \U0001F4AC' if slot.name else ''),
|
||||
'start': str(pytz.utc.localize(slot.start_datetime).astimezone(employee_tz).replace(tzinfo=None)),
|
||||
'end': str(pytz.utc.localize(slot.end_datetime).astimezone(employee_tz).replace(tzinfo=None)),
|
||||
'color': self._format_planning_shifts(slot.role_id.color),
|
||||
'alloc_hours': slot.allocated_hours,
|
||||
'slot_id': slot.id,
|
||||
'note': slot.name,
|
||||
'allow_self_unassign': slot.allow_self_unassign
|
||||
})
|
||||
else:
|
||||
open_slots.append(slot)
|
||||
|
||||
return request.render('planning.period_report_template', {
|
||||
'employee_slots_fullcalendar_data': employee_fullcalendar_data,
|
||||
'open_slots_ids': open_slots,
|
||||
'planning_slots_ids': planning_slots,
|
||||
'planning_planning_id': planning_sudo,
|
||||
'employee': employee_sudo,
|
||||
'format_datetime': lambda dt, dt_format: tools.format_datetime(request.env, dt, dt_format=dt_format),
|
||||
'message_slug': message,
|
||||
})
|
||||
|
||||
@http.route('/planning/<string:token_planning>/<string:token_employee>/assign/<int:slot_id>', type="http", auth="public", methods=['post'], website=True)
|
||||
def planning_self_assign(self, token_planning, token_employee, slot_id, **kwargs):
|
||||
slot_sudo = request.env['planning.slot'].sudo().browse(slot_id)
|
||||
if not slot_sudo.exists():
|
||||
return request.not_found()
|
||||
|
||||
if slot_sudo.employee_id:
|
||||
raise Forbidden(_('You can not assign yourself to this shift.'))
|
||||
|
||||
employee_sudo = request.env['hr.employee'].sudo().search([('employee_token', '=', token_employee)], limit=1)
|
||||
if not employee_sudo:
|
||||
return request.not_found()
|
||||
|
||||
planning_sudo = request.env['planning.planning'].sudo().search([('access_token', '=', token_planning)], limit=1)
|
||||
if not planning_sudo or slot_sudo.id not in planning_sudo.slot_ids._ids:
|
||||
return request.not_found()
|
||||
|
||||
slot_sudo.write({'employee_id': employee_sudo.id})
|
||||
return redirect('/planning/%s/%s?message=%s' % (token_planning, token_employee, 'assign'))
|
||||
|
||||
@http.route('/planning/<string:token_planning>/<string:token_employee>/unassign/<int:shift_id>', type="http", auth="public", methods=['post'], website=True)
|
||||
def planning_self_unassign(self, token_planning, token_employee, shift_id, **kwargs):
|
||||
slot_sudo = request.env['planning.slot'].sudo().search([('id', '=', shift_id)], limit=1)
|
||||
if not slot_sudo or not slot_sudo.allow_self_unassign:
|
||||
return request.not_found()
|
||||
|
||||
employee_sudo = request.env['hr.employee'].sudo().search([('employee_token', '=', token_employee)], limit=1)
|
||||
if not employee_sudo or employee_sudo.id != slot_sudo.employee_id.id:
|
||||
return request.not_found()
|
||||
|
||||
planning_sudo = request.env['planning.planning'].sudo().search([('access_token', '=', token_planning)], limit=1)
|
||||
if not planning_sudo or slot_sudo.id not in planning_sudo.slot_ids._ids:
|
||||
return request.not_found()
|
||||
|
||||
slot_sudo.write({'employee_id': False})
|
||||
|
||||
return redirect('/planning/%s/%s?message=%s' % (token_planning, token_employee, 'unassign'))
|
||||
|
||||
@staticmethod
|
||||
def _format_planning_shifts(color_code):
|
||||
"""Take a color code from Odoo's Kanban view and returns an hex code compatible with the fullcalendar library"""
|
||||
|
||||
switch_color = {
|
||||
0: '#008784', # No color (doesn't work actually...)
|
||||
1: '#EE4B39', # Red
|
||||
2: '#F29648', # Orange
|
||||
3: '#F4C609', # Yellow
|
||||
4: '#55B7EA', # Light blue
|
||||
5: '#71405B', # Dark purple
|
||||
6: '#E86869', # Salmon pink
|
||||
7: '#008784', # Medium blue
|
||||
8: '#267283', # Dark blue
|
||||
9: '#BF1255', # Fushia
|
||||
10: '#2BAF73', # Green
|
||||
11: '#8754B0' # Purple
|
||||
}
|
||||
|
||||
return switch_color[color_code]
|
|
@ -0,0 +1,118 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="email_template_slot_single" model="mail.template">
|
||||
<field name="name">Planning: new schedule (single shift)</field>
|
||||
<field name="email_from">${(object.company_id.email or '')}</field>
|
||||
<field name="subject">Planning: new schedule (single shift)</field>
|
||||
<field name="email_to">${object.employee_id.work_email}</field>
|
||||
<field name="model_id" ref="model_planning_slot"/>
|
||||
<field name="auto_delete" eval="False"/>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
<p>Dear ${object.employee_id.name or ''},</p><br/>
|
||||
<p>You have been assigned the following schedule:</p><br/>
|
||||
<table style="table-layout: fixed; width: 80%; margin: auto;">
|
||||
<tr>
|
||||
<th style="padding: 5px;text-align: left; width: 15%;">From</th>
|
||||
<td style="padding: 5px;">${format_datetime(object.start_datetime, tz=object.employee_id.tz)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="padding: 5px;text-align: left; width: 15%;">To</th>
|
||||
<td style="padding: 5px;">${format_datetime(object.end_datetime, tz=object.employee_id.tz)}</td>
|
||||
</tr>
|
||||
% if object.role_id
|
||||
<tr>
|
||||
<th style="padding: 5px;text-align: left; width: 15%;">Role</th>
|
||||
<td style="padding: 5px;">${object.role_id.name or ''}</td>
|
||||
</tr>
|
||||
% endif
|
||||
% if object.project_id
|
||||
<tr>
|
||||
<th style="padding: 5px;text-align: left; width: 15%;">Project</th>
|
||||
<td style="padding: 5px;">${object.project_id.name or ''}</td>
|
||||
</tr>
|
||||
% endif
|
||||
% if object.name
|
||||
<tr>
|
||||
<th style="padding: 5px;text-align: left; width: 15%;">Note</th>
|
||||
<td style="padding: 5px;">${object.name or ''}</td>
|
||||
</tr>
|
||||
% endif
|
||||
</table>
|
||||
<div style="text-align: center">
|
||||
% if ctx.get('render_link')
|
||||
<div style="display: inline-block; margin: 15px; text-align: center">
|
||||
<a href="${ctx.unavailable_path}${object.employee_id.employee_token}" target="_blank"
|
||||
style="padding: 5px 10px; color: #875A7B; text-decoration: none; background-color: #FFFFFF; border: 1px solid #FFFFFF; border-radius: 3px"
|
||||
>I am unavailable</a>
|
||||
</div>
|
||||
% endif
|
||||
% if ctx.get('render_link')
|
||||
<div style="display: inline-block; margin: 15px; text-align: center">
|
||||
<a href="/web?#action=${ctx.get('action_id')}&model=planning.slot&menu_id=${ctx.get('menu_id')}&db=${'dbname' in ctx and ctx['dbname'] or '' }" target="_blank"
|
||||
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px"
|
||||
>View Planning</a>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="email_template_planning_planning" model="mail.template">
|
||||
<field name="name">Planning: new schedule (multiple shifts)</field>
|
||||
<field name="email_from">${(object.company_id.email or '')}</field>
|
||||
<field name="subject">Your planning from ${format_datetime(object.start_datetime, tz=ctx.get('employee').tz or 'UTC', dt_format='short')} to ${format_datetime(object.end_datetime, tz=employee.tz if employee else 'UTC', dt_format='short')}</field>
|
||||
<field name="email_to"></field><!-- Set in the code -->
|
||||
<field name="model_id" ref="model_planning_planning"/>
|
||||
<field name="auto_delete" eval="False"/><!-- TODO JEM change this as we are testing -->
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
% if ctx.get('employee'):
|
||||
<p>Dear ${ctx['employee'].name},</p>
|
||||
% else:
|
||||
<p>Hello,</p>
|
||||
% endif
|
||||
<p>
|
||||
You have been assigned new shifts:
|
||||
</p>
|
||||
|
||||
<table style="table-layout: fixed; width: 80%; margin: auto;">
|
||||
<tr>
|
||||
<th style="padding: 5px;text-align: left; width: 15%;">From</th>
|
||||
<td style="padding: 5px;">${format_datetime(object.start_datetime, tz=ctx.get('employee').tz or 'UTC', dt_format='short')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="padding: 5px;text-align: left; width: 15%;">To</th>
|
||||
<td style="padding: 5px;">${format_datetime(object.end_datetime, tz=ctx.get('employee').tz or 'UTC', dt_format='short')}</td>
|
||||
</tr>
|
||||
% if object.project_id
|
||||
<tr>
|
||||
<th style="padding: 5px;text-align: left; width: 15%;">Project</th>
|
||||
<td style="padding: 5px;">${object.project_id.name or ''}</td>
|
||||
</tr>
|
||||
% endif
|
||||
</table>
|
||||
|
||||
% if ctx.get('planning_url'):
|
||||
<div style="margin: 15px;">
|
||||
<a href="${ctx.get('planning_url')}" target="_blank"
|
||||
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">View Your Planning</a>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
% if ctx.get('slot_unassigned_count'):
|
||||
<p>There are new open shifts available. Please assign yourself if you are available.</p>
|
||||
% endif
|
||||
|
||||
% if ctx.get('message'):
|
||||
<p>${ctx['message']}</p>
|
||||
% endif
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="ir_cron_forecast_schedule" model="ir.cron">
|
||||
<field name="name">Planning: generate next recurring shifts</field>
|
||||
<field name="model_id" ref="model_planning_recurrency"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_schedule_next()</field>
|
||||
<field name="interval_type">weeks</field>
|
||||
<field name="numbercall">-1</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
|
@ -0,0 +1,547 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Project forecast will warn if the user assigned to a forecast doesn't have a timezone -->
|
||||
<record id="base.user_demo" model="res.users">
|
||||
<field name="tz">Europe/Brussels</field>
|
||||
<field name="groups_id" eval="[(4,ref('planning.group_planning_user'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Roles -->
|
||||
<record id="planning_role_bartender" model="planning.role">
|
||||
<field name="name">Bartender</field>
|
||||
<field name="color">2</field>
|
||||
</record>
|
||||
<record id="planning_role_waiter" model="planning.role">
|
||||
<field name="name">Waiter</field>
|
||||
<field name="color">3</field>
|
||||
</record>
|
||||
<record id="planning_role_chef" model="planning.role">
|
||||
<field name="name">Chef</field>
|
||||
<field name="color">4</field>
|
||||
</record>
|
||||
|
||||
<!-- Shift templates (morning shifts) -->
|
||||
<record id="planning_template_chef_morning" model="planning.slot.template">
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="start_time" eval="8"/>
|
||||
<field name="duration" eval="6"/>
|
||||
</record>
|
||||
<record id="planning_template_bartender_morning" model="planning.slot.template">
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="start_time" eval="8"/>
|
||||
<field name="duration" eval="8"/>
|
||||
</record>
|
||||
<record id="planning_template_waiter_morning" model="planning.slot.template">
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="start_time" eval="8"/>
|
||||
<field name="duration" eval="8"/>
|
||||
</record>
|
||||
|
||||
<!-- Shift templates (evening shifts) -->
|
||||
<record id="planning_template_chef_evening" model="planning.slot.template">
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="start_time" eval="14"/>
|
||||
<field name="duration" eval="8"/>
|
||||
</record>
|
||||
<record id="planning_template_bartender_evening" model="planning.slot.template">
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="start_time" eval="16"/>
|
||||
<field name="duration" eval="8"/>
|
||||
</record>
|
||||
<record id="planning_template_waiter_evening" model="planning.slot.template">
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="start_time" eval="16"/>
|
||||
<field name="duration" eval="8"/>
|
||||
</record>
|
||||
|
||||
<!-- Recurrencies -->
|
||||
<record id="planning_recurrency_1" model="planning.recurrency">
|
||||
<field name="repeat_interval" eval="1"/>
|
||||
<field name="repeat_type">forever</field>
|
||||
</record>
|
||||
<record id="planning_recurrency_2" model="planning.recurrency">
|
||||
<field name="repeat_interval" eval="1"/>
|
||||
<field name="repeat_type">forever</field>
|
||||
</record>
|
||||
<record id="planning_recurrency_3" model="planning.recurrency">
|
||||
<field name="repeat_interval" eval="1"/>
|
||||
<field name="repeat_type">forever</field>
|
||||
</record>
|
||||
|
||||
<!-- Week 1 -->
|
||||
|
||||
<!-- Monday morning shifts -->
|
||||
<record id="planning_slot_111" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_112" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 12:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_stw"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_113" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_mit"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Monday evening shifts -->
|
||||
<record id="planning_slot_114" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 20:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_jth"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_115" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_chs"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_116" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday())).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_niv"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Tuesday morning shifts -->
|
||||
<record id="planning_slot_121" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_122" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 12:00:00')"/>
|
||||
<field name="employee_id" eval="False"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="name" eval="'Check with Katy from the employment agency if they have someone to replace Randall this day'"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_123" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_jep"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Tuesday evening shifts -->
|
||||
<record id="planning_slot_124" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 20:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_jth"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_125" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_chs"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_126" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 1)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_niv"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Wednesday morning shifts -->
|
||||
<record id="planning_slot_131" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_132" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 12:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_stw"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_133" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_mit"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Wednesday evening shifts -->
|
||||
<record id="planning_slot_134" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 20:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_jth"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_135" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="name" eval="'Mitchel is replacing Jennie until the end of the week'"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_136" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 2)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_niv"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Thursday morning shifts -->
|
||||
<record id="planning_slot_141" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_142" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 12:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_stw"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_143" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_mit"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Thursday evening shifts -->
|
||||
<record id="planning_slot_144" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 20:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_jth"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_145" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_146" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 3)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_niv"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Friday morning shifts -->
|
||||
<record id="planning_slot_151" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" eval="False"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_152" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 12:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_stw"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_153" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_mit"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Friday evening shifts -->
|
||||
<record id="planning_slot_154" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 20:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_jth"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_155" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_156" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 4)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_niv"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Week 2 -->
|
||||
|
||||
<!-- Monday morning shifts -->
|
||||
<record id="planning_slot_211" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_212" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 12:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_stw"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_213" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_mit"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Monday evening shifts -->
|
||||
<record id="planning_slot_214" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 20:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_jth"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_215" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_chs"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_216" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_niv"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Tuesday morning shifts -->
|
||||
<record id="planning_slot_221" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_222" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 12:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_stw"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_223" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_jep"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Tuesday evening shifts -->
|
||||
<record id="planning_slot_224" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 20:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_jth"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_225" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_chs"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_226" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 1)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" eval="False"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Wednesday morning shifts -->
|
||||
<record id="planning_slot_231" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_232" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 12:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_stw"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_233" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_mit"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Wednesday evening shifts -->
|
||||
<record id="planning_slot_234" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 20:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_jth"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_235" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_chs"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_236" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 2)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" eval="False"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Thursday morning shifts -->
|
||||
<record id="planning_slot_241" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_242" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 12:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_stw"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_243" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_mit"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Thursday evening shifts -->
|
||||
<record id="planning_slot_244" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 20:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="name" eval="'Ask Toni if she can show Mitchel how things are done in the kitchen because it is her first time working as a chef'"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_245" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_chs"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_246" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 3)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_niv"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Friday morning shifts -->
|
||||
<record id="planning_slot_251" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="recurrency_id" ref="planning_recurrency_1"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_252" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 12:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_stw"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="recurrency_id" ref="planning_recurrency_2"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_253" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 06:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="employee_id" eval="False"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Friday evening shifts -->
|
||||
<record id="planning_slot_254" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 20:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="role_id" ref="planning_role_chef"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_255" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_chs"/>
|
||||
<field name="role_id" ref="planning_role_waiter"/>
|
||||
<field name="recurrency_id" ref="planning_recurrency_3"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
<record id="planning_slot_256" model="planning.slot">
|
||||
<field name="start_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 14:00:00')"/>
|
||||
<field name="end_datetime" eval="(datetime.today() - timedelta(days=datetime.today().weekday() - 7 - 4)).strftime('%Y-%m-%d 22:00:00')"/>
|
||||
<field name="employee_id" ref="hr.employee_niv"/>
|
||||
<field name="role_id" ref="planning_role_bartender"/>
|
||||
<field name="publication_warning" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Duplicate shifts that have a recurrence set -->
|
||||
<function model="planning.recurrency" name="_repeat_slot" eval="[[ref('planning_recurrency_1'), ref('planning_recurrency_2'), ref('planning_recurrency_3')]]"/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import hr
|
||||
from . import planning_recurrency
|
||||
from . import planning
|
||||
from . import planning_template
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
|
@ -0,0 +1,44 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Employee(models.Model):
|
||||
_inherit = "hr.employee"
|
||||
|
||||
def _default_employee_token(self):
|
||||
return str(uuid.uuid4())
|
||||
|
||||
planning_role_id = fields.Many2one('planning.role', string="Default Planning Role", groups='hr.group_hr_user')
|
||||
employee_token = fields.Char('Security Token', default=_default_employee_token, copy=False, groups='hr.group_hr_user', readonly=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('employee_token_unique', 'unique(employee_token)', 'Error: each employee token must be unique')
|
||||
]
|
||||
|
||||
def _init_column(self, column_name):
|
||||
# to avoid generating a single default employee_token when installing the module,
|
||||
# we need to set the default row by row for this column
|
||||
if column_name == "employee_token":
|
||||
_logger.debug("Table '%s': setting default value of new column %s to unique values for each row", self._table, column_name)
|
||||
self.env.cr.execute("SELECT id FROM %s WHERE employee_token IS NULL" % self._table)
|
||||
acc_ids = self.env.cr.dictfetchall()
|
||||
query_list = [{'id': acc_id['id'], 'employee_token': self._default_employee_token()} for acc_id in acc_ids]
|
||||
query = 'UPDATE ' + self._table + ' SET employee_token = %(employee_token)s WHERE id = %(id)s;'
|
||||
self.env.cr._obj.executemany(query, query_list)
|
||||
else:
|
||||
super(Employee, self)._init_column(column_name)
|
||||
|
||||
def _planning_get_url(self, planning):
|
||||
result = {}
|
||||
for employee in self:
|
||||
if employee.user_id and employee.user_id.has_group('planning.group_planning_user'):
|
||||
result[employee.id] = '/web?#action=planning.planning_action_open_shift'
|
||||
else:
|
||||
result[employee.id] = '/planning/%s/%s' % (planning.access_token, employee.employee_token)
|
||||
return result
|
|
@ -0,0 +1,680 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from ast import literal_eval
|
||||
from datetime import datetime, timedelta
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import json
|
||||
import logging
|
||||
import pytz
|
||||
import uuid
|
||||
from math import ceil
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, AccessError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
from odoo.tools import format_time
|
||||
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def days_span(start_datetime, end_datetime):
|
||||
if not isinstance(start_datetime, datetime):
|
||||
raise ValueError
|
||||
if not isinstance(end_datetime, datetime):
|
||||
raise ValueError
|
||||
end = datetime.combine(end_datetime, datetime.min.time())
|
||||
start = datetime.combine(start_datetime, datetime.min.time())
|
||||
duration = end - start
|
||||
return duration.days + 1
|
||||
|
||||
|
||||
class Planning(models.Model):
|
||||
_name = 'planning.slot'
|
||||
_description = 'Planning Shift'
|
||||
_order = 'start_datetime,id desc'
|
||||
_rec_name = 'name'
|
||||
_check_company_auto = True
|
||||
|
||||
def _default_employee_id(self):
|
||||
return self.env['hr.employee'].search([('user_id', '=', self.env.uid), ('company_id', '=', self.env.company.id)])
|
||||
|
||||
def _default_start_datetime(self):
|
||||
return fields.Datetime.to_string(datetime.combine(fields.Datetime.now(), datetime.min.time()))
|
||||
|
||||
def _default_end_datetime(self):
|
||||
return fields.Datetime.to_string(datetime.combine(fields.Datetime.now(), datetime.max.time()))
|
||||
|
||||
name = fields.Text('Note')
|
||||
employee_id = fields.Many2one('hr.employee', "Employee", default=_default_employee_id, group_expand='_read_group_employee_id', check_company=True)
|
||||
user_id = fields.Many2one('res.users', string="User", related='employee_id.user_id', store=True, readonly=True)
|
||||
company_id = fields.Many2one('res.company', string="Company", required=True, compute="_compute_planning_slot_company_id", store=True, readonly=False)
|
||||
role_id = fields.Many2one('planning.role', string="Role")
|
||||
color = fields.Integer("Color", related='role_id.color')
|
||||
was_copied = fields.Boolean("This shift was copied from previous week", default=False, readonly=True)
|
||||
|
||||
start_datetime = fields.Datetime("Start Date", required=True, default=_default_start_datetime)
|
||||
end_datetime = fields.Datetime("End Date", required=True, default=_default_end_datetime)
|
||||
|
||||
# UI fields and warnings
|
||||
allow_self_unassign = fields.Boolean('Let employee unassign themselves', related='company_id.planning_allow_self_unassign')
|
||||
is_assigned_to_me = fields.Boolean('Is this shift assigned to the current user', compute='_compute_is_assigned_to_me')
|
||||
overlap_slot_count = fields.Integer('Overlapping slots', compute='_compute_overlap_slot_count')
|
||||
|
||||
# time allocation
|
||||
allocation_type = fields.Selection([
|
||||
('planning', 'Planning'),
|
||||
('forecast', 'Forecast')
|
||||
], compute='_compute_allocation_type')
|
||||
allocated_hours = fields.Float("Allocated hours", default=0, compute='_compute_allocated_hours', store=True)
|
||||
allocated_percentage = fields.Float("Allocated Time (%)", default=100, help="Percentage of time the employee is supposed to work during the shift.")
|
||||
working_days_count = fields.Integer("Number of working days", compute='_compute_working_days_count', store=True)
|
||||
|
||||
# publication and sending
|
||||
is_published = fields.Boolean("Is the shift sent", default=False, readonly=True, help="If checked, this means the planning entry has been sent to the employee. Modifying the planning entry will mark it as not sent.")
|
||||
publication_warning = fields.Boolean("Modified since last publication", default=False, readonly=True, help="If checked, it means that the shift contains has changed since its last publish.", copy=False)
|
||||
|
||||
# template dummy fields (only for UI purpose)
|
||||
template_creation = fields.Boolean("Save as a Template", default=False, store=False, inverse='_inverse_template_creation')
|
||||
template_autocomplete_ids = fields.Many2many('planning.slot.template', store=False, compute='_compute_template_autocomplete_ids')
|
||||
template_id = fields.Many2one('planning.slot.template', string='Planning Templates', store=False)
|
||||
|
||||
# Recurring (`repeat_` fields are none stored, only used for UI purpose)
|
||||
recurrency_id = fields.Many2one('planning.recurrency', readonly=True, index=True, ondelete="set null", copy=False)
|
||||
repeat = fields.Boolean("Repeat", compute='_compute_repeat', inverse='_inverse_repeat')
|
||||
repeat_interval = fields.Integer("Repeat every", default=1, compute='_compute_repeat', inverse='_inverse_repeat')
|
||||
repeat_type = fields.Selection([('forever', 'Forever'), ('until', 'Until')], string='Repeat Type', default='forever', compute='_compute_repeat', inverse='_inverse_repeat')
|
||||
repeat_until = fields.Date("Repeat Until", compute='_compute_repeat', inverse='_inverse_repeat', help="If set, the recurrence stop at that date. Otherwise, the recurrence is applied indefinitely.")
|
||||
|
||||
_sql_constraints = [
|
||||
('check_start_date_lower_end_date', 'CHECK(end_datetime > start_datetime)', 'Shift end date should be greater than its start date'),
|
||||
('check_allocated_hours_positive', 'CHECK(allocated_hours >= 0)', 'You cannot have negative shift'),
|
||||
]
|
||||
|
||||
@api.depends('employee_id')
|
||||
def _compute_planning_slot_company_id(self):
|
||||
if self.employee_id:
|
||||
self.company_id = self.employee_id.company_id.id
|
||||
if not self.company_id.id:
|
||||
self.company_id = self.env.company
|
||||
|
||||
@api.depends('user_id')
|
||||
def _compute_is_assigned_to_me(self):
|
||||
for slot in self:
|
||||
slot.is_assigned_to_me = slot.user_id == self.env.user
|
||||
|
||||
@api.depends('start_datetime', 'end_datetime')
|
||||
def _compute_allocation_type(self):
|
||||
for slot in self:
|
||||
if slot.start_datetime and slot.end_datetime and (slot.end_datetime - slot.start_datetime).total_seconds() / 3600.0 < 24:
|
||||
slot.allocation_type = 'planning'
|
||||
else:
|
||||
slot.allocation_type = 'forecast'
|
||||
|
||||
@api.depends('start_datetime', 'end_datetime', 'employee_id.resource_calendar_id', 'allocated_percentage')
|
||||
def _compute_allocated_hours(self):
|
||||
for slot in self:
|
||||
if slot.start_datetime and slot.end_datetime:
|
||||
percentage = slot.allocated_percentage / 100.0 or 1
|
||||
if slot.allocation_type == 'planning' and slot.start_datetime and slot.end_datetime:
|
||||
slot.allocated_hours = (slot.end_datetime - slot.start_datetime).total_seconds() * percentage / 3600.0
|
||||
else:
|
||||
if slot.employee_id:
|
||||
slot.allocated_hours = slot.employee_id._get_work_days_data(slot.start_datetime, slot.end_datetime, compute_leaves=True)['hours'] * percentage
|
||||
else:
|
||||
slot.allocated_hours = 0.0
|
||||
|
||||
@api.depends('start_datetime', 'end_datetime', 'employee_id')
|
||||
def _compute_working_days_count(self):
|
||||
for slot in self:
|
||||
if slot.employee_id:
|
||||
slot.working_days_count = ceil(slot.employee_id._get_work_days_data(slot.start_datetime, slot.end_datetime, compute_leaves=True)['days'])
|
||||
else:
|
||||
slot.working_days_count = 0
|
||||
|
||||
@api.depends('start_datetime', 'end_datetime', 'employee_id')
|
||||
def _compute_overlap_slot_count(self):
|
||||
if self.ids:
|
||||
self.flush(['start_datetime', 'end_datetime', 'employee_id'])
|
||||
query = """
|
||||
SELECT S1.id,count(*) FROM
|
||||
planning_slot S1, planning_slot S2
|
||||
WHERE
|
||||
S1.start_datetime < S2.end_datetime and S1.end_datetime > S2.start_datetime and S1.id <> S2.id and S1.employee_id = S2.employee_id
|
||||
GROUP BY S1.id;
|
||||
"""
|
||||
self.env.cr.execute(query, (tuple(self.ids),))
|
||||
overlap_mapping = dict(self.env.cr.fetchall())
|
||||
for slot in self:
|
||||
slot.overlap_slot_count = overlap_mapping.get(slot.id, 0)
|
||||
else:
|
||||
self.overlap_slot_count = 0
|
||||
|
||||
@api.depends('role_id')
|
||||
def _compute_template_autocomplete_ids(self):
|
||||
domain = []
|
||||
if self.role_id:
|
||||
domain = [('role_id', '=', self.role_id.id)]
|
||||
self.template_autocomplete_ids = self.env['planning.slot.template'].search(domain, order='start_time', limit=10)
|
||||
|
||||
@api.depends('recurrency_id')
|
||||
def _compute_repeat(self):
|
||||
for slot in self:
|
||||
if slot.recurrency_id:
|
||||
slot.repeat = True
|
||||
slot.repeat_interval = slot.recurrency_id.repeat_interval
|
||||
slot.repeat_until = slot.recurrency_id.repeat_until
|
||||
slot.repeat_type = slot.recurrency_id.repeat_type
|
||||
else:
|
||||
slot.repeat = False
|
||||
slot.repeat_interval = False
|
||||
slot.repeat_until = False
|
||||
slot.repeat_type = False
|
||||
|
||||
def _inverse_repeat(self):
|
||||
for slot in self:
|
||||
if slot.repeat and not slot.recurrency_id.id: # create the recurrence
|
||||
recurrency_values = {
|
||||
'repeat_interval': slot.repeat_interval,
|
||||
'repeat_until': slot.repeat_until if slot.repeat_type == 'until' else False,
|
||||
'repeat_type': slot.repeat_type,
|
||||
'company_id': slot.company_id.id,
|
||||
}
|
||||
recurrence = self.env['planning.recurrency'].create(recurrency_values)
|
||||
slot.recurrency_id = recurrence
|
||||
slot.recurrency_id._repeat_slot()
|
||||
# user wants to delete the recurrence
|
||||
# here we also check that we don't delete by mistake a slot of which the repeat parameters have been changed
|
||||
elif not slot.repeat and slot.recurrency_id.id and (
|
||||
slot.repeat_type == slot.recurrency_id.repeat_type and
|
||||
slot.repeat_until == slot.recurrency_id.repeat_until and
|
||||
slot.repeat_interval == slot.recurrency_id.repeat_interval
|
||||
):
|
||||
slot.recurrency_id._delete_slot(slot.end_datetime)
|
||||
slot.recurrency_id.unlink() # will set recurrency_id to NULL
|
||||
|
||||
def _inverse_template_creation(self):
|
||||
values_list = []
|
||||
existing_values = []
|
||||
for slot in self:
|
||||
if slot.template_creation:
|
||||
values_list.append(slot._prepare_template_values())
|
||||
# Here we check if there's already a template w/ the same data
|
||||
existing_templates = self.env['planning.slot.template'].read_group([], ['role_id', 'start_time', 'duration'], ['role_id', 'start_time', 'duration'], limit=None, lazy=False)
|
||||
if len(existing_templates):
|
||||
for element in existing_templates:
|
||||
role_id = element['role_id'][0] if element.get('role_id') else False
|
||||
existing_values.append({'role_id': role_id, 'start_time': element['start_time'], 'duration': element['duration']})
|
||||
self.env['planning.slot.template'].create([x for x in values_list if x not in existing_values])
|
||||
|
||||
@api.onchange('employee_id')
|
||||
def _onchange_employee_id(self):
|
||||
if self.employee_id:
|
||||
start = self.start_datetime or datetime.combine(fields.Datetime.now(), datetime.min.time())
|
||||
end = self.end_datetime or datetime.combine(fields.Datetime.now(), datetime.max.time())
|
||||
work_interval = self.employee_id.resource_id._get_work_interval(start, end)
|
||||
start_datetime, end_datetime = work_interval[self.employee_id.resource_id]
|
||||
#start_datetime, end_datetime = work_interval[self.employee_id.resource_id.id]
|
||||
if start_datetime:
|
||||
self.start_datetime = start_datetime.astimezone(pytz.utc).replace(tzinfo=None)
|
||||
if end_datetime:
|
||||
self.end_datetime = end_datetime.astimezone(pytz.utc).replace(tzinfo=None)
|
||||
# Set default role if the role field is empty
|
||||
if not self.role_id and self.employee_id.sudo().planning_role_id:
|
||||
self.role_id = self.employee_id.sudo().planning_role_id
|
||||
|
||||
@api.onchange('start_datetime', 'end_datetime', 'employee_id')
|
||||
def _onchange_dates(self):
|
||||
if self.employee_id and self.is_published:
|
||||
self.publication_warning = True
|
||||
|
||||
@api.onchange('template_creation')
|
||||
def _onchange_template_autocomplete_ids(self):
|
||||
templates = self.env['planning.slot.template'].search([], order='start_time', limit=10)
|
||||
if templates:
|
||||
if not self.template_creation:
|
||||
self.template_autocomplete_ids = templates
|
||||
else:
|
||||
self.template_autocomplete_ids = False
|
||||
else:
|
||||
self.template_autocomplete_ids = False
|
||||
|
||||
@api.onchange('template_id')
|
||||
def _onchange_template_id(self):
|
||||
user_tz = pytz.timezone(self.env.user.tz or 'UTC')
|
||||
if self.template_id and self.start_datetime:
|
||||
h, m = divmod(self.template_id.start_time, 1)
|
||||
start = pytz.utc.localize(self.start_datetime).astimezone(user_tz)
|
||||
start = start.replace(hour=int(h), minute=int(m * 60))
|
||||
self.start_datetime = start.astimezone(pytz.utc).replace(tzinfo=None)
|
||||
h, m = divmod(self.template_id.duration, 1)
|
||||
delta = timedelta(hours=int(h), minutes=int(m * 60))
|
||||
self.end_datetime = fields.Datetime.to_string(self.start_datetime + delta)
|
||||
|
||||
self.role_id = self.template_id.role_id
|
||||
|
||||
@api.onchange('repeat')
|
||||
def _onchange_default_repeat_values(self):
|
||||
""" When checking the `repeat` flag on an existing record, the values of recurring fields are `False`. This onchange
|
||||
restore the default value for usability purpose.
|
||||
"""
|
||||
recurrence_fields = ['repeat_interval', 'repeat_until', 'repeat_type']
|
||||
default_values = self.default_get(recurrence_fields)
|
||||
for fname in recurrence_fields:
|
||||
self[fname] = default_values.get(fname)
|
||||
|
||||
|
||||
# ----------------------------------------------------
|
||||
# ORM overrides
|
||||
# ----------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
|
||||
result = super(Planning, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
|
||||
view_type = self.env.context.get('view_type')
|
||||
if 'employee_id' in groupby and view_type == 'gantt':
|
||||
# Always prepend 'Undefined Employees' (will be printed 'Open Shifts' when called by the frontend)
|
||||
d = {}
|
||||
for field in fields:
|
||||
d.update({field: False})
|
||||
result.insert(0, d)
|
||||
return result
|
||||
|
||||
def name_get(self):
|
||||
group_by = self.env.context.get('group_by', [])
|
||||
field_list = [fname for fname in self._name_get_fields() if fname not in group_by][:2] # limit to 2 labels
|
||||
|
||||
result = []
|
||||
for slot in self:
|
||||
# label part, depending on context `groupby`
|
||||
name = ' - '.join([self._fields[fname].convert_to_display_name(slot[fname], slot) for fname in field_list if slot[fname]])
|
||||
|
||||
# date / time part
|
||||
destination_tz = pytz.timezone(self.env.user.tz or 'UTC')
|
||||
start_datetime = pytz.utc.localize(slot.start_datetime).astimezone(destination_tz).replace(tzinfo=None)
|
||||
end_datetime = pytz.utc.localize(slot.end_datetime).astimezone(destination_tz).replace(tzinfo=None)
|
||||
if slot.end_datetime - slot.start_datetime <= timedelta(hours=24): # shift on a single day
|
||||
name = '%s - %s %s' % (
|
||||
format_time(self.env, start_datetime.time(), time_format='short'),
|
||||
format_time(self.env, end_datetime.time(), time_format='short'),
|
||||
name
|
||||
)
|
||||
else:
|
||||
name = '%s - %s %s' % (
|
||||
start_datetime.date(),
|
||||
end_datetime.date(),
|
||||
name
|
||||
)
|
||||
|
||||
# add unicode bubble to tell there is a note
|
||||
if slot.name:
|
||||
name = u'%s \U0001F4AC' % name
|
||||
|
||||
result.append([slot.id, name])
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if not vals.get('company_id') and vals.get('employee_id'):
|
||||
vals['company_id'] = self.env['hr.employee'].browse(vals.get('employee_id')).company_id.id
|
||||
if not vals.get('company_id'):
|
||||
vals['company_id'] = self.env.company.id
|
||||
return super().create(vals)
|
||||
|
||||
def write(self, values):
|
||||
# detach planning entry from recurrency
|
||||
if any(fname in values.keys() for fname in self._get_fields_breaking_recurrency()) and not values.get('recurrency_id'):
|
||||
values.update({'recurrency_id': False})
|
||||
# warning on published shifts
|
||||
if 'publication_warning' not in values and (set(values.keys()) & set(self._get_fields_breaking_publication())):
|
||||
values['publication_warning'] = True
|
||||
result = super(Planning, self).write(values)
|
||||
# recurrence
|
||||
if any(key in ('repeat', 'repeat_type', 'repeat_until', 'repeat_interval') for key in values):
|
||||
# User is trying to change this record's recurrence so we delete future slots belonging to recurrence A
|
||||
# and we create recurrence B from now on w/ the new parameters
|
||||
for slot in self:
|
||||
if slot.recurrency_id and values.get('repeat') is None:
|
||||
recurrency_values = {
|
||||
'repeat_interval': values.get('repeat_interval') or slot.recurrency_id.repeat_interval,
|
||||
'repeat_until': values.get('repeat_until') if values.get('repeat_type') == 'until' else False,
|
||||
'repeat_type': values.get('repeat_type'),
|
||||
'company_id': slot.company_id.id,
|
||||
}
|
||||
# Kill recurrence A
|
||||
slot.recurrency_id.repeat_type = 'until'
|
||||
slot.recurrency_id.repeat_until = slot.start_datetime
|
||||
slot.recurrency_id._delete_slot(slot.end_datetime)
|
||||
# Create recurrence B
|
||||
recurrence = slot.env['planning.recurrency'].create(recurrency_values)
|
||||
slot.recurrency_id = recurrence
|
||||
slot.recurrency_id._repeat_slot()
|
||||
return result
|
||||
|
||||
# ----------------------------------------------------
|
||||
# Actions
|
||||
# ----------------------------------------------------
|
||||
|
||||
def action_unlink(self):
|
||||
self.unlink()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def action_see_overlaping_slots(self):
|
||||
domain_map = self._get_overlap_domain()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'planning.slot',
|
||||
'name': _('Shifts in conflict'),
|
||||
'view_mode': 'gantt,list,form',
|
||||
'domain': domain_map[self.id],
|
||||
'context': {
|
||||
'initialDate': min([slot.start_datetime for slot in self.search(domain_map[self.id])])
|
||||
}
|
||||
}
|
||||
|
||||
def action_self_assign(self):
|
||||
""" Allow planning user to self assign open shift. """
|
||||
self.ensure_one()
|
||||
# user must at least 'read' the shift to self assign (Prevent any user in the system (portal, ...) to assign themselves)
|
||||
if not self.check_access_rights('read', raise_exception=False):
|
||||
raise AccessError(_("You don't the right to self assign."))
|
||||
if self.employee_id:
|
||||
raise UserError(_("You can not assign yourself to an already assigned shift."))
|
||||
return self.sudo().write({'employee_id': self.env.user.employee_id.id if self.env.user.employee_id else False})
|
||||
|
||||
def action_self_unassign(self):
|
||||
""" Allow planning user to self unassign from a shift, if the feature is activated """
|
||||
self.ensure_one()
|
||||
# The following condition will check the read access on planning.slot, and that user must at least 'read' the
|
||||
# shift to self unassign. Prevent any user in the system (portal, ...) to unassign any shift.
|
||||
if not self.allow_self_unassign:
|
||||
raise UserError(_("The company does not allow you to self unassign."))
|
||||
if self.employee_id != self.env.user.employee_id:
|
||||
raise UserError(_("You can not unassign another employee than yourself."))
|
||||
return self.sudo().write({'employee_id': False})
|
||||
|
||||
# ----------------------------------------------------
|
||||
# Gantt view
|
||||
# ----------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def gantt_unavailability(self, start_date, end_date, scale, group_bys=None, rows=None):
|
||||
start_datetime = fields.Datetime.from_string(start_date)
|
||||
end_datetime = fields.Datetime.from_string(end_date)
|
||||
employee_ids = set()
|
||||
|
||||
# function to "mark" top level rows concerning employees
|
||||
# the propagation of that item to subrows is taken care of in the traverse function below
|
||||
def tag_employee_rows(rows):
|
||||
for row in rows:
|
||||
group_bys = row.get('groupedBy')
|
||||
res_id = row.get('resId')
|
||||
if group_bys:
|
||||
# if employee_id is the first grouping attribute, we mark the row
|
||||
if group_bys[0] == 'employee_id' and res_id:
|
||||
employee_id = res_id
|
||||
employee_ids.add(employee_id)
|
||||
row['employee_id'] = employee_id
|
||||
# else we recursively traverse the rows where employee_id appears in the group_by
|
||||
elif 'employee_id' in group_bys:
|
||||
tag_employee_rows(row.get('rows'))
|
||||
|
||||
tag_employee_rows(rows)
|
||||
employees = self.env['hr.employee'].browse(employee_ids)
|
||||
leaves_mapping = employees.mapped('resource_id')._get_unavailable_intervals(start_datetime, end_datetime)
|
||||
|
||||
# function to recursively replace subrows with the ones returned by func
|
||||
def traverse(func, row):
|
||||
new_row = dict(row)
|
||||
if new_row.get('employee_id'):
|
||||
for sub_row in new_row.get('rows'):
|
||||
sub_row['employee_id'] = new_row['employee_id']
|
||||
new_row['rows'] = [traverse(func, row) for row in new_row.get('rows')]
|
||||
return func(new_row)
|
||||
|
||||
cell_dt = timedelta(hours=1) if scale in ['day', 'week'] else timedelta(hours=12)
|
||||
|
||||
# for a single row, inject unavailability data
|
||||
def inject_unavailability(row):
|
||||
new_row = dict(row)
|
||||
|
||||
if row.get('employee_id'):
|
||||
employee_id = self.env['hr.employee'].browse(row.get('employee_id'))
|
||||
if employee_id:
|
||||
# remove intervals smaller than a cell, as they will cause half a cell to turn grey
|
||||
# ie: when looking at a week, a employee start everyday at 8, so there is a unavailability
|
||||
# like: 2019-05-22 20:00 -> 2019-05-23 08:00 which will make the first half of the 23's cell grey
|
||||
notable_intervals = filter(lambda interval: interval[1] - interval[0] >= cell_dt, leaves_mapping[employee_id.resource_id.id])
|
||||
new_row['unavailabilities'] = [{'start': interval[0], 'stop': interval[1]} for interval in notable_intervals]
|
||||
return new_row
|
||||
|
||||
return [traverse(inject_unavailability, row) for row in rows]
|
||||
|
||||
# ----------------------------------------------------
|
||||
# Period Duplication
|
||||
# ----------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def action_copy_previous_week(self, date_start_week):
|
||||
date_end_copy = datetime.strptime(date_start_week, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
date_start_copy = date_end_copy - relativedelta(days=7)
|
||||
domain = [
|
||||
('start_datetime', '>=', date_start_copy),
|
||||
('end_datetime', '<=', date_end_copy),
|
||||
('recurrency_id', '=', False),
|
||||
('was_copied', '=', False)
|
||||
]
|
||||
slots_to_copy = self.search(domain)
|
||||
|
||||
new_slot_values = []
|
||||
for slot in slots_to_copy:
|
||||
if not slot.was_copied:
|
||||
values = slot.copy_data()[0]
|
||||
if values.get('start_datetime'):
|
||||
values['start_datetime'] += relativedelta(days=7)
|
||||
if values.get('end_datetime'):
|
||||
values['end_datetime'] += relativedelta(days=7)
|
||||
values['is_published'] = False
|
||||
new_slot_values.append(values)
|
||||
slots_to_copy.write({'was_copied': True})
|
||||
return self.create(new_slot_values)
|
||||
|
||||
# ----------------------------------------------------
|
||||
# Sending Shifts
|
||||
# ----------------------------------------------------
|
||||
|
||||
def action_send(self):
|
||||
group_planning_user = self.env.ref('planning.group_planning_user')
|
||||
template = self.env.ref('planning.email_template_slot_single')
|
||||
# update context to build a link for view in the slot
|
||||
view_context = dict(self._context)
|
||||
view_context.update({
|
||||
'menu_id': str(self.env.ref('planning.planning_menu_root').id),
|
||||
'action_id': str(self.env.ref('planning.planning_action_open_shift').id),
|
||||
'dbname': self.env.cr.dbname,
|
||||
'render_link': self.employee_id.user_id and self.employee_id.user_id in group_planning_user.users,
|
||||
'unavailable_path': '/planning/myshifts/',
|
||||
})
|
||||
slot_template = template.with_context(view_context)
|
||||
|
||||
mails_to_send = self.env['mail.mail']
|
||||
for slot in self:
|
||||
if slot.employee_id and slot.employee_id.work_email:
|
||||
mail_id = slot_template.with_context(view_context).send_mail(slot.id, notif_layout='mail.mail_notification_light')
|
||||
current_mail = self.env['mail.mail'].browse(mail_id)
|
||||
mails_to_send |= current_mail
|
||||
|
||||
if mails_to_send:
|
||||
mails_to_send.send()
|
||||
|
||||
self.write({
|
||||
'is_published': True,
|
||||
'publication_warning': False,
|
||||
})
|
||||
return mails_to_send
|
||||
|
||||
def action_publish(self):
|
||||
self.write({
|
||||
'is_published': True,
|
||||
'publication_warning': False,
|
||||
})
|
||||
return True
|
||||
|
||||
# ----------------------------------------------------
|
||||
# Business Methods
|
||||
# ----------------------------------------------------
|
||||
def _name_get_fields(self):
|
||||
""" List of fields that can be displayed in the name_get """
|
||||
return ['employee_id', 'role_id']
|
||||
|
||||
def _get_fields_breaking_publication(self):
|
||||
""" Fields list triggering the `publication_warning` to True when updating shifts """
|
||||
return [
|
||||
'employee_id',
|
||||
'start_datetime',
|
||||
'end_datetime',
|
||||
'role_id',
|
||||
]
|
||||
|
||||
def _get_fields_breaking_recurrency(self):
|
||||
"""Returns the list of field which when changed should break the relation of the forecast
|
||||
with it's recurrency
|
||||
"""
|
||||
return [
|
||||
'employee_id',
|
||||
'role_id',
|
||||
]
|
||||
|
||||
def _get_overlap_domain(self):
|
||||
""" get overlapping domain for current shifts
|
||||
:returns dict : map with slot id as key and domain as value
|
||||
"""
|
||||
domain_mapping = {}
|
||||
for slot in self:
|
||||
domain_mapping[slot.id] = [
|
||||
'&',
|
||||
'&',
|
||||
('employee_id', '!=', False),
|
||||
('employee_id', '=', slot.employee_id.id),
|
||||
'&',
|
||||
('start_datetime', '<', slot.end_datetime),
|
||||
('end_datetime', '>', slot.start_datetime)
|
||||
]
|
||||
return domain_mapping
|
||||
|
||||
def _prepare_template_values(self):
|
||||
""" extract values from shift to create a template """
|
||||
# compute duration w/ tzinfo otherwise DST will not be taken into account
|
||||
destination_tz = pytz.timezone(self.env.user.tz or 'UTC')
|
||||
start_datetime = pytz.utc.localize(self.start_datetime).astimezone(destination_tz)
|
||||
end_datetime = pytz.utc.localize(self.end_datetime).astimezone(destination_tz)
|
||||
|
||||
# convert time delta to hours and minutes
|
||||
total_seconds = (end_datetime - start_datetime).total_seconds()
|
||||
m, s = divmod(total_seconds, 60)
|
||||
h, m = divmod(m, 60)
|
||||
|
||||
return {
|
||||
'start_time': start_datetime.hour + start_datetime.minute / 60.0,
|
||||
'duration': h + (m / 60.0),
|
||||
'role_id': self.role_id.id
|
||||
}
|
||||
|
||||
def _read_group_employee_id(self, employees, domain, order):
|
||||
if self._context.get('planning_expand_employee'):
|
||||
return self.env['planning.slot'].search([('create_date', '>', datetime.now() - timedelta(days=30))]).mapped('employee_id')
|
||||
return employees
|
||||
|
||||
|
||||
class PlanningRole(models.Model):
|
||||
_name = 'planning.role'
|
||||
_description = "Planning Role"
|
||||
_order = 'name,id desc'
|
||||
_rec_name = 'name'
|
||||
|
||||
name = fields.Char('Name', required=True)
|
||||
color = fields.Integer("Color", default=0)
|
||||
|
||||
|
||||
class PlanningPlanning(models.Model):
|
||||
_name = 'planning.planning'
|
||||
_description = 'Planning sent by email'
|
||||
|
||||
@api.model
|
||||
def _default_access_token(self):
|
||||
return str(uuid.uuid4())
|
||||
|
||||
start_datetime = fields.Datetime("Start Date", required=True)
|
||||
end_datetime = fields.Datetime("Stop Date", required=True)
|
||||
include_unassigned = fields.Boolean("Includes Open shifts", default=True)
|
||||
access_token = fields.Char("Security Token", default=_default_access_token, required=True, copy=False, readonly=True)
|
||||
last_sent_date = fields.Datetime("Last sent date")
|
||||
slot_ids = fields.Many2many('planning.slot', "Shifts", compute='_compute_slot_ids')
|
||||
company_id = fields.Many2one('res.company', "Company", required=True, default=lambda self: self.env.company)
|
||||
|
||||
_sql_constraints = [
|
||||
('check_start_date_lower_stop_date', 'CHECK(end_datetime > start_datetime)', 'Planning end date should be greater than its start date'),
|
||||
]
|
||||
|
||||
@api.depends('start_datetime', 'end_datetime')
|
||||
def _compute_display_name(self):
|
||||
""" This override is need to have a human readable string in the email light layout header (`message.record_name`) """
|
||||
for planning in self:
|
||||
number_days = (planning.end_datetime - planning.start_datetime).days
|
||||
planning.display_name = _('Planning of %s days') % (number_days,)
|
||||
|
||||
@api.depends('start_datetime', 'end_datetime', 'include_unassigned')
|
||||
def _compute_slot_ids(self):
|
||||
domain_map = self._get_domain_slots()
|
||||
for planning in self:
|
||||
domain = domain_map[planning.id]
|
||||
if not planning.include_unassigned:
|
||||
domain = expression.AND([domain, [('employee_id', '!=', False)]])
|
||||
planning.slot_ids = self.env['planning.slot'].search(domain)
|
||||
|
||||
# ----------------------------------------------------
|
||||
# Business Methods
|
||||
# ----------------------------------------------------
|
||||
|
||||
def _get_domain_slots(self):
|
||||
result = {}
|
||||
for planning in self:
|
||||
domain = ['&', '&', ('start_datetime', '<=', planning.end_datetime), ('end_datetime', '>', planning.start_datetime), ('company_id', '=', planning.company_id.id)]
|
||||
result[planning.id] = domain
|
||||
return result
|
||||
|
||||
def send_planning(self, message=None):
|
||||
email_from = self.env.user.email or self.env.user.company_id.email or ''
|
||||
sent_slots = self.env['planning.slot']
|
||||
for planning in self:
|
||||
# prepare planning urls, recipient employees, ...
|
||||
slots = planning.slot_ids
|
||||
slots_open = slots.filtered(lambda slot: not slot.employee_id)
|
||||
|
||||
# extract planning URLs
|
||||
employees = slots.mapped('employee_id')
|
||||
employee_url_map = employees.sudo()._planning_get_url(planning)
|
||||
|
||||
# send planning email template with custom domain per employee
|
||||
template = self.env.ref('planning.email_template_planning_planning', raise_if_not_found=False)
|
||||
template_context = {
|
||||
'slot_unassigned_count': len(slots_open),
|
||||
'slot_total_count': len(slots),
|
||||
'message': message,
|
||||
}
|
||||
if template:
|
||||
# /!\ For security reason, we only given the public employee to render mail template
|
||||
for employee in self.env['hr.employee.public'].browse(employees.ids):
|
||||
if employee.work_email:
|
||||
template_context['employee'] = employee
|
||||
template_context['planning_url'] = employee_url_map[employee.id]
|
||||
template.with_context(**template_context).send_mail(planning.id, email_values={'email_to': employee.work_email, 'email_from': email_from}, notif_layout='mail.mail_notification_light')
|
||||
sent_slots |= slots
|
||||
# mark as sent
|
||||
self.write({'last_sent_date': fields.Datetime.now()})
|
||||
sent_slots.write({
|
||||
'is_published': True,
|
||||
'publication_warning': False
|
||||
})
|
||||
return True
|
|
@ -0,0 +1,101 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools import get_timedelta
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class PlanningRecurrency(models.Model):
|
||||
_name = 'planning.recurrency'
|
||||
_description = "Planning Recurrence"
|
||||
|
||||
slot_ids = fields.One2many('planning.slot', 'recurrency_id', string="Related planning entries")
|
||||
repeat_interval = fields.Integer("Repeat every", default=1, required=True)
|
||||
repeat_type = fields.Selection([('forever', 'Forever'), ('until', 'Until')], string='weeks', default='forever')
|
||||
repeat_until = fields.Datetime(string="Repeat until", help="Up to which date should the plannings be repeated")
|
||||
last_generated_end_datetime = fields.Datetime("Last Generated End Date", readonly=True)
|
||||
company_id = fields.Many2one('res.company', string="Company", readonly=True, required=True, default=lambda self: self.env.company)
|
||||
|
||||
_sql_constraints = [
|
||||
('check_repeat_interval_positive', 'CHECK(repeat_interval >= 1)', 'Recurrency repeat interval should be at least 1'),
|
||||
('check_until_limit', "CHECK((repeat_type = 'until' AND repeat_until IS NOT NULL) OR (repeat_type != 'until'))", 'A recurrence repeating itself until a certain date must have its limit set'),
|
||||
]
|
||||
|
||||
@api.constrains('company_id', 'slot_ids')
|
||||
def _check_multi_company(self):
|
||||
for recurrency in self:
|
||||
if not all(recurrency.company_id == planning.company_id for planning in recurrency.slot_ids):
|
||||
raise ValidationError(_('An shift must be in the same company as its recurrency.'))
|
||||
|
||||
def name_get(self):
|
||||
result = []
|
||||
for recurrency in self:
|
||||
if recurrency.repeat_type == 'forever':
|
||||
name = _('Forever, every %s week(s)') % (recurrency.repeat_interval,)
|
||||
else:
|
||||
name = _('Every %s week(s) until %s') % (recurrency.repeat_interval, recurrency.repeat_until)
|
||||
result.append([recurrency.id, name])
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def _cron_schedule_next(self):
|
||||
companies = self.env['res.company'].search([])
|
||||
now = fields.Datetime.now()
|
||||
stop_datetime = None
|
||||
for company in companies:
|
||||
delta = get_timedelta(company.planning_generation_interval, 'month')
|
||||
|
||||
recurrencies = self.search([
|
||||
'&',
|
||||
'&',
|
||||
('company_id', '=', company.id),
|
||||
('last_generated_end_datetime', '<', now + delta),
|
||||
'|',
|
||||
('repeat_until', '=', False),
|
||||
('repeat_until', '>', now - delta),
|
||||
])
|
||||
recurrencies._repeat_slot(now + delta)
|
||||
|
||||
def _repeat_slot(self, stop_datetime=False):
|
||||
for recurrency in self:
|
||||
slot = self.env['planning.slot'].search([('recurrency_id', '=', recurrency.id)], limit=1, order='start_datetime DESC')
|
||||
|
||||
if slot:
|
||||
# find the end of the recurrence
|
||||
recurrence_end_dt = False
|
||||
if recurrency.repeat_type == 'until':
|
||||
recurrence_end_dt = recurrency.repeat_until
|
||||
|
||||
# find end of generation period (either the end of recurrence (if this one ends before the cron period), or the given `stop_datetime` (usually the cron period))
|
||||
if not stop_datetime:
|
||||
stop_datetime = fields.Datetime.now() + get_timedelta(recurrency.company_id.planning_generation_interval, 'month')
|
||||
range_limit = min([dt for dt in [recurrence_end_dt, stop_datetime] if dt])
|
||||
|
||||
# generate recurring slots
|
||||
recurrency_delta = get_timedelta(recurrency.repeat_interval, 'week')
|
||||
next_start = slot.start_datetime + recurrency_delta
|
||||
|
||||
slot_values_list = []
|
||||
while next_start < range_limit:
|
||||
slot_values = slot.copy_data({
|
||||
'start_datetime': next_start,
|
||||
'end_datetime': next_start + (slot.end_datetime - slot.start_datetime),
|
||||
'recurrency_id': recurrency.id,
|
||||
'company_id': recurrency.company_id.id,
|
||||
'repeat': True,
|
||||
'is_published': False
|
||||
})[0]
|
||||
slot_values_list.append(slot_values)
|
||||
next_start = next_start + recurrency_delta
|
||||
|
||||
self.env['planning.slot'].create(slot_values_list)
|
||||
recurrency.write({'last_generated_end_datetime': next_start - recurrency_delta})
|
||||
|
||||
else:
|
||||
recurrency.unlink()
|
||||
|
||||
def _delete_slot(self, start_datetime):
|
||||
slots = self.env['planning.slot'].search([('recurrency_id', 'in', self.ids), ('start_datetime', '>=', start_datetime), ('is_published', '=', False)])
|
||||
slots.unlink()
|
|
@ -0,0 +1,35 @@
|
|||
import math
|
||||
|
||||
from datetime import datetime, timedelta, time, date
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools import format_time
|
||||
|
||||
|
||||
class PlanningTemplate(models.Model):
|
||||
_name = 'planning.slot.template'
|
||||
_description = "Shift Template"
|
||||
|
||||
role_id = fields.Many2one('planning.role', string="Role")
|
||||
start_time = fields.Float('Start hour', default=0)
|
||||
duration = fields.Float('Duration (hours)', default=0)
|
||||
|
||||
_sql_constraints = [
|
||||
('check_start_time_lower_than_24', 'CHECK(start_time <= 24)', 'You cannot have a start hour greater than 24'),
|
||||
('check_start_time_positive', 'CHECK(start_time >= 0)', 'Start hour must be a positive number'),
|
||||
('check_duration_positive', 'CHECK(duration >= 0)', 'You cannot have a negative duration')
|
||||
]
|
||||
|
||||
def name_get(self):
|
||||
result = []
|
||||
for shift_template in self:
|
||||
start_time = time(hour=int(shift_template.start_time), minute=round(math.modf(shift_template.start_time)[0] / (1 / 60.0)))
|
||||
duration = timedelta(hours=int(shift_template.duration), minutes=round(math.modf(shift_template.duration)[0] / (1 / 60.0)))
|
||||
end_time = datetime.combine(date.today(), start_time) + duration
|
||||
name = '%s - %s %s %s' % (
|
||||
format_time(shift_template.env, start_time, time_format='h a' if start_time.minute == 0 else 'h:mm a'),
|
||||
format_time(shift_template.env, end_time.time(), time_format='h a' if end_time.minute == 0 else 'h:mm a'),
|
||||
'(%s days span)' % (duration.days + 1) if duration.days > 0 else '',
|
||||
shift_template.role_id.name if shift_template.role_id.name is not False else ''
|
||||
)
|
||||
result.append([shift_template.id, name])
|
||||
return result
|
|
@ -0,0 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class Company(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
planning_generation_interval = fields.Integer("Rate of shift generation", required=True, readonly=False, default=3, help="Delay for the rate at which recurring shift should be generated in month")
|
||||
|
||||
planning_allow_self_unassign = fields.Boolean("Can employee un-assign themselves?", default=False,
|
||||
help="Let your employees un-assign themselves from shifts when unavailable")
|
|
@ -0,0 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
planning_generation_interval = fields.Integer("Rate of shift generation", required=True,
|
||||
related="company_id.planning_generation_interval", readonly=False, help="Delay for the rate at which recurring shifts should be generated")
|
||||
|
||||
planning_allow_self_unassign = fields.Boolean("Allow Unassignment", default=False, readonly=False,
|
||||
related="company_id.planning_allow_self_unassign", help="Let your employees un-assign themselves from shifts when unavailable")
|
|
@ -0,0 +1,4 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import planning_report
|
|
@ -0,0 +1,40 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import tools
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class PlanningReport(models.Model):
|
||||
_name = "planning.slot.report.analysis"
|
||||
_description = "Planning Statistics"
|
||||
_auto = False
|
||||
_rec_name = 'entry_date'
|
||||
_order = 'entry_date desc'
|
||||
|
||||
entry_date = fields.Date('Date', readonly=True)
|
||||
employee_id = fields.Many2one('hr.employee', 'Employee', readonly=True)
|
||||
role_id = fields.Many2one('planning.role', string='Role', readonly=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', readonly=True)
|
||||
number_hours = fields.Float("Allocated Hours", readonly=True)
|
||||
|
||||
def init(self):
|
||||
tools.drop_view_if_exists(self.env.cr, self._table)
|
||||
# We dont take ressource into account as we would need to move
|
||||
# the generate_series() in the FROM and use a condition on the
|
||||
# join to exclude holidays like it's done in timesheet.
|
||||
self.env.cr.execute("""
|
||||
CREATE or REPLACE VIEW %s as (
|
||||
(
|
||||
SELECT
|
||||
p.id,
|
||||
generate_series(start_datetime,end_datetime,'1 day'::interval) entry_date,
|
||||
p.role_id AS role_id,
|
||||
p.company_id AS company_id,
|
||||
p.employee_id AS employee_id,
|
||||
p.allocated_hours / ((p.end_datetime::date - p.start_datetime::date)+1) AS number_hours
|
||||
FROM
|
||||
planning_slot p
|
||||
)
|
||||
)
|
||||
""" % (self._table,))
|
|
@ -0,0 +1,72 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="planning_slot_report_view_pivot" model="ir.ui.view">
|
||||
<field name="name">planning.slot.report.pivot</field>
|
||||
<field name="model">planning.slot.report.analysis</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Planning Analysis">
|
||||
<field name="entry_date" interval="month" type="col"/>
|
||||
<field name="employee_id" type="row"/>
|
||||
<field name="number_hours" type="measure"/>
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="planning_slot_report_view_graph" model="ir.ui.view">
|
||||
<field name="name">planning.slot.report.graph</field>
|
||||
<field name="model">planning.slot.report.analysis</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Planning Analysis" type="bar">
|
||||
<field name="entry_date" type="row"/>
|
||||
<field name="number_hours" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="planning_slot_report_view_search" model="ir.ui.view">
|
||||
<field name="name">planning.slot.report.search</field>
|
||||
<field name="model">planning.slot.report.analysis</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Planning Analysis">
|
||||
<field name="employee_id" filter_domain="[('employee_id', 'ilike', self)]"/>
|
||||
<field name="role_id" filter_domain="[('role_id', 'ilike', self)]"/>
|
||||
<field name="entry_date"/>
|
||||
<filter string="Date" name="year" date="entry_date"/>
|
||||
<separator/>
|
||||
<group expand="1" string="Group By">
|
||||
<filter string="Employee" name="resource_employee" context="{'group_by':'employee_id'}"/>
|
||||
<filter string="Role" name="resource_role" context="{'group_by':'role_id'}"/>
|
||||
<filter string="Company" name="resource_company" context="{'group_by':'company_id'}"
|
||||
groups="base.group_multi_company"/>
|
||||
<separator/>
|
||||
<filter string="Date" name="date_month" context="{'group_by':'entry_date:month'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="planning_action_analysis" model="ir.actions.act_window">
|
||||
<field name="name">Shifts Analysis</field>
|
||||
<field name="res_model">planning.slot.report.analysis</field>
|
||||
<field name="view_mode">pivot,graph</field>
|
||||
</record>
|
||||
|
||||
<!-- Filter for graph view -->
|
||||
<record id="planning_filter_by_employee" model="ir.filters">
|
||||
<field name="name">Hours per Employee</field>
|
||||
<field name="model_id">planning.slot.report.analysis</field>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="is_default" eval="True"/>
|
||||
<field name="context">{
|
||||
'pivot_measures': ['number_hours'],
|
||||
'pivot_column_groupby': ['entry_date:month'],
|
||||
'pivot_row_groupby': ['employee_id'],
|
||||
'graph_measures': ['number_hours'],
|
||||
'graph_column_groupby': ['entry_date:month'],
|
||||
'graph_row_groupby': ['employee_id']
|
||||
}</field>
|
||||
<field name="action_id" ref="planning_action_analysis"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
|
@ -0,0 +1,11 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_planning_slot_user,planning.slot.user,planning.model_planning_slot,planning.group_planning_user,1,0,0,0
|
||||
access_planning_slot_manager,planning.slot.manager,planning.model_planning_slot,planning.group_planning_manager,1,1,1,1
|
||||
access_planning_role_user,planning.role.user,planning.model_planning_role,planning.group_planning_user,1,0,0,0
|
||||
access_planning_role_manager,planning.role.manager,planning.model_planning_role,planning.group_planning_manager,1,1,1,1
|
||||
access_planning_recurrency_user,planning.recurrency.user,planning.model_planning_recurrency,planning.group_planning_user,1,0,0,0
|
||||
access_planning_recurrency_manager,planning.recurrency.manager,planning.model_planning_recurrency,planning.group_planning_manager,1,1,1,1
|
||||
access_planning_planning,access_planning_planning,model_planning_planning,planning.group_planning_manager,1,1,1,1
|
||||
access_planning_slot_template_user,planning.slot.template.user,planning.model_planning_slot_template,planning.group_planning_user,1,0,0,0
|
||||
access_planning_slot_template_manager,planning.slot.template.manager,planning.model_planning_slot_template,planning.group_planning_manager,1,1,1,1
|
||||
access_planning_slot_report_analysis,access_planning_slot_report_analysis,model_planning_slot_report_analysis,base.group_user,1,1,1,1
|
|
|
@ -0,0 +1,89 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
|
||||
<record id="group_planning_user" model="res.groups">
|
||||
<field name="name">User</field>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="category_id" ref="base.module_category_human_resources_planning"/>
|
||||
</record>
|
||||
|
||||
<record id="group_planning_manager" model="res.groups">
|
||||
<field name="name">Manager</field>
|
||||
<field name="category_id" ref="base.module_category_human_resources_planning"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_planning_user'))]"/>
|
||||
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_planning_show_percentage" model="res.groups">
|
||||
<field name="name">Show Allocated Percentage</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="planning_rule_user_modify" model="ir.rule">
|
||||
<field name="name">Planning: user can modify their own shifts</field>
|
||||
<field name="model_id" ref="planning.model_planning_slot"/>
|
||||
<field name="domain_force">[('user_id', '=', user.id)]</field>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="groups" eval="[(4,ref('group_planning_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="planning_rule_user_is_published" model="ir.rule">
|
||||
<field name="name">Planning: user can only see published shifts</field>
|
||||
<field name="model_id" ref="planning.model_planning_slot"/>
|
||||
<field name="domain_force">[('is_published', '=', True)]</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="groups" eval="[(4,ref('group_planning_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="planning_rule_manager" model="ir.rule">
|
||||
<field name="name">Planning: manager can create/update/delete all planning entries</field>
|
||||
<field name="model_id" ref="planning.model_planning_slot"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
<field name="groups" eval="[(4,ref('group_planning_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="planning_slot_rule_multi_company" model="ir.rule">
|
||||
<field name="name">Planning Shift multi-company</field>
|
||||
<field name="model_id" ref="planning.model_planning_slot"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
|
||||
<field name="global" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="planning_recurrency_rule_multi_company" model="ir.rule">
|
||||
<field name="name">Planning Recurrence multi-company</field>
|
||||
<field name="model_id" ref="planning.model_planning_recurrency"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
|
||||
<field name="global" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="planning_planning_rule_multi_company" model="ir.rule">
|
||||
<field name="name">Planning Planning multi-company</field>
|
||||
<field name="model_id" ref="planning.model_planning_planning"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
|
||||
<field name="global" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="planning_planning_rule_multi_company" model="ir.rule">
|
||||
<field name="name">Planning Analysis multi-company</field>
|
||||
<field name="model_id" ref="planning.model_planning_slot_report_analysis"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
|
||||
<field name="global" eval="True"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="100%"><stop offset="0%" stop-color="#269396"/><stop offset="100%" stop-color="#218689"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><g><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/></g><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><path fill="#393939" d="M3.935 69c-1.3 0-4-.4-4-3.5L0 27.1l13.33-9.8 36.4 5.9L40 28.7l5.6.1 11.8 4.4-3.825 7.42 3.025 9.807L44.235 69h-40.3z" opacity=".32"/><g fill="#000" fill-rule="nonzero" opacity=".3"><path d="M48.1 40.6c-5.3 0-9.6 4.3-9.6 9.7 0 5.4 4.3 9.7 9.6 9.7s9.6-4.3 9.6-9.7c0-5.4-4.3-9.7-9.6-9.7zm2 10.8c0 .1-.1.3-.2.4l-3.4 2.5c-.2.2-.5.1-.7-.1l-1.1-1.5c-.2-.2-.1-.5.1-.7l2.5-1.8v-5.4c0-.3.2-.5.5-.5h1.9c.3 0 .5.2.5.5v6.6h-.1zm-.7-25.5H14c-.6 0-1.2-.5-1.2-1.2v-3.8c0-.6.5-1.2 1.2-1.2h35.4c.6 0 1.2.5 1.2 1.2v3.8c-.1.6-.6 1.2-1.2 1.2zm6.8 11.2H20.8c-.6 0-1.2-.5-1.2-1.2v-3.8c0-.6.5-1.2 1.2-1.2h35.4c.6 0 1.2.5 1.2 1.2v3.8c-.1.7-.6 1.2-1.2 1.2zM35.473 47.3c0-1.9.41-3.6 1.227-5.1H15.518c-.49 0-.818.5-.818 1v4.1c0 .6.409 1 .818 1h20.037c-.082-.4-.082-.7-.082-1z"/><path d="M42.5 43.2v-1.3l-26.8 1.3c-.6 0-1 .5-1 1v4.1c0 .6.5 1 1 1h24.5c0-2.4.9-4.5 2.3-6.1z"/></g><path fill="#FFF" fill-rule="nonzero" d="M49.4 23.2H14c-.6 0-1.2-.5-1.2-1.2v-3.8c0-.6.5-1.2 1.2-1.2h35.4c.6 0 1.2.5 1.2 1.2V22c-.1.7-.6 1.2-1.2 1.2zm6.8 11.2H20.8c-.6 0-1.2-.5-1.2-1.2v-3.8c0-.6.5-1.2 1.2-1.2h35.4c.6 0 1.2.5 1.2 1.2v3.8c-.1.7-.6 1.2-1.2 1.2zm-18.5 6.1H15.527c-.496 0-.827.5-.827 1v4.1c0 .6.414 1 .827 1h20.27c0-2.3.745-4.5 1.903-6.1zm10.4-3.3c-5.3 0-9.6 4.3-9.6 9.7 0 5.4 4.3 9.7 9.6 9.7s9.6-4.3 9.6-9.7c0-5.4-4.3-9.7-9.6-9.7zm2 10.8c0 .1-.1.3-.2.4l-3.4 2.5c-.2.2-.5.1-.7-.1l-1.1-1.5c-.2-.2-.1-.5.1-.7l2.5-1.8v-5.4c0-.3.2-.5.5-.5h1.9c.3 0 .5.2.5.5V48h-.1z"/></g></g></svg>
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -0,0 +1,109 @@
|
|||
odoo.define('web.FieldColorPicker', function (require) {
|
||||
"use strict";
|
||||
|
||||
var basic_fields = require('web.basic_fields');
|
||||
var FieldInteger = basic_fields.FieldInteger;
|
||||
var field_registry = require('web.field_registry');
|
||||
|
||||
var core = require('web.core');
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var FieldColorPicker = FieldInteger.extend({
|
||||
/**
|
||||
* Prepares the rendering, since we are based on an input but not using it
|
||||
* setting tagName after parent init force the widget to not render an input
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.tagName = 'div';
|
||||
},
|
||||
/**
|
||||
* Render the widget when it is edited.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_renderEdit: function () {
|
||||
this.$el.html(QWeb.render('web.ColorPicker'));
|
||||
this._setupColorPicker();
|
||||
this._highlightSelectedColor();
|
||||
},
|
||||
/**
|
||||
* Render the widget when it is NOT edited.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_renderReadonly: function () {
|
||||
this.$el.html(QWeb.render('web.ColorPickerReadonly', {active_color: this.value,}));
|
||||
this.$el.on('click', 'a', function(ev){ ev.preventDefault(); });
|
||||
},
|
||||
/**
|
||||
* Render the kanban colors inside first ul element.
|
||||
* This is the same template as in KanbanRecord.
|
||||
*
|
||||
* <a> elements click are bound to _onColorChanged
|
||||
*
|
||||
*/
|
||||
_setupColorPicker: function () {
|
||||
var $colorpicker = this.$('ul');
|
||||
if (!$colorpicker.length) {
|
||||
return;
|
||||
}
|
||||
$colorpicker.html(QWeb.render('KanbanColorPicker'));
|
||||
$colorpicker.on('click', 'a', this._onColorChanged.bind(this));
|
||||
},
|
||||
/**
|
||||
* Returns the widget value.
|
||||
* Since NumericField is based on an input, but we don't use it,
|
||||
* we override this function to use the internal value of the widget.
|
||||
*
|
||||
*
|
||||
* @override
|
||||
* @returns {string}
|
||||
*/
|
||||
_getValue: function (){
|
||||
return this.value;
|
||||
},
|
||||
/**
|
||||
* Listener in edit mode for click on a color.
|
||||
* The actual color can be found in the data-color
|
||||
* attribute of the target element.
|
||||
*
|
||||
* We re-render the widget after the update because
|
||||
* the selected color has changed and it should
|
||||
* be reflected in the ui.
|
||||
*
|
||||
* @param ev
|
||||
*/
|
||||
_onColorChanged: function(ev) {
|
||||
ev.preventDefault();
|
||||
var color = null;
|
||||
if(ev.currentTarget && ev.currentTarget.dataset && ev.currentTarget.dataset.color){
|
||||
color = ev.currentTarget.dataset.color;
|
||||
}
|
||||
if(color){
|
||||
this.value = color;
|
||||
this._onChange();
|
||||
this._renderEdit();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Helper to modify the active color's style
|
||||
* while in edit mode.
|
||||
*
|
||||
*/
|
||||
_highlightSelectedColor: function(){
|
||||
try{
|
||||
$(this.$('li')[parseInt(this.value)]).css('border', '2px solid teal');
|
||||
} catch(err) {
|
||||
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
field_registry.add('color_picker', FieldColorPicker);
|
||||
|
||||
return FieldColorPicker;
|
||||
|
||||
});
|
|
@ -0,0 +1,142 @@
|
|||
odoo.define('planning.PlanningGanttController', function (require) {
|
||||
'use strict';
|
||||
|
||||
var GanttController = require('web_gantt.GanttController');
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
var confirmDialog = require('web.Dialog').confirm;
|
||||
var dialogs = require('web.view_dialogs');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var PlanningGanttController = GanttController.extend({
|
||||
events: _.extend({}, GanttController.prototype.events, {
|
||||
'click .o_gantt_button_copy_previous_week': '_onCopyWeekClicked',
|
||||
'click .o_gantt_button_send_all': '_onSendAllClicked',
|
||||
}),
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {jQueryElement} $node to which the buttons will be appended
|
||||
*/
|
||||
renderButtons: function ($node) {
|
||||
if ($node) {
|
||||
var state = this.model.get();
|
||||
this.$buttons = $(QWeb.render('PlanningGanttView.buttons', {
|
||||
groupedBy: state.groupedBy,
|
||||
widget: this,
|
||||
SCALES: this.SCALES,
|
||||
activateScale: state.scale,
|
||||
allowedScales: this.allowedScales,
|
||||
activeActions: this.activeActions,
|
||||
}));
|
||||
this.$buttons.appendTo($node);
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Opens dialog to add/edit/view a record
|
||||
* Override required to execute the reload of the gantt view when an action is performed on a
|
||||
* single record.
|
||||
*
|
||||
* @private
|
||||
* @param {integer|undefined} resID
|
||||
* @param {Object|undefined} context
|
||||
*/
|
||||
_openDialog: function (resID, context) {
|
||||
var self = this;
|
||||
var record = resID ? _.findWhere(this.model.get().records, {id: resID,}) : {};
|
||||
var title = resID ? record.display_name : _t("Open");
|
||||
|
||||
var dialog = new dialogs.FormViewDialog(this, {
|
||||
title: _.str.sprintf(title),
|
||||
res_model: this.modelName,
|
||||
view_id: this.dialogViews[0][0],
|
||||
res_id: resID,
|
||||
readonly: !this.is_action_enabled('edit'),
|
||||
deletable: this.is_action_enabled('edit') && resID,
|
||||
context: _.extend({}, this.context, context),
|
||||
on_saved: this.reload.bind(this, {}),
|
||||
on_remove: this._onDialogRemove.bind(this, resID),
|
||||
});
|
||||
dialog.on('closed', this, function(ev){
|
||||
// we reload as record can be created or modified (sent, unpublished, ...)
|
||||
self.reload();
|
||||
});
|
||||
|
||||
return dialog.open();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onCopyWeekClicked: function (ev) {
|
||||
ev.preventDefault();
|
||||
var state = this.model.get();
|
||||
var self = this;
|
||||
self._rpc({
|
||||
model: self.modelName,
|
||||
method: 'action_copy_previous_week',
|
||||
args: [
|
||||
self.model.convertToServerTime(state.startDate),
|
||||
],
|
||||
context: _.extend({}, self.context || {}),
|
||||
})
|
||||
.then(function(){
|
||||
self.reload();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onSendAllClicked: function (ev) {
|
||||
ev.preventDefault();
|
||||
var self = this;
|
||||
var state = this.model.get();
|
||||
var additional_context = _.extend({}, this.context, {
|
||||
'default_start_datetime': this.model.convertToServerTime(state.startDate),
|
||||
'default_end_datetime': this.model.convertToServerTime(state.stopDate),
|
||||
'scale': state.scale,
|
||||
'active_domain': this.model.domain,
|
||||
'active_ids': this.model.get().records
|
||||
});
|
||||
return this.do_action('planning.planning_send_action', {
|
||||
additional_context: additional_context,
|
||||
on_close: function () {
|
||||
self.reload();
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onScaleClicked: function (ev) {
|
||||
this._super.apply(this, arguments);
|
||||
var $button = $(ev.currentTarget);
|
||||
var scale = $button.data('value');
|
||||
if (scale !== 'week') {
|
||||
this.$('.o_gantt_button_copy_previous_week').hide();
|
||||
} else {
|
||||
this.$('.o_gantt_button_copy_previous_week').show();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return PlanningGanttController;
|
||||
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
odoo.define('planning.PlanningGanttModel', function (require) {
|
||||
"use strict";
|
||||
|
||||
var GanttModel = require('web_gantt.GanttModel');
|
||||
var _t = require('web.core')._t;
|
||||
|
||||
var PlanningGanttModel = GanttModel.extend({
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_generateRows: function (params) {
|
||||
var rows = this._super(params);
|
||||
// is the data grouped by?
|
||||
if(params.groupedBy && params.groupedBy.length){
|
||||
// in the last row is the grouped by field is null
|
||||
if(rows && rows.length && rows[rows.length - 1] && !rows[rows.length - 1].resId){
|
||||
// then make it the first one
|
||||
rows.unshift(rows.pop());
|
||||
}
|
||||
}
|
||||
// rename 'Undefined Employee' into 'Open Shifts'
|
||||
_.each(rows, function(row){
|
||||
if(row.groupedByField === 'employee_id' && !row.resId){
|
||||
row.name = _t('Open Shifts');
|
||||
}
|
||||
});
|
||||
return rows;
|
||||
},
|
||||
});
|
||||
|
||||
return PlanningGanttModel;
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
odoo.define('planning.PlanningGanttView', function (require) {
|
||||
'use strict';
|
||||
|
||||
var GanttView = require('web_gantt.GanttView');
|
||||
var PlanningGanttController = require('planning.PlanningGanttController');
|
||||
var PlanningGanttModel = require('planning.PlanningGanttModel');
|
||||
|
||||
var view_registry = require('web.view_registry');
|
||||
|
||||
var PlanningGanttView = GanttView.extend({
|
||||
config: _.extend({}, GanttView.prototype.config, {
|
||||
Controller: PlanningGanttController,
|
||||
Model: PlanningGanttModel,
|
||||
}),
|
||||
});
|
||||
|
||||
view_registry.add('planning_gantt', PlanningGanttView);
|
||||
|
||||
return PlanningGanttView;
|
||||
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
odoo.define('planning.tour', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
tour.register('planning_tour', {
|
||||
'skip_enabled': true,
|
||||
}, [{
|
||||
trigger: '.o_app[data-menu-xmlid="planning.planning_menu_root"]',
|
||||
content: _t("Let's start managing your employees' schedule!"),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: ".o_menu_header_lvl_1[data-menu-xmlid='planning.planning_menu_schedule']",
|
||||
content: _t("Use this menu to visualize and schedule shifts"),
|
||||
position: "bottom"
|
||||
}, {
|
||||
trigger: ".o_menu_entry_lvl_2[data-menu-xmlid='planning.planning_menu_schedule_by_employee']",
|
||||
content: _t("Use this menu to visualize and schedule shifts"),
|
||||
position: "bottom"
|
||||
}, {
|
||||
trigger: ".o_gantt_button_add",
|
||||
content: _t("Create your first shift by clicking on Add. Alternatively, you can use the (+) on the Gantt view."),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: "button[special='save']",
|
||||
content: _t("Save this shift as a template to reuse it, or make it recurrent. This will greatly ease your encoding process."),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: ".o_gantt_button_send_all",
|
||||
content: _t("Send the schedule to your employees once it is ready."),
|
||||
position: "right",
|
||||
},{
|
||||
trigger: "button[name='action_send']",
|
||||
content: _t("Send the schedule and mark the shifts as published. Congratulations!"),
|
||||
position: "right",
|
||||
},
|
||||
]);
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
.o_field_color_picker {
|
||||
display: flex;
|
||||
ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
@include o-kanban-colorpicker;
|
||||
@include o-kanban-record-color;
|
||||
max-width: unset;
|
||||
> li {
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 0 0 1px gray('300');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_field_color_picker_preview {
|
||||
@include o-kanban-record-color;
|
||||
> li {
|
||||
display: inline-block;
|
||||
margin: $o-kanban-inner-hmargin $o-kanban-inner-hmargin 0 0;
|
||||
border: 1px solid white;
|
||||
box-shadow: 0 0 0 1px gray('300');
|
||||
|
||||
> a {
|
||||
display: block;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
// No Color
|
||||
a.oe_kanban_color_0 {
|
||||
position: relative;
|
||||
&::before {
|
||||
content: "";
|
||||
@include o-position-absolute(-2px, $left: 10px);
|
||||
display: block;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
transform: rotate(45deg);
|
||||
background-color: red;
|
||||
}
|
||||
&::after {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
.o_planning_calendar_container {
|
||||
|
||||
#calendar_employee {
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.fc-day-grid-event {
|
||||
margin: 5px 2px 0;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.fc-day-grid-event .fc-content {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.o_planning_calendar_open_shifts {
|
||||
|
||||
.close {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates>
|
||||
|
||||
<t t-name="web.ColorPicker">
|
||||
<div class="o_field_color_picker">
|
||||
<ul>
|
||||
</ul>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="web.ColorPickerReadonly">
|
||||
<div class="o_field_color_picker_preview">
|
||||
<li><a role="menuitem" href="#" t-att-data-color="active_color" t-att-class="'oe_kanban_color_'+active_color" title="No color" aria-label="No color"/></li>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
<div t-name="PlanningGanttView.buttons">
|
||||
<button t-if="widget.is_action_enabled('create')" class="o_gantt_button_add btn btn-primary mr-3" title="Add record">
|
||||
Add
|
||||
</button>
|
||||
<div class="d-inline-block mr-3">
|
||||
<button class="o_gantt_button_prev btn btn-primary" title="Previous">
|
||||
<span class="fa fa-arrow-left"/>
|
||||
</button>
|
||||
<button class="o_gantt_button_today btn btn-primary">
|
||||
Today
|
||||
</button>
|
||||
<button class="o_gantt_button_next btn btn-primary" title="Next">
|
||||
<span class="fa fa-arrow-right"/>
|
||||
</button>
|
||||
</div>
|
||||
<button t-foreach="allowedScales" t-as="scale" t-attf-class="o_gantt_button_scale btn btn-secondary #{activateScale == scale ? 'active' : ''}" type="button" t-att-data-value="scale">
|
||||
<t t-esc="SCALES[scale].string"/>
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<button class="o_gantt_button_expand_rows btn btn-secondary" title="Expand rows">
|
||||
<i class="fa fa-expand"/>
|
||||
</button>
|
||||
<button class="o_gantt_button_collapse_rows btn btn-secondary" title="Collapse rows">
|
||||
<i class="fa fa-compress"/>
|
||||
</button>
|
||||
</div>
|
||||
<button t-if="activeActions.create && activateScale == 'week'" class="o_gantt_button_copy_previous_week btn btn-secondary mr-3" title="Copy previous week">
|
||||
Copy previous week
|
||||
</button>
|
||||
<button t-if="activeActions.edit" class="o_gantt_button_send_all btn btn-primary" title="Send schedule">
|
||||
Send schedule
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<t t-name="PlanningGanttView.Row" t-extend="GanttView.Row">
|
||||
<t t-jquery=".o_gantt_row_title" t-operation="prepend">
|
||||
<t t-if="widget.groupedByField == 'employee_id'">
|
||||
<img t-att-src="'/web/image?model=hr.employee&field=image_128&id='+widget.resId+'&unique='" style="height: 30px; width: 30px;" class="o_object_fit_cover rounded-circle"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
|
@ -0,0 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details
|
||||
|
||||
from . import test_recurrency
|
||||
from . import test_publication
|
||||
# from . import test_period_duplication
|
|
@ -0,0 +1,48 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
from odoo import fields
|
||||
|
||||
from odoo.tests.common import SavepointCase
|
||||
|
||||
|
||||
class TestCommonPlanning(SavepointCase):
|
||||
|
||||
@contextmanager
|
||||
def _patch_now(self, datetime_str):
|
||||
datetime_now_old = getattr(fields.Datetime, 'now')
|
||||
datetime_today_old = getattr(fields.Datetime, 'today')
|
||||
|
||||
def new_now():
|
||||
return fields.Datetime.from_string(datetime_str)
|
||||
|
||||
def new_today():
|
||||
return fields.Datetime.from_string(datetime_str).replace(hour=0, minute=0, second=0)
|
||||
|
||||
try:
|
||||
setattr(fields.Datetime, 'now', new_now)
|
||||
setattr(fields.Datetime, 'today', new_today)
|
||||
|
||||
yield
|
||||
finally:
|
||||
# back
|
||||
setattr(fields.Datetime, 'now', datetime_now_old)
|
||||
setattr(fields.Datetime, 'today', datetime_today_old)
|
||||
|
||||
def get_by_employee(self, employee):
|
||||
return self.env['planning.slot'].search([('employee_id', '=', employee.id)])
|
||||
|
||||
@classmethod
|
||||
def setUpEmployees(cls):
|
||||
cls.employee_joseph = cls.env['hr.employee'].create({
|
||||
'name': 'joseph',
|
||||
'work_email': 'joseph@a.be',
|
||||
'tz': 'UTC'
|
||||
})
|
||||
cls.employee_bert = cls.env['hr.employee'].create({
|
||||
'name': 'bert',
|
||||
'work_email': 'bert@a.be',
|
||||
'tz': 'UTC'
|
||||
})
|
|
@ -0,0 +1,59 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details
|
||||
|
||||
from datetime import datetime, date
|
||||
from odoo.addons.planning.tests.common import TestCommonPlanning
|
||||
|
||||
|
||||
class TestRecurrencySlotGeneration(TestCommonPlanning):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestRecurrencySlotGeneration, cls).setUpClass()
|
||||
cls.setUpEmployees()
|
||||
|
||||
cls.recurrency = cls.env['planning.recurrency'].create({
|
||||
'repeat_interval': 1,
|
||||
'repeat_unit': 'week',
|
||||
})
|
||||
|
||||
cls.env['planning.slot'].create({
|
||||
'employee_id': cls.employee_bert.id,
|
||||
'start_datetime': datetime(2019, 6, 2, 8, 0),
|
||||
'end_datetime': datetime(2019, 6, 2, 17, 0),
|
||||
})
|
||||
cls.env['planning.slot'].create({
|
||||
'employee_id': cls.employee_bert.id,
|
||||
'start_datetime': datetime(2019, 6, 4, 8, 0),
|
||||
'end_datetime': datetime(2019, 6, 5, 17, 0),
|
||||
})
|
||||
|
||||
cls.env['planning.slot'].create({
|
||||
'employee_id': cls.employee_bert.id,
|
||||
'start_datetime': datetime(2019, 6, 3, 8, 0),
|
||||
'end_datetime': datetime(2019, 6, 3, 17, 0),
|
||||
'recurrency_id': cls.recurrency.id
|
||||
})
|
||||
|
||||
def test_dont_duplicate_recurring_slots(self):
|
||||
"""Original week : 6/2/2019 -> 6/8/2019
|
||||
Destination week : 6/9/2019 -> 6/15/2019
|
||||
slots:
|
||||
6/2/2019 08:00 -> 6/2/2019 17:00
|
||||
6/4/2019 08:00 -> 6/5/2019 17:00
|
||||
6/3/2019 08:00 -> 6/3/2019 17:00 --> this one should be recurrent therefore not duplicated
|
||||
"""
|
||||
employee = self.employee_bert
|
||||
|
||||
self.assertEqual(len(self.get_by_employee(employee)), 3)
|
||||
|
||||
self.env['planning.slot'].action_copy_previous_week(date(2019, 6, 9))
|
||||
|
||||
self.assertEqual(len(self.get_by_employee(employee)), 5, 'duplicate has only duplicated slots that fit entirely in the period')
|
||||
|
||||
duplicated_slots = self.env['planning.slot'].search([
|
||||
('employee_id', '=', employee.id),
|
||||
('start_datetime', '>', datetime(2019, 6, 9, 0, 0)),
|
||||
('end_datetime', '<', datetime(2019, 6, 15, 23, 59)),
|
||||
])
|
||||
self.assertEqual(len(duplicated_slots), 2, 'duplicate has only duplicated slots that fit entirely in the period')
|
|
@ -0,0 +1,56 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details
|
||||
from datetime import datetime
|
||||
|
||||
from .common import TestCommonPlanning
|
||||
|
||||
|
||||
class TestPlanningPublishing(TestCommonPlanning):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestPlanningPublishing, cls).setUpClass()
|
||||
|
||||
cls.setUpEmployees()
|
||||
|
||||
# employee without work email
|
||||
cls.employee_dirk_no_mail = cls.env['hr.employee'].create({
|
||||
'user_id': False,
|
||||
'name': 'Dirk',
|
||||
'work_email': False,
|
||||
'tz': 'UTC'
|
||||
})
|
||||
|
||||
values = {
|
||||
'employee_id': cls.employee_joseph.id,
|
||||
'allocated_hours': 8,
|
||||
'start_datetime': datetime(2019, 6, 6, 8, 0, 0),
|
||||
'end_datetime': datetime(2019, 6, 6, 17, 0, 0)
|
||||
}
|
||||
cls.shift = cls.env['planning.slot'].create(values)
|
||||
|
||||
def test_planning_publication(self):
|
||||
self.shift.write({
|
||||
'allocated_hours': 10
|
||||
})
|
||||
|
||||
Mails = self.env['mail.mail']
|
||||
before_mails = Mails.search([])
|
||||
|
||||
self.shift.action_send()
|
||||
self.assertTrue(self.shift.is_published, 'planning is is_published when we call its action_send')
|
||||
|
||||
shift_mails = set(Mails.search([])) ^ set(before_mails)
|
||||
self.assertEqual(len(shift_mails), 1, 'only one mail is created when publishing planning')
|
||||
|
||||
def test_sending_planning_do_not_create_mail_if_employee_has_no_email(self):
|
||||
self.shift.write({'employee_id': self.employee_dirk_no_mail.id})
|
||||
|
||||
self.assertFalse(self.employee_dirk_no_mail.work_email) # if no work_email
|
||||
|
||||
Mails = self.env['mail.mail']
|
||||
before_mails = Mails.search([])
|
||||
|
||||
self.shift.action_send()
|
||||
shift_mails = set(Mails.search([])) ^ set(before_mails)
|
||||
self.assertEqual(len(shift_mails), 0, 'no mail should be sent when the employee has no work email')
|
|
@ -0,0 +1,409 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details
|
||||
|
||||
from datetime import datetime, date
|
||||
|
||||
from .common import TestCommonPlanning
|
||||
|
||||
import unittest
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class TestRecurrencySlotGeneration(TestCommonPlanning):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestRecurrencySlotGeneration, cls).setUpClass()
|
||||
cls.setUpEmployees()
|
||||
|
||||
def configure_recurrency_span(self, span_qty):
|
||||
self.env.company.write({
|
||||
'planning_generation_interval': span_qty,
|
||||
})
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Repeat "until" mode
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def test_repeat_until(self):
|
||||
""" Normal case: Test slots get repeated at the right time with company limit
|
||||
company_span: 2 weeks
|
||||
first run:
|
||||
now : 6/27/2019
|
||||
initial_start : 6/27/2019
|
||||
repeat_end : 7/11/2019 now + 2 weeks
|
||||
generated slots:
|
||||
6/27/2019
|
||||
7/4/2019
|
||||
NOT 7/11/2019 because it hits the soft limit
|
||||
1st cron
|
||||
now : 7/11/2019 2 weeks later
|
||||
last generated start : 7/4/2019
|
||||
repeat_end : 7/25/2019 now + 2 weeks
|
||||
generated_slots:
|
||||
7/11/2019
|
||||
7/18/2019
|
||||
NOT 7/25/2019 because it hits the soft limit
|
||||
"""
|
||||
with self._patch_now('2019-06-27 08:00:00'):
|
||||
self.configure_recurrency_span(1)
|
||||
|
||||
self.assertFalse(self.get_by_employee(self.employee_joseph))
|
||||
|
||||
# repeat once, since repeat span is two week and there's no repeat until, we should have 2 slot
|
||||
# because we hit the 'soft_limit'
|
||||
slot = self.env['planning.slot'].create({
|
||||
'start_datetime': datetime(2019, 6, 27, 8, 0, 0),
|
||||
'end_datetime': datetime(2019, 6, 27, 17, 0, 0),
|
||||
'employee_id': self.employee_joseph.id,
|
||||
'repeat': True,
|
||||
'repeat_type': 'until',
|
||||
'repeat_until': datetime(2022, 6, 27, 17, 0, 0),
|
||||
'repeat_interval': 1,
|
||||
})
|
||||
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 5, 'initial run should have created 2 slots')
|
||||
# TODO JEM: check same employee, attached to same reccurrence, same role, ...
|
||||
|
||||
# now run cron two weeks later, should yield two more slots
|
||||
with self._patch_now('2019-07-11 08:00:00'):
|
||||
self.env['planning.recurrency']._cron_schedule_next()
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 7, 'first cron run should have generated 2 more forecasts')
|
||||
|
||||
def test_repeat_until_no_repeat(self):
|
||||
"""create a recurrency with repeat until set which is less than next cron span, should
|
||||
stop repeating upon creation
|
||||
company_span: 2 weeks
|
||||
first run:
|
||||
now : 6/27/2019
|
||||
initial_start : 6/27/2019
|
||||
repeat_end : 6/29/2019 recurrency's repeat_until
|
||||
generated slots:
|
||||
6/27/2019
|
||||
NOT 7/4/2019 because it hits the recurrency's repeat_until
|
||||
"""
|
||||
with self._patch_now('2019-06-27 08:00:00'):
|
||||
|
||||
self.configure_recurrency_span(1)
|
||||
|
||||
self.assertFalse(self.get_by_employee(self.employee_joseph))
|
||||
|
||||
slot = self.env['planning.slot'].create({
|
||||
'start_datetime': datetime(2019, 6, 27, 8, 0, 0),
|
||||
'end_datetime': datetime(2019, 6, 27, 17, 0, 0),
|
||||
'employee_id': self.employee_joseph.id,
|
||||
'repeat': True,
|
||||
'repeat_type': 'until',
|
||||
'repeat_interval': 1,
|
||||
'repeat_until': datetime(2019, 6, 29, 8, 0, 0),
|
||||
})
|
||||
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 1, 'first run should only have created 1 slot since repeat until is set at 1 week')
|
||||
|
||||
def test_repeat_until_cron_idempotent(self):
|
||||
"""Create a recurrency with repeat_until set, it allows a full first run, but not on next cron
|
||||
first run:
|
||||
now : 6/27/2019
|
||||
initial_start : 6/27/2019
|
||||
repeat_end : 7/11/2019 recurrency's repeat_until
|
||||
generated slots:
|
||||
6/27/2019
|
||||
7/4/2019
|
||||
NOT 7/11/2019 because it hits the recurrency's repeat_until
|
||||
first cron:
|
||||
now: 7/12/2019
|
||||
last generated start: 7/4/2019
|
||||
repeat_end: 7/11/2019 still recurrency's repeat_until
|
||||
generated slots:
|
||||
NOT 7/11/2019 because it still hits the repeat end
|
||||
"""
|
||||
with self._patch_now('2019-06-27 08:00:00'):
|
||||
self.configure_recurrency_span(1)
|
||||
|
||||
self.assertFalse(self.get_by_employee(self.employee_joseph))
|
||||
|
||||
# repeat until is big enough for the first pass to generate all 2 slots
|
||||
slot = self.env['planning.slot'].create({
|
||||
'start_datetime': datetime(2019, 6, 27, 8, 0, 0),
|
||||
'end_datetime': datetime(2019, 6, 27, 17, 0, 0),
|
||||
'employee_id': self.employee_joseph.id,
|
||||
'repeat': True,
|
||||
'repeat_type': 'until',
|
||||
'repeat_interval': 1,
|
||||
'repeat_until': datetime(2019, 7, 11, 8, 0, 0),
|
||||
})
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 2, 'initial run should have generated 2 slots')
|
||||
|
||||
# run the cron, since last generated slot almost hits the repeat until, there won't be more. still two left
|
||||
self.env['planning.recurrency']._cron_schedule_next()
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 2, 'runing the cron right after do not generate new slots because of repeat until')
|
||||
|
||||
def test_repeat_until_cron_generation(self):
|
||||
"""Generate a recurrence with repeat_until that allow first run, then first cron, but shouldn't
|
||||
keep generating slots on the second
|
||||
first run:
|
||||
now : 8/31/2019
|
||||
initial_start : 9/1/2019
|
||||
repeat_end : forever
|
||||
generated slots:
|
||||
9/1/2019
|
||||
9/8/2019
|
||||
9/15/2019
|
||||
9/22/2019
|
||||
9/29/2019
|
||||
first cron:
|
||||
now: 9/14/2019 two weeks later
|
||||
repeat_end: forever
|
||||
generated slots:
|
||||
9/1/2019
|
||||
9/8/2019
|
||||
9/15/2019
|
||||
9/22/2019
|
||||
9/29/2019
|
||||
10/6/2019
|
||||
10/13/2019
|
||||
second cron:
|
||||
now: 9/16/2019 two days later
|
||||
last generated start: 10/13/2019
|
||||
repeat_end: forever
|
||||
generated slots:
|
||||
NOT 10/20/2019 because all recurring slots are already generated in the company interval
|
||||
"""
|
||||
with self._patch_now('2019-08-31 08:00:00'):
|
||||
self.configure_recurrency_span(1)
|
||||
|
||||
self.assertFalse(self.get_by_employee(self.employee_joseph))
|
||||
|
||||
# first run, two slots generated
|
||||
slot = self.env['planning.slot'].create({
|
||||
'start_datetime': datetime(2019, 9, 1, 8, 0, 0),
|
||||
'end_datetime': datetime(2019, 9, 1, 17, 0, 0),
|
||||
'employee_id': self.employee_joseph.id,
|
||||
'repeat': True,
|
||||
'repeat_type': 'forever',
|
||||
'repeat_interval': 1,
|
||||
'repeat_until': False,
|
||||
})
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 5, 'first run should have geenrated 2 slots')
|
||||
# run the cron, since last generated slot do not hit the repeat until, there will be 2 more
|
||||
with self._patch_now('2019-09-14 08:00:00'):
|
||||
self.env['planning.recurrency']._cron_schedule_next()
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 7, 'first cron should have generated 2 more slot')
|
||||
# run the cron again, since last generated slot do hit the repeat until, there won't be more
|
||||
with self._patch_now('2019-09-16 08:00:00'):
|
||||
self.env['planning.recurrency']._cron_schedule_next()
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 7, 'second cron has not generated any foreasts because of repeat until')
|
||||
|
||||
def test_repeat_until_long_limit(self):
|
||||
"""Since the recurrency cron is meant to run every week, make sure generation works accordingly when
|
||||
the company's repeat span is much larger
|
||||
first run:
|
||||
now : 6/1/2019
|
||||
initial_start : 6/1/2019
|
||||
repeat_end : 12/1/2019 initial_start + 6 months
|
||||
generated slots:
|
||||
6/1/2019
|
||||
...
|
||||
11/30/2019 (27 items)
|
||||
first cron:
|
||||
now: 6/8/2019
|
||||
last generated start 11/30/2019
|
||||
repeat_end 12/8/2019
|
||||
generated slots:
|
||||
12/7/2019
|
||||
only one slot generated: since we are one week later, repeat_end is only one week later and slots are generated every week.
|
||||
So there is just enough room for one.
|
||||
This ensure slots are always generated up to x time in advance with x being the company's repeat span
|
||||
"""
|
||||
with self._patch_now('2019-06-01 08:00:00'):
|
||||
self.configure_recurrency_span(6)
|
||||
|
||||
self.assertFalse(self.get_by_employee(self.employee_joseph))
|
||||
|
||||
slot = self.env['planning.slot'].create({
|
||||
'start_datetime': datetime(2019, 6, 1, 8, 0, 0),
|
||||
'end_datetime': datetime(2019, 6, 1, 17, 0, 0),
|
||||
'employee_id': self.employee_joseph.id,
|
||||
'repeat': True,
|
||||
'repeat_type': 'until',
|
||||
'repeat_interval': 1,
|
||||
'repeat_until': datetime(2020, 7, 25, 8, 0, 0),
|
||||
})
|
||||
# over 6 month, we should have generated 27 slots
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 27, 'first run has generated 27 slots')
|
||||
# one week later, always having the slots generated 6 months in advance means we
|
||||
# have generated one more, which makes 28
|
||||
with self._patch_now('2019-06-08 08:00:00'):
|
||||
self.env['planning.recurrency']._cron_schedule_next()
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 28, 'second cron only has generated 1 more slot because of company span')
|
||||
|
||||
def test_repeat_forever(self):
|
||||
""" Since the recurrency cron is meant to run every week, make sure generation works accordingly when
|
||||
both the company's repeat span and the repeat interval are much larger
|
||||
Company's span is 6 months and repeat_interval is 1 month
|
||||
first run:
|
||||
now : 6/1/2019
|
||||
initial_start : 6/1/2019
|
||||
repeat_end : 12/1/2019 initial_start + 6 months
|
||||
generated slots:
|
||||
6/1/2019
|
||||
...
|
||||
11/1/2019 (27 items)
|
||||
first cron:
|
||||
now: 6/8/2019
|
||||
last generated start 11/30/2019
|
||||
repeat_end 12/8/2019
|
||||
generated slots:
|
||||
12/1/2019
|
||||
second cron:
|
||||
now: 6/15/2019
|
||||
last generated start 12/1/2019
|
||||
repeat_end 12/15/2019
|
||||
generated slots:
|
||||
N/A (we are still 6 months in advance)
|
||||
"""
|
||||
with self._patch_now('2019-06-01 08:00:00'):
|
||||
self.configure_recurrency_span(6)
|
||||
|
||||
self.assertFalse(self.get_by_employee(self.employee_joseph))
|
||||
|
||||
slot = self.env['planning.slot'].create({
|
||||
'start_datetime': datetime(2019, 6, 1, 8, 0, 0),
|
||||
'end_datetime': datetime(2019, 6, 1, 17, 0, 0),
|
||||
'employee_id': self.employee_joseph.id,
|
||||
'repeat': True,
|
||||
'repeat_type': 'forever',
|
||||
'repeat_interval': 1,
|
||||
})
|
||||
|
||||
# over 6 month, we should have generated 6 slots
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 27, 'first run has generated 6 slots')
|
||||
|
||||
# one week later, always having the slots generated 6 months in advance means we
|
||||
# have generated one more, which makes 7
|
||||
with self._patch_now('2019-06-08 08:00:00'):
|
||||
self.env['planning.recurrency']._cron_schedule_next()
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 28, 'first cron generated one more slot')
|
||||
|
||||
# again one week later, we are now up-to-date so there should still be 7 slots
|
||||
with self._patch_now('2019-06-15 08:00:00'):
|
||||
self.env['planning.recurrency']._cron_schedule_next()
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 29, 'second run has not generated any forecats because of company span')
|
||||
|
||||
@unittest.skip
|
||||
def kkktest_slot_remove_all(self):
|
||||
with self._patch_now('2019-06-01 08:00:00'):
|
||||
self.configure_recurrency_span(6)
|
||||
initial_start_dt = datetime(2019, 6, 1, 8, 0, 0)
|
||||
initial_end_dt = datetime(2019, 6, 1, 17, 0, 0)
|
||||
slot_values = {
|
||||
'employee_id': self.employee_joseph.id,
|
||||
}
|
||||
|
||||
recurrency = self.env['planning.recurrency'].create({
|
||||
'repeat_interval': 1,
|
||||
})
|
||||
self.assertFalse(self.get_by_employee(self.employee_joseph))
|
||||
recurrency.create_slot(initial_start_dt, initial_end_dt, slot_values)
|
||||
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 27, 'first run has generated 27 slots')
|
||||
recurrency.action_remove_all()
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 0, 'calling remove after on any slot from the recurrency remove all slots linked to the recurrency')
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Recurring Slot Misc
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def test_recurring_slot_company(self):
|
||||
with self._patch_now('2019-06-01 08:00:00'):
|
||||
initial_company = self.env['res.company'].create({'name': 'original'})
|
||||
initial_company.write({
|
||||
'planning_generation_interval': 2,
|
||||
})
|
||||
|
||||
with self.assertRaises(UserError, msg="The employee should be in the same company as the shift"), self.cr.savepoint():
|
||||
slot1 = self.env['planning.slot'].create({
|
||||
'start_datetime': datetime(2019, 6, 1, 8, 0, 0),
|
||||
'end_datetime': datetime(2019, 6, 1, 17, 0, 0),
|
||||
'employee_id': self.employee_bert.id,
|
||||
'repeat': True,
|
||||
'repeat_type': 'forever',
|
||||
'repeat_interval': 1,
|
||||
'company_id': initial_company.id,
|
||||
})
|
||||
|
||||
# put the employee in the second company
|
||||
self.employee_bert.write({'company_id': initial_company.id})
|
||||
|
||||
slot1 = self.env['planning.slot'].create({
|
||||
'start_datetime': datetime(2019, 6, 1, 8, 0, 0),
|
||||
'end_datetime': datetime(2019, 6, 1, 17, 0, 0),
|
||||
'employee_id': self.employee_bert.id,
|
||||
'repeat': True,
|
||||
'repeat_type': 'forever',
|
||||
'repeat_interval': 1,
|
||||
'company_id': initial_company.id,
|
||||
})
|
||||
|
||||
other_company = self.env['res.company'].create({'name': 'other'})
|
||||
other_company.write({
|
||||
'planning_generation_interval': 1,
|
||||
})
|
||||
self.employee_joseph.write({'company_id': other_company.id})
|
||||
slot2 = self.env['planning.slot'].create({
|
||||
'start_datetime': datetime(2019, 6, 1, 8, 0, 0),
|
||||
'end_datetime': datetime(2019, 6, 1, 17, 0, 0),
|
||||
'employee_id': self.employee_joseph.id,
|
||||
'repeat': True,
|
||||
'repeat_type': 'forever',
|
||||
'repeat_interval': 1,
|
||||
'company_id': other_company.id,
|
||||
})
|
||||
|
||||
# initial company's recurrency should have created 9 slots since it's span is two month
|
||||
# other company's recurrency should have create 5 slots since it's span is one month
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_bert)), 9, 'initial company\'s span is two month, so 9 slots')
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 5, 'other company\'s span is one month, so only 5 slots')
|
||||
|
||||
self.assertEqual(slot1.company_id, slot1.recurrency_id.company_id, "Recurrence and slots (1) must have the same company")
|
||||
self.assertEqual(slot1.recurrency_id.company_id, slot1.recurrency_id.slot_ids.mapped('company_id'), "All slots in the same recurrence (1) must have the same company")
|
||||
self.assertEqual(slot2.company_id, slot2.recurrency_id.company_id, "Recurrence and slots (2) must have the same company")
|
||||
self.assertEqual(slot2.recurrency_id.company_id, slot2.recurrency_id.slot_ids.mapped('company_id'), "All slots in the same recurrence (1) must have the same company")
|
||||
|
||||
def test_slot_detach_if_some_fields_change(self):
|
||||
with self._patch_now('2019-06-27 08:00:00'):
|
||||
self.configure_recurrency_span(1)
|
||||
|
||||
self.assertFalse(self.get_by_employee(self.employee_joseph))
|
||||
|
||||
slot = self.env['planning.slot'].create({
|
||||
'start_datetime': datetime(2019, 6, 27, 8, 0, 0),
|
||||
'end_datetime': datetime(2019, 6, 27, 17, 0, 0),
|
||||
'employee_id': self.employee_joseph.id,
|
||||
'repeat': True,
|
||||
'repeat_type': 'until',
|
||||
'repeat_until': datetime(2019, 9, 27, 17, 0, 0), # 3 months
|
||||
'repeat_interval': 1,
|
||||
})
|
||||
recurrence = slot.recurrency_id
|
||||
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 5)
|
||||
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), len(recurrence.slot_ids), 'the recurrency has generated 5 slots')
|
||||
|
||||
self.get_by_employee(self.employee_joseph)[0].write({'employee_id': self.employee_bert.id})
|
||||
|
||||
self.assertEqual(len(recurrence.slot_ids), 4, 'writing on the slot detach it from the recurrency')
|
||||
|
||||
def test_empty_recurrency(self):
|
||||
""" Check empty recurrency is removed by cron """
|
||||
with self._patch_now('2020-06-27 08:00:00'):
|
||||
self.env['planning.recurrency'].create({
|
||||
'repeat_interval': 1,
|
||||
'repeat_type': 'forever',
|
||||
'repeat_until': False,
|
||||
'last_generated_end_datetime': datetime(2019, 6, 27, 8, 0, 0)
|
||||
})
|
||||
|
||||
self.assertEqual(len(self.env['planning.recurrency'].search([])), 1)
|
||||
self.env['planning.recurrency']._cron_schedule_next()
|
||||
self.assertFalse(len(self.env['planning.recurrency'].search([])), 'cron with no slot gets deleted (there is no original slot to copy from)')
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue