[FIX] hr_payroll: Generate all benefits at once without SQL

It's easier, shorter, more logical to verify that a conflict will happen,
rather than combining multiple hacks to hide errors.

On the other hand, the method will now be called only once when generating
benefits on leaves/attendances for several employees
This commit is contained in:
Yannick Tivisse
2019-02-18 15:28:15 +00:00
parent 4ea60907f1
commit 1f39611fcc
3 changed files with 53 additions and 32 deletions
+30 -18
View File
@@ -37,7 +37,7 @@ class HrBenefit(models.Model):
leave_id = fields.Many2one('hr.leave', string='Leave')
_sql_constraints = [
('_unique', 'unique (employee_id, date_start, date_stop, benefit_type_id)', "Benefit already exists for this attendence"),
('_unique', 'unique (date_start, date_stop, employee_id, benefit_type_id)', "Benefit already exists for this attendance"),
('_benefit_has_end', 'check (date_stop IS NOT NULL OR duration <> 0)', 'Benefit must end. Please define an end date or a duration.'),
('_benefit_start_before_end', 'check (date_stop is null OR (date_stop > date_start))', 'Starting time should be before end time.')
]
@@ -46,12 +46,16 @@ class HrBenefit(models.Model):
def _onchange_duration(self):
self._inverse_duration()
def _get_duration(self, date_start, date_stop):
if not date_start or not date_stop:
return 0
dt = date_stop - date_start
return dt.days * 24 + dt.seconds / 3600 # Number of hours
@api.depends('date_stop', 'date_start')
def _compute_duration(self):
for benefit in self:
if benefit.date_start and benefit.date_stop:
dt = benefit.date_stop - benefit.date_start
benefit.duration = dt.days * 24 + dt.seconds / 3600 # Number of hours
benefit.duration = benefit._get_duration(benefit.date_start, benefit.date_stop)
def _inverse_duration(self):
for benefit in self:
@@ -98,7 +102,6 @@ class HrBenefit(models.Model):
@api.multi
def _compute_conflicts_leaves_to_approve(self):
if not self:
return False
@@ -123,21 +126,30 @@ class HrBenefit(models.Model):
})
return bool(conflicts)
def _safe_duplicate_create(self, vals):
def _safe_duplicate_create(self, vals_list, date_start, date_stop):
"""
Create benefit but silently abord if it already exists in the database (according to the _unique constraints).
@return: new record if it didn't exist, empty recordset otherwise.
Create benefits between date_start and date_stop according to vals_list.
Skip the values in vals_list if a benefit already exists for the given
date_start, date_stop, employee_id, benefit_type_id
:return: new record id if it didn't exist.
"""
try:
with mute_logger('odoo.sql_db'), self.env.cr.savepoint():
# Any database call will be run inside a postgresql savepoint which is
# rolledback if an exception occurs allowing to make subsequent calls in the transaction.
# (Otherwise InternalError is raised by any subsequent database operations)
return self.create(vals)
except IntegrityError as err:
if 'hr_benefit__unique' not in err.pgerror:
raise err
return self.env['hr.benefit']
# The search_read should be fast as date_start and date_stop are indexed from the
# unique sql constraint
month_recs = self.search_read([('date_start', '>=', date_start), ('date_stop', '<=', date_stop)],
['employee_id', 'date_start', 'date_stop', 'benefit_type_id'])
existing_entries = {(
r['date_start'],
r['date_stop'],
r['employee_id'][0],
r['benefit_type_id'][0] if r['benefit_type_id'] else False,
) for r in month_recs}
new_vals = [v for v in vals_list if (v['date_start'].replace(tzinfo=None), v['date_stop'].replace(tzinfo=None), v['employee_id'], v['benefit_type_id']) not in existing_entries]
# Remove duplicates from vals_list, shouldn't be necessary from saas-12.2
unique_new_vals = set()
for values in new_vals:
unique_new_vals.add(tuple(values.items()))
new_vals = [dict(values) for values in unique_new_vals]
return self.create(new_vals)
def action_leave(self):
leave = self.leave_id
+10 -7
View File
@@ -26,6 +26,9 @@ class HrEmployee(models.Model):
date_start = date_start.replace(tzinfo=pytz.utc)
date_stop = date_stop.replace(tzinfo=pytz.utc)
vals_list = []
leaves = self.env['hr.leave']
for employee in self:
# Approved leaves
@@ -36,11 +39,10 @@ class HrEmployee(models.Model):
r.date_to.replace(tzinfo=pytz.utc) >= date_start
)
global_leaves = employee.resource_calendar_id.global_leave_ids
for calendar_leave in emp_leaves | global_leaves:
hr_leave = calendar_leave.holiday_id
hr_leave._copy_to_benefits()
new_benefits = self.env['hr.benefit']
employee_leaves = (emp_leaves | global_leaves).mapped('holiday_id')
vals_list.extend(employee_leaves._get_benefits_values())
for contract in employee._get_contracts(date_start, date_stop):
date_start_benefits = max(date_start, datetime.combine(contract.date_start, datetime.min.time()).replace(tzinfo=pytz.utc))
@@ -52,13 +54,14 @@ class HrEmployee(models.Model):
# Attendances
for interval in attendances:
benefit_type_id = interval[2].mapped('benefit_type_id')[:1] or self.env.ref('hr_payroll.benefit_type_attendance')
new_benefits |= self.env['hr.benefit']._safe_duplicate_create({
vals_list += [{
'name': "%s: %s" % (benefit_type_id.name, employee.name),
'date_start': interval[0].astimezone(pytz.utc),
'date_stop': interval[1].astimezone(pytz.utc),
'benefit_type_id': benefit_type_id.id,
'employee_id': employee.id,
'state': 'confirmed',
})
}]
new_benefits._compute_conflicts_leaves_to_approve()
new_benefits = self.env['hr.benefit']._safe_duplicate_create(vals_list, date_start, date_stop)
new_benefits._compute_conflicts_leaves_to_approve()
+13 -7
View File
@@ -14,25 +14,27 @@ class HrLeave(models.Model):
_inherit = 'hr.leave'
@api.multi
def _copy_to_benefits(self):
def _get_benefits_values(self):
vals_list = []
for leave in self:
benefit_type = leave.holiday_status_id.benefit_type_id
self.env['hr.benefit']._safe_duplicate_create({
vals_list += [{
'name': "%s%s" % (benefit_type.name + ": " if benefit_type else "", leave.employee_id.name),
'date_start': leave.date_from,
'date_stop': leave.date_to,
'benefit_type_id': benefit_type.id,
'display_warning': not bool(benefit_type),
'employee_id': leave.employee_id.id,
'leave_id': self.id,
'leave_id': leave.id,
'state': 'confirmed',
})
}]
return vals_list
@api.multi
def _cancel_benefit_conflict(self):
benefits = self.env['hr.benefit'].search([('leave_id', 'in', self.ids)])
if benefits:
self._copy_to_benefits()
vals_list = self._get_benefits_values()
# create new benefits where the leave does not cover the full benefit
benefits_intervals = Intervals(intervals=[(b.date_start, b.date_stop, b) for b in benefits])
@@ -48,14 +50,18 @@ class HrLeave(models.Model):
benefit_start = interval[0] + relativedelta(seconds=1) if leave.date_to == interval[0] else interval[0]
benefit_stop = interval[1] - relativedelta(seconds=1) if leave.date_from == interval[1] else interval[1]
self.env['hr.benefit']._safe_duplicate_create({
vals_list += [{
'name': "%s: %s" % (benefit_type.name, employee.name),
'date_start': benefit_start,
'date_stop': benefit_stop,
'benefit_type_id': benefit_type.id,
'employee_id': employee.id,
'state': 'confirmed',
})
}]
date_start = min(benefits.mapped('date_start'))
date_stop = max(benefits.mapped('date_stop'))
self.env['hr.benefit']._safe_duplicate_create(vals_list, date_start, date_stop)
benefits.unlink()
@api.multi