[NEW] hr-luncheon-vouchers: create addons

This commit is contained in:
Stéphan Sainléger
2022-07-13 16:42:37 +02:00
parent 2a4f509178
commit 4a5f926b1d
20 changed files with 732 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
from . import calendar_event_type
from . import hr_employee
from . import hr_lv_allocation
from . import resource
from . import res_company
from . import res_config_settings

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from odoo import fields, models, _
class MeetingType(models.Model):
_inherit = "calendar.event.type"
ref = fields.Char(
string=_("Reference"),
copy=False,
store=True,
)
remove_luncheon_voucher = fields.Boolean(
string=_("Remove luncheon voucher"),
copy=True,
store=True,
)

View File

@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
from xml.dom.minicompat import EmptyNodeList
from odoo import fields, models, api, _
class HrEmployeeBase(models.AbstractModel):
_inherit = "hr.employee.base"
lv_allocations_ids = fields.One2many("hr.lv.allocation", "employee_id")
total_acquired_lv = fields.Integer(
string=_("Total allocated luncheon vouchers"), store=True, copy=False
)
distributed_lv = fields.Integer(
string=_("Distributed luncheon vouchers"), store=True, copy=False
)
dued_lv = fields.Integer(
string=_("Remaining luncheon vouchers"), store=True, copy=False
)
default_monthly_lv = fields.Integer(
string=_("Default monthly distribution"), store=True, copy=True
)
def refresh_lv_values(self):
for record in self:
record._compute_total_acquired_lv()
record._compute_distributed_lv()
record._compute_dued_lv()
def _compute_total_acquired_lv(self):
for record in self:
allocations = self.env["hr.lv.allocation"].search(
[("employee_id", "=", record.id), ("state", "=", ["confirmed", "distributed"])]
)
record.total_acquired_lv = sum(allocations.mapped("number_acquired_lv"))
def _compute_distributed_lv(self):
for record in self:
allocations = self.env["hr.lv.allocation"].search(
[("employee_id", "=", record.id), ("state", "=", "distributed")]
)
record.distributed_lv = sum(allocations.mapped("number_distributed_lv"))
def _compute_dued_lv(self):
for record in self:
record.dued_lv = record.total_acquired_lv - record.distributed_lv
def generate_mass_lv_allocation(self, values):
for record in self:
record.generate_lv_allocation(values)
def generate_lv_allocation(self, values):
self.ensure_one()
values["employee_id"] = self.id
values["name"] = values["distrib_campaign_name"] + " - " + self.name
self.env["hr.lv.allocation"].create(values)
def action_lv_allocations(self):
action = self.env["ir.actions.act_window"]._for_xml_id("hr-luncheon-voucher.act_lv_allocations")
action['context'] = {
'search_default_employee_id': self.id,
'default_employee_id': self.id,
}
action['domain'] = [('employee_id', '=', self.id)]
return action
def action_lv_allocations_requests_wizard(self):
action = self.env["ir.actions.act_window"]._for_xml_id(
"hr-luncheon-voucher.lv_allocations_requests_wizard_action"
)
ctx = dict(self.env.context)
ctx["active_ids"] = self.ids
action["context"] = ctx
return action

View File

