134 lines
6.9 KiB
Python
134 lines
6.9 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from collections import defaultdict
|
|
from odoo import api, models, _
|
|
from odoo.exceptions import UserError
|
|
|
|
|
|
def batch(iterable, batch_size):
|
|
l = len(iterable)
|
|
for n in range(0, l, batch_size):
|
|
yield iterable[n:min(n + batch_size, l)]
|
|
|
|
|
|
class PlanningSlot(models.Model):
|
|
_inherit = 'planning.slot'
|
|
|
|
def _create_work_entries(self):
|
|
# Similar to `_create_work_entries` for attendances but this function assumes big batches
|
|
# (due to the publish button that published all slots)
|
|
# Also note that slots are assumed to stay as is after being published, any other change will need a
|
|
# full work entry regeneration.
|
|
self_with_employee = self.filtered(lambda s: s.employee_id)
|
|
if not self_with_employee:
|
|
return
|
|
# The procedure for creating a work entry within an already generated period
|
|
# is more complicated for planning slots than for leaves because leaves override
|
|
# attendance periods, here we are not able to just archive work entries we do not want
|
|
# etc...
|
|
# We will instead archive all work entries that are touched by the current planning slot's period
|
|
# (slots don't have an overlap constraint), and regenerate all work entries that were covered by them.
|
|
# Since all we need is already in the database we can use that query to have better performance.
|
|
self.flush_model(['start_datetime', 'end_datetime', 'employee_id'])
|
|
self.env['hr.version'].flush_model([
|
|
'employee_id', 'work_entry_source',
|
|
'contract_date_start', 'contract_date_end', 'date_generated_from', 'date_generated_to'
|
|
])
|
|
self.env['hr.work.entry'].flush_model(['employee_id', 'date'])
|
|
self.env.cr.execute("""
|
|
SELECT slot.id as id,
|
|
ARRAY_AGG(DISTINCT version.id) as version_ids,
|
|
ARRAY_AGG(DISTINCT hwe.id) as work_entry_ids,
|
|
slot.start_datetime as start,
|
|
slot.end_datetime as stop
|
|
FROM planning_slot slot
|
|
JOIN hr_employee employee
|
|
ON slot.employee_id = employee.id AND
|
|
employee.active
|
|
-- Keeping only the last version.
|
|
-- We consider that work entries that covers two versions should not be handled as super rare.
|
|
INNER JOIN (SELECT DISTINCT ON (employee_id) *
|
|
FROM hr_version v
|
|
ORDER BY employee_id, date_version DESC) version
|
|
ON version.employee_id = employee.id AND
|
|
version.work_entry_source = 'planning' AND
|
|
version.date_generated_from < slot.end_datetime AND
|
|
version.date_generated_to > slot.start_datetime AND
|
|
version.contract_date_start <= slot.end_datetime AND
|
|
(version.contract_date_end IS NULL OR
|
|
version.contract_date_end >= slot.start_datetime)
|
|
LEFT JOIN hr_work_entry hwe
|
|
ON hwe.employee_id = slot.employee_id AND
|
|
hwe.date <= slot.end_datetime::date AND
|
|
hwe.date >= slot.start_datetime::date
|
|
WHERE slot.id in %s
|
|
GROUP BY slot.id
|
|
""", [tuple(self_with_employee.ids)])
|
|
query_result = self.env.cr.dictfetchall()
|
|
# Group by period to generate to profit from batching
|
|
# Contains [(start, stop)] = [version_ids]
|
|
periods_to_generate = defaultdict(list)
|
|
work_entries_to_archive = []
|
|
for row in query_result:
|
|
periods_to_generate[row['start'], row['stop']].extend(row['version_ids'])
|
|
if any(row['work_entry_ids']):
|
|
work_entries_to_archive.extend(row['work_entry_ids'])
|
|
self.env['hr.work.entry'].sudo().browse(work_entries_to_archive).write({'active': False})
|
|
work_entries_vals_list = []
|
|
for period, version_ids in periods_to_generate.items():
|
|
if not version_ids:
|
|
continue
|
|
contracts = self.env['hr.version'].sudo().browse(version_ids)
|
|
work_entries_vals_list.extend(contracts._get_work_entries_values(period[0], period[1]))
|
|
work_entries_vals_list = self.env['hr.version']._generate_work_entries_postprocess(work_entries_vals_list)
|
|
self.env['hr.work.entry'].sudo().create(work_entries_vals_list)
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
res = super().create(vals_list)
|
|
res.filtered(lambda s: s.state == 'published')._create_work_entries()
|
|
return res
|
|
|
|
def write(self, vals):
|
|
if vals.get('resource_id') or vals.get('start_datetime') or vals.get('end_datetime') or vals.get('allocated_hours'):
|
|
validated_work_entries = self.env['hr.work.entry'].sudo().search([('planning_slot_id', 'in', self.ids), ('state', '=', 'validated')])
|
|
if validated_work_entries:
|
|
raise UserError(_("This shift record is linked to a validated working entry. You can't modify it."))
|
|
state = vals.get('state')
|
|
concerned_slots = self.filtered(lambda s: s.state != state) if state\
|
|
else self.env['planning.slot']
|
|
res = super().write(vals)
|
|
concerned_slots._create_work_entries()
|
|
return res
|
|
|
|
@api.ondelete(at_uninstall=False)
|
|
def _unlink_except_validated_work_entries(self):
|
|
validated_work_entries = self.env['hr.work.entry'].sudo().search([('planning_slot_id', 'in', self.ids), ('state', '=', 'validated')])
|
|
if validated_work_entries:
|
|
raise UserError(_("This shift record is linked to a validated working entry. You can't delete it."))
|
|
|
|
def unlink(self):
|
|
# Archive linked work entries upon deleting slots
|
|
self.env['hr.work.entry'].sudo().search([('planning_slot_id', 'in', self.ids)]).write({'active': False})
|
|
return super().unlink()
|
|
|
|
def _get_planning_duration(self, date_start, date_stop):
|
|
'''
|
|
If the interval(date_start, date_stop) is equal to the planning_slot's interval, return the slot's allocated hours.
|
|
If the interval(date_start, date_stop) is a subset of the planning_slot's interval,
|
|
a new (non saved) planning_slot will be created to compute the duration according to planning rules.
|
|
If the interval(date_start, date_stop) is not fully inside of the planning_slot's interval the behaviour is undefined
|
|
|
|
:return: The real duration according to the planning app
|
|
:rtype: number
|
|
'''
|
|
self.ensure_one()
|
|
if self.start_datetime == date_start and self.end_datetime == date_stop:
|
|
return self.allocated_hours
|
|
new_slot = self.env['planning.slot'].new({
|
|
**self.read(['employee_id', 'company_id', 'allocated_percentage', 'resource_id'])[0],
|
|
'start_datetime': date_start,
|
|
'end_datetime': date_stop,
|
|
})
|
|
return new_slot.allocated_hours
|