odoo18/addons_extensions/offer_letters/models/offer_letter.py

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},
}