@@ -0,0 +1,187 @@
# -*- coding: utf-8 -*-
import math
from datetime import datetime, date, timedelta, time
from dateutil.rrule import rrule, DAILY
from pytz import timezone, UTC, utc
from odoo import fields, models, api, _
class LuncheonVouchersAllocation(models.Model):
_name = "hr.lv.allocation"
_description = "Luncheon Vouchers Allocation"
_order = "create_date desc"
_inherit = ["mail.thread", "mail.activity.mixin"]
_mail_post_access = "read"
name = fields.Char('Name')
distrib_campaign_name = fields.Char('Distribution campaign')
state = fields.Selection(
[
("draft", "Draft"),
("confirmed", "Confirmed"),
("distributed", "Distributed"),
],
string="Status",
readonly=True,
tracking=True,
copy=False,
default="draft",
help="The status is set to 'Draft', when an allocation request is created."
+ "\nThe status is 'Confirmed', when an allocation request is confirmed by HR manager."
+ "\nThe status is 'Distributed', when the luncheon vouchers have been distributed.",
)
date_from = fields.Datetime(
string=_("Start Date"),
store=True,
readonly=False,
copy=False,
tracking=True,
states={
"confirmed": [("readonly", True)],
"distributed": [("readonly", True)],
},
)
date_to = fields.Datetime(
string=_("End Date"),
store=True,
readonly=False,
copy=False,
tracking=True,
states={
"confirmed": [("readonly", True)],
"distributed": [("readonly", True)],
},
)
employee_id = fields.Many2one(
"hr.employee",
store=True,
string=_("Employee"),
index=True,
readonly=False,
ondelete="restrict",
tracking=True,
states={
"confirmed": [("readonly", True)],
"distributed": [("readonly", True)],
},
)
number_acquired_lv = fields.Integer(
string=_("Acquired Vouchers"),
store=True,
readonly=False,
tracking=True,
states={
"confirmed": [("readonly", True)],
"distributed": [("readonly", True)],
},
)
number_dued_lv = fields.Integer(
string=_("Dued Vouchers"),
store=True,
readonly=False,
tracking=True,
states={
"confirmed": [("readonly", True)],
"distributed": [("readonly", True)],
},
)
number_distributed_lv = fields.Integer(
string=_("Distributed Vouchers"),
store=True,
readonly=False,
tracking=True,
states={
"confirmed": [("readonly", False)],
"distributed": [("readonly", True)],
},
)
def create(self, values):
res = super(LuncheonVouchersAllocation, self).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)])
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):
# The luncheon voucher is acquired only if the employee has worked the entire day
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):
# The luncheon voucher is acquired only if the employee has worked the entire day
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

View File

@@ -0,0 +1,7 @@
from odoo import fields, models
class Company(models.Model):
_inherit = 'res.company'
hr_half_day_cancels_voucher = fields.Boolean(string="Half working days cancel luncheon vouchers")

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
hr_half_day_cancels_voucher = fields.Boolean(string="Half working days cancel luncheon vouchers", related="company_id.hr_half_day_cancels_voucher", readonly=False)

View File

@@ -0,0 +1,59 @@
from datetime import timedelta
import math
from odoo import models, fields
class ResourceCalendar(models.Model):
_inherit = "resource.calendar"
def _retrieve_day_matching_attendances(self, day):
domain = [("calendar_id", "=", self.id),("dayofweek", "=", day.weekday())]
if self.two_weeks_calendar:
# Employee has Even/Odd weekly calendar
week_type = 1 if int(math.floor((day.toordinal() - 1) / 7) % 2) else 0
domain.append(("week_type", "=", week_type))
result = self.env["resource.calendar.attendance"].search(domain)
return result
def is_working_day(self, day):
day_attendances = self._retrieve_day_matching_attendances(day)
if len(day_attendances) == 0:
# This day of the week is not supposed to be a working day
return False
else:
# This day of the week is supposed to be a working day
return True
def is_full_working_day(self, day):
day_attendances = self._retrieve_day_matching_attendances(day)
morning_worked = len(day_attendances.filtered(lambda x: x.day_period == "morning")) > 0
afternoon_worked = len(day_attendances.filtered(lambda x: x.day_period == "afternoon")) > 0
return morning_worked and afternoon_worked
def _is_worked_attendance(self, resource, day, attendance):
attendance_start = fields.Datetime.to_datetime(day.date()) + timedelta(hours=attendance.hour_from)
attendance_end = fields.Datetime.to_datetime(day.date()) + timedelta(hours=attendance.hour_to)
resource_leaves = self.env["resource.calendar.leaves"].search([("resource_id", "=", resource.id), ("date_from", "<=", attendance_start), ("date_to", ">=", attendance_end)])
if resource_leaves:
return False
else:
# a part or the whole attendance is worked
return True
def is_worked_day(self, resource, day):
day_attendances = self._retrieve_day_matching_attendances(day)
# If at least one attendance is worked, return True
for attendance in day_attendances:
if self._is_worked_attendance(resource, day, attendance):
return True
return False
def all_attendances_worked(self, resource, day):
day_attendances = self._retrieve_day_matching_attendances(day)
# If at least one attendance is not worked, return False
for attendance in day_attendances:
if not self._is_worked_attendance(resource, day, attendance):
return False
return True