from datetime import datetime, time, timedelta from dateutil.rrule import DAILY, rrule from pytz import UTC from odoo import api, fields, models class LuncheonVouchersAllocation(models.Model): _name = "hr.lv.allocation" _description = "Luncheon Vouchers Allocation" name = fields.Char() distrib_campaign_name = fields.Char("Distribution campaign") state = fields.Selection( [ ("draft", "Draft"), ("confirmed", "Confirmed"), ("distributed", "Distributed"), ], string="Status", readonly=True, copy=False, default="draft", help=( "The status is set to 'Draft', when an allocation request is created. " "The status is 'Confirmed', when an allocation request is confirmed by " "HR manager. The status is 'Distributed', when the luncheon vouchers " "have been distributed." ), ) date_from = fields.Datetime( string="Start Date", store=True, readonly=False, copy=False, states={ "confirmed": [("readonly", True)], "distributed": [("readonly", True)], }, ) date_to = fields.Datetime( string="End Date", store=True, readonly=False, copy=False, states={ "confirmed": [("readonly", True)], "distributed": [("readonly", True)], }, ) employee_id = fields.Many2one( "hr.employee", store=True, string="Employee", index=True, readonly=False, ondelete="restrict", states={ "confirmed": [("readonly", True)], "distributed": [("readonly", True)], }, ) number_acquired_lv = fields.Integer( string="Acquired Vouchers", store=True, readonly=False, states={ "confirmed": [("readonly", True)], "distributed": [("readonly", True)], }, ) number_dued_lv = fields.Integer( string="Dued Vouchers", store=True, readonly=False, states={ "confirmed": [("readonly", True)], "distributed": [("readonly", True)], }, ) number_distributed_lv = fields.Integer( string="Distributed Vouchers", store=True, readonly=False, states={ "confirmed": [("readonly", False)], "distributed": [("readonly", True)], }, ) lv_balance = fields.Integer( "Vouchers balance", compute="_compute_lv_balance", help="Vouchers available after distribution. Dued vouchers - Distributed.", ) @api.depends("number_dued_lv", "number_distributed_lv") def _compute_lv_balance(self): for allocation in self: allocation.lv_balance = ( allocation.number_dued_lv - allocation.number_distributed_lv ) @api.model_create_multi def create(self, values): res = super().create(values) res._calculate_number_acquired_lv() res._calculate_number_dued_lv() res._default_number_distributed_lv() return res @api.depends("employee_id") def _default_number_distributed_lv(self): for record in self: record.number_distributed_lv = record.employee_id.default_monthly_lv def _has_cancelling_voucher_event(self, day): category_no_voucher_ids = self.env["calendar.event.type"].search( [("remove_luncheon_voucher", "=", True)] ) events = self.env["calendar.event"].search( [ ("categ_ids", "in", category_no_voucher_ids.ids), ("partner_ids", "in", self.employee_id.user_id.partner_id.id), ] ) day_start = fields.Datetime.to_datetime(day.date()) day_end = fields.Datetime.to_datetime(day.date()) + timedelta(hours=24) cancelling_events = events.filtered( lambda x: not ((x.start < day_start) and (x.stop <= day_start)) and not ((x.start >= day_end) and (x.stop > day_end)) ) if len(cancelling_events) > 0: return True else: return False def _calculate_number_acquired_lv(self): nb_eligible_days = 0 dfrom = datetime.combine( fields.Date.from_string(self.date_from), time.min ).replace(tzinfo=UTC) dto = datetime.combine(fields.Date.from_string(self.date_to), time.max).replace( tzinfo=UTC ) period_days = rrule(DAILY, dfrom, until=dto) calendar_resource = self.employee_id.resource_calendar_id for day in period_days: # Check if this days is a working day if not calendar_resource.is_working_day(day): continue # The employee should work this day but... if ( self.env.company.hr_half_day_cancels_voucher and not calendar_resource.is_full_working_day(day) ): # Voucher requires full-day attendance continue # Check leaves if not calendar_resource.is_worked_day(self.employee_id, day): continue # The employee has worked this day but... if ( self.env.company.hr_half_day_cancels_voucher and not calendar_resource.all_attendances_worked( self.employee_id.resource_id, day ) ): # Voucher requires all attendances to be worked continue # Check there is no event cancelling the voucher if self._has_cancelling_voucher_event(day): continue # All checks passed, the days is eligible for a voucher nb_eligible_days += 1 self.number_acquired_lv = nb_eligible_days def _calculate_number_dued_lv(self): for record in self: if record.state == "distributed": record.number_dued_lv = record.employee_id.dued_lv else: record.number_dued_lv = ( record.employee_id.dued_lv + record.number_acquired_lv ) def confirm_allocation(self): for record in self: if record.state == "draft": record.state = "confirmed" record.employee_id._compute_total_acquired_lv() record.employee_id._compute_dued_lv() def back_to_draft(self): for record in self: if record.state in ["confirmed", "distributed"]: record.state = "draft" record.employee_id._compute_total_acquired_lv() record.employee_id._compute_distributed_lv() record.employee_id._compute_dued_lv() def distribute_allocation(self): for record in self: if record.state == "confirmed": record.state = "distributed" record.employee_id._compute_distributed_lv() record.employee_id._compute_dued_lv() def adjust_distribution(self): for record in self: for record in self: if record.state == "draft": record.number_distributed_lv = ( record.employee_id.dued_lv + record.number_acquired_lv )