Merge branch 'feature/odoo18'
This commit is contained in:
commit
2fd52bfb86
|
|
@ -41,6 +41,7 @@ package.json
|
|||
.husky
|
||||
|
||||
# various virtualenv
|
||||
/odoo-venv/
|
||||
/bin/
|
||||
/build/
|
||||
/dist/
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ class HrEmployee(models.Model):
|
|||
@api.model
|
||||
def get_user_employee_details(self):
|
||||
uid = request.session.uid
|
||||
self.env["hr.employee"].sudo().search([("user_id", "=", uid)])._compute_experience()
|
||||
"""To fetch the details of employee"""
|
||||
return self.env["hr.employee"].sudo().search_read(
|
||||
[("user_id", "=", uid)],
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@
|
|||
'data': [
|
||||
'security/security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/cron.xml',
|
||||
'data/mail.xml',
|
||||
'views/res_config_settings.xml',
|
||||
'views/hr_employee.xml',
|
||||
'views/bank_details.xml',
|
||||
'wizards/work_location_wizard.xml'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 & 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 & 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>
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
from . import res_config_settings
|
||||
from . import hr_employee
|
||||
from . import work_location_history
|
||||
from . import education_history
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class HrEmployeeBase(models.AbstractModel):
|
|||
@api.constrains('identification_id')
|
||||
def _check_identification_id(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.")
|
||||
|
||||
@api.depends('doj')
|
||||
|
|
@ -156,4 +156,67 @@ class HrEmployee(models.Model):
|
|||
passport_issued_location = fields.Char(string='Passport Issued Location')
|
||||
|
||||
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'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)])
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?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>
|
||||
</odoo>
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Payroll',
|
||||
'category': 'Human Resources/Payroll',
|
||||
'sequence': 290,
|
||||
'summary': 'Manage your employee payroll records',
|
||||
'installable': True,
|
||||
'application': True,
|
||||
|
|
@ -11,7 +9,7 @@
|
|||
# 'hr_work_entry_contract_enterprise',
|
||||
'mail',
|
||||
'web_editor',
|
||||
'hr_work_entry_contract',
|
||||
'hr_work_entry_contract','web',
|
||||
# 'hr_gantt'
|
||||
],
|
||||
'data': [
|
||||
|
|
@ -59,6 +57,7 @@
|
|||
'views/hr_payroll_menu.xml',
|
||||
# 'views/hr_work_entry_views.xml',
|
||||
'views/hr_work_entry_export_mixin_views.xml',
|
||||
'views/ftp_payslip.xml',
|
||||
'report/hr_contract_history_report_views.xml',
|
||||
'wizard/hr_payroll_payment_report_wizard.xml',
|
||||
],
|
||||
|
|
@ -82,6 +81,6 @@
|
|||
# 'hr_payroll/static/tests/**/*.js',
|
||||
],
|
||||
},
|
||||
'license': 'OEEL-1',
|
||||
'license': 'LGPL-3',
|
||||
'post_init_hook': '_post_init_hook',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,22 +69,4 @@ class HrEmployee(models.Model):
|
|||
|
||||
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'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,152 @@
|
|||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<template id="report_payslip_ftp">
|
||||
<t t-call="web.external_layout_boxed">
|
||||
<hr class="border-top" />
|
||||
|
||||
<div class="page" style="font-size: 12px;">
|
||||
<h2><span t-field="o.name"/></h2>
|
||||
<div class="employee-section-title" style="font-weight: bold; margin-top: 20px;">Employee Information</div>
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 10px;">
|
||||
<tr>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; font-weight: bold;">Employee Name:</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;" colspan="2"><t t-esc="o.employee_id.display_name"/></td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; font-weight: bold;">Employee ID:</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;" colspan="2"><t t-esc="o.employee_id.employee_id"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; font-weight: bold;">Job Position:</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;" colspan="2"><t t-esc="o.employee_id.job_id.display_name"/></td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; font-weight: bold;">Department:</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;" colspan="2"><t t-esc="o.employee_id.department_id.display_name"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; font-weight: bold;">Date Of Joining:</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;" colspan="2"><t t-esc="o.employee_id.doj"/></td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; font-weight: bold;">Date Of Birth:</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;" colspan="2"><t t-esc="o.employee_id.birthday"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; font-weight: bold;">Bank Account:</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;" colspan="2"><t t-esc="o.employee_id.bank_account_id.acc_number"/></td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; font-weight: bold;">PAN:</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;" colspan="2"><t t-esc="o.employee_id.l10n_in_pan"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; font-weight: bold;">UAN:</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;" colspan="2"><t t-esc="o.employee_id.l10n_in_uan"/></td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; font-weight: bold;">ESIC:</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;" colspan="2"><t t-esc="o.employee_id.l10n_in_esic_number"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="employee-section-title" style="font-weight: bold; margin-top: 20px;">Pay Period & Leave Summary</div>
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 10px;">
|
||||
<tr>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;">
|
||||
<strong>Pay Period:</strong> <t style="padding: 6px;" t-esc="o.date_from"/> - <t t-esc="o.date_to"/><br/>
|
||||
<t t-set="days" t-value="(o.date_to - o.date_from).days + 1"/>
|
||||
<strong>Number of Days:</strong> <t style="padding: 6px;" t-esc="days"/> Days<br/>
|
||||
<strong>Worked Days:</strong> <t style="padding: 6px;" t-esc="days"/> Days
|
||||
</td>
|
||||
<t t-set="timeoff_data_table" t-value="o._get_employee_timeoff_data()"/>
|
||||
<td t-if="timeoff_data_table" style="border: 1px solid #ccc; padding: 6px;">
|
||||
<div t-foreach="timeoff_data_table" t-as="timeoff_data">
|
||||
<strong t-out="timeoff_data[0] + ':'"/>
|
||||
<t t-out="timeoff_data[1].get('remaining_leaves')"/> /
|
||||
<t t-out="timeoff_data[1].get('max_leaves')"/>
|
||||
<t t-if="timeoff_data[1].get('request_unit') == 'hour'">Hours</t>
|
||||
<t t-else="">Days</t>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="employee-section-title" style="font-weight: bold; margin-top: 20px;">Salary Details</div>
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 12px;">
|
||||
<tr>
|
||||
<th style="border: 1px solid #ccc; padding: 6px; text-align: left;font-weight: bold;">Income</th>
|
||||
<th style="border: 1px solid #ccc; padding: 6px; text-align: right;font-weight: bold;">Amount</th>
|
||||
<th style="border: 1px solid #ccc; padding: 6px; text-align: left;font-weight: bold;">Contribution</th>
|
||||
<th style="border: 1px solid #ccc; padding: 6px; text-align: right;font-weight: bold;">Amount</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;">
|
||||
<t t-set="income" t-value="0"/>
|
||||
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.code in ['BASIC','HRA','LTA','SPA'])" t-as="l">
|
||||
<t t-esc="l.name"/><br/>
|
||||
</div>
|
||||
</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;">
|
||||
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.code in ['BASIC','HRA','LTA','SPA'])" t-as="l">
|
||||
<t t-esc="l.amount"/><br/>
|
||||
<t t-set="income" t-value="income + l.amount"/>
|
||||
</div>
|
||||
</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;">
|
||||
<t t-set="contribution" t-value="0"/>
|
||||
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.code in ['PFE','ESICF'])" t-as="l">
|
||||
<t t-esc="l.name"/><br/>
|
||||
</div>
|
||||
</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;">
|
||||
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.code in ['PFE','ESICF'])" t-as="l">
|
||||
<t t-esc="l.amount"/><br/>
|
||||
<t t-set="contribution" t-value="contribution + l.amount"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" style="border: 1px solid #ccc;"></td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;">
|
||||
<t t-set="ded" t-value="0"/>
|
||||
<strong>Cost To Company</strong><br/><br/>
|
||||
<strong>Deductions</strong><br/>
|
||||
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code == 'DED')" t-as="l">
|
||||
<t t-esc="l.name"/><br/>
|
||||
</div>
|
||||
</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;">
|
||||
<strong><t t-esc="contribution + income"/></strong><br/><br/>
|
||||
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code == 'DED')" t-as="l">
|
||||
<t t-esc="l.amount"/><br/>
|
||||
<t t-set="ded" t-value="ded + l.amount"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;"><strong>Gross Salary</strong></td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;"><strong><t t-esc="income"/></strong></td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;"><strong>Total Deduction</strong></td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;"><strong><t t-esc="ded"/></strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="net-salary" colspan="3" style="border: 1px solid #ccc; padding: 6px;"><strong>Net Salary:</strong></td>
|
||||
<td class="net-salary" colspan="1" style="border: 1px solid #ccc; padding: 6px; text-align: right;">
|
||||
<t t-esc="(contribution + income) + ded"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="to-pay" style="margin-top: 20px;">
|
||||
<p t-if="o.net_wage >= 0">
|
||||
To pay <strong><span t-esc="(contribution + income) + ded"/></strong> (<span style="padding-right: 5px;" t-esc="o.env.company.currency_id.amount_to_text((contribution + income) + ded) "/> only) to <i><span t-field="o.employee_id.legal_name"/></i> - <b><span t-field="o.employee_id.bank_account_id.bank_id.name"/> Account : <span t-field="o.employee_id.bank_account_id.acc_number">XXXXXXXXXXXX</span></b>
|
||||
|
||||
|
||||
</p>
|
||||
<p style="padding: 8px;"><strong>NOTE:</strong> This is computer generated salary slip. Signature is not required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</template>
|
||||
<template id="report_payslip_ftps">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<t t-set="o" t-value="o.with_context(lang=o.employee_id.lang or o.env.lang)"/>
|
||||
<t t-set="company" t-value="o.env.company"/>
|
||||
<t t-call="hr_payroll.report_payslip_ftp" t-lang="o.env.lang"/>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -13,6 +13,19 @@
|
|||
<field name="attachment"></field>
|
||||
<field name="attachment_use" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="action_report_payslip_ftp" model="ir.actions.report">
|
||||
<field name="name">Payslip Ftp</field>
|
||||
<field name="model">hr.payslip</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">hr_payroll.report_payslip_ftps</field>
|
||||
<field name="report_file">hr_payroll.report_payslip_ftps</field>
|
||||
<field name="print_report_name">'Payslip - %s' % (object.name)</field>
|
||||
<field name="binding_model_id" ref="model_hr_payslip"/>
|
||||
<field name="binding_type">report</field>
|
||||
<field name="attachment"></field>
|
||||
<field name="attachment_use" eval="False"/>
|
||||
</record>
|
||||
<record id="action_report_light_payslip" model="ir.actions.report">
|
||||
<field name="name">Payslip (Light)</field>
|
||||
<field name="model">hr.payslip</field>
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import attendance_report
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
from odoo import models, fields, api
|
||||
from datetime import timedelta
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from pytz import timezone, UTC
|
||||
|
||||
|
||||
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)
|
||||
|
||||
user_tz = self.env.user.tz or 'UTC'
|
||||
tz = timezone(user_tz)
|
||||
|
||||
# Search for attendance in UTC (Odoo stores in UTC)
|
||||
attendances = self.env['hr.attendance'].search([
|
||||
('check_in', '>=', str(last_monday)),
|
||||
('check_out', '<=', str(last_sunday + timedelta(days=1)))
|
||||
])
|
||||
|
||||
employee_data = defaultdict(list)
|
||||
grouped_attendance = defaultdict(lambda: defaultdict(list)) # {emp: {date: [attendances]}}
|
||||
|
||||
# Group attendances by employee and local date
|
||||
for att in attendances:
|
||||
emp = att.employee_id.name
|
||||
check_in_local = att.check_in.astimezone(tz)
|
||||
date_local = check_in_local.date()
|
||||
grouped_attendance[emp][date_local].append(att)
|
||||
|
||||
# Process each employee's attendance
|
||||
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.astimezone(tz).strftime('%H:%M')
|
||||
last_out = 'N/A'
|
||||
|
||||
for rec in records:
|
||||
if rec.check_in and rec.check_out:
|
||||
check_in_local = rec.check_in.astimezone(tz)
|
||||
check_out_local = rec.check_out.astimezone(tz)
|
||||
total_seconds += (check_out_local - check_in_local).total_seconds()
|
||||
last_out = check_out_local.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()
|
||||
|
|
@ -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
|
||||
|
|
|
@ -79,8 +79,6 @@ class BiometricDeviceDetails(models.Model):
|
|||
device_password = fields.Integer(string='Password',
|
||||
help='Enter the device password')
|
||||
|
||||
|
||||
|
||||
def device_connect(self, zk):
|
||||
"""Function for connecting the device with Odoo"""
|
||||
try:
|
||||
|
|
@ -172,39 +170,40 @@ class BiometricDeviceDetails(models.Model):
|
|||
if conn:
|
||||
conn.disable_device()
|
||||
self.get_all_users()
|
||||
# self.action_set_timezone()
|
||||
user = conn.get_users()
|
||||
# fingers = conn.get_templates()
|
||||
# for use in user:
|
||||
# for finger in fingers:
|
||||
# if finger.uid == use.uid:
|
||||
# templates = conn.get_user_template(uid=use.uid,
|
||||
# temp_id=finger.fid,
|
||||
# user_id=use.user_id)
|
||||
# hex_data = templates.template.hex()
|
||||
# # Convert hex data to binary
|
||||
# binary_data = binascii.unhexlify(hex_data)
|
||||
# base64_data = base64.b64encode(binary_data).decode(
|
||||
# 'utf-8')
|
||||
# employee = self.env['hr.employee'].search(
|
||||
# [('device_id_num', '=', use.user_id),('company_id', '=', self.env.company.id)],limit=1)
|
||||
# employee.device_ids |= self
|
||||
# if str(finger.fid) in employee.fingerprint_ids.mapped(
|
||||
# 'finger_id'):
|
||||
# employee.fingerprint_ids.search(
|
||||
# [('finger_id', '=', finger.fid)]).update({
|
||||
# 'finger_template': base64_data,
|
||||
# })
|
||||
# else:
|
||||
# employee.fingerprint_ids.create({
|
||||
# 'finger_template': base64_data,
|
||||
# 'finger_id': finger.fid,
|
||||
# 'employee_bio_id': employee.id,
|
||||
# 'filename': f'{employee.name}-finger-{finger.fid}'
|
||||
# })
|
||||
# get All Fingerprints
|
||||
fingers = conn.get_templates()
|
||||
for use in user:
|
||||
for finger in fingers:
|
||||
if finger.uid == use.uid:
|
||||
templates = conn.get_user_template(uid=use.uid,
|
||||
temp_id=finger.fid,
|
||||
user_id=use.user_id)
|
||||
hex_data = templates.template.hex()
|
||||
# Convert hex data to binary
|
||||
binary_data = binascii.unhexlify(hex_data)
|
||||
base64_data = base64.b64encode(binary_data).decode(
|
||||
'utf-8')
|
||||
employee = self.env['hr.employee'].search(
|
||||
[('device_id_num', '=', use.user_id), ('company_id', '=', self.env.company.id)],
|
||||
limit=1)
|
||||
employee.device_ids |= self
|
||||
if str(finger.fid) in employee.fingerprint_ids.mapped(
|
||||
'finger_id'):
|
||||
employee.fingerprint_ids.search(
|
||||
[('finger_id', '=', finger.fid)]).update({
|
||||
'finger_template': base64_data,
|
||||
})
|
||||
else:
|
||||
employee.fingerprint_ids.create({
|
||||
'finger_template': base64_data,
|
||||
'finger_id': finger.fid,
|
||||
'employee_bio_id': employee.id,
|
||||
'filename': f'{employee.name}-finger-{finger.fid}'
|
||||
})
|
||||
# get all attendances
|
||||
print(help(zk.get_attendance))
|
||||
attendance = conn.get_attendance()
|
||||
print(attendance)
|
||||
if attendance:
|
||||
filtered_attendance = []
|
||||
|
||||
|
|
@ -240,50 +239,54 @@ class BiometricDeviceDetails(models.Model):
|
|||
|
||||
for uid in user:
|
||||
if uid.user_id == each.user_id:
|
||||
employee = self.env['hr.employee'].search([
|
||||
('device_id_num', '=', each.user_id),
|
||||
('company_id', '=', self.env.company.id)
|
||||
], limit=1)
|
||||
get_user_id = self.env['hr.employee'].search(
|
||||
[('device_id_num', '=', each.user_id), ('company_id', '=', self.env.company.id)],
|
||||
limit=1)
|
||||
check_in_today = hr_attendance.search([(
|
||||
'employee_id', '=', get_user_id.id),
|
||||
('check_in', '!=', False), ('check_out', '=', False)])
|
||||
from datetime import timedelta
|
||||
|
||||
if not employee:
|
||||
continue
|
||||
# Define the tolerance (10 minutes)
|
||||
tolerance = timedelta(minutes=10)
|
||||
|
||||
# Convert atten_time to datetime
|
||||
# Convert the atten_time string to a datetime object
|
||||
|
||||
# Calculate the lower and upper bounds with the tolerance
|
||||
atten_time_obj = datetime.datetime.strptime(atten_time, "%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Find existing attendance for today
|
||||
start_of_day = atten_time_obj.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_of_day = atten_time_obj.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
# Calculate the lower and upper bounds with the tolerance
|
||||
lower_bound = atten_time_obj - tolerance
|
||||
upper_bound = atten_time_obj + tolerance
|
||||
|
||||
attendance_today = self.env['hr.attendance'].search([
|
||||
('employee_id', '=', employee.id),
|
||||
('check_in', '>=', start_of_day),
|
||||
('check_in', '<=', end_of_day)
|
||||
], limit=1)
|
||||
# Ensure the 'check_in' and 'check_out' fields are datetime objects and compare them
|
||||
next_in = hr_attendance.search([
|
||||
('employee_id', '=', get_user_id.id),
|
||||
('check_in', '>=', lower_bound),
|
||||
('check_in', '<=', upper_bound)
|
||||
])
|
||||
|
||||
# IN logic
|
||||
if self.display_name == 'IN':
|
||||
if not attendance_today:
|
||||
# No attendance yet, create new with check_in only
|
||||
self.env['hr.attendance'].create({
|
||||
'employee_id': employee.id,
|
||||
'check_in': atten_time_obj,
|
||||
next_out = hr_attendance.search([
|
||||
('employee_id', '=', get_user_id.id),
|
||||
('check_out', '>=', lower_bound),
|
||||
('check_out', '<=', upper_bound)
|
||||
])
|
||||
if get_user_id:
|
||||
if self.display_name == 'IN' and not check_in_today:
|
||||
if next_in:
|
||||
continue
|
||||
hr_attendance.create({
|
||||
'employee_id': get_user_id.id,
|
||||
'check_in': atten_time,
|
||||
})
|
||||
employee.attendance_state = 'checked_in'
|
||||
else:
|
||||
attendance_today.check_out = False
|
||||
employee.attendance_state = 'checked_in'
|
||||
continue
|
||||
|
||||
# OUT logic
|
||||
elif self.display_name != 'IN':
|
||||
if attendance_today:
|
||||
# Only update checkout if it's not set or is earlier than current atten_time
|
||||
if not attendance_today.check_out or attendance_today.check_out < atten_time_obj:
|
||||
attendance_today.write({
|
||||
'check_out': atten_time_obj,
|
||||
})
|
||||
employee.attendance_state = 'checked_out'
|
||||
get_user_id.attendance_state = 'checked_in'
|
||||
elif check_in_today and self.display_name != 'IN':
|
||||
if fields.Datetime.to_string(check_in_today.check_in) > atten_time or next_out:
|
||||
continue
|
||||
check_in_today.write({
|
||||
'check_out': atten_time,
|
||||
})
|
||||
get_user_id.attendance_state = 'checked_out'
|
||||
|
||||
else:
|
||||
pass
|
||||
|
|
@ -293,7 +296,7 @@ class BiometricDeviceDetails(models.Model):
|
|||
('company_id', '=', self.env.company.id)])
|
||||
if not duplicate_atten_ids:
|
||||
zk_attendance.create({
|
||||
'employee_id': employee.id,
|
||||
'employee_id': get_user_id.id,
|
||||
'device_id_num': each.user_id,
|
||||
'attendance_type': str(1),
|
||||
'punch_type': '0' if self.display_name == 'IN' else '1',
|
||||
|
|
@ -478,13 +481,12 @@ class BiometricDeviceDetails(models.Model):
|
|||
if conn:
|
||||
try:
|
||||
admin_user_id = 9 # You can implement a method for this
|
||||
conn.unlock(admin_user_id) # Unlock using the admin user ID
|
||||
conn.unlock(admin_user_id) # Unlock using the admin user ID
|
||||
except Exception as e:
|
||||
raise UserError(_("Failed to unlock door: %s") % str(e))
|
||||
else:
|
||||
raise UserError(_("Unable to establish a connection to the device."))
|
||||
|
||||
|
||||
def get_all_users(self):
|
||||
"""Function to get all user's details"""
|
||||
for info in self:
|
||||
|
|
@ -504,7 +506,7 @@ class BiometricDeviceDetails(models.Model):
|
|||
users = conn.get_users()
|
||||
for user in users:
|
||||
employee = self.env['hr.employee'].search(
|
||||
[('device_id_num', '=', user.user_id) ])
|
||||
[('device_id_num', '=', user.user_id)])
|
||||
if employee:
|
||||
pass
|
||||
# employee.write({
|
||||
|
|
@ -563,7 +565,7 @@ class BiometricDeviceDetails(models.Model):
|
|||
"group_id: %s\n"
|
||||
"user_id: %s\n"
|
||||
"Here is the debugging information:\n%s\n"
|
||||
"Try Reqing the device")
|
||||
"Try Restarting the device")
|
||||
% (candidate_uid, employee.name, privilege, password,
|
||||
group_id, str(candidate_uid), e))
|
||||
conn.enable_device()
|
||||
|
|
@ -697,7 +699,7 @@ class ZKBioAttendance(Thread):
|
|||
live attendance data.
|
||||
"""
|
||||
|
||||
def __init__(self, machine_ip, port_no,password, record):
|
||||
def __init__(self, machine_ip, port_no, password, record):
|
||||
"""Function to Initialize the thread"""
|
||||
Thread.__init__(self)
|
||||
self.machine_ip = machine_ip
|
||||
|
|
|
|||
Loading…
Reference in New Issue