19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:00 +01:00
parent a1137a1456
commit e1d89e11e3
2789 changed files with 1093187 additions and 605897 deletions

View file

@ -0,0 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import hr_work_entry_regeneration_wizard

View file

@ -0,0 +1,127 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from itertools import groupby
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from datetime import timedelta
from dateutil.relativedelta import relativedelta
class HrWorkEntryRegenerationWizard(models.TransientModel):
_name = 'hr.work.entry.regeneration.wizard'
_description = 'Regenerate Employee Work Entries'
earliest_available_date = fields.Date('Earliest date', compute='_compute_earliest_available_date')
earliest_available_date_message = fields.Char(readonly=True, store=False, default='')
latest_available_date = fields.Date('Latest date', compute='_compute_latest_available_date')
latest_available_date_message = fields.Char(readonly=True, store=False, default='')
date_from = fields.Date('From', required=True, default=lambda self: self.env.context.get('date_start'))
date_to = fields.Date('To', required=True, compute='_compute_date_to', store=True,
readonly=False, default=lambda self: self.env.context.get('date_end'))
employee_ids = fields.Many2many('hr.employee', string='Employees',
domain=lambda self: [('company_id', 'in', self.env.companies.ids)], required=True)
validated_work_entry_employee_ids = fields.Many2many('hr.employee', export_string_translation=False,
compute='_compute_validated_work_entry_employee_ids')
search_criteria_completed = fields.Boolean(compute='_compute_search_criteria_completed')
valid = fields.Boolean(compute='_compute_valid')
@api.depends('date_from')
def _compute_date_to(self):
for wizard in self:
wizard.date_to = wizard.date_from and wizard.date_from + relativedelta(months=+1, day=1, days=-1)
@api.depends('employee_ids')
def _compute_earliest_available_date(self):
for wizard in self:
dates = wizard.employee_ids.version_ids.mapped('date_generated_from')
wizard.earliest_available_date = min(dates) if dates else None
@api.depends('employee_ids')
def _compute_latest_available_date(self):
for wizard in self:
dates = wizard.employee_ids.version_ids.mapped('date_generated_to')
wizard.latest_available_date = max(dates) if dates else None
@api.depends('date_from', 'date_to', 'employee_ids')
def _compute_validated_work_entry_employee_ids(self):
for wizard in self:
employee_ids = self.env['hr.employee']
if wizard.search_criteria_completed:
validated_work_entry_by_employee = self.env['hr.work.entry']._read_group([
('employee_id', 'in', wizard.employee_ids.ids),
('date', '>=', wizard.date_from),
('date', '<=', wizard.date_to),
('state', '=', 'validated')
], ['employee_id'])
for per_employee in validated_work_entry_by_employee:
employee_ids |= per_employee[0]
wizard.validated_work_entry_employee_ids = employee_ids
@api.depends('validated_work_entry_employee_ids', 'employee_ids')
def _compute_valid(self):
for wizard in self:
wizard.valid = wizard.search_criteria_completed and len(wizard.employee_ids - wizard.validated_work_entry_employee_ids) > 0
@api.depends('date_from', 'date_to', 'employee_ids')
def _compute_search_criteria_completed(self):
for wizard in self:
wizard.search_criteria_completed = wizard.date_from and wizard.date_to and wizard.employee_ids and wizard.earliest_available_date and wizard.latest_available_date
@api.onchange('date_from', 'date_to', 'employee_ids')
def _check_dates(self):
for wizard in self:
wizard.earliest_available_date_message = ''
wizard.latest_available_date_message = ''
if wizard.search_criteria_completed:
if wizard.date_from > wizard.date_to:
date_from = wizard.date_from
wizard.date_from = wizard.date_to
wizard.date_to = date_from
if wizard.earliest_available_date and wizard.date_from < wizard.earliest_available_date:
wizard.date_from = wizard.earliest_available_date
wizard.earliest_available_date_message = f'The earliest available date is {self._date_to_string(wizard.earliest_available_date)}'
if wizard.latest_available_date and wizard.date_to > wizard.latest_available_date:
wizard.date_to = wizard.latest_available_date
wizard.latest_available_date_message = f'The latest available date is {self._date_to_string(wizard.latest_available_date)}'
@api.model
def _date_to_string(self, date):
if not date:
return ''
user_date_format = self.env['res.lang']._get_data(code=self.env.user.lang).date_format
return date.strftime(user_date_format)
def _work_entry_fields_to_nullify(self):
return ['active']
def regenerate_work_entries(self, slots=None, record_ids=None):
if not slots:
if not self.env.context.get('work_entry_skip_validation'):
if not self.search_criteria_completed:
raise ValidationError(_("In order to regenerate the work entries, you need to provide the wizard with an employee_id, a date_from and a date_to."))
if self.date_from < self.earliest_available_date or self.date_to > self.latest_available_date:
raise ValidationError(_("The from date must be >= '%(earliest_available_date)s' and the to date must be <= '%(latest_available_date)s', which correspond to the generated work entries time interval.", earliest_available_date=self._date_to_string(self.earliest_available_date), latest_available_date=self._date_to_string(self.latest_available_date)))
if not self.valid:
raise ValidationError(self.env._("No work entry can be regenerated in this range of dates and these employees."))
valid_employees = self.employee_ids - self.validated_work_entry_employee_ids
date_from = max(self.date_from, self.earliest_available_date) if self.earliest_available_date else self.date_from
date_to = min(self.date_to, self.latest_available_date) if self.latest_available_date else self.date_to
valid_employees.generate_work_entries(date_from, date_to, True)
else:
range_by_employee = defaultdict(list)
slots.sort(key=lambda d: (d['employee_id'], d['date']))
for employee_id, records in groupby(slots, lambda d: d['employee_id']):
dates = [fields.Date.from_string(r['date']) for r in records]
start = end = dates[0]
for current in dates[1:]:
if current - end != timedelta(days=1):
range_by_employee[start, end].append(employee_id)
start = current
end = current
range_by_employee[start, end].append(employee_id)
for (date_from, date_to), employee_ids in range_by_employee.items():
valid_employees = self.env["hr.employee"].browse(employee_ids)
valid_employees.generate_work_entries(date_from, date_to, True)

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="hr_work_entry_regeneration_wizard" model="ir.ui.view">
<field name="name">hr_work_entry_regeneration_wizard</field>
<field name="model">hr.work.entry.regeneration.wizard</field>
<field name="arch" type="xml">
<form string="Regenerate Employee Work Entries">
<group>
<field name="employee_ids" widget="many2many_avatar_employee_class"
in_error="validated_work_entry_employee_ids"
domain="[('company_id', 'in', allowed_company_ids), ('version_ids.id','!=', False)]"/>
<field name="date_to" invisible="1"/>
<label for="date_from" string="Period"/>
<field name="date_from" style="width: 200px;" nolabel="1" widget="daterange" options="{'end_date_field': 'date_to'}"/>
<div colspan="2" class="text-info" invisible="earliest_available_date_message == ''">
<i class="fa fa-info-circle me-1" title="Hint"/>
<field name="earliest_available_date_message" class="oe_inline" nolabel="1"/>
</div>
<div colspan="2" class="text-info" invisible="latest_available_date_message == ''">
<i class="fa fa-info-circle me-1" title="Hint"/>
<field name="latest_available_date_message" class="oe_inline" nolabel="1"/>
</div>
</group>
<div>
<span class="text-muted">Warning: The work entry regeneration will delete all manual changes on the selected period.</span>
</div>
<field name="search_criteria_completed" invisible="1"/>
<div invisible="not search_criteria_completed or not validated_work_entry_employee_ids">
<div class="text-danger"><i class="fa fa-exclamation-triangle me-1" title="Warning"/>Employees in red will be skipped because they have at least one validated work entry.</div>
</div>
<footer>
<button name="regenerate_work_entries"
string="Regenerate Work Entries" data-hotkey="q"
class="btn btn-primary oe_highlight"
type="object"
invisible="not valid"/>
<button name="regenerate_work_entries_disabled"
string="Regenerate Work Entries"
class="btn btn-primary disabled"
invisible="valid"/>
<button name="cancel_button" string="Cancel" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>
</record>
<record id="hr_work_entry_regeneration_wizard_action" model="ir.actions.act_window">
<field name="name">Work Entry Regeneration</field>
<field name="res_model">hr.work.entry.regeneration.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>