From a8b3102f3c1e1c31d44dfd3359537b9dafa6a81e Mon Sep 17 00:00:00 2001 From: raman Date: Thu, 25 Sep 2025 16:07:48 +0530 Subject: [PATCH] new hr menus and offer letters --- .../models/batch_payslip.py | 2 +- addons_extensions/hr_ftp/__init__.py | 3 + addons_extensions/hr_ftp/__manifest__.py | 23 + addons_extensions/hr_ftp/models/__init__.py | 0 addons_extensions/hr_ftp/models/hr.py | 0 .../hr_ftp/security/ir.model.access.csv | 1 + addons_extensions/hr_ftp/views/hr_views.xml | 97 +++++ .../hr_payroll/models/hr_payslip.py | 2 + addons_extensions/offer_letters/__init__.py | 1 + .../offer_letters/__manifest__.py | 30 ++ .../offer_letters/models/__init__.py | 1 + .../offer_letters/models/offer_letter.py | 225 ++++++++++ .../report/offer_letter_report.xml | 14 + .../report/offer_letter_template.xml | 396 ++++++++++++++++++ .../security/ir.model.access.csv | 3 + .../static/src/js/pay_details_widget.js | 70 ++++ .../offer_letters/views/menu_views.xml | 4 + .../views/offer_letter_views.xml | 64 +++ 18 files changed, 935 insertions(+), 1 deletion(-) create mode 100644 addons_extensions/hr_ftp/__init__.py create mode 100644 addons_extensions/hr_ftp/__manifest__.py create mode 100644 addons_extensions/hr_ftp/models/__init__.py create mode 100644 addons_extensions/hr_ftp/models/hr.py create mode 100644 addons_extensions/hr_ftp/security/ir.model.access.csv create mode 100644 addons_extensions/hr_ftp/views/hr_views.xml create mode 100644 addons_extensions/offer_letters/__init__.py create mode 100644 addons_extensions/offer_letters/__manifest__.py create mode 100644 addons_extensions/offer_letters/models/__init__.py create mode 100644 addons_extensions/offer_letters/models/offer_letter.py create mode 100644 addons_extensions/offer_letters/report/offer_letter_report.xml create mode 100644 addons_extensions/offer_letters/report/offer_letter_template.xml create mode 100644 addons_extensions/offer_letters/security/ir.model.access.csv create mode 100644 addons_extensions/offer_letters/static/src/js/pay_details_widget.js create mode 100644 addons_extensions/offer_letters/views/menu_views.xml create mode 100644 addons_extensions/offer_letters/views/offer_letter_views.xml diff --git a/addons_extensions/consolidated_batch_payslip/models/batch_payslip.py b/addons_extensions/consolidated_batch_payslip/models/batch_payslip.py index a7f7bfb63..8a1b1c7da 100644 --- a/addons_extensions/consolidated_batch_payslip/models/batch_payslip.py +++ b/addons_extensions/consolidated_batch_payslip/models/batch_payslip.py @@ -45,7 +45,7 @@ class HrPayslipRun(models.Model): 'attendance_days': attendance_days, 'leave_days': leave_days, 'lop_days': lop_days, - 'doj':employee.doj, + 'doj':contract.date_start, 'birthday':employee.birthday, 'bank': employee.bank_account_id.display_name if employee.bank_account_id else '-', 'sick_leave_balance': leave_balances.get('LEAVE110', 0), diff --git a/addons_extensions/hr_ftp/__init__.py b/addons_extensions/hr_ftp/__init__.py new file mode 100644 index 000000000..5305644df --- /dev/null +++ b/addons_extensions/hr_ftp/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models \ No newline at end of file diff --git a/addons_extensions/hr_ftp/__manifest__.py b/addons_extensions/hr_ftp/__manifest__.py new file mode 100644 index 000000000..bdf84ffbe --- /dev/null +++ b/addons_extensions/hr_ftp/__manifest__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Human Resources', + 'version': '1.0', + 'summary': 'Human Resources all', + 'description': ''' + Human Resources of the module + ''', + 'category': 'Human Resources', + 'author': 'Raman Marikanti', + 'depends': ['base', 'mail', + 'hr_employee_extended','hr_contract','hr_payroll', + 'hr_attendance_extended','hr_payroll_holidays', + 'hr_recruitment_extended'], + 'data': [ + 'security/ir.model.access.csv', + 'views/hr_views.xml', + ], + 'license': 'LGPL-3', + 'installable': True, + 'application': False, + 'auto_install': False, +} \ No newline at end of file diff --git a/addons_extensions/hr_ftp/models/__init__.py b/addons_extensions/hr_ftp/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/addons_extensions/hr_ftp/models/hr.py b/addons_extensions/hr_ftp/models/hr.py new file mode 100644 index 000000000..e69de29bb diff --git a/addons_extensions/hr_ftp/security/ir.model.access.csv b/addons_extensions/hr_ftp/security/ir.model.access.csv new file mode 100644 index 000000000..97dd8b917 --- /dev/null +++ b/addons_extensions/hr_ftp/security/ir.model.access.csv @@ -0,0 +1 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink diff --git a/addons_extensions/hr_ftp/views/hr_views.xml b/addons_extensions/hr_ftp/views/hr_views.xml new file mode 100644 index 000000000..dd4d48d87 --- /dev/null +++ b/addons_extensions/hr_ftp/views/hr_views.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons_extensions/hr_payroll/models/hr_payslip.py b/addons_extensions/hr_payroll/models/hr_payslip.py index f6c204451..7115616f3 100644 --- a/addons_extensions/hr_payroll/models/hr_payslip.py +++ b/addons_extensions/hr_payroll/models/hr_payslip.py @@ -1382,6 +1382,8 @@ class HrPayslip(models.Model): def days_count(self): joining_date = self.contract_id.date_start + if not joining_date or joining_date == self.date_from: + return 0 date_from = min(joining_date, self.date_from) if joining_date > date_from: diff --git a/addons_extensions/offer_letters/__init__.py b/addons_extensions/offer_letters/__init__.py new file mode 100644 index 000000000..9a7e03ede --- /dev/null +++ b/addons_extensions/offer_letters/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/addons_extensions/offer_letters/__manifest__.py b/addons_extensions/offer_letters/__manifest__.py new file mode 100644 index 000000000..4bd6155d8 --- /dev/null +++ b/addons_extensions/offer_letters/__manifest__.py @@ -0,0 +1,30 @@ +{ + 'name': 'Offer Letters', + 'version': '1.0.0', + 'summary': 'Generate and manage employee offer letters', + 'description': """ + This module allows HR to create, manage and send offer letters to candidates + with a modern React.js interface for enhanced user experience. + """, + 'author': 'Raman Marikanti', + 'category': 'Human Resources', + 'depends': ['base', 'hr_recruitment','hr_payroll','hr_ftp'], + 'data': [ + 'security/ir.model.access.csv', + 'views/offer_letter_views.xml', + # 'views/templates.xml', + 'views/menu_views.xml', + 'report/offer_letter_report.xml', + 'report/offer_letter_template.xml', + ], +'assets': { + 'web.assets_backend': [ + 'offer_letters/static/src/js/pay_details_widget.js', + ], +}, + + 'demo': [], + 'installable': True, + 'application': True, + 'license': 'LGPL-3', +} \ No newline at end of file diff --git a/addons_extensions/offer_letters/models/__init__.py b/addons_extensions/offer_letters/models/__init__.py new file mode 100644 index 000000000..9bba4c43b --- /dev/null +++ b/addons_extensions/offer_letters/models/__init__.py @@ -0,0 +1 @@ +from . import offer_letter \ No newline at end of file diff --git a/addons_extensions/offer_letters/models/offer_letter.py b/addons_extensions/offer_letters/models/offer_letter.py new file mode 100644 index 000000000..80df50ccb --- /dev/null +++ b/addons_extensions/offer_letters/models/offer_letter.py @@ -0,0 +1,225 @@ +from odoo import models, fields, api, _ +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 + +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 + ) + candidate_id = fields.Many2one( 'hr.applicant', string='Candidate', required=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([ + ('draft', 'Draft'), + ('sent', 'Sent'), + ('accepted', 'Accepted'), + ('rejected', 'Rejected'), + ('expired', 'Expired')], + string='Status', + default='draft', + 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') + + + @api.model + def _default_terms(self): + return """ +

1. This offer is contingent upon satisfactory reference checks.

+

2. You will be required to sign a confidentiality agreement.

+

3. The company reserves the right to modify job responsibilities.

+ """ + + @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') + return super(OfferLetter, self).create(vals) + + def action_send_offer(self): + self.ensure_one() + # template = self.env.ref('offer_letters.email_template_offer_letter') + self.write({'state': 'sent', 'sent_date': fields.Datetime.now()}) + # template.send_mail(self.id, force_send=True) + return True + + def action_accept_offer(self): + self.ensure_one() + # employee = self.env['hr.employee'].create({ + # 'name': self.candidate_id.partner_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): + self.ensure_one() + self.write({ + 'state': 'rejected', + 'response_date': fields.Datetime.now() + }) + return True + + @api.onchange('candidate_id') + def _onchange_candidate_id(self): + self.position = self.candidate_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'].new({ + 'date_from': first_day, + 'date_to': last_day, + }) + + contract = self.env['hr.contract'].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.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._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._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._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) \ No newline at end of file diff --git a/addons_extensions/offer_letters/report/offer_letter_report.xml b/addons_extensions/offer_letters/report/offer_letter_report.xml new file mode 100644 index 000000000..a66fd018a --- /dev/null +++ b/addons_extensions/offer_letters/report/offer_letter_report.xml @@ -0,0 +1,14 @@ + + + + + Offer Letter + offer.letter + qweb-pdf + offer_letters.report_offer_letter + offer_letters.report_offer_letter + 'Offer Letter - %s' % (object.name).replace('/', '') + + report + + \ No newline at end of file diff --git a/addons_extensions/offer_letters/report/offer_letter_template.xml b/addons_extensions/offer_letters/report/offer_letter_template.xml new file mode 100644 index 000000000..9e0208268 --- /dev/null +++ b/addons_extensions/offer_letters/report/offer_letter_template.xml @@ -0,0 +1,396 @@ + + + + + + diff --git a/addons_extensions/offer_letters/security/ir.model.access.csv b/addons_extensions/offer_letters/security/ir.model.access.csv new file mode 100644 index 000000000..8c7e29bdc --- /dev/null +++ b/addons_extensions/offer_letters/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_offer_letter_user,offer.letter.user,model_offer_letter,base.group_user,1,1,1,0 +access_offer_letter_manager,offer.letter.manager,model_offer_letter,hr.group_hr_manager,1,1,1,1 diff --git a/addons_extensions/offer_letters/static/src/js/pay_details_widget.js b/addons_extensions/offer_letters/static/src/js/pay_details_widget.js new file mode 100644 index 000000000..e32bfb55f --- /dev/null +++ b/addons_extensions/offer_letters/static/src/js/pay_details_widget.js @@ -0,0 +1,70 @@ +/** @odoo-module **/ + +import { Component, useState, xml } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { CharField, charField } from "@web/views/fields/char/char_field"; + +// Local utility function to parse the field value +function parseData(value) { + try { + return JSON.parse(value); + } catch (e) { + console.warn("Failed to parse value in PayDetailsWidget:", value); + return []; + } +} + +export class PayDetailsWidget extends CharField { + static props = { + ...CharField.props, + resModel: { type: String, optional: true }, + onlySearchable: { type: Boolean, optional: true }, + followRelations: { type: Boolean, optional: true }, + }; + + setup() { + this.state = useState({ + lines: parseData(this.props.record.data.terms_conditions) || [] + }); + } + + formatAmount(amount) { + return parseFloat(amount).toFixed(2); + } +} + +PayDetailsWidget.template = xml` +
+ + + + + + + + + + + + + + + + + + + + + + + +
SequenceCodeNameAmountYear Total
+
+`; + +export const PayDetailsWidgets = { + ...charField, + component: PayDetailsWidget, +}; + +registry.category("fields").add("pay_details_widget", PayDetailsWidgets); diff --git a/addons_extensions/offer_letters/views/menu_views.xml b/addons_extensions/offer_letters/views/menu_views.xml new file mode 100644 index 000000000..9e689b964 --- /dev/null +++ b/addons_extensions/offer_letters/views/menu_views.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/addons_extensions/offer_letters/views/offer_letter_views.xml b/addons_extensions/offer_letters/views/offer_letter_views.xml new file mode 100644 index 000000000..d958c8920 --- /dev/null +++ b/addons_extensions/offer_letters/views/offer_letter_views.xml @@ -0,0 +1,64 @@ + + + + offer.letter.list + offer.letter + + + + + + + + + + + + + offer.letter.form + offer.letter + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + Offer Letters + offer.letter + list,form + + +
\ No newline at end of file