Merge branch 'develop'

This commit is contained in:
administrator 2025-06-02 15:22:50 +05:30
commit 24c6b24790
13 changed files with 347 additions and 20 deletions

View File

@ -18,12 +18,19 @@
'version': '0.1', 'version': '0.1',
# any module necessary for this one to work correctly # any module necessary for this one to work correctly
'depends': ['base','hr','account','mail','hr_skills', 'hr_contract'], 'depends': ['base','hr','account','mail','hr_skills', 'hr_contract'],
# always loaded # always loaded
'data': [ 'data': [
'security/security.xml', 'security/security.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'data/cron.xml',
'data/mail.xml',
'views/res_config_settings.xml',
'views/hr_employee.xml', 'views/hr_employee.xml',
'views/bank_details.xml', 'views/bank_details.xml',
'wizards/work_location_wizard.xml' 'wizards/work_location_wizard.xml'

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data noupdate="1">
<record model="ir.cron" id="hr_employee_event_remainder_trigger">
<field name="name">Employee: Event Remainder</field>
<field name="model_id" ref="hr.model_hr_employee"/>
<field name="state">code</field>
<field name="code">model.hr_employee_event_remainder()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="nextcall" eval="(DateTime.now().replace(hour=9, minute=15, second=0, microsecond=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<record id="hr_employee_event_reminder_template" model="mail.template">
<field name="name">Employee Birthday &amp; Anniversary Reminder</field>
<field name="model_id" ref="hr.model_hr_employee"/>
<field name="email_from">{{ user.email_formatted }}</field>
<field name="email_to">hr@ftprotech.com</field> <!-- Replace with real HR email -->
<field name="subject">Upcoming Employee Events: Birthdays &amp; Anniversaries</field>
<field name="description">Notify HR of employees with upcoming birthdays or anniversaries.</field>
<field name="body_html" type="html">
<t t-set="birthday_list" t-value="ctx.get('birthday_list')"/>
<t t-set="anniversary_list" t-value="ctx.get('anniversary_list')"/>
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Dear HR Team,
<br/>
<br/>
Here are the upcoming Employee Events:
<br/>
<br/>
<t t-if="birthday_list">
<strong>🎂 Birthdays:</strong>
<br/>
<ul>
<t t-foreach="birthday_list" t-as="emp">
<li>
<t t-esc="emp['name']"/>
<t t-esc="emp['birthday']"/>
</li>
</t>
</ul>
</t>
<t t-if="anniversary_list">
<strong>💍 Marriage Anniversaries:</strong><br/>
<ul>
<t t-foreach="anniversary_list" t-as="emp">
<li>
<t t-esc="emp['name']"/>
<t t-esc="emp['marriage_anniversary_date']"/>
</li>
</t>
</ul>
</t>
<br/>
Regards,
<br/>
<t t-out="user.name or 'Odoo Reminder Bot'"/>
</p>
</field>
</record>
</data>
</odoo>

View File

@ -1,3 +1,4 @@
from . import res_config_settings
from . import hr_employee from . import hr_employee
from . import work_location_history from . import work_location_history
from . import education_history from . import education_history

View File

@ -38,7 +38,7 @@ class HrEmployeeBase(models.AbstractModel):
@api.constrains('identification_id') @api.constrains('identification_id')
def _check_identification_id(self): def _check_identification_id(self):
for record in self: for record in self:
if not record.identification_id.isdigit(): if record.identification_id and not record.identification_id.isdigit():
raise ValidationError("Identification ID should only contain numbers.") raise ValidationError("Identification ID should only contain numbers.")
@api.depends('doj') @api.depends('doj')
@ -157,3 +157,66 @@ class HrEmployee(models.Model):
previous_company_pf_no = fields.Char(string='Previous Company PF No') previous_company_pf_no = fields.Char(string='Previous Company PF No')
previous_company_uan_no = fields.Char(string='Previous Company UAN No') previous_company_uan_no = fields.Char(string='Previous Company UAN No')
def hr_employee_event_remainder(self):
self.send_birthday_reminders()
today = fields.Date.today()
tomorrow = today + timedelta(days=1)
day_after = today + timedelta(days=2)
tomorrow_md = tomorrow.strftime('-%m-%d')
day_after_md = day_after.strftime('-%m-%d')
if today.weekday() == 4: # Friday
birthday_domain = ['|', ('birthday', 'like', f'%{tomorrow_md}'), ('birthday', 'like', f'%{day_after_md}')]
anniversary_domain = [('marital', '=', 'married'), '|',
('marriage_anniversary_date','like', f'%{tomorrow_md}'),
('marriage_anniversary_date', 'like', f'%{day_after_md}')]
else:
birthday_domain = [('birthday','like', f'%{tomorrow_md}')]
anniversary_domain = [('marital', '=', 'married'),
('marriage_anniversary_date','like', f'%{tomorrow_md}')]
birthday_emps = self.search(birthday_domain)
anniversary_emps = self.search(anniversary_domain)
birthday_list = [{'name': emp.name, 'birthday': emp.birthday} for emp in birthday_emps]
anniversary_list = [{'name': emp.name, 'marriage_anniversary_date': emp.marriage_anniversary_date} for emp in anniversary_emps]
context = {
'birthday_list': birthday_list,
'anniversary_list': anniversary_list,
}
email_values = {'auto_delete': True}
template = self.env.ref('hr_employee_extended.hr_employee_event_reminder_template')
if template and (birthday_list or anniversary_list):
hr_id = self.env['ir.config_parameter'].sudo().get_param('hr_employee_extended.emp_hr_id')
if hr_id:
emp_hr = self.env['res.users'].sudo().browse(int(hr_id)) if hr_id else False
email_values['email_to'] = emp_hr.email
else:
emp_hr = self.env['res.users'].sudo().search([('groups_id', 'in', self.env.ref('hr.group_hr_manager').id)],limit=1)
# Use `self[0]` as dummy record for sending
template.with_context(**context).send_mail(
emp_hr.employee_id.id, force_send=True, email_values=email_values,
)
@api.model
def send_birthday_reminders(self):
today = datetime.today().strftime('%m-%d')
employees = self.search([('birthday', '!=', False)])
birthday_emps = employees.filtered(
lambda emp: emp.birthday.strftime('%m-%d') == today
)
if birthday_emps:
channel = self.env['discuss.channel'].search([('name', 'ilike', 'General')], limit=1)
if channel:
for emp in birthday_emps:
message = f"🎉 Happy Birthday {emp.name}! 🎂 Wishing you a fantastic day! 🥳"
channel.message_post(
body=message,
message_type='comment',
subtype_xmlid='mail.mt_comment'
)

View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api, _
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
emp_hr_id = fields.Many2one('res.users',config_parameter='hr_employee_extended.emp_hr_id', string='Employee HR',
domain=lambda self: [
('groups_id', 'in', self.env.ref('hr.group_hr_manager').id)])

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.employee</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="75"/>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//block[@name='employees_setting_container']" position="after">
<block title="Employee Management" name="employee_rights_setting_container">
<setting string="HR Employee Access"
help="Select the HR responsible for employees"
id="employee_hr_access_control">
<field name="emp_hr_id"
options="{'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
</setting>
</block>
</xpath>
</field>
</record>
<record id="action_hr_employee_appraisal_configuration" model="ir.actions.act_window">
<field name="name">Settings</field>
<field name="res_model">res.config.settings</field>
<field name="view_mode">form</field>
<field name="target">inline</field>
<field name="context">{'module' : 'hr_appraisal', 'bin_size': False}</field>
</record>
<menuitem
id="menu_hr_appraisal_global_settings"
name="Settings"
parent="hr_employee_appraisal.menu_configurations"
sequence="0"
action="hr_employee_appraisal.action_hr_employee_appraisal_configuration"
groups="base.group_system"/>
</odoo>

View File

@ -69,22 +69,4 @@ class HrEmployee(models.Model):
return self.env.cr.dictfetchall() return self.env.cr.dictfetchall()
@api.model
def send_birthday_reminders(self):
today = datetime.today().strftime('%m-%d')
employees = self.search([('birthday', '!=', False)])
birthday_emps = employees.filtered(
lambda emp: emp.birthday.strftime('%m-%d') == today
)
if birthday_emps:
channel = self.env['discuss.channel'].search([('name', 'ilike', 'General')], limit=1)
if channel:
for emp in birthday_emps:
message = f"🎉 Happy Birthday {emp.name}! 🎂 Wishing you a fantastic day! 🥳"
channel.message_post(
body=message,
message_type='comment',
subtype_xmlid='mail.mt_comment'
)

View File

@ -0,0 +1 @@
from . import models

View File

@ -0,0 +1,11 @@
{
'name': 'Weekly Attendance Email Report',
'version': '1.0',
'category': 'Human Resources',
'depends': ['hr_attendance', 'mail'],
'data': [
'security/ir.model.access.csv',
],
'installable': True,
'application': False,
}

View File

@ -0,0 +1 @@
from . import attendance_report

View File

@ -0,0 +1,131 @@
from odoo import models, fields, api
from datetime import timedelta
import json
from collections import defaultdict
from datetime import datetime
class AttendanceWeeklyReport(models.Model):
_name = 'attendance.weekly.report'
_description = 'Weekly Attendance Report'
def send_weekly_attendance_email(self):
management_emails = self.env['hr.employee'].search([
('department_id.name', '=', 'Administration'),
('work_email', '!=', False)
]).mapped('work_email')
if not management_emails:
return
today = fields.Date.context_today(self)
last_monday = today - timedelta(days=today.weekday() + 7)
last_sunday = last_monday + timedelta(days=6)
attendances = self.env['hr.attendance'].search([
('check_in', '>=', str(last_monday)),
('check_out', '<=', str(last_sunday + timedelta(days=1)))
])
employee_data = {}
employee_data = defaultdict(list)
grouped_attendance = defaultdict(lambda: defaultdict(list)) # {emp: {date: [attendances]}}
for att in attendances:
emp = att.employee_id.name
date = att.check_in.date()
grouped_attendance[emp][date].append(att)
for emp_name, dates in grouped_attendance.items():
for date, records in dates.items():
records = sorted(records, key=lambda a: a.check_in)
total_seconds = 0
first_in = records[0].check_in.time().strftime('%H:%M')
last_out = 'N/A'
for rec in records:
if rec.check_in and rec.check_out:
total_seconds += (rec.check_out - rec.check_in).total_seconds()
last_out = rec.check_out.time().strftime('%H:%M')
employee_data[emp_name].append({
'date': date.strftime('%Y-%m-%d'),
'in': first_in,
'out': last_out,
'hours': f'{total_seconds / 3600:.2f}',
})
# Inline QWeb-compatible HTML template (must be in a real view in production)
html_template = """
<div style="max-width:800px;margin:auto;background-color:#fff;padding:20px;border:1px solid #ddd;border-radius:8px;">
<p>Dear Management,</p>
<p>
Please find below the attendance summary for the period
<strong>%(from_date)s</strong> to <strong>%(to_date)s</strong>.
</p>
%(employee_tables)s
<p style="margin-top: 24px;">Regards,<br/><strong>Odoo HR System</strong></p>
<a href="https://ftprotech.in/odoo/attendances" target="_blank" style="color:#1e88e5;text-decoration:none;">For more details, visit ftprotech.in</a>
</div>
"""
employee_tables_html = ""
if employee_data:
for emp_name, records in employee_data.items():
rows = ""
total = 0
for line in records:
total += float(line['hours']) if line['hours'] != False else 0
rows += f"""
<tr style="background-color:#fafafa;">
<td style="border:1px solid #ddd;padding:8px;">{line['date']}</td>
<td style="border:1px solid #ddd;padding:8px;">{line['in']}</td>
<td style="border:1px solid #ddd;padding:8px;">{line['out']}</td>
<td style="border:1px solid #ddd;padding:8px;">{line['hours']}</td>
</tr>
"""
t =f"""
<tr>
<td style="text-align:left;font-size: 14px;">
<strong>Total worked Hours {total:.2f}</strong>
</td>
</tr>
"""
table = f"""
<div style="margin-bottom: 30px;">
<h3 style="margin-bottom:10px;color:#2c3e50;font-size:16px;">{emp_name}</h3>
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr>
<th style="border:1px solid #ccc;padding:8px;background-color:#e0e0e0;text-align:left;">Date</th>
<th style="border:1px solid #ccc;padding:8px;background-color:#e0e0e0;text-align:left;">Check In</th>
<th style="border:1px solid #ccc;padding:8px;background-color:#e0e0e0;text-align:left;">Check Out</th>
<th style="border:1px solid #ccc;padding:8px;background-color:#e0e0e0;text-align:left;">Hours Worked</th>
</tr>
</thead>
<tbody>{rows}{t}
</tbody>
</table>
</div>
"""
employee_tables_html += table
else:
employee_tables_html = "<p>No attendance data available for this period.</p>"
# Final HTML body
body_html = html_template % {
'from_date': last_monday.strftime('%Y-%m-%d'),
'to_date': last_sunday.strftime('%Y-%m-%d'),
'employee_tables': employee_tables_html
}
# Send email to all management emails
for email in management_emails:
self.env['mail.mail'].create({
'email_to': email,
'subject': f"Weekly Attendance Report: {last_monday} to {last_sunday}",
'body_html': body_html,
}).send()

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id,perm_read,perm_write,perm_create,perm_unlink
access_attendance_weekly_report,access_attendance_weekly_report,model_attendance_weekly_report,,1,1,1,1
1 id name model_id:id group_id perm_read perm_write perm_create perm_unlink
2 access_attendance_weekly_report access_attendance_weekly_report model_attendance_weekly_report 1 1 1 1