316 lines
12 KiB
Python
316 lines
12 KiB
Python
from odoo import models, fields, api, _
|
|
from odoo.api import readonly
|
|
from odoo.exceptions import UserError
|
|
from collections import defaultdict
|
|
from datetime import timedelta, datetime
|
|
|
|
from odoo.tools.safe_eval import safe_eval
|
|
import json
|
|
import calendar
|
|
import secrets
|
|
|
|
class DefaultDictroll(defaultdict):
|
|
def get(self, key, default=None):
|
|
if key not in self and default is not None:
|
|
self[key] = default
|
|
return self[key]
|
|
|
|
class OfferLetter(models.Model):
|
|
_name = 'offer.letter'
|
|
_description = 'Employee Offer Letter'
|
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
|
_order = 'create_date desc'
|
|
|
|
name = fields.Char(
|
|
string='Reference',
|
|
required=True,
|
|
default=lambda self: _('New'),
|
|
copy=False
|
|
)
|
|
bridge_id = fields.Many2one(
|
|
'recruitment.employee.bridge',
|
|
string='Onboarding',
|
|
ondelete='restrict',
|
|
tracking=True,
|
|
index=True,
|
|
)
|
|
main_candidate_name = fields.Char(string="Candidate Name", compute="_compute_bridge_contact", store=True)
|
|
recipient_email = fields.Char(compute="_compute_bridge_contact", store=True)
|
|
recipient_street = fields.Char(compute="_compute_bridge_contact", store=True)
|
|
recipient_street2 = fields.Char(compute="_compute_bridge_contact", store=True)
|
|
recipient_city = fields.Char(compute="_compute_bridge_contact", store=True)
|
|
recipient_zip = fields.Char(compute="_compute_bridge_contact", store=True)
|
|
recipient_country_id = fields.Many2one("res.country", compute="_compute_bridge_contact", store=True)
|
|
|
|
@api.depends(
|
|
'bridge_id',
|
|
'bridge_id.employee_name',
|
|
'bridge_id.employee_id.name',
|
|
'bridge_id.work_email',
|
|
'bridge_id.private_email',
|
|
'bridge_id.employee_id.work_email',
|
|
'bridge_id.employee_id.private_email',
|
|
'bridge_id.private_street',
|
|
'bridge_id.private_street2',
|
|
'bridge_id.private_city',
|
|
'bridge_id.private_zip',
|
|
'bridge_id.private_country_id',
|
|
)
|
|
def _compute_bridge_contact(self):
|
|
for rec in self:
|
|
bridge = rec.bridge_id
|
|
employee = bridge.employee_id
|
|
rec.main_candidate_name = bridge.employee_name or employee.name or ''
|
|
rec.recipient_email = bridge.work_email or bridge.private_email or employee.work_email or employee.private_email or ''
|
|
rec.recipient_street = bridge.private_street
|
|
rec.recipient_street2 = bridge.private_street2
|
|
rec.recipient_city = bridge.private_city
|
|
rec.recipient_zip = bridge.private_zip
|
|
rec.recipient_country_id = bridge.private_country_id
|
|
requested_by_id = fields.Many2one('res.users', string='Requested By', readonly=True, tracking=True)
|
|
request_date = fields.Datetime(string='Requested On', readonly=True, tracking=True)
|
|
|
|
employee_id = fields.Char(
|
|
string='Employee ID',
|
|
readonly=True
|
|
)
|
|
position = fields.Char(
|
|
string='Position',
|
|
required=True
|
|
)
|
|
|
|
salary = fields.Float(
|
|
string='Salary',
|
|
required=True
|
|
)
|
|
mi = fields.Float(
|
|
string='Medical Insurance',
|
|
)
|
|
currency_id = fields.Many2one(
|
|
'res.currency',
|
|
string='Currency',
|
|
default=lambda self: self.env.company.currency_id
|
|
)
|
|
joining_date = fields.Date(
|
|
string='Joining Date',
|
|
default=lambda self: (datetime.now() + timedelta(days=14)).strftime('%Y-%m-%d'))
|
|
contract_type = fields.Selection([
|
|
('permanent', 'Permanent'),
|
|
('contract', 'Fixed Term Contract'),
|
|
('intern', 'Internship')],
|
|
string='Contract Type',
|
|
default='permanent'
|
|
)
|
|
probation_period = fields.Integer(
|
|
string='Probation Period (months)',
|
|
default=3
|
|
)
|
|
terms_conditions = fields.Text(
|
|
string='Terms and Conditions',
|
|
default=lambda self: self._default_terms()
|
|
)
|
|
state = fields.Selection([
|
|
('requested', 'Requested'),
|
|
('sent', 'Sent'),
|
|
('accepted', 'Accepted'),
|
|
('rejected', 'Rejected'),
|
|
('expired', 'Expired')],
|
|
string='Status',
|
|
default='requested',
|
|
tracking=True
|
|
)
|
|
sent_date = fields.Datetime(string='Sent Date')
|
|
response_date = fields.Datetime(string='Response Date')
|
|
pay_struct_id = fields.Many2one('hr.payroll.structure', string="Salary Structure", required=True)
|
|
manager_id = fields.Many2one('hr.employee', string='Manager')
|
|
rejection_reason = fields.Char()
|
|
response_token = fields.Char(copy=False, index=True)
|
|
|
|
def _issue_response_token(self):
|
|
while True:
|
|
token = secrets.token_urlsafe(32)
|
|
if not self.search_count([('response_token', '=', token)]):
|
|
return token
|
|
|
|
@api.model
|
|
def _default_terms(self):
|
|
return """
|
|
<p>1. This offer is contingent upon satisfactory reference checks.</p>
|
|
<p>2. You will be required to sign a confidentiality agreement.</p>
|
|
<p>3. The company reserves the right to modify job responsibilities.</p>
|
|
"""
|
|
|
|
@api.model
|
|
def create(self, vals):
|
|
if vals.get('name', _('New')) == _('New'):
|
|
vals['name'] = self.env['ir.sequence'].next_by_code('offer.letter') or _('New')
|
|
offer_letter = super(OfferLetter, self).create(vals)
|
|
if vals.get('bridge_id') and 'salary' in vals and 'finalized_ctc' in offer_letter.bridge_id._fields:
|
|
offer_letter.bridge_id.finalized_ctc = offer_letter.salary
|
|
return offer_letter
|
|
|
|
def write(self, vals):
|
|
result = super(OfferLetter, self).write(vals)
|
|
if 'salary' in vals:
|
|
for record in self.filtered('bridge_id'):
|
|
if 'finalized_ctc' in record.bridge_id._fields:
|
|
record.bridge_id.finalized_ctc = record.salary
|
|
return result
|
|
|
|
def action_open_send_offer_wizard(self):
|
|
self.ensure_one()
|
|
if not self.env.user.has_group('hr.group_hr_manager'):
|
|
raise UserError(_("Only HR can release the offer letter to the applicant."))
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': _('Send Offer'),
|
|
'res_model': 'applicant.offer.mail.wizard',
|
|
'view_mode': 'form',
|
|
'view_id': self.env.ref('offer_letters.view_applicant_offer_mail_wizard_form').id,
|
|
'target': 'new',
|
|
'context': {
|
|
'active_model': 'offer.letter',
|
|
'active_id': self.id,
|
|
},
|
|
}
|
|
|
|
def action_send_offer(self):
|
|
self.ensure_one()
|
|
if not self.env.user.has_group('hr.group_hr_manager'):
|
|
raise UserError(_("Only HR can mark the offer as sent."))
|
|
self.write({'state': 'sent', 'sent_date': fields.Datetime.now()})
|
|
return True
|
|
|
|
def action_accept_offer(self):
|
|
self.ensure_one()
|
|
# employee = self.env['hr.employee'].create({
|
|
# 'name': self.main_candidate_name,
|
|
# 'job_title': self.position,
|
|
# 'department_id': self.department_id.id,
|
|
# 'currency_id': self.currency_id.id,
|
|
# })
|
|
self.write({
|
|
'state': 'accepted',
|
|
# 'employee_id': employee,
|
|
'response_date': fields.Datetime.now()
|
|
})
|
|
return True
|
|
|
|
def action_reject_offer(self, reason=False):
|
|
self.ensure_one()
|
|
self.write({
|
|
'state': 'rejected',
|
|
'response_date': fields.Datetime.now(),
|
|
'rejection_reason': reason
|
|
})
|
|
return True
|
|
|
|
@api.onchange('bridge_id')
|
|
def _onchange_bridge_id(self):
|
|
self.position = self.bridge_id.job_id.name
|
|
|
|
def get_paydetailed_lines(self):
|
|
today = fields.Date.today()
|
|
first_day = today.replace(day=1)
|
|
last_day = today.replace(day=calendar.monthrange(today.year, today.month)[1])
|
|
|
|
payslip = self.env['hr.payslip'].sudo().new({
|
|
'date_from': first_day,
|
|
'date_to': last_day,
|
|
})
|
|
|
|
contract = self.env['hr.contract'].sudo().new({
|
|
'date_start': first_day,
|
|
'date_end': last_day,
|
|
'l10n_in_medical_insurance':self.mi,
|
|
'l10n_in_provident_fund': True,
|
|
'name': 'test',
|
|
'wage': self.salary / 12,
|
|
})
|
|
|
|
categories_dict = {}
|
|
rules_dict = {}
|
|
result = {}
|
|
|
|
localdict = {
|
|
'payslip': payslip,
|
|
'contract': contract,
|
|
'worked_days': {},
|
|
'categories': defaultdict(lambda: 0), # Fixed: Changed DefaultDictroll to defaultdict
|
|
'rules': defaultdict(lambda: dict(total=0, amount=0, quantity=0)),
|
|
'result': None,
|
|
'result_qty': 1.0,
|
|
'result_rate': 100,
|
|
'result_name': False,
|
|
'inputs': {},
|
|
}
|
|
blacklisted_ids = set(self.env.context.get('prevent_payslip_computation_line_ids', []))
|
|
for rule in sorted(self.sudo().pay_struct_id.rule_ids, key=lambda r: r.sequence):
|
|
if rule.id in blacklisted_ids or not rule._satisfy_condition(localdict):
|
|
continue
|
|
qty = 1.0
|
|
rate = 100.0
|
|
amount = 0.0
|
|
try:
|
|
if rule.amount_select == 'fix':
|
|
amount = rule.amount_fix
|
|
elif rule.amount_select == 'percentage':
|
|
base = float(safe_eval(rule.amount_percentage_base or '0.0', localdict, mode='exec', nocopy=True))
|
|
amount = base * rule.amount_percentage / 100
|
|
elif rule.amount_select == 'code':
|
|
safe_eval(rule.amount_python_compute or '0.0', localdict, mode='exec', nocopy=True)
|
|
amount = float(localdict.get('result', 0.0))
|
|
except Exception as e:
|
|
raise UserError(_("Error in rule %s: %s") % (rule.name, str(e)))
|
|
|
|
total = payslip.sudo()._get_payslip_line_total(amount, qty, rate, rule)
|
|
rule_code = rule.code
|
|
previous_amount = localdict.get(rule.code, 0.0)
|
|
category_code = rule.category_id.code
|
|
tot_rule = payslip.sudo()._get_payslip_line_total(amount, qty, rate, rule)
|
|
|
|
# Make sure _sum_salary_rule_category method exists
|
|
if hasattr(rule.category_id, '_sum_salary_rule_category'):
|
|
localdict = rule.category_id.sudo()._sum_salary_rule_category(localdict, tot_rule - previous_amount)
|
|
|
|
localdict[rule_code] = total
|
|
rules_dict[rule_code] = rule
|
|
categories_dict[category_code] = categories_dict.get(category_code, 0.0) + amount
|
|
|
|
result[rule_code] = {
|
|
'sequence': rule.sequence,
|
|
'code': rule_code,
|
|
'name': rule.name, # Simplified name retrieval
|
|
'salary_rule_id': rule.id,
|
|
'amount': round(amount,2),
|
|
'y_amount': round((amount * 12),2),
|
|
'quantity': qty,
|
|
'rate': rate,
|
|
'total': round(total,2),
|
|
}
|
|
|
|
self.terms_conditions = json.dumps(list(result.values()))
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': self._name,
|
|
'res_id': self.id,
|
|
'view_mode': 'form',
|
|
'view_type': 'form',
|
|
'target': 'current',
|
|
}
|
|
|
|
def generate_pdf_report(self):
|
|
return self.env.ref('offer_letters.hr_offer_letters_employee_print').report_action(self)
|
|
|
|
|
|
def action_open_reject_wizard(self):
|
|
self.ensure_one()
|
|
return {
|
|
"type": "ir.actions.act_window",
|
|
"name": _("Reject Offer"),
|
|
"res_model": "offer.letter.reject.wizard",
|
|
"view_mode": "form",
|
|
"target": "new",
|
|
"context": {"default_offer_letter_id": self.id},
|
|
}
|