diff --git a/addons/hr_payroll/models/hr_benefit.py b/addons/hr_payroll/models/hr_benefit.py index c02da93e47f..7d0801656da 100644 --- a/addons/hr_payroll/models/hr_benefit.py +++ b/addons/hr_payroll/models/hr_benefit.py @@ -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 diff --git a/addons/hr_payroll/models/hr_employee.py b/addons/hr_payroll/models/hr_employee.py index 1025eead194..ec8a9af9590 100644 --- a/addons/hr_payroll/models/hr_employee.py +++ b/addons/hr_payroll/models/hr_employee.py @@ -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() diff --git a/addons/hr_payroll/models/hr_leave.py b/addons/hr_payroll/models/hr_leave.py index f311ce726dd..efa1d8bc2ce 100644 --- a/addons/hr_payroll/models/hr_leave.py +++ b/addons/hr_payroll/models/hr_leave.py @@ -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