Compare commits

..

52 Commits

Author SHA1 Message Date
raman b479fee7c9 update changes 2025-08-18 10:39:18 +05:30
raman 672551cd0a Recruitment Web UI Changes 2025-08-08 09:53:52 +05:30
raman 0b93dd1a87 ats CSS 2025-08-07 14:53:44 +05:30
Pranay e88f7da488 recruitment web first commit 2025-08-07 12:43:43 +05:30
administrator 79d14a2023 Initial commit 2025-07-29 17:57:00 +05:30
administrator 0193fe04fb Initial commit 2025-07-29 17:57:00 +05:30
administrator b10e096540 Initial commit 2025-07-29 17:56:59 +05:30
administrator a8df849cc3 Initial commit 2025-07-29 17:56:59 +05:30
administrator d154a96b2b Initial commit 2025-07-29 17:56:59 +05:30
administrator 8c6b7669a3 Initial commit 2025-07-29 17:56:59 +05:30
administrator d56eaaa306 Initial commit 2025-07-29 17:56:59 +05:30
administrator 8c4e5d82da Initial commit 2025-07-29 17:56:59 +05:30
administrator 6d9933c2a1 Initial commit 2025-07-29 17:56:59 +05:30
administrator 2084217fbc Initial commit 2025-07-29 17:56:59 +05:30
administrator 2aef56be49 Initial commit 2025-07-29 17:56:59 +05:30
administrator 4194fbfecf Initial commit 2025-07-29 17:56:59 +05:30
administrator 95a50af0f3 Initial commit 2025-07-29 17:56:59 +05:30
administrator a64a2f8016 Initial commit 2025-07-29 17:56:59 +05:30
administrator 4f4b7e0c36 Initial commit 2025-07-29 17:56:59 +05:30
administrator 6b97411909 Initial commit 2025-07-29 17:56:59 +05:30
administrator 2896e9e83b Initial commit 2025-07-29 17:56:59 +05:30
administrator 785a7da404 Initial commit 2025-07-29 17:56:59 +05:30
administrator c21f46bf33 Initial commit 2025-07-29 17:56:59 +05:30
administrator d4f31b5af5 Initial commit 2025-07-29 17:56:59 +05:30
administrator 1e9485bb5a Initial commit 2025-07-29 17:56:59 +05:30
administrator 541272a7c1 pull commit 2025-07-29 17:56:59 +05:30
administrator 5b0d3f4a3e Initial commit 2025-07-29 17:56:59 +05:30
Pranay b195382233 TimeOff Fix 2025-07-29 17:56:59 +05:30
Pranay 709e17ad7b time-off FIX 2025-07-29 17:56:59 +05:30
Pranay b9c7d43541 Recruitment Changes 2025-07-29 17:56:58 +05:30
Pranay 3207b47f72 fix whatsapp 2025-07-29 17:56:58 +05:30
Pranay 5fdb26226c update whatsapp code 2025-07-29 17:56:58 +05:30
administrator 640ddcdaa5 Initial commit 2025-07-29 17:56:58 +05:30
administrator 6053b90162 Initial commit 2025-07-29 17:56:58 +05:30
administrator 54fafbb1e9 Initial commit 2025-07-29 17:56:58 +05:30
administrator f9985e9c58 Initial commit 2025-07-29 17:56:58 +05:30
administrator c5c89709e4 Initial commit 2025-07-29 17:56:58 +05:30
administrator 871ad34f7a Initial commit 2025-07-29 17:56:58 +05:30
administrator 7188c475d5 Initial commit 2025-07-29 17:56:58 +05:30
administrator e650a1f1bf Initial commit 2025-07-29 17:56:58 +05:30
administrator d9f2183c2b Initial commit 2025-07-29 17:56:58 +05:30
administrator 19f54e5ba0 Initial commit 2025-07-29 17:56:58 +05:30
administrator 3e6eb7799b Initial commit 2025-07-29 17:56:58 +05:30
administrator 4a2026faae Initial commit 2025-07-29 17:56:58 +05:30
administrator 5cdbdd75b0 Initial commit 2025-07-29 17:56:58 +05:30
administrator f7c05c7b2e Initial commit 2025-07-29 17:56:58 +05:30
administrator 6273e2fa96 Initial commit 2025-07-29 17:56:58 +05:30
administrator 7600729b8b Initial commit 2025-07-29 17:56:58 +05:30
administrator 25aa25c6f5 Initial commit 2025-07-29 17:56:58 +05:30
administrator a0961f338b Initial commit 2025-07-29 17:56:58 +05:30
administrator 196f7fb371 Initial commit 2025-07-29 17:56:56 +05:30
administrator e7a8762880 Initial commit 2025-07-29 17:56:56 +05:30
675 changed files with 23557 additions and 41247 deletions

View File

@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import api, SUPERUSER_ID
from . import models
from . import report
from . import wizard
# TODO: Generate Sequence For each company for Equipment
def pre_init_hook(env):
company_ids = env['res.company'].search([])
for company_id in company_ids:
sequence_id = env['ir.sequence'].search(
[('name', '=', 'Equipment Company Sequence'), ('company_id', '=', company_id.id)])
if not sequence_id:
env['ir.sequence'].create({
'name': 'Equipment Company Sequence',
'prefix': company_id.id,
'padding': 5,
'number_increment': 1,
'company_id': company_id.id
})

View File

@ -1,25 +0,0 @@
{
"name": "QR Code on Equipment",
'category': '',
"summary": "Add QR Code on equipment .",
'license': 'LGPL-3',
"price": 00.00,
'description': """
The Equipment Management Module generates unique QR codes for each asset, offering instant details and direct Odoo profile access for seamless management.
""",
"author": "Raman Marikanti",
"depends": ['account','maintenance'],
"external_dependencies": {
'python': ['qrcode']
},
"data": [
'views/maintenance_equipment.xml',
'security/ir.model.access.csv',
'report/custom_qrcode.xml',
'wizard/equipment_label_layout_views.xml',
],
'pre_init_hook': 'pre_init_hook',
"application": True,
"installable": True,
}

View File

@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
from . import maintenance_equipment
from . import res_company

View File

@ -1,28 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class MaintenanceEquipment(models.Model):
_inherit = 'maintenance.equipment'
qr_code = fields.Binary("QR Code")
comp_serial_no = fields.Char("Inventory Serial No", tracking=True)
serial_no = fields.Char('Mfg. Serial Number', copy=False)
def action_print_qrcode_layout(self):
action = self.env['ir.actions.act_window']._for_xml_id('aspl_equipment_qrcode_generator.action_open_label_layout_equipment')
action['context'] = {'default_equipment_ids': self.ids}
return action
def generate_serial_no(self):
for equipment_id in self:
if not equipment_id.comp_serial_no:
company_id = equipment_id.company_id.id
sequence_id = self.env['ir.sequence'].search(
[('name', '=', 'Equipment Company Sequence'), ('company_id', '=', company_id)])
if sequence_id:
data = sequence_id._next()
equipment_id.write({
'comp_serial_no': data
})

View File

@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models, api
class ResCompany(models.Model):
_inherit = 'res.company'
@api.model_create_multi
def create(self, vals):
result = super(ResCompany, self).create(vals)
sequence_id = self.env['ir.sequence'].search(
[('name', '=', 'Equipment Company Sequence'), ('company_id', '=', result.id)])
if not sequence_id:
self.env['ir.sequence'].create({
'name': 'Equipment Company Sequence',
'prefix': result.id,
'padding': 5,
'number_increment': 1,
'company_id': result.id
})
return result

View File

@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import custom_qrcode_generator

View File

@ -1,188 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="report_equipment_simple_label2x7">
<t t-set="qrcode_size" t-value="'height:14mm'"/>
<t t-set="table_style" t-value="'width:97mm;height:37.1mm;margin:inherit;' + table_style"/>
<td t-att-style="make_invisible and 'visibility:hidden;'" >
<div class="o_label_full" t-att-style="table_style">
<div class="o_label_name">
<strong t-field="equipment.name"/>
</div>
<div class="o_label_data">
<div class="text-center o_label_right_column">
<t t-if="equipment.qr_code">
<div t-field="equipment.qr_code" t-options="{'widget': 'image'}" style="width: 130px;margin-top: -40px;"/>
</t>
</div>
<div class="text-left" style="line-height:normal;word-wrap: break-word;">
<span class="text-nowrap" t-field="equipment.serial_no"/>
<div class="o_label_extra_data">
<span t-field="equipment.comp_serial_no"/>
</div>
<t t-if="equipment.warranty_date">
<strong t-field="equipment.warranty_date"/>
</t>
</div>
<div class="o_label_clear"></div>
</div>
</div>
</td>
</template>
<template id="report_equipment_simple_label4x7">
<t t-set="barcode_size" t-value="'width:80px;'"/>
<t t-set="table_style" t-value="'width:47mm;height:37.1mm;margin:inherit;' + table_style"/>
<td t-att-style="make_invisible and 'visibility:hidden;'" >
<div class="o_label_full" t-att-style="table_style">
<div class="o_label_name">
<strong t-field="equipment.name"/>
</div>
<div class= "text-center o_label_right_column">
<t t-if="equipment.qr_code">
<div t-field="equipment.qr_code" t-options="{'widget': 'image'}" style="width:95px;padding:0px;margin-top:-10px"/>
</t>
</div>
<div class="text-left o_label_left_column" style="line-height:normal;word-wrap: break-word;">
<div class="o_label_data">
<strong t-field="equipment.serial_no"/>
<span t-field="equipment.comp_serial_no"/>
<t t-if="equipment.warranty_date">
<strong t-field="equipment.warranty_date"/>
</t>
</div>
</div>
</div>
</td>
</template>
<template id="report_equipment_simple_label2x5">
<div class="d-flex flex-column" style="
width: 87mm;
height:40mm;
border: 1.5px solid black;
border-radius: 6px;
padding: 6px;
font-family: Arial, Roboto, sans-serif;
font-size: 11px;
margin-bottom: 15px;
">
<!-- Header -->
<div class="text-center fw-bold" style="
font-size: 15px;
border-bottom: 1px solid #ccc;
padding-bottom: 4px;
white-space: nowrap;
">
FTPROTECH - Asset Management Team
</div>
<!-- Main Content -->
<div class="d-flex" style="flex: 1; padding-top: 6px;">
<!-- Left Column -->
<div class="flex-grow-1 pe-2" style="white-space: nowrap;">
<div class="d-flex">
<div class="fw-bold" style="width: 40%;">Asset Tag:</div>
<div style="width: 60%; overflow: visible;"><t style="padding-left:5px;" t-esc="equipment.name"/></div>
</div>
<div class="d-flex">
<div class="fw-bold" style="width: 40%;">Serial Number:</div>
<div style="width: 60%; overflow: visible;"><t style="padding-left:5px;" t-esc="equipment.serial_no"/></div>
</div>
<div class="d-flex">
<div class="fw-bold" style="width: 40%;">Model Number:</div>
<div style="width: 60%; overflow: visible;"><t style="padding-left:5px;" t-esc="equipment.model"/></div>
</div>
</div>
<!-- Right Column - QR Code -->
<div class="d-flex justify-content-end align-items-center" style="width: 30%;">
<t t-if="equipment.name">
<div t-field="equipment.qr_code"
t-options="{'widget': 'image'}"
style="width: 90px; height: 90px; display: inline-block;">
</div>
</t>
</div>
</div>
</div>
</template>
<template id="report_quipmentlabel">
<t t-call="web.html_container">
<t t-if="columns and rows">
<t t-if="columns == 2 and rows == 7">
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label2x7'"/>
</t>
<t t-if="columns == 4 and rows == 7">
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label4x7'"/>
</t>
<t t-if="columns == 2 and rows == 5">
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label2x5'"/>
</t>
<t t-foreach="range(page_numbers)" t-as="page">
<div class="o_label_sheet" t-att-style="padding_page">
<table class="my-0 table table-sm table-borderless" style="border-spacing: 5mm 3mm;">
<t t-foreach="range(rows)" t-as="row">
<tr>
<t t-foreach="range(columns)" t-as="column">
<t t-if="equipment_data">
<t t-set="current_data" t-value="equipment_data.popitem()"/>
<t t-set="equipment" t-value="current_data[0]"/>
<t t-set="table_style" t-value="'border: 1px solid black;'"/>
<t t-call="{{report_to_call}}"/>
</t>
</t>
</tr>
</t>
</table>
</div>
</t>
</t>
</t>
</template>
<template id="maintenance_quip">
<t t-call="web.basic_layout">
<div class="page">
<t t-call="aspl_equipment_qrcode_generator.report_quipmentlabel">
<t t-set="products" t-value="products"/>
</t>
</div>
</t>
</template>
<record id="paperformat_label_sheet_qrcode" model="report.paperformat">
<field name="name">A4 Label Sheet</field>
<field name="default" eval="True"/>
<field name="format">A4</field>
<field name="page_height">0</field>
<field name="page_width">0</field>
<field name="orientation">Portrait</field>
<field name="margin_top">0</field>
<field name="margin_bottom">0</field>
<field name="margin_left">0</field>
<field name="margin_right">0</field>
<field name="disable_shrinking" eval="True"/>
<field name="dpi">96</field>
</record>
<record id="report_equipment_label" model="ir.actions.report">
<field name="name">Equipment QR-code (PDF)</field>
<field name="model">maintenance.equipment</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">aspl_equipment_qrcode_generator.maintenance_quip</field>
<field name="report_file">aspl_equipment_qrcode_generator.maintenance_quip</field>
<field name="paperformat_id" ref="aspl_equipment_qrcode_generator.paperformat_label_sheet_qrcode"/>
<field name="print_report_name">'Products Labels - %s' % (object.name)</field>
<field name="binding_model_id" eval="False"/>
<field name="binding_type">report</field>
</record>
</data>
</odoo>

View File

@ -1,86 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import math
from io import BytesIO
import qrcode
from odoo import models
def generate_qr_code(value):
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=20,
border=4,
)
qr.add_data(value)
qr.make(fit=True)
img = qr.make_image()
temp = BytesIO()
img.save(temp, format="PNG")
qr_img = base64.b64encode(temp.getvalue())
return qr_img
def _prepare_data(env, data):
equipment_label_layout_id = env['equipment.label.layout'].browse(data['equipment_label_layout_id'])
equipment_dict = {}
equipment_ids = equipment_label_layout_id.equipment_ids
for equipment in equipment_ids:
if not equipment.name:
continue
equipment_dict[equipment] = 1
combine_equipment_detail = ""
# Generate Equipment Redirect LInk
url = env['ir.config_parameter'].sudo().get_param('web.base.url')
menuId = env.ref('maintenance.menu_equipment_form').sudo().id
actionId = env.ref('maintenance.hr_equipment_action').sudo().id
equipment_link = url + '/web#id=' + str(equipment.id) + '&menu_id=' + str(menuId) + '&action=' + str(
actionId) + '&model=maintenance.equipment&view_type=form'
# Prepare main Equipment Detail
main_equipment_detail = ""
main_equipment_detail = main_equipment_detail.join(
"Name: " + str(equipment.name) + "\n" +
"Model: " + str(equipment.model) + "\n" +
"Mfg serial no: " + str(equipment.serial_no) + "\n"
"Warranty Exp. Date: " +str(equipment.warranty_date) + "\n"
"Category: " +str(equipment.category_id.name)+ "\n"
"Contact No: "+str(equipment.technician_user_id.phone) +"\n"
"Contact Email: "+str(equipment.technician_user_id.login)
)
# main_equipment_detail = equipment_link + '\n' + '\n' + main_equipment_detail
# Prepare Child Equipment Detail
combine_equipment_detail = main_equipment_detail
combine_equipment_detail += '\n' + '\n' + equipment_link
# Generate Qr Code depends on Details
qr_image = generate_qr_code(combine_equipment_detail)
equipment.write({
'qr_code': qr_image
})
env.cr.commit()
page_numbers = (len(equipment_ids) - 1) // (equipment_label_layout_id.rows * equipment_label_layout_id.columns) + 1
dict_equipment = {
'rows': equipment_label_layout_id.rows,
'columns': equipment_label_layout_id.columns,
'page_numbers': page_numbers,
'equipment_data': equipment_dict
}
return dict_equipment
class ReportProductTemplateLabel(models.AbstractModel):
_name = 'report.aspl_equipment_qrcode_generator.maintenance_quip'
_description = 'Equipment QR-code Report'
def _get_report_values(self, docids, data):
return _prepare_data(self.env, data)

View File

@ -1,2 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_equipment_label_layout,access.equipment_label_layout,model_equipment_label_layout,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_equipment_label_layout access.equipment_label_layout model_equipment_label_layout 1 1 1 1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -1,56 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_extend_equipment_tree" model="ir.ui.view">
<field name="name">maintenance.equipment.tree.inherit</field>
<field name="model">maintenance.equipment</field>
<field name="priority" eval="16"/>
<field name="inherit_id" ref="maintenance.hr_equipment_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//list" position="inside">
<header>
<button string="Print QR-code" type="object" name="action_print_qrcode_layout"/>
</header>
</xpath>
</field>
</record>
<record id="view_extend_equipment_form" model="ir.ui.view">
<field name="name">maintenance.equipment.form.inherit</field>
<field name="model">maintenance.equipment</field>
<field name="priority" eval="16"/>
<field name="inherit_id" ref="maintenance.hr_equipment_view_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='product_information']" position="inside">
<group>
<field name="comp_serial_no"/>
</group>
</xpath>
<xpath expr="//sheet" position="before">
<header>
<button string="Generate Serial Number" type="object" name="generate_serial_no" class="oe_highlight"/>
</header>
</xpath>
</field>
</record>
<record id="generate_serial_no_action" model="ir.actions.server">
<field name="name">Generate Serial Number</field>
<field name="model_id" ref="model_maintenance_equipment"/>
<field name="binding_model_id" ref="model_maintenance_equipment"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">action = records.generate_serial_no()</field>
</record>
<record id="generate_qrcode_no_action" model="ir.actions.server">
<field name="name">Print QR-Code</field>
<field name="model_id" ref="model_maintenance_equipment"/>
<field name="binding_model_id" ref="model_maintenance_equipment"/>
<field name="binding_view_types">form</field>
<field name="state">code</field>
<field name="code">action = records.action_print_qrcode_layout()</field>
</record>
</odoo>

View File

@ -1,3 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import equipment_label_layout

View File

@ -1,36 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
class EquipmentLabelLayout(models.TransientModel):
_name = 'equipment.label.layout'
_description = 'Choose the sheet layout to print the labels'
print_format = fields.Selection([
('2x5', '2 x 5'),
('2x7', '2 x 7'),
('4x7', '4 x 7')], string="Format", default='2x5', required=True)
equipment_ids = fields.Many2many('maintenance.equipment')
rows = fields.Integer(compute='_compute_dimensions')
columns = fields.Integer(compute='_compute_dimensions')
@api.depends('print_format')
def _compute_dimensions(self):
for wizard in self:
if 'x' in wizard.print_format:
columns, rows = wizard.print_format.split('x')[:2]
wizard.columns = int(columns)
wizard.rows = int(rows)
else:
wizard.columns, wizard.rows = 1, 1
def process_label(self):
xml_id = 'aspl_equipment_qrcode_generator.report_equipment_label'
data = {
'equipment_label_layout_id':self.id
}
return self.env.ref(xml_id).report_action(None, data=data)

View File

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="equipment_label_layout_form" model="ir.ui.view">
<field name="name">equipment.label.layout.form</field>
<field name="model">equipment.label.layout</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<form>
<group>
<group>
<field name="print_format" widget="radio"/>
</group>
</group>
<footer>
<button name="process_label" string="Confirm" type="object" class="btn-primary"/>
<button string="Discard" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_open_label_layout_equipment" model="ir.actions.act_window">
<field name="name">Choose Labels Layout</field>
<field name="res_model">equipment.label.layout</field>
<field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'form', 'view_id': ref('equipment_label_layout_form')})]" />
<field name="target">new</field>
</record>
</odoo>

View File

@ -45,7 +45,7 @@ class HrPayslipRun(models.Model):
'attendance_days': attendance_days, 'attendance_days': attendance_days,
'leave_days': leave_days, 'leave_days': leave_days,
'lop_days': lop_days, 'lop_days': lop_days,
'doj':contract.date_start, 'doj':employee.doj,
'birthday':employee.birthday, 'birthday':employee.birthday,
'bank': employee.bank_account_id.display_name if employee.bank_account_id else '-', 'bank': employee.bank_account_id.display_name if employee.bank_account_id else '-',
'sick_leave_balance': leave_balances.get('LEAVE110', 0), 'sick_leave_balance': leave_balances.get('LEAVE110', 0),

View File

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

View File

@ -1,60 +0,0 @@
# -*- coding: utf-8 -*-
{
'name': "Payroll Tax Management",
'summary': "Manage Income Tax Declarations for Employees in Payroll",
'description': """
Payroll IT Declarations
========================
This module allows HR and payroll departments to manage and track Income Tax (IT) declarations submitted by employees.
Features:
---------
- Employee-wise tax declaration submission
- HR approval workflow for declarations
- Category-wise declaration limits (e.g. 80C, HRA, LTA, etc.)
- Auto-calculation of eligible deductions
- Integration with Odoo Payroll for accurate tax computation
- Attach supporting documents (PDFs, images)
- Employee self-service through portal
Built with usability and compliance in mind, this module streamlines the IT declaration process and ensures transparency and efficiency across the organization.
Developed by: Pranay
""",
'author': "Pranay",
'website': "https://www.ftprotech.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Human Resources',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base','hr','hr_payroll','hr_employee_extended'],
# always loaded
'data': [
'security/ir.model.access.csv',
'views/payroll_periods.xml',
'views/investment_types.xml',
# 'views/payroll_periods.xml',
'views/slab_master.xml',
'views/emp_it_declaration.xml',
'views/report_it_tax_statement.xml',
'report/report_action.xml',
'report/it_tax_template.xml',
'views/it_tax_menu_and_wizard_view.xml',
'wizards/children_education_costing.xml',
'wizards/employee_life_insurance.xml',
'wizards/nsc_declaration.xml',
'wizards/self_occupied_property.xml',
'wizards/letout_house_property.xml',
'wizards/nsc_income_loss.xml',
# 'views/it_investment_type.xml',
# 'views/it_investment_costing.xml'
],
}

View File

@ -1,7 +0,0 @@
from . import payroll_periods
from . import investment_types
from . import investment_costings
from . import emp_it_declaration
from . import slab_master
from . import it_tax_statement
from . import it_tax_statement_wiz

View File

@ -1,274 +0,0 @@
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
from datetime import datetime, timedelta
import calendar
class EmpITDeclaration(models.Model):
_name = 'emp.it.declaration'
_rec_name = 'employee_id'
_description = "IT Declaration"
# @api.depends('period_id', 'employee_id')
# def _compute_name(self):
# for sheet in self:
# # sheet.name = _('%(period_id)s, %(emp_name)s', period_id=sheet.period_id.name, emp_name=sheet.employee_id.name)
# sheet.name='hello world'
employee_id = fields.Many2one(
'hr.employee',
string="Employee",
default=lambda self: self.env.user.employee_id,
required=True
)
period_id = fields.Many2one(
'payroll.period',
string="Payroll Period",
required=True
)
display_period_label = fields.Char(string="Period Label", compute='_compute_display_period_label')
@api.depends('period_id.name')
def _compute_display_period_label(self):
for rec in self:
if rec.period_id:
rec.display_period_label = f"Financial Year {rec.period_id.name}"
else:
rec.display_period_label = ""
tax_regime = fields.Selection([
('new', 'New Regime'),
('old', 'Old Regime')
], string="Tax Regime", required=True, default='new')
total_investment = fields.Float(string='Total Investment')
costing_details_generated = fields.Boolean(default=False)
investment_costing_ids = fields.One2many('investment.costings','it_declaration_id')
house_rent_costing_id = fields.Many2one('investment.costings', compute="_compute_investment_costing")
is_section_open = fields.Boolean()
@api.depends('costing_details_generated','investment_costing_ids')
def _compute_investment_costing(self):
for rec in self:
if rec.investment_costing_ids and rec.costing_details_generated:
rec.house_rent_costing_id = rec.investment_costing_ids.filtered(
lambda e: e.investment_type_id.investment_type == 'house_rent'
)[:1]
else:
rec.house_rent_costing_id = False
past_employment_costings = fields.One2many('past_employment.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
past_employment_costings_new = fields.One2many('past_employment.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
us80c_costings = fields.One2many('us80c.costing.type','it_declaration_id')
us80d_selection_type = fields.Selection([('self_family','Self-family'),('self_family_parent','Self-family and parent'),('self_family_senior_parent','Self-family and senior parent')], default='self_family',required=True)
us80d_health_checkup = fields.Boolean(string='Preventive Health Checkup')
us80d_costings = fields.One2many('us80d.costing.type','it_declaration_id',domain=[('investment_type_line_id.for_family','=',True),('investment_type_line_id.for_parents','=',False),('investment_type_line_id.for_senior_parent','=',False)])
us80d_costings_parents = fields.One2many('us80d.costing.type','it_declaration_id',domain=['|',('investment_type_line_id.for_family','=',True),('investment_type_line_id.for_parents','=',True),('investment_type_line_id.for_senior_parent','=',False)])
us80d_costings_senior_parents = fields.One2many('us80d.costing.type','it_declaration_id',domain=['|','|',('investment_type_line_id.for_family','=',True),('investment_type_line_id.for_parents','=',True),('investment_type_line_id.for_senior_parent','=',True)])
us10_costings = fields.One2many('us10.costing.type','it_declaration_id')
us80g_costings = fields.One2many('us80g.costing.type','it_declaration_id')
chapter_via_costings = fields.One2many('chapter.via.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
chapter_via_costings_new = fields.One2many('chapter.via.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
us17_costings = fields.One2many('us17.costing.type','it_declaration_id')
house_rent_costings = fields.One2many('house.rent.declaration','it_declaration_id')
other_il_costings = fields.One2many('other.il.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
other_il_costings_new = fields.One2many('other.il.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
other_declaration_costings = fields.One2many('other.declaration.costing.type','it_declaration_id')
def toggle_section_visibility(self):
for rec in self:
rec.is_section_open = not rec.is_section_open
if rec.is_section_open:
for investment_type in rec.investment_costing_ids:
if investment_type.investment_type_id.investment_type == 'past_employment':
if rec.tax_regime == 'old':
investment_type.amount = sum(
cost.declaration_amount
for cost in rec.past_employment_costings
if not cost.investment_type_line_id.compute_method
)
else:
investment_type.amount = sum(
cost.declaration_amount
for cost in rec.past_employment_costings_new
if not cost.investment_type_line_id.compute_method
)
elif investment_type.investment_type_id.investment_type == 'us_80c':
investment_type.amount = sum(
cost.declaration_amount
for cost in rec.us80c_costings
if not cost.investment_type_line_id.compute_method
) if rec.tax_regime == 'old' else 0
elif investment_type.investment_type_id.investment_type == 'us_80d':
if rec.us80d_selection_type == 'self_family':
investment_type.amount = sum(rec.us80d_costings.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0
if rec.us80d_selection_type == 'self_family_parent':
investment_type.amount = sum(rec.us80d_costings_parents.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0
if rec.us80d_selection_type == 'self_family_senior_parent':
investment_type.amount = sum(rec.us80d_costings_senior_parents.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0
elif investment_type.investment_type_id.investment_type == 'us_10':
investment_type.amount = sum(rec.us10_costings.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0
elif investment_type.investment_type_id.investment_type == 'us_80g':
investment_type.amount = sum(rec.us80g_costings.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0
elif investment_type.investment_type_id.investment_type == 'chapter_via':
if rec.tax_regime == 'old':
investment_type.amount = sum(rec.chapter_via_costings.mapped('declaration_amount') or [0])
else:
investment_type.amount = sum(rec.chapter_via_costings_new.mapped('declaration_amount') or [0])
elif investment_type.investment_type_id.investment_type == 'us_17':
investment_type.amount = sum(rec.us17_costings.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0
elif investment_type.investment_type_id.investment_type == 'house_rent':
investment_type.amount = sum(rec.house_rent_costings.mapped('rent_amount') or [0]) if rec.tax_regime == 'old' else 0
elif investment_type.investment_type_id.investment_type == 'other_i_or_l':
if rec.tax_regime == 'old':
investment_type.amount = sum(rec.other_il_costings.mapped('declaration_amount') or [0])
else:
investment_type.amount = sum(rec.other_il_costings_new.mapped('declaration_amount') or [0])
elif investment_type.investment_type_id.investment_type == 'other_declaration':
investment_type.amount = sum(rec.other_declaration_costings.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0
@api.onchange('tax_regime')
def _onchange_tax_regime(self):
if self.tax_regime:
# res = super(empITDeclaration, self).fields_get(allfields, attributes)
# self.fields_get()
if self.tax_regime == 'new':
domain = [('investment_type_line_id.tax_regime', 'in', ['new', 'both'])]
elif self.tax_regime == 'old':
domain = [('investment_type_line_id.tax_regime', 'in', ['old', 'both'])]
else:
domain = [] # Default case, although 'tax_regime' is required
return {'domain': {'past_employment_costings': domain}}
else:
return {'domain': {'past_employment_costings': []}} # Handle potential empty state
# def fields_get(self, allfields=None, attributes=None):
# import pdb
# pdb.set_trace()
# res = super(empITDeclaration, self).fields_get(allfields, attributes)
# print(res)
#
# # Example: Modify domain of field_1 based on field_2
# if 'tax_regime' in res:
# if self.tax_regime == '':
# res['field_1']['domain'] = [('some_field', '=', 123)]
# else:
# res['field_1']['domain'] = [('some_field', '=', 456)]
#
# return res
# import pdb
# pdb.set_trace()
# if rec.tax_regime:
# return {'domain': {'past_employment_costings': [('investment_type_line_id.tax_regime', 'in', ['new','both'])]}}
# return {
# 'domain': {
# 'past_employment_costings': [('investment_type_line_id.tax_regime', 'in', ['new','both'])]
# }
# }
def generate_declarations(self):
for rec in self:
investment_types = self.env['it.investment.type'].sudo().search([])
for inv_type in investment_types:
investment_costing = self.env['investment.costings'].sudo().create({
'investment_type_id': inv_type.id,
'it_declaration_id': rec.id,
})
if inv_type.investment_type == 'past_employment':
past_emp_costing_ids = [
self.env['past_employment.costing.type'].sudo().create({
'costing_type': investment_costing.id,
'it_declaration_id': rec.id,
'investment_type_line_id': investment_line.id,
'limit': investment_line.limit
}).id
for investment_line in inv_type.past_employment_ids
]
if inv_type.investment_type == 'us_80c':
us80c_costing_ids = [
self.env['us80c.costing.type'].sudo().create({
'costing_type': investment_costing.id,
'it_declaration_id': rec.id,
'investment_type_line_id': investment_line.id,
'limit': investment_line.limit
}).id
for investment_line in inv_type.us80c_ids
]
if inv_type.investment_type == 'us_80d':
us80d_costing_ids = [
self.env['us80d.costing.type'].sudo().create({
'costing_type': investment_costing.id,
'it_declaration_id': rec.id,
'investment_type_line_id': investment_line.id,
'limit': investment_line.limit
}).id
for investment_line in inv_type.us80d_ids
]
if inv_type.investment_type == 'us_10':
us10_costing_ids = [
self.env['us10.costing.type'].sudo().create({
'costing_type': investment_costing.id,
'it_declaration_id': rec.id,
'investment_type_line_id': investment_line.id,
'limit': investment_line.limit
}).id
for investment_line in inv_type.us10_ids
]
if inv_type.investment_type == 'us_80g':
us80g_costing_ids = [
self.env['us80g.costing.type'].sudo().create({
'costing_type': investment_costing.id,
'it_declaration_id': rec.id,
'investment_type_line_id': investment_line.id,
'limit': investment_line.limit
}).id
for investment_line in inv_type.us80g_ids
]
if inv_type.investment_type == 'chapter_via':
chapter_via_ids = [
self.env['chapter.via.costing.type'].sudo().create({
'costing_type': investment_costing.id,
'it_declaration_id': rec.id,
'investment_type_line_id': investment_line.id,
'limit': investment_line.limit
}).id
for investment_line in inv_type.chapter_via_ids
]
if inv_type.investment_type == 'us_17':
us17_costing_ids = [
self.env['us17.costing.type'].sudo().create({
'costing_type': investment_costing.id,
'it_declaration_id': rec.id,
'investment_type_line_id': investment_line.id,
'limit': investment_line.limit
}).id
for investment_line in inv_type.us17_ids
]
if inv_type.investment_type == 'other_i_or_l':
other_il_costing_ids = [
self.env['other.il.costing.type'].sudo().create({
'costing_type': investment_costing.id,
'it_declaration_id': rec.id,
'investment_type_line_id': investment_line.id,
'limit': investment_line.limit
}).id
for investment_line in inv_type.other_il_ids
]
if inv_type.investment_type == 'other_declaration':
other_declaration_costing_ids = [
self.env['other.declaration.costing.type'].sudo().create({
'costing_type': investment_costing.id,
'it_declaration_id': rec.id,
'investment_type_line_id': investment_line.id,
'limit': investment_line.limit
}).id
for investment_line in inv_type.other_declaration_ids
]
rec.costing_details_generated = True

View File

@ -1,309 +0,0 @@
from odoo import models, fields, api
from odoo.exceptions import ValidationError
from datetime import datetime, timedelta
import calendar
import re
class investmentCostings(models.Model):
_name = 'investment.costings'
_rec_name = 'investment_type_id'
investment_type_id = fields.Many2one('it.investment.type')
amount = fields.Integer()
it_declaration_id = fields.Many2one('emp.it.declaration')
employee_id = fields.Many2one(
'hr.employee',
string="Employee",
related='it_declaration_id.employee_id'
)
period_id = fields.Many2one(
'payroll.period',
string="Payroll Period",
related='it_declaration_id.period_id'
)
class pastEmpcostingType(models.Model):
_name = 'past_employment.costing.type'
_rec_name = 'investment_type_line_id'
costing_type = fields.Many2one('investment.costings')
it_declaration_id = fields.Many2one('emp.it.declaration')
investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type')
investment_type_line_id = fields.Many2one('past_employment.investment.type')
declaration_amount = fields.Integer(string='Declaration Amount',compute='_compute_declaration_amount',store=True,readonly=False)
proof_amount = fields.Integer(string="Proof Amount")
remarks = fields.Text(string="Remarks")
proof = fields.Binary(string="PROOF")
proof_name = fields.Char()
limit = fields.Integer()
@api.depends(
'it_declaration_id.past_employment_costings.declaration_amount',
'it_declaration_id.past_employment_costings_new.declaration_amount',
'investment_type_line_id.compute_method',
'investment_type_line_id.compute_code',
'it_declaration_id.tax_regime',
'declaration_amount'
)
def _compute_declaration_amount(self):
for rec in self:
line = rec.investment_type_line_id
if not line or not rec.it_declaration_id:
rec.declaration_amount = 0
continue
if line.compute_method and line.compute_code:
siblings = (
rec.it_declaration_id.past_employment_costings
if rec.it_declaration_id.tax_regime == 'old'
else rec.it_declaration_id.past_employment_costings_new
)
code_vars = {}
for sibling in siblings:
code = sibling.investment_type_line_id.investment_code
if code:
code_vars[code] = sibling.declaration_amount or 0
try:
# Extract variable names from compute_code
var_names = set(re.findall(r'\b[A-Z]+\b', line.compute_code))
for var in var_names:
code_vars.setdefault(var, 0) # Ensure missing variables default to 0
rec.declaration_amount = int(eval(line.compute_code, {"__builtins__": {}}, code_vars))
except Exception as e:
raise ValidationError(f"Error in compute_code for {line.name}: {e}")
else:
# Allow manual entry
pass
@api.onchange('investment_type_line_id', 'declaration_amount')
def _onchange_declaration_amount_live(self):
for rec in self:
line = rec.investment_type_line_id
if not line or not rec.it_declaration_id:
return
siblings = rec.it_declaration_id.past_employment_costings | rec.it_declaration_id.past_employment_costings_new
code_vars = {}
for sibling in siblings:
code = sibling.investment_type_line_id.investment_code
if code:
code_vars[code] = sibling.declaration_amount or 0
if line.compute_method and line.compute_code:
try:
rec.declaration_amount = int(eval(line.compute_code, {"__builtins__": {}}, code_vars))
except Exception as e:
rec.declaration_amount = 0 # fallback
class us80cCostingType(models.Model):
_name = 'us80c.costing.type'
_rec_name = 'investment_type_line_id'
costing_type = fields.Many2one('investment.costings')
it_declaration_id = fields.Many2one('emp.it.declaration')
investment_type_line_id = fields.Many2one('us80c.investment.type')
investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type')
declaration_amount = fields.Integer(string='Declaration Amount')
action_id = fields.Many2one('ir.actions.act_window', related='investment_type_line_id.action_id')
proof_amount = fields.Integer(string="Proof Amount")
remarks = fields.Text(string="Remarks")
proof = fields.Binary(string="PROOF")
proof_name = fields.Char()
limit = fields.Integer()
def open_action_wizard(self):
self.ensure_one()
model = self.env[self.action_id.res_model]
action_model = model.sudo().search([('it_declaration_id','=',self.it_declaration_id.id),('us80c_id','=',self.id)],order='id desc',limit=1)
# it_declaration_id
if not action_model:
# Explicitly create record so children get added in create()
action_model = model.sudo().create({
'it_declaration_id': self.it_declaration_id.id,
'us80c_id': self.id,
})
return {
'type': 'ir.actions.act_window',
'name': self.action_id.name,
'res_model': self.action_id.res_model,
'res_id': action_model.id,
'view_mode': self.action_id.view_mode,
'target': 'new',
}
class us80dCostingType(models.Model):
_name = 'us80d.costing.type'
_rec_name = 'investment_type_line_id'
costing_type = fields.Many2one('investment.costings')
it_declaration_id = fields.Many2one('emp.it.declaration')
investment_type_line_id = fields.Many2one('us80d.investment.type')
investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type')
declaration_amount = fields.Integer(string='Declaration Amount')
proof_amount = fields.Integer(string="Proof Amount")
remarks = fields.Text(string="Remarks")
proof = fields.Binary(string="PROOF")
proof_name = fields.Char()
limit = fields.Integer()
class us10CostingType(models.Model):
_name = 'us10.costing.type'
_rec_name = 'investment_type_line_id'
costing_type = fields.Many2one('investment.costings')
it_declaration_id = fields.Many2one('emp.it.declaration')
investment_type_line_id = fields.Many2one('us10.investment.type')
investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type')
declaration_amount = fields.Integer(string='Declaration Amount')
proof_amount = fields.Integer(string="Proof Amount")
remarks = fields.Text(string="Remarks")
proof = fields.Binary(string="PROOF")
proof_name = fields.Char()
limit = fields.Integer()
class us80gCostingType(models.Model):
_name = 'us80g.costing.type'
_rec_name = 'investment_type_line_id'
costing_type = fields.Many2one('investment.costings')
it_declaration_id = fields.Many2one('emp.it.declaration')
investment_type_line_id = fields.Many2one('us80g.investment.type')
investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type')
declaration_amount = fields.Integer(string='Declaration Amount')
proof_amount = fields.Integer(string="Proof Amount")
remarks = fields.Text(string="Remarks")
proof = fields.Binary(string="PROOF")
proof_name = fields.Char()
limit = fields.Integer()
class chapterViaCostingType(models.Model):
_name = 'chapter.via.costing.type'
_rec_name = 'investment_type_line_id'
costing_type = fields.Many2one('investment.costings')
it_declaration_id = fields.Many2one('emp.it.declaration')
investment_type_line_id = fields.Many2one('chapter.via.investment.type')
investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type')
declaration_amount = fields.Integer(string='Declaration Amount')
proof_amount = fields.Integer(string="Proof Amount")
remarks = fields.Text(string="Remarks")
proof = fields.Binary(string="PROOF")
proof_name = fields.Char()
limit = fields.Integer()
class us17CostingType(models.Model):
_name = 'us17.costing.type'
_rec_name = 'investment_type_line_id'
costing_type = fields.Many2one('investment.costings')
it_declaration_id = fields.Many2one('emp.it.declaration')
investment_type_line_id = fields.Many2one('us17.investment.type')
declaration_amount = fields.Integer(string='Declaration Amount')
proof_amount = fields.Integer(string="Proof Amount")
remarks = fields.Text(string="Remarks")
proof = fields.Binary(string="PROOF")
proof_name = fields.Char()
limit = fields.Integer()
class OtherILCostingType(models.Model):
_name = 'other.il.costing.type'
_rec_name = 'investment_type_line_id'
costing_type = fields.Many2one('investment.costings')
it_declaration_id = fields.Many2one('emp.it.declaration')
investment_type_line_id = fields.Many2one('other.il.investment.type')
declaration_amount = fields.Integer(string='Declaration Amount')
action_id = fields.Many2one('ir.actions.act_window', related='investment_type_line_id.action_id')
proof_amount = fields.Integer(string="Proof Amount")
remarks = fields.Text(string="Remarks")
proof = fields.Binary(string="PROOF")
proof_name = fields.Char()
limit = fields.Integer()
def open_action_wizard(self):
self.ensure_one()
model = self.env[self.action_id.res_model]
action_model = model.sudo().search([('it_declaration_id','=',self.it_declaration_id.id),('other_il_id','=',self.id)],order='id desc',limit=1)
# it_declaration_id
if not action_model:
# Explicitly create record so children get added in create()
action_model = model.sudo().create({
'it_declaration_id': self.it_declaration_id.id,
'other_il_id': self.id,
})
return {
'type': 'ir.actions.act_window',
'name': self.action_id.name,
'res_model': self.action_id.res_model,
'res_id': action_model.id,
'view_mode': self.action_id.view_mode,
'target': 'new',
}
class OtherDeclarationCostingType(models.Model):
_name = 'other.declaration.costing.type'
_rec_name = 'investment_type_line_id'
costing_type = fields.Many2one('investment.costings')
it_declaration_id = fields.Many2one('emp.it.declaration')
investment_type_line_id = fields.Many2one('other.declaration.investment.type')
declaration_amount = fields.Integer(string='Declaration Amount')
proof_amount = fields.Integer(string="Proof Amount")
remarks = fields.Text(string="Remarks")
proof = fields.Binary(string="PROOF")
proof_name = fields.Char()
limit = fields.Integer()
class HouseRentDeclaration(models.Model):
_name = 'house.rent.declaration'
_description = 'House Rent Declaration'
it_declaration_id = fields.Many2one('emp.it.declaration')
costing_type = fields.Many2one('investment.costings')
hra_exemption_type = fields.Selection([
('u_s_10', 'U/S 10 - HRA Exemption'),
('u_s_80gg', 'U/S 80GG - HRA Exemption')
], string="HRA Exemption Type", required=True, default='u_s_10')
rent_amount = fields.Float(string="Total Rent for the Period", required=True)
from_date = fields.Date(string="From Date", required=True)
to_date = fields.Date(string="To Date", required=True)
remarks = fields.Text(string="Remarks")
landlord_pan_no = fields.Char(string="Landlord PAN No")
landlord_name_address = fields.Text(string="Landlord Name & Address")
landlord_pan_status = fields.Selection([
('has_pan', 'Landlord has PAN CARD'),
('declaration', 'Declaration By Landlord')
], string="Landlord PAN Status", required=True, default='has_pan')
attachment = fields.Binary(string="Proof Attachment")
attachment_filename = fields.Char(string="Attachment Filename")
@api.model
def create(self, vals):
# Auto-link applicant_id if context is passed correctly
if self.env.context.get('default_it_declaration_id'):
import pdb
pdb.set_trace()
costing_id = self.env['investment.costings'].sudo().search([('id','=',self.env.context.get('it_declaration_id')),('investment_type_id.investment_type','=','house_rent')],limit=1)
vals['costing_type'] = costing_id.id
return super().create(vals)

View File

@ -1,220 +0,0 @@
from odoo import models, fields
class ItInvestmentType(models.Model):
_name = 'it.investment.type'
_rec_name = 'investment_type'
sequence = fields.Integer()
investment_type = fields.Selection(
[('past_employment', 'PAST EMPLOYMENT'), ('us_80c', 'US 80C'), ('us_80d', 'US 80D'), ('us_10', 'US 10'),
('us_80g', 'US 80G'), ('chapter_via', 'CHAPTER VIA'), ('us_17', 'US 17'), ('house_rent', 'HOUSE RENT'),
('other_i_or_l', 'OTHER INCOME/LOSS'), ('other_declaration', 'OTHER DECLARATION')], string="Investment Type",
required=True)
active = fields.Boolean(default=True)
past_employment_ids = fields.One2many('past_employment.investment.type','investment_type')
us80c_ids = fields.One2many('us80c.investment.type', 'investment_type')
us80d_ids = fields.One2many('us80d.investment.type', 'investment_type')
us10_ids = fields.One2many('us10.investment.type', 'investment_type')
us80g_ids = fields.One2many('us80g.investment.type', 'investment_type')
chapter_via_ids = fields.One2many('chapter.via.investment.type', 'investment_type')
us17_ids = fields.One2many('us17.investment.type', 'investment_type')
other_il_ids = fields.One2many('other.il.investment.type', 'investment_type')
other_declaration_ids = fields.One2many('other.declaration.investment.type', 'investment_type')
class pastEmpInvestmentType(models.Model):
_name = 'past_employment.investment.type'
_rec_name = 'name'
_sql_constraints = [
('investment_code_unique', 'unique(investment_code)', 'Code must be unique'),
]
name = fields.Char(string='GENERAL', required=True)
investment_type = fields.Many2one('it.investment.type')
investment_code = fields.Char()
compute_method = fields.Boolean()
compute_code = fields.Text('Python Code')
limit = fields.Integer(string='LIMIT')
sequence = fields.Integer()
active = fields.Boolean(default=True)
tax_regime = fields.Selection([
('new', 'New Regime'),
('old', 'Old Regime'),
('both', 'Both')
], string="Tax Regime", store=True, required=True)
class us80cInvestmentType(models.Model):
_name = 'us80c.investment.type'
_rec_name = 'name'
name = fields.Char(string='GENERAL', required=True)
investment_type = fields.Many2one('it.investment.type')
investment_code = fields.Char()
compute_method = fields.Boolean()
compute_code = fields.Text('Python Code')
limit = fields.Integer(string='LIMIT')
sequence = fields.Integer()
require_action = fields.Boolean()
action_id = fields.Many2one('ir.actions.act_window')
active = fields.Boolean(default=True)
tax_regime = fields.Selection([
('new', 'New Regime'),
('old', 'Old Regime'),
('both', 'Both')
], string="Tax Regime", store=True, required=True)
class us80dInvestmentType(models.Model):
_name = 'us80d.investment.type'
_rec_name = 'name'
name = fields.Char(string='GENERAL', required=True)
investment_type = fields.Many2one('it.investment.type')
limit = fields.Integer(string='LIMIT')
for_family = fields.Boolean()
for_parents = fields.Boolean()
for_senior_parent = fields.Boolean()
sequence = fields.Integer()
active = fields.Boolean(default=True)
tax_regime = fields.Selection([
('new', 'New Regime'),
('old', 'Old Regime'),
('both', 'Both')
], string="Tax Regime", store=True, required=True)
class us10InvestmentType(models.Model):
_name = 'us10.investment.type'
_rec_name = 'name'
name = fields.Char(string='GENERAL', required=True)
investment_type = fields.Many2one('it.investment.type')
limit = fields.Integer(string='LIMIT')
sequence = fields.Integer()
active = fields.Boolean(default=True)
tax_regime = fields.Selection([
('new', 'New Regime'),
('old', 'Old Regime'),
('both', 'Both')
], string="Tax Regime", store=True, required=True)
class us80gInvestmentType(models.Model):
_name = 'us80g.investment.type'
_rec_name = 'name'
name = fields.Char(string='GENERAL', required=True)
investment_type = fields.Many2one('it.investment.type')
limit = fields.Integer(string='LIMIT')
sequence = fields.Integer()
active = fields.Boolean(default=True)
tax_regime = fields.Selection([
('new', 'New Regime'),
('old', 'Old Regime'),
('both', 'Both')
], string="Tax Regime", required=True)
class chapterViaInvestmentType(models.Model):
_name = 'chapter.via.investment.type'
_rec_name = 'name'
name = fields.Char(string='GENERAL', required=True)
investment_type = fields.Many2one('it.investment.type')
limit = fields.Integer(string='LIMIT')
sequence = fields.Integer()
active = fields.Boolean(default=True)
tax_regime = fields.Selection([
('new', 'New Regime'),
('old', 'Old Regime'),
('both', 'Both')
], string="Tax Regime", store=True, required=True)
class us17InvestmentType(models.Model):
_name = 'us17.investment.type'
_rec_name = 'name'
name = fields.Char(string='GENERAL', required=True)
investment_type = fields.Many2one('it.investment.type')
limit = fields.Integer(string='LIMIT')
sequence = fields.Integer()
active = fields.Boolean(default=True)
tax_regime = fields.Selection([
('new', 'New Regime'),
('old', 'Old Regime'),
('both', 'Both')
], string="Tax Regime", store=True, required=True)
class OtherILInvestmentType(models.Model):
_name = 'other.il.investment.type'
_rec_name = 'name'
name = fields.Char(string='GENERAL', required=True)
investment_type = fields.Many2one('it.investment.type')
limit = fields.Integer(string='LIMIT')
sequence = fields.Integer()
require_action = fields.Boolean()
action_id = fields.Many2one('ir.actions.act_window')
active = fields.Boolean(default=True)
tax_regime = fields.Selection([
('new', 'New Regime'),
('old', 'Old Regime'),
('both', 'Both')
], string="Tax Regime", store=True, required=True)
class OtherDeclarationInvestmentType(models.Model):
_name = 'other.declaration.investment.type'
_rec_name = 'name'
name = fields.Char(string='GENERAL', required=True)
investment_type = fields.Many2one('it.investment.type')
limit = fields.Integer(string='LIMIT')
sequence = fields.Integer()
active = fields.Boolean(default=True)
tax_regime = fields.Selection([
('new', 'New Regime'),
('old', 'Old Regime'),
('both', 'Both')
], string="Tax Regime", store=True, required=True)
#
# class ItInvestmentTypeLine(models.Model):
# _name = 'it.investment.type.line'
# _description = 'IT Investment Type Line'
# _rec_name = 'name'
#
# investment_type_id = fields.Many2one(
# 'it.investment.type',
# string="Investment Type",
# required=True,
# ondelete='cascade'
# )
# investment_type = fields.Selection(
# [('past_employment', 'PAST EMPLOYMENT'), ('us_80c', 'US 80C'), ('us_80d', 'US 80D'), ('us_10', 'US 10'),
# ('us_80g', 'US 80G'), ('chapter_via', 'CHAPTER VIA'), ('us_17', 'US 17'), ('house_rent', 'HOUSE RENT'),
# ('other_i_or_l', 'OTHER INCOME/LOSS'), ('other_declaration', 'OTHER DECLARATION')], string="Investment Type",
# related='investment_type_id.name')
# name = fields.Char(string="Line Name", required=True)
# need_action = fields.Boolean(string="Needs Action")
# action_id = fields.Many2one(
# 'ir.actions.actions',
# string="Action",
# help="Linked Odoo action if needed"
# )
# tax_regime = fields.Selection([
# ('new', 'New Regime'),
# ('old', 'Old Regime')
# ], string="Tax Regime", required=True)
# sequence = fields.Integer()
# active = fields.Boolean(default=True)
# limit = fields.Integer()

View File

@ -1,20 +0,0 @@
from odoo import models, fields, api, _
class ITTaxStatement(models.Model):
_name = 'it.tax.statement'
_description = 'IT Tax Statement'
_rec_name = 'employee_id'
employee_id = fields.Many2one('hr.employee', required=True)
declaration_id = fields.Many2one('emp.it.declaration', required=True)
period_id = fields.Many2one('payroll.period', related='declaration_id.period_id', store=True)
period_line = fields.Many2one('payroll.period.line')
total_declared_amount = fields.Float(compute='_compute_totals')
tax_regime = fields.Selection(related='declaration_id.tax_regime')
pdf_generated = fields.Boolean(default=False)
@api.depends('declaration_id')
def _compute_totals(self):
for rec in self:
rec.total_declared_amount = sum(rec.declaration_id.investment_costing_ids.mapped('amount'))

View File

@ -1,594 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from datetime import date, timedelta
from dateutil.relativedelta import relativedelta
import math
class ITTaxStatementWizard(models.TransientModel):
_name = 'it.tax.statement.wizard'
_rec_name = 'employee_id'
_description = 'Generate IT Tax Statement (Old vs New)'
# Inputs
employee_id = fields.Many2one('hr.employee', required=True, default=lambda self: self.env.user.employee_id.id)
emp_doj = fields.Date(related='employee_id.doj', store=True)
contract_id = fields.Many2one('hr.contract', related='employee_id.contract_id', required=True)
period_id = fields.Many2one('payroll.period', required=True)
period_line = fields.Many2one('payroll.period.line')
# Taxpayer profile
taxpayer_name = fields.Char(related='employee_id.name')
taxpayer_age = fields.Integer(required=True, default=False)
residential_status = fields.Selection([
('RESIDENT', 'Resident'),
('NON-RESIDENT', 'Non-Resident')
], default='RESIDENT', required=True)
parent_age = fields.Integer(default=65)
# Tax regime selection
tax_regime = fields.Selection([
('new', 'New Regime'),
('old', 'Old Regime')
], string="Tax Regime", required=True, default='new')
lock_regime = fields.Selection([
('UNLOCKED', 'Unlocked'),
('LOCKED', 'Locked')
], default='UNLOCKED', string="Lock Regime")
# Computed fields for salary breakdown
basic_salary = fields.Float(compute='_compute_salary_components', store=False)
hra_salary = fields.Float(compute='_compute_salary_components', store=False)
lta_salary = fields.Float(compute='_compute_salary_components', store=False)
special_allowance = fields.Float(compute='_compute_salary_components', store=False)
gross_salary = fields.Float(compute='_compute_salary_components', store=False)
# Deductions
professional_tax = fields.Float(compute='_compute_deductions', store=False)
standard_deduction = fields.Float(compute='_compute_deductions', store=False)
nps_employer_contribution = fields.Float(compute='_compute_deductions', store=False)
# Other income
other_income = fields.Float(string="Other Income", default=0.0)
# Additional fields for tax calculation
hra_exemption = fields.Float(string="HRA Exemption", default=0.0)
interest_home_loan_self = fields.Float(string="Interest on Home Loan (Self Occupied)", default=0.0)
interest_home_loan_letout = fields.Float(string="Interest on Home Loan (Let Out)", default=0.0)
rental_income = fields.Float(string="Rental Income", default=0.0)
# Deductions under Chapter VI-A
ded_80C = fields.Float(string="Deduction under 80C", default=0.0)
ded_80CCD1B = fields.Float(string="Deduction under 80CCD(1B)", default=0.0)
ded_80D_self = fields.Float(string="Deduction under 80D (Self)", default=0.0)
ded_80D_parents = fields.Float(string="Deduction under 80D (Parents)", default=0.0)
ded_80G = fields.Float(string="Deduction under 80G", default=0.0)
ded_other = fields.Float(string="Other Deductions", default=0.0)
def _get_applicable_slab(self, regime, age, residence_type):
"""Get the applicable tax slab based on regime, age, and residence type"""
# Determine age category
if age < 60:
age_category = 'below_60'
elif age < 80:
age_category = '60_to_80'
else:
age_category = 'above_80'
# Search for slab master
slab_master = self.env['it.slab.master'].search([
('regime', '=', regime),
('age_category', '=', age_category),
'|',
('residence_type', '=', residence_type.lower()),
('residence_type', '=', 'both')
], limit=1)
if not slab_master:
raise ValidationError(_(
"No tax slab found for %s Regime with Age Category: %s and Residence Type: %s"
) % (regime.capitalize(), age_category.replace('_', ' ').title(), residence_type))
return slab_master
def _compute_tax_using_slab(self, taxable, slab_master):
"""Compute tax using slab master rules"""
tax = 0.0
# Get rules sorted by min_income
rules = slab_master.rules.sorted('min_income')
for rule in rules:
if taxable <= rule.min_income:
continue
# Calculate amount in this bracket
bracket_max = rule.max_income if rule.max_income else float('inf')
amount_in_bracket = min(taxable, bracket_max) - rule.min_income
# Apply tax calculation based on rule structure
if rule.fixed_amount and rule.excess_threshold:
# Rule with fixed amount and excess threshold
excess_amount = max(0, taxable - rule.excess_threshold)
tax_for_bracket = rule.fixed_amount + (excess_amount * rule.tax_rate / 100)
else:
# Standard bracket calculation
tax_for_bracket = amount_in_bracket * rule.tax_rate / 100
tax += tax_for_bracket
return tax
@api.depends('employee_id', 'contract_id', 'period_id')
def _compute_salary_components(self):
"""Compute salary components from payroll data"""
for rec in self:
if not rec.employee_id or not rec.contract_id:
continue
# Get payslip for the period
payslip = self.env['hr.payslip'].search([
('employee_id', '=', rec.employee_id.id),
('date_from', '>=', rec.period_line.from_date),
('date_to', '<=', rec.period_line.to_date),
('state', 'in', ['verify','done','paid'])
], limit=1)
if payslip:
# Extract salary components from payslip lines
rec.basic_salary = self._get_salary_rule_amount(payslip, 'BASIC')
rec.hra_salary = self._get_salary_rule_amount(payslip, 'HRA')
rec.lta_salary = self._get_salary_rule_amount(payslip, 'LTA')
rec.special_allowance = self._get_salary_rule_amount(payslip, 'SPA')
rec.gross_salary = self._get_salary_rule_amount(payslip, 'GROSS')
else:
# Fallback to contract values
rec.basic_salary = rec.contract_id.wage * 0.4 # Assuming 40% basic
rec.hra_salary = rec.contract_id.wage * 0.2 # Assuming 20% HRA
rec.lta_salary = rec.contract_id.wage * 0.1 # Assuming 10% LTA
rec.special_allowance = rec.contract_id.wage * 0.3 # Remaining as special allowance
rec.gross_salary = rec.contract_id.wage
def fetch_salary_components(self):
"""fetch salary components from payroll data"""
for rec in self:
if not rec.employee_id or not rec.contract_id:
continue
data = {
'basic_salary' : {'actual':[],'projected':[]},
'hra_salary': {'actual': [], 'projected': []},
'lta_salary': {'actual': [], 'projected': []},
'special_allowance' : {'actual':[],'projected':[]},
'gross_salary' : {'actual':[],'projected':[]}
}
period_lines = rec.period_id.period_line_ids
for line in period_lines:
basic_salary = float()
hra_salary = float()
lta_salary = float()
special_allowance = float()
gross_salary = float()
payslip = self.env['hr.payslip'].search([
('employee_id', '=', rec.employee_id.id),
('date_from', '>=', line.from_date),
('date_to', '<=', line.to_date),
('state', 'in', ['verify', 'done', 'paid'])
], limit=1)
if payslip:
# Extract salary components from payslip lines
basic_salary = self._get_salary_rule_amount(payslip, 'BASIC')
hra_salary = self._get_salary_rule_amount(payslip, 'HRA')
lta_salary = self._get_salary_rule_amount(payslip, 'LTA')
special_allowance = self._get_salary_rule_amount(payslip, 'SPA')
gross_salary = self._get_salary_rule_amount(payslip, 'GROSS')
else:
payslip = self.env['hr.payslip'].sudo().create({
'name': 'Test Payslip',
'employee_id': rec.employee_id.id,
'date_from': line.from_date,
'date_to': line.to_date
})
payslip.sudo().compute_sheet()
# Extract salary components from payslip lines
basic_salary = self._get_salary_rule_amount(payslip, 'BASIC')
hra_salary = self._get_salary_rule_amount(payslip, 'HRA')
lta_salary = self._get_salary_rule_amount(payslip, 'LTA')
special_allowance = self._get_salary_rule_amount(payslip, 'SPA')
gross_salary = self._get_salary_rule_amount(payslip, 'GROSS')
payslip.sudo().action_payslip_cancel()
payslip.sudo().unlink()
if line.from_date <= rec.period_line.from_date:
data['basic_salary']['actual'].append(basic_salary)
data['hra_salary']['actual'].append(hra_salary)
data['lta_salary']['actual'].append(lta_salary)
data['special_allowance']['actual'].append(special_allowance)
data['gross_salary']['actual'].append(gross_salary)
else:
data['basic_salary']['projected'].append(basic_salary)
data['hra_salary']['projected'].append(hra_salary)
data['lta_salary']['projected'].append(lta_salary)
data['special_allowance']['projected'].append(special_allowance)
data['gross_salary']['projected'].append(gross_salary)
return data
def _get_salary_rule_amount(self, payslip, rule_code):
"""Get amount for a specific salary rule from payslip"""
line = payslip.line_ids.filtered(lambda l: l.salary_rule_id.code == rule_code)
return line.total if line else 0.0
@api.depends('employee_id', 'contract_id', 'period_id', 'tax_regime')
def _compute_deductions(self):
"""Compute deductions from payroll data"""
for rec in self:
if not rec.employee_id or not rec.contract_id:
continue
# Get payslip for the period
payslip = self.env['hr.payslip'].search([
('employee_id', '=', rec.employee_id.id),
('date_from', '>=', rec.period_id.from_date),
('date_to', '<=', rec.period_id.to_date),
('state', 'in', ['verify', 'done', 'paid'])
], limit=1)
fy_start = self.period_id.from_date
fy_end = self.period_id.to_date
total_months = ((fy_end.year - fy_start.year) * 12 +
(fy_end.month - fy_start.month) + 1)
line_start = self.period_line.from_date
current_month_index = ((line_start.year - fy_start.year) * 12 +
(line_start.month - fy_start.month) + 1)
if payslip:
rec.professional_tax = (self._get_salary_rule_amount(payslip, 'PT'))*current_month_index
rec.nps_employer_contribution = self._get_salary_rule_amount(payslip, 'PFE')
else:
rec.professional_tax = 0.0
rec.nps_employer_contribution = 0.0
# Get standard deduction from slab master
if rec.tax_regime == 'new':
slab_master = self._get_applicable_slab('new', rec.taxpayer_age, rec.residential_status)
else:
slab_master = self._get_applicable_slab('old', rec.taxpayer_age, rec.residential_status)
rec.standard_deduction = slab_master.standard_deduction if slab_master else (
75000 if rec.tax_regime == 'new' else 50000
)
@api.onchange('employee_id')
def onchange_employee_id(self):
for rec in self:
if rec.employee_id and rec.employee_id.birthday:
age = relativedelta(date.today(), rec.employee_id.birthday).years
rec.taxpayer_age = age
else:
rec.taxpayer_age = rec.taxpayer_age or 0
@api.onchange('basic_salary', 'hra_salary')
def onchange_hra_exemption(self):
"""Calculate HRA exemption based on salary components"""
for rec in self:
if rec.basic_salary and rec.hra_salary:
# Basic formula for HRA exemption
# Minimum of:
# 1. Actual HRA received
# 2. 50% of basic salary (for metro cities) or 40% (non-metro)
# 3. Rent paid minus 10% of basic salary
# Assuming metro city for calculation
exemption_option1 = rec.hra_salary
exemption_option2 = 0.5 * rec.basic_salary
# Rent paid would need to be input by user
rent_paid = rec.hra_exemption or 0
exemption_option3 = max(0, rent_paid - 0.1 * rec.basic_salary)
rec.hra_exemption = min(exemption_option1, exemption_option2, exemption_option3)
# --- Tax Calculation Methods ---
def _old_basic_exemption(self, age):
if age < 60:
return 250000.0
elif age < 80:
return 300000.0
return 500000.0
def _rebate_old(self, taxable, slab_tax):
if self.residential_status != 'RESIDENT':
return 0.0
if taxable <= 500000.0:
return min(12500.0, slab_tax)
return 0.0
def _rebate_new(self, taxable, slab_tax):
if self.residential_status != 'RESIDENT':
return 0.0
if taxable >= 1200000.0:
needed = taxable - 1200000.0
if slab_tax >= needed:
return max(0.0, slab_tax - needed)
return 0.0
else:
return min(60000.0, slab_tax)
def _apply_surcharge_with_mr(self, slab_master, taxable, tax_after_rebate, regime):
rules = slab_master.rules.sorted('min_income')
table = [(rule.min_income, rule.surcharge_rate) for rule in rules if rule.surcharge_rate > 0]
threshold = None
rate = 0.0
for th, rt in table:
if taxable > th:
threshold, rate = th, rt
if not threshold:
return 0.0, 0.0, tax_after_rebate
surcharge = tax_after_rebate * rate
total_before_mr = tax_after_rebate + surcharge
mr = max(0.0, total_before_mr)
tax_with_surcharge = total_before_mr - mr
return surcharge, mr, tax_with_surcharge
def _compute_tax_old_regime(self, taxable):
# Get applicable slab
slab_master = self._get_applicable_slab('old', self.taxpayer_age, self.residential_status)
# Compute slab tax
slab_tax = self._compute_tax_using_slab(taxable, slab_master)
# Apply rebate
rebate = self._rebate_old(taxable, slab_tax)
tax_after_rebate = max(0.0, slab_tax - rebate)
# Apply surcharge and marginal relief
surcharge, marginal_relief, tax_with_surcharge = self._apply_surcharge_with_mr(
slab_master, taxable, tax_after_rebate, regime='old'
)
# Apply cess
rules = slab_master.rules.sorted('min_income')
cess_rate = [rule.cess_rate for rule in rules if rule.min_income<=taxable and rule.max_income >= taxable]
cess = tax_with_surcharge * cess_rate[0]/100
total_tax = tax_with_surcharge + cess
return {
'taxable_income': taxable,
'slab_tax': slab_tax,
'rebate_87a': rebate,
'tax_after_rebate': tax_after_rebate,
'surcharge': surcharge,
'marginal_relief': marginal_relief,
'tax_with_surcharge': tax_with_surcharge,
'cess_4pct': cess,
'total_tax': total_tax
}
def _compute_tax_new_regime(self, taxable):
# Get applicable slab (new regime doesn't depend on age)
slab_master = self._get_applicable_slab('new', self.taxpayer_age, self.residential_status)
# Compute slab tax
slab_tax = self._compute_tax_using_slab(taxable, slab_master)
# Apply rebate
rebate = self._rebate_new(taxable, slab_tax)
tax_after_rebate = max(0.0, slab_tax - rebate)
# Apply surcharge and marginal relief
surcharge, marginal_relief, tax_with_surcharge = self._apply_surcharge_with_mr(
slab_master, taxable, tax_after_rebate, regime='new'
)
rules = slab_master.rules.sorted('min_income')
cess_rate = [rule.cess_rate for rule in rules if rule.min_income<=taxable and rule.max_income >= taxable]
# Apply cess
cess = tax_with_surcharge * cess_rate[0]/100
total_tax = tax_with_surcharge + cess
return {
'taxable_income': taxable,
'slab_tax': slab_tax,
'rebate_87a': rebate,
'tax_after_rebate': tax_after_rebate,
'surcharge': surcharge,
'marginal_relief': marginal_relief,
'tax_with_surcharge': tax_with_surcharge,
'cess_4pct': cess,
'total_tax': total_tax
}
def _compute_house_property_income(self):
"""Returns net house property income"""
rent = float(self.rental_income or 0.0)
if rent > 0.0:
nav = rent
std_ded = 0.30 * nav
interest_allowed = float(self.interest_home_loan_letout or 0.0)
hp_income = nav - std_ded - interest_allowed
else:
interest_allowed = min(float(self.interest_home_loan_self or 0.0), 200000.0)
hp_income = -interest_allowed
return hp_income
def _prepare_income_tax_data(self):
"""Prepare data for the tax statement report"""
today = date.today()
fy_start = self.period_id.from_date
fy_end = self.period_id.to_date
total_months = ((fy_end.year - fy_start.year) * 12 +
(fy_end.month - fy_start.month) + 1)
line_start = self.period_line.from_date
current_month_index = ((line_start.year - fy_start.year) * 12 +
(line_start.month - fy_start.month) + 1)
if today.month >= 4:
fy_start = date(today.year, 4, 1)
fy_end = date(today.year + 1, 3, 31)
else:
fy_start = date(today.year - 1, 4, 1)
fy_end = date(today.year, 3, 31)
# Calculate taxable income for both regimes
# Old regime
old_deductions = (
self.standard_deduction +
self.hra_exemption +
self.professional_tax +
self.ded_80C +
self.ded_80CCD1B +
self.ded_80D_self +
self.ded_80D_parents +
self.ded_80G +
self.ded_other +
self.nps_employer_contribution
)
# House property income
hp_income = self._compute_house_property_income()
# Taxable income for old regime
taxable_old = max(0.0, (self.gross_salary * total_months) + self.other_income + hp_income - self.standard_deduction)
# New regime - fewer deductions
new_deductions = (
self.standard_deduction +
self.professional_tax +
self.nps_employer_contribution
)
# Taxable income for new regime
taxable_new = max(0.0, (self.gross_salary * total_months) + self.other_income + hp_income - self.standard_deduction)
# Compute tax for both regimes
tax_result_old = self._compute_tax_old_regime(taxable_old)
tax_result_new = self._compute_tax_new_regime(taxable_new)
# Determine which regime to use
if self.tax_regime == 'old':
tax_result = tax_result_old
taxable_income = taxable_old
chosen = 'old'
else:
tax_result = tax_result_new
taxable_income = taxable_new
chosen = 'new'
# Calculate tax savings
tax_savings = abs(tax_result_old['total_tax'] - tax_result_new['total_tax'])
beneficial_regime = 'old' if tax_result_old['total_tax'] < tax_result_new['total_tax'] else 'new'
# Prepare data structure matching screenshot format
# Financial year (period_id)
fy_start = self.period_id.from_date
fy_end = self.period_id.to_date
total_months = ((fy_end.year - fy_start.year) * 12 +
(fy_end.month - fy_start.month) + 1)
# Current month (period_line)
line_start = self.period_line.from_date
current_month_index = ((line_start.year - fy_start.year) * 12 +
(line_start.month - fy_start.month) + 1)
tax_result['roundoff_taxable_income'] = float(round(tax_result["taxable_income"] / 10) * 10)
birthday = self.employee_id.birthday
if birthday:
diff = relativedelta(date.today(), birthday)
years_months = f"{diff.years} years {diff.months} months"
else:
years_months = "N/A"
month_age = str(self.period_line.name)+ " / " + str(years_months)
salary_components_data = self.fetch_salary_components()
data = {
'financial_year': f"{fy_start.year}-{fy_end.year}",
'assessment_year': fy_end.year + 1,
'report_time': today.strftime('%d-%m-%Y %H:%M'),
'user': 'ESS',
'emp_code': self.employee_id.employee_id,
'company_name': self.employee_id.company_id.name,
'profile': {
'name': self.taxpayer_name or (self.employee_id.name if self.employee_id else ''),
'age': self.taxpayer_age,
'residential_status': self.residential_status,
'parent_age': self.parent_age,
'doj': self.employee_id.doj,
'pan': self.employee_id.pan_no if hasattr(self.employee_id, 'pan_no') else '',
'gender': self.employee_id.gender,
'month_age': month_age
},
'regime_info': {
'lock_regime': self.lock_regime,
'tax_regime': 'NEW REGIME' if self.tax_regime == 'new' else 'OLD REGIME',
},
'salary_components': {
'basic': {'actual': sum(salary_components_data['basic_salary']['actual']), 'projected': sum(salary_components_data['basic_salary']['projected']), 'total':sum(salary_components_data['basic_salary']['actual']) + sum(salary_components_data['basic_salary']['projected'])},
'house_rent': {'actual': sum(salary_components_data['hra_salary']['actual']), 'projected': sum(salary_components_data['hra_salary']['projected']), 'total':sum(salary_components_data['hra_salary']['actual']) + sum(salary_components_data['hra_salary']['projected'])},
'lta': {'actual': sum(salary_components_data['lta_salary']['actual']), 'projected': sum(salary_components_data['lta_salary']['projected']), 'total':sum(salary_components_data['lta_salary']['actual']) + sum(salary_components_data['lta_salary']['projected'])},
'special_allowance':{'actual': sum(salary_components_data['special_allowance']['actual']), 'projected': sum(salary_components_data['special_allowance']['projected']), 'total':sum(salary_components_data['special_allowance']['actual']) + sum(salary_components_data['special_allowance']['projected'])},
'perquisites': {'actual': 0 * current_month_index, 'projected': 0 * (total_months - current_month_index), 'total': 0 * total_months},
'reimbursement': {'actual': 0 * current_month_index, 'projected': 0 * (total_months - current_month_index), 'total': 0 * total_months},
'gross_salary': {'actual': sum(salary_components_data['gross_salary']['actual']), 'projected': sum(salary_components_data['gross_salary']['projected']), 'total':sum(salary_components_data['gross_salary']['actual']) + sum(salary_components_data['gross_salary']['projected'])},
'net_salary': {'actual': self.gross_salary * current_month_index, 'projected': self.gross_salary * (total_months - current_month_index),
'total': self.gross_salary * total_months}
},
'deductions': {
'professional_tax': self.professional_tax,
'standard_deduction': self.standard_deduction,
'nps_employer': self.nps_employer_contribution,
'hra_exemption': self.hra_exemption,
'interest_home_loan': self.interest_home_loan_self + self.interest_home_loan_letout,
'ded_80C': self.ded_80C,
'ded_80CCD1B': self.ded_80CCD1B,
'ded_80D_self': self.ded_80D_self,
'ded_80D_parents': self.ded_80D_parents,
'ded_80G': self.ded_80G,
'ded_other': self.ded_other,
'total_deductions': old_deductions if self.tax_regime == 'old' else new_deductions,
},
'income_details': {
'gross_salary': self.gross_salary,
'other_income': self.other_income,
'house_property_income': hp_income,
'gross_total_income': (self.gross_salary * total_months) + self.other_income + hp_income - self.standard_deduction,
},
'taxable_income': {
'old_regime': taxable_old,
'new_regime': taxable_new,
'current_regime': taxable_income,
},
'tax_computation': tax_result,
'regime_used': chosen,
'comparison': {
'old_regime_tax': tax_result_old['total_tax'],
'new_regime_tax': tax_result_new['total_tax'],
'tax_savings': tax_savings,
'beneficial_regime': beneficial_regime,
}
}
return {'data': data}
def action_generate_report(self):
report_data = self._prepare_income_tax_data()
return self.env.ref('employee_it_declaration.income_tax_statement_action_report').report_action(
self,
data={'report_data': report_data},
)

View File

@ -1,114 +0,0 @@
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
from datetime import date, datetime, time
class ITTaxStatementWizard(models.TransientModel):
_name = 'it.tax.statement.wizard'
_description = 'Generate IT Tax Statement'
def _period_line_id_domain(self):
pass
employee_id = fields.Many2one('hr.employee', required=True, default=lambda self: self.env.user.employee_id.id)
emp_doj = fields.Date(related='employee_id.doj', store=True)
contract_id = fields.Many2one('hr.contract',related='employee_id.contract_id')
period_id = fields.Many2one('payroll.period', required=True)
period_line = fields.Many2one('payroll.period.line')
tax_regime = fields.Selection([
('new', 'New Regime'),
('old', 'Old Regime')
], string="Tax Regime", required=True, default='new')
def action_generate_report(self):
# declaration = self.env['emp.it.declaration'].search([
# ('employee_id', '=', self.employee_id.id),
# ('period_id', '=', self.period_id.id)
# ], limit=1)
#
# if not declaration:
# raise ValidationError("No IT Declaration found for the selected employee and period.")
# Prepare data for the report
report_data = self._prepare_income_tax_data()
# Return the report action
return {
'type': 'ir.actions.report',
'report_name': 'employee_it_declaration.income_tax_statement_report',
'report_type': 'qweb-pdf',
'data': {'model': 'it.tax.statement.wizard', 'ids': self.ids, 'report_data': report_data},
'context': {'active_model': 'it.tax.statement.wizard', 'active_id': self.id},
}
# return self.env.ref('your_module_name.action_report_it_tax_statement').report_action(declaration)
def _prepare_income_tax_data(self):
# Get fiscal year (April to March)
today = date.today()
if today.month >= 4:
fiscal_year_start = date(today.year, 4, 1)
fiscal_year_end = date(today.year + 1, 3, 31)
else:
fiscal_year_start = date(today.year - 1, 4, 1)
fiscal_year_end = date(today.year, 3, 31)
fiscal_year_str = f"{fiscal_year_start.year}-{fiscal_year_end.year}"
# Get company information
company = self.env.company
company_address = ', '.join(filter(None, [
company.street,
company.street2,
company.city,
company.state_id.name,
company.country_id.name,
company.zip
]))
# Get PAN and TAN (assuming they're stored in company)
pan_number = company.pan_number if hasattr(company, 'pan_number') else 'PAN1234567'
tan_number = company.tan_number if hasattr(company, 'tan_number') else 'TAN1234567'
# Prepare report data
data = {
'company_name': company.name,
'company_address': company_address,
'pan_number': pan_number,
'tan_number': tan_number,
'assessment_year': fiscal_year_end.year + 1,
'financial_year': fiscal_year_str,
'date': today.strftime('%d/%m/%Y'),
# Income details (simplified - you would query actual data)
'salary_income': 1200000.00,
'house_property_income': 300000.00,
'business_income': 500000.00,
'capital_gains': 200000.00,
'other_income': 100000.00,
'total_income': 2300000.00,
# Deductions
'section_80c': 150000.00,
'section_80d': 50000.00,
'section_80g': 10000.00,
'other_deductions': 30000.00,
'total_deductions': 240000.00,
# Tax computation
'taxable_income': 2060000.00,
'tax_payable': 450000.00,
'tax_paid': 400000.00,
'balance_tax': 50000.00,
# Additional details
'bank_name': 'State Bank of India',
'account_number': '1234567890',
'ifsc_code': 'SBIN0001234',
# For employee reports (if needed)
'employee_name': 'John Doe',
'employee_pan': 'ABCDE1234F',
'employee_address': '123, Main Street, Bangalore, Karnataka - 560001',
}
return {'data': data}

View File

@ -1,61 +0,0 @@
from odoo import models, fields, api
from odoo.exceptions import ValidationError
from datetime import datetime, timedelta
import calendar
class PayrollPeriod(models.Model):
_name = 'payroll.period'
_description = 'Payroll Period'
_rec_name = 'name'
_sql_constraints = [
('unique_name', 'unique(name)', 'The name must be unique.')
]
from_date = fields.Date(string="From Date", required=True)
to_date = fields.Date(string="To Date", required=True)
name = fields.Char(string="Name", required=True)
period_line_ids = fields.One2many('payroll.period.line', 'period_id', string="Monthly Periods")
@api.onchange('from_date', 'to_date')
def onchange_from_to_date(self):
for rec in self:
if rec.from_date and rec.to_date:
rec.name = f"{rec.from_date.year}-{rec.to_date.year}"
def action_generate_month_lines(self):
self.ensure_one()
if not (self.from_date and self.to_date):
raise ValidationError("Please set both From Date and To Date.")
lines = []
current = self.from_date.replace(day=1)
while current <= self.to_date:
end_of_month = current.replace(day=calendar.monthrange(current.year, current.month)[1])
if end_of_month > self.to_date:
end_of_month = self.to_date
lines.append((0, 0, {
'from_date': current,
'to_date': end_of_month,
'name': f"{current.strftime('%b')} - {str(current.year)[-2:]}"
}))
if current.month == 12:
current = current.replace(year=current.year + 1, month=1)
else:
current = current.replace(month=current.month + 1)
self.period_line_ids = lines
class PayrollPeriodLine(models.Model):
_name = 'payroll.period.line'
_description = 'Payroll Period Line'
_rec_name = 'name'
period_id = fields.Many2one('payroll.period', string="Payroll Period", required=True, ondelete='cascade')
from_date = fields.Date(string="From Date")
to_date = fields.Date(string="To Date")
name = fields.Char(string="Name")

View File

@ -1,72 +0,0 @@
from odoo import fields, models, api, _
from odoo.exceptions import ValidationError
class IncomeTaxSlabMaster(models.Model):
_name = 'it.slab.master'
_description = 'Income Tax Slab Master'
_rec_name = 'name'
_sql_constraints = [
(
'unique_slab',
'unique(regime, age_category, residence_type)',
'Slab must be unique for the same Regime, Age Category, and Residence Type!'
)
]
name = fields.Char(string="Slab Name", required=True)
regime = fields.Selection([
('old', 'Old Tax Regime'),
('new', 'New Tax Regime')
], required=True)
age_category = fields.Selection([
('below_60', 'Below 60 Years'),
('60_to_80', '60-80 Years'),
('above_80', 'Above 80 Years')
], required=True)
residence_type = fields.Selection([
('resident', 'Resident'),
('non_resident', 'Non Resident'),
('both', 'Both')
], required=True)
standard_deduction = fields.Float(string="Standard Deduction")
active = fields.Boolean(default=True)
rules = fields.One2many('it.slab.master.rules','slab_id', string="Slab Rules")
class IncomeTaxSlabMasterRules(models.Model):
_name = 'it.slab.master.rules'
_description = 'Income Tax slab rules'
_rec_name = 'slab_id'
_sql_constraints = [
(
'check_min_max_income',
'CHECK (max_income IS NULL OR max_income > min_income)',
'Max Income must be greater than Min Income!'
)
]
min_income = fields.Float(string="Min Income (₹)", required=True)
max_income = fields.Float(string="Max Income (₹)")
tax_rate = fields.Float(string="Tax Rate (%)", required=True)
fixed_amount = fields.Float(string="Fixed Amount (₹)")
excess_threshold = fields.Float(string="Excess Threshold (₹)")
surcharge_rate = fields.Float(string="Surcharge Rate (%)")
cess_rate = fields.Float(string="Health & Education (%)", default=4.0)
slab_id = fields.Many2one('it.slab.master')
@api.constrains('min_income', 'max_income', 'slab_id')
def _check_overlap(self):
"""Ensure no overlapping or duplicate ranges within the same slab"""
for rule in self:
domain = [
('slab_id', '=', rule.slab_id.id),
('id', '!=', rule.id)
]
others = self.search(domain)
for other in others:
if not (rule.max_income and other.min_income >= rule.max_income) and \
not (other.max_income and rule.min_income >= other.max_income):
raise ValidationError(
f"Income ranges overlap with another slab rule: {other.min_income} - {other.max_income}"
)

View File

@ -1,145 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="income_tax_statement_report">
<t t-call="web.external_layout">
<main class="page">
<t t-set="data" t-value="report_data['data']"/>
<div>
<!-- Header -->
<div class="row text-center" style="margin-bottom: 20px; page-break-inside: avoid;">
<h2>INCOME TAX STATEMENT</h2>
<h4>Assessment Year: <span t-esc="data.get('assessment_year')"/></h4>
<h4>Financial Year: <span t-esc="data.get('financial_year')"/></h4>
</div>
<!-- Company Information -->
<div class="row" style="margin-bottom: 20px; page-break-inside: avoid;">
<div class="col-md-6">
<strong>Company Name:</strong> <span t-esc="data.get('company_name')"/><br/>
<strong>Address:</strong> <span t-esc="data.get('company_address')"/><br/>
<strong>PAN:</strong> <span t-esc="data.get('pan_number')"/><br/>
<strong>TAN:</strong> <span t-esc="data.get('tan_number')"/><br/>
</div>
<div class="col-md-6 text-right">
<strong>Date:</strong> <span t-esc="data.get('date')"/><br/>
</div>
</div>
<!-- Income Details -->
<div class="row" style="margin-bottom: 20px;">
<h4>Income Details</h4>
<table class="table table-bordered">
<thead>
<tr>
<th>Particulars</th>
<th class="text-right">Amount (₹)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Income from Salary</td>
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('salary_income', 0))"/>
</tr>
<tr>
<td>Income from House Property</td>
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('house_property_income', 0))"/>
</tr>
<tr>
<td>Income from Business/Profession</td>
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('business_income', 0))"/>
</tr>
<tr>
<td>Capital Gains</td>
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('capital_gains', 0))"/>
</tr>
<tr>
<td>Income from Other Sources</td>
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('other_income', 0))"/>
</tr>
<tr style="font-weight: bold;">
<td>Total Income</td>
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('total_income', 0))"/>
</tr>
</tbody>
</table>
</div>
<!-- Deductions -->
<div class="row" style="margin-bottom: 20px; page-break-inside: avoid;">
<h4>Deductions Under Chapter VI-A</h4>
<table class="table table-bordered">
<thead>
<tr>
<th>Section</th>
<th>Particulars</th>
<th class="text-right">Amount (₹)</th>
</tr>
</thead>
<tbody>
<tr>
<td>80C</td>
<td>Life Insurance, PF, PPF, etc.</td>
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('section_80c', 0))"/>
</tr>
<tr>
<td>80D</td>
<td>Medical Insurance Premium</td>
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('section_80d', 0))"/>
</tr>
<tr>
<td>80G</td>
<td>Donations to Charitable Institutions</td>
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('section_80g', 0))"/>
</tr>
<tr>
<td>Others</td>
<td>Other Deductions</td>
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('other_deductions', 0))"/>
</tr>
<tr style="font-weight: bold;">
<td colspan="2">Total Deductions</td>
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('total_deductions', 0))"/>
</tr>
</tbody>
</table>
</div>
<!-- Tax Computation -->
<div class="row" style="margin-bottom: 20px; page-break-inside: avoid;">
<h4>Tax Computation</h4>
<table class="table table-bordered">
<tbody>
<tr>
<td>Total Income</td>
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('total_income', 0))"/>
</tr>
<tr>
<td>Less: Deductions</td>
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('total_deductions', 0))"/>
</tr>
<tr style="font-weight: bold;">
<td>Taxable Income</td>
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('taxable_income', 0))"/>
</tr>
<tr>
<td>Tax Payable</td>
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('tax_payable', 0))"/>
</tr>
<tr>
<td>Less: Tax Paid (Advance/Self-assessment)</td>
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('tax_paid', 0))"/>
</tr>
<tr style="font-weight: bold;">
<td>Balance Tax Payable</td>
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('balance_tax', 0))"/>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</t>
</template>
</odoo>

View File

@ -1,726 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template id="generate_income_tax_statement_rpt">
<t t-call="web.html_container">
<div class="page">
<t t-set="data" t-value="report_data['data']"/>
<t t-set="profile" t-value="data.get('profile')"/>
<t t-set="company_name" t-value="data.get('company_name')"/>
<t t-set="regime_info" t-value="data.get('regime_info')"/>
<t t-set="salary_components" t-value="data.get('salary_components')"/>
<t t-set="deductions" t-value="data.get('deductions')"/>
<t t-set="income_details" t-value="data.get('income_details')"/>
<t t-set="taxable_income" t-value="data.get('taxable_income')"/>
<t t-set="tax_computation" t-value="data.get('tax_computation')"/>
<t t-set="regime_used" t-value="data.get('regime_used')"/>
<t t-set="comparison" t-value="data.get('comparison')"/>
<!-- Set Dynamic Variables -->
<t t-set="emp_code" t-value="data.get('emp_code')"/>
<t t-set="name" t-value="profile.get('name', '')"/>
<t t-set="pan" t-value="profile.get('pan', '')"/>
<t t-set="month_age" t-value="profile.get('month_age', '')"/>
<t t-set="gender" t-value="(profile.get('gender', '') or '').upper()"/>
<t t-set="joining_date" t-value="profile.get('doj', '')"/>
<t t-set="leaving_date" t-value=""/>
<t t-set="status" t-value="Active"/>
<t t-set="lock_regime" t-value="regime_info.get('lock_regime', '')"/>
<t t-set="net_salary"
t-value="'{:,.0f}'.format(salary_components.get('net_salary', {}).get('total', 0))"/>
<t t-set="tax_employment" t-value="'{:,.0f}'.format(deductions.get('professional_tax', 0))"/>
<t t-set="standard_deduction" t-value="'{:,.0f}'.format(deductions.get('standard_deduction', 0))"/>
<t t-set="other_income" t-value="'{:,.0f}'.format(income_details.get('other_income', 0))"/>
<t t-set="gross_total_income" t-value="'{:,.0f}'.format(income_details.get('gross_total_income', 0))"/>
<t t-set="taxable_income" t-value="'{:,.0f}'.format(tax_computation.get('taxable_income', 0))"/>
<t t-set="roundoff_taxable_income" t-value="'{:,.0f}'.format(tax_computation.get('roundoff_taxable_income', 0))"/>
<t t-set="tax_payable" t-value="'{:,.0f}'.format(tax_computation.get('slab_tax', 0))"/>
<t t-set="rebate_87a" t-value="'{:,.0f}'.format(tax_computation.get('rebate_87a', 0))"/>
<t t-set="cess" t-value="'{:,.0f}'.format(tax_computation.get('cess_4pct', 0))"/>
<t t-set="total_tax" t-value="'{:,.0f}'.format(tax_computation.get('total_tax', 0))"/>
<t t-set="report_time" t-value="data.get('report_time', '')"/>
<!-- Header -->
<div style="text-align: center; margin-bottom: 20px;">
<h2 style="font-weight: bold; margin-bottom: 5px;"><t t-esc="company_name"/></h2>
<h3 style="font-weight: bold; margin-top: 0;">INCOME TAX COMPUTATION STATEMENT</h3>
</div>
<!-- Employee Info -->
<table class="table table-sm" style="width: 100%; margin-bottom: 25px;">
<tr>
<td style="width: 33%;">
<strong>Emp Code:</strong>
<t t-esc="emp_code"/>
</td>
<td style="width: 33%;">
<strong>Name:</strong>
<t t-esc="name"/>
</td>
</tr>
</table>
<table class="table table-sm" style="width: 100%; margin-bottom: 25px;">
<tr>
<td>
<strong>Assessment Year:</strong>
<t t-esc="data.get('assessment_year', '')"/>-
<t t-esc="(data.get('assessment_year', 0) or 0) + 1"/>
</td>
<td>
<strong>Month / Age:</strong>
<t t-esc="month_age"/>
</td>
<td style="width: 34%;">
<strong>PAN:</strong>
<t t-esc="pan"/>
</td>
</tr>
<tr>
<td>
<strong>Date of Joining:</strong>
<t t-esc="joining_date"/>
</td>
<td>
<strong>Leaving Date:</strong>
<t t-esc="leaving_date"/>
</td>
<td>
<strong>Gender:</strong>
<t t-esc="gender"/>
</td>
</tr>
</table>
<!-- Head of Income -->
<h4 style="font-weight: bold; border-bottom: 2px solid #ddd; padding-bottom: 5px; margin-bottom: 15px;">
Head of Income
</h4>
<table class="table table-bordered table-sm" style="width: 100%; margin-bottom: 20px;">
<thead>
<tr style="background-color: #f8f9fa;">
<th style="font-weight: bold; text-align: left;">Description</th>
<th style="font-weight: bold; text-align: right;">Actual</th>
<th style="font-weight: bold; text-align: right;">Projected</th>
<th style="font-weight: bold; text-align: right;">Total</th>
</tr>
</thead>
<tbody>
<tr>
<td>Basic</td>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('basic', {}).get('actual', 0))"/>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('basic', {}).get('projected', 0))"/>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('basic', {}).get('total', 0))"/>
</tr>
<tr>
<td>House Rent</td>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('house_rent', {}).get('actual', 0))"/>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('house_rent', {}).get('projected', 0))"/>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('house_rent', {}).get('total', 0))"/>
</tr>
<tr>
<td>LTA</td>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('lta', {}).get('actual', 0))"/>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('lta', {}).get('projected', 0))"/>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('lta', {}).get('total', 0))"/>
</tr>
<tr>
<td>Special Allowance</td>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('special_allowance', {}).get('actual', 0))"/>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('special_allowance', {}).get('projected', 0))"/>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('special_allowance', {}).get('total', 0))"/>
</tr>
<tr>
<td>Reimbursement</td>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('reimbursement', {}).get('actual', 0))"/>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('reimbursement', {}).get('projected', 0))"/>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('reimbursement', {}).get('total', 0))"/>
</tr>
<tr style="border-top: 2px solid #ddd; font-weight: bold;">
<td>Gross Salary</td>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('gross_salary', {}).get('actual', 0))"/>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('gross_salary', {}).get('projected', 0))"/>
<td style="text-align: right;"
t-esc="'{:,.0f}'.format(salary_components.get('gross_salary', {}).get('total', 0))"/>
</tr>
<tr>
<td>Less: Exemption under section 10</td>
<td colspan="3"></td>
</tr>
<tr style="border-top: 2px solid #ddd; font-weight: bold; margin-bottom: 8px; margin-top: 10px;">
<td>Net Salary</td>
<td colspan="2"></td>
<td style="text-align: right;"
t-esc="net_salary"/>
</tr>
<tr style="border-top: 2px solid #ddd; margin-bottom: 8px; margin-top: 10px;">
<td><strong>Less:</strong> Deduction under section 16</td>
<td colspan="3"></td>
</tr>
<tr style="border-top: 2px solid #ddd; margin-bottom: 8px; margin-top: 10px;">
<td style="padding-left: 30px;">Tax on Employment: Sec 16 - Prof. Tax</td>
<td style="text-align: right;"
t-esc="tax_employment"/>
<td colspan="2"></td>
</tr>
<tr style="border-top: 2px solid #ddd; margin-bottom: 8px; margin-top: 10px;">
<td style="padding-left: 30px;">Less: Standard Deduction for Salaried Employees</td>
<td colspan="2"></td>
<td style="text-align: right;"
t-esc="standard_deduction"/>
</tr>
<tr style="border-top: 2px solid #ddd; margin-bottom: 8px; margin-top: 10px;">
<td style="padding-left: 30px;">Total</td>
<td style="text-align: right;"
t-esc="tax_employment"/>
<td colspan="1"></td>
<td style="text-align: right;"
t-esc="standard_deduction"/>
</tr>
<tr style="border-top: 2px solid #ddd; font-weight: bold; margin-bottom: 8px; margin-top: 10px;">
<td>Other Income</td>
<td colspan="3"></td>
</tr>
</tbody>
</table>
<!-- <div style="margin-bottom: 8px;">-->
<!-- <strong>Net Salary:</strong>-->
<!-- <span style="float: right;" t-esc="net_salary"/>-->
<!-- </div>-->
<!-- <div style="margin-bottom: 8px;">-->
<!-- <strong>Other Income:</strong>-->
<!-- <span style="float: right;" t-esc="other_income"/>-->
<!-- </div>-->
<div style="margin-bottom: 20px; font-weight: bold; border-top: 1px solid #ddd; padding-top: 8px;">
<strong>Gross Total Income:</strong>
<span style="float: right;" t-esc="gross_total_income"/>
</div>
<!-- Deductions -->
<h4 style="border-bottom: 2px #ddd; padding-bottom: 5px; margin-bottom: 15px;">
<strong>Less</strong> Deductions under Chapter VIA
<span style="float: right;">0</span>
</h4>
<!-- <table class="table table-bordered table-sm" style="width: 100%; margin-bottom: 20px;">-->
<!-- <thead>-->
<!-- <tr style="background-color: #f8f9fa;">-->
<!-- <th style="font-weight: bold; text-align: left;">Investment</th>-->
<!-- <th style="font-weight: bold; text-align: left;">Section</th>-->
<!-- <th style="font-weight: bold; text-align: right;">Gross</th>-->
<!-- <th style="font-weight: bold; text-align: right;">Qualifying</th>-->
<!-- <th style="font-weight: bold; text-align: right;">Deductible</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- <tr>-->
<!-- <td>National Pension Scheme (Employer contribution)</td>-->
<!-- <td>80CCD(2)</td>-->
<!-- <td style="text-align: right;" t-esc="'{:,.0f}'.format(deductions.get('nps_employer', 0))"/>-->
<!-- <td style="text-align: right;" t-esc="'{:,.0f}'.format(deductions.get('nps_employer', 0))"/>-->
<!-- <td style="text-align: right;" t-esc="'{:,.0f}'.format(deductions.get('nps_employer', 0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Others</td>-->
<!-- <td>-</td>-->
<!-- <td style="text-align: right;"-->
<!-- t-esc="'{:,.0f}'.format(deductions.get('total_deductions', 0) - deductions.get('nps_employer', 0))"/>-->
<!-- <td style="text-align: right;"-->
<!-- t-esc="'{:,.0f}'.format(deductions.get('total_deductions', 0) - deductions.get('nps_employer', 0))"/>-->
<!-- <td style="text-align: right;"-->
<!-- t-esc="'{:,.0f}'.format(deductions.get('total_deductions', 0) - deductions.get('nps_employer', 0))"/>-->
<!-- </tr>-->
<!-- </tbody>-->
<!-- </table>-->
<!-- Tax Computation -->
<h4 style="font-weight: bold; border-bottom: 2px solid #ddd; padding-bottom: 5px; margin-bottom: 15px;">
Tax Calculation
</h4>
<table class="table table-sm" style="width: 100%; margin-bottom: 20px;">
<tr>
<td style="width: 70%;">
<strong>Taxable Income:</strong>
</td>
<td style="width: 30%; text-align: right;">
<t t-esc="taxable_income"/>
</td>
</tr>
<tr>
<td style="width: 70%;">
Round off to nearest 10 Rupee:
</td>
<td style="width: 30%; text-align: right;">
<t t-esc="roundoff_taxable_income"/>
</td>
</tr>
<tr>
<td>
<strong>Tax Payable on Total Income:</strong>
</td>
<td style="text-align: right;">
<t t-esc="tax_payable"/>
</td>
</tr>
<tr>
<td>
<strong>Less: Rebate u/s 87A (Tax Credit)</strong>
</td>
<td style="text-align: right;">
<t t-esc="rebate_87a"/>
</td>
</tr>
<tr>
<td>
<strong>Add: Surcharge</strong>
</td>
<td style="text-align: right;">
0
</td>
</tr>
<tr>
<td>
<strong>Add: Health and Education Cess:</strong>
</td>
<td style="text-align: right;">
<t t-esc="cess"/>
</td>
</tr>
<tr>
<td>
<strong>Less: Relief under section 89(1)</strong>
</td>
<td style="text-align: right;">
0
</td>
</tr>
<tr>
<td>
<strong>Less: Details of Tax Collected at Source</strong>
</td>
<td style="text-align: right;">
0
</td>
</tr>
<tr style="border-top: 2px solid #ddd; font-weight: bold;">
<td>
<strong>Total Tax Payable:</strong>
</td>
<td style="text-align: right;">
<t t-esc="total_tax"/>
</td>
</tr>
</table>
<!-- Tax Breakdown -->
<h4 style="font-weight: bold; border-bottom: 2px solid #ddd; padding-bottom: 5px; margin-bottom: 15px;">
Tax Deduction Details
</h4>
<table class="table table-bordered table-sm" style="width: 100%; margin-bottom: 20px;">
<thead>
<tr style="background-color: #f8f9fa;">
<th style="font-weight: bold; text-align: left;">Particulars</th>
<th style="font-weight: bold; text-align: right;">Taxable Income</th>
<th style="font-weight: bold; text-align: right;">Income Tax</th>
</tr>
</thead>
<tbody>
<tr>
<td>Total annual income and tax</td>
<td style="text-align: right;" t-esc="taxable_income"/>
<td style="text-align: right;" t-esc="total_tax"/>
</tr>
<tr>
<td>Less: Tax deducted current employer (up to previous month)</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0</td>
</tr>
<tr>
<td>Less: Tax deducted from previous Employer / Self Tax Paid</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0</td>
</tr>
<tr style="font-weight: bold;">
<td>Balance Tax for the year</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;" t-esc="total_tax"/>
</tr>
<tr>
<td>Less: Adhoc tax deducted in Off-Cycle in current month</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0</td>
</tr>
<tr style="font-weight: bold;">
<td><strong>Balance Tax</strong></td>
<td style="text-align: right;">0</td>
<td style="text-align: right;" t-esc="total_tax"/>
</tr>
<tr>
<td><strong>Tax deducted from current month salary</strong></td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0</td>
</tr>
<tr>
<td>Per month Tax (balance / No. of months incl. current month)</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0</td>
</tr>
<tr style="font-weight: bold; border-top: 2px solid #ddd;">
<td>Adhoc Tax to be deducted from current month salary</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;" t-esc="total_tax"/>
</tr>
<tr style="font-weight: bold; border-top: 2px solid #ddd;">
<td><strong>Total Tax deducted from current month salary + adhoc + off-cycle</strong></td>
<td style="text-align: right;">0</td>
<td style="text-align: right;" t-esc="total_tax"/>
</tr>
</tbody>
</table>
<div style="margin-top: 30px; display: flex; justify-content: space-between;">
<div>
<strong>Report Time:</strong>
<t t-esc="report_time"/>
</div>
<div style="text-align: right;">
<strong>User:</strong>
<t t-esc="data.get('user', '')"/>
</div>
</div>
</div>
</t>
</template>
<!-- <template id="generate_income_tax_statement_rpt">-->
<!-- <t t-call="web.basic_layout">-->
<!-- <main class="page">-->
<!-- <t t-set="data" t-value="report_data['data']"/>-->
<!-- <t t-set="profile" t-value="data.get('profile')"/>-->
<!-- <t t-set="regime_info" t-value="data.get('regime_info')"/>-->
<!-- <t t-set="salary_components" t-value="data.get('salary_components')"/>-->
<!-- <t t-set="deductions" t-value="data.get('deductions')"/>-->
<!-- <t t-set="income_details" t-value="data.get('income_details')"/>-->
<!-- <t t-set="taxable_income" t-value="data.get('taxable_income')"/>-->
<!-- <t t-set="tax_computation" t-value="data.get('tax_computation')"/>-->
<!-- <t t-set="regime_used" t-value="data.get('regime_used')"/>-->
<!-- <t t-set="comparison" t-value="data.get('comparison')"/>-->
<!-- &lt;!&ndash; Report Title &ndash;&gt;-->
<!-- <div class="text-center" style="margin-bottom: 25px;">-->
<!-- <h3 style="font-size:20px; font-weight:bold; text-decoration: underline;">INCOME TAX COMPUTATION STATEMENT</h3>-->
<!-- <p style="font-size:14px; margin-top: 5px;">-->
<!-- Assessment Year:-->
<!-- <t t-esc="data.get('assessment_year')"/>-->
<!-- |-->
<!-- Financial Year:-->
<!-- <t t-esc="data.get('financial_year')"/>-->
<!-- |-->
<!-- <t t-esc="regime_info.get('tax_regime')"/>-->
<!-- </p>-->
<!-- </div>-->
<!-- &lt;!&ndash; Employee Info &ndash;&gt;-->
<!-- <table class="table table-sm" style="margin-bottom: 25px; font-size: 13px;">-->
<!-- <tr>-->
<!-- <td><strong>Employee Name</strong></td>-->
<!-- <td><t t-esc="profile.get('name')"/></td>-->
<!-- <td><strong>PAN</strong></td>-->
<!-- <td><t t-esc="profile.get('pan', '')"/></td>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td><strong>Age</strong></td>-->
<!-- <td><t t-esc="profile.get('age')"/></td>-->
<!-- <td><strong>Residential Status</strong></td>-->
<!-- <td><t t-esc="profile.get('residential_status')"/></td>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td><strong>Parent's Age</strong></td>-->
<!-- <td><t t-esc="profile.get('parent_age')"/></td>-->
<!-- <td><strong>User</strong></td>-->
<!-- <td><t t-esc="data.get('user')"/></td>-->
<!-- </tr>-->
<!-- </table>-->
<!-- <table class="table table-bordered table-sm" style="font-size: 13px;">-->
<!-- <tbody>-->
<!-- <tr>-->
<!-- <td>Regime Lock Status</td>-->
<!-- <td>-->
<!-- <t t-esc="regime_info.get('lock_regime')"/>-->
<!-- </td>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Current Tax Regime</td>-->
<!-- <td>-->
<!-- <t t-esc="regime_info.get('tax_regime')"/>-->
<!-- </td>-->
<!-- </tr>-->
<!-- </tbody>-->
<!-- </table>-->
<!-- &lt;!&ndash; Head of Income &ndash;&gt;-->
<!-- <div style="page-break-inside: avoid; margin-bottom:20px;">-->
<!-- <h4 style="background:#f2f2f2; padding:5px; border-left:4px solid #007bff;">Head of Income</h4>-->
<!-- <table class="table table-bordered table-sm" style="font-size: 13px;">-->
<!-- <thead style="background:#e9ecef;">-->
<!-- <tr>-->
<!-- <th>Particulars</th>-->
<!-- <th class="text-right">Actual</th>-->
<!-- <th class="text-right">Projected</th>-->
<!-- <th class="text-right">Total</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- <tr>-->
<!-- <td>Basic</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('basic', {}).get('actual',0))"/>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('basic', {}).get('projected',0))"/>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('basic', {}).get('total',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>House Rent</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('house_rent', {}).get('actual',0))"/>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('house_rent', {}).get('projected',0))"/>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('house_rent', {}).get('total',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>LTA</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('lta', {}).get('actual',0))"/>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('lta', {}).get('projected',0))"/>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('lta', {}).get('total',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Special Allowance</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('special_allowance', {}).get('actual',0))"/>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('special_allowance', {}).get('projected',0))"/>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('special_allowance', {}).get('total',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Perquisites</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('perquisites', {}).get('actual',0))"/>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('perquisites', {}).get('projected',0))"/>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('perquisites', {}).get('total',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Reimbursement</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('reimbursement', {}).get('actual',0))"/>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('reimbursement', {}).get('projected',0))"/>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('reimbursement', {}).get('total',0))"/>-->
<!-- </tr>-->
<!-- <tr style="font-weight:bold; background:#f8f9fa;">-->
<!-- <td>Gross Salary</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('gross_salary', {}).get('actual',0))"/>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('gross_salary', {}).get('projected',0))"/>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(salary_components.get('gross_salary', {}).get('total',0))"/>-->
<!-- </tr>-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- &lt;!&ndash; Deductions &ndash;&gt;-->
<!-- <div style="page-break-inside: avoid; margin-bottom:20px;">-->
<!-- <h4 style="background:#f2f2f2; padding:5px; border-left:4px solid #007bff;">Deductions</h4>-->
<!-- <table class="table table-bordered table-sm" style="font-size: 13px;">-->
<!-- <thead style="background:#e9ecef;">-->
<!-- <tr>-->
<!-- <th>Particulars</th>-->
<!-- <th class="text-right">Amount (₹)</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- <tr>-->
<!-- <td>Professional Tax</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(deductions.get('professional_tax',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Standard Deduction</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(deductions.get('standard_deduction',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>NPS Employer Contribution</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(deductions.get('nps_employer',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>HRA Exemption</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(deductions.get('hra_exemption',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Interest on Home Loan</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(deductions.get('interest_home_loan',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Deduction under 80C</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(deductions.get('ded_80C',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Deduction under 80CCD(1B)</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(deductions.get('ded_80CCD1B',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Deduction under 80D (Self)</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(deductions.get('ded_80D_self',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Deduction under 80D (Parents)</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(deductions.get('ded_80D_parents',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Deduction under 80G</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(deductions.get('ded_80G',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Other Deductions</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(deductions.get('ded_other',0))"/>-->
<!-- </tr>-->
<!-- <tr style="font-weight:bold; background:#f8f9fa;">-->
<!-- <td>Total Deductions</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(deductions.get('total_deductions',0))"/>-->
<!-- </tr>-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- &lt;!&ndash; Income Details &ndash;&gt;-->
<!-- <div style="page-break-inside: avoid; margin-bottom:20px;">-->
<!-- <h4 style="background:#f2f2f2; padding:5px; border-left:4px solid #007bff;">Income Details</h4>-->
<!-- <table class="table table-bordered table-sm" style="font-size: 13px;">-->
<!-- <thead style="background:#e9ecef;">-->
<!-- <tr>-->
<!-- <th>Particulars</th>-->
<!-- <th class="text-right">Amount (₹)</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- <tr>-->
<!-- <td>Gross Salary</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(income_details.get('gross_salary',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Other Income</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(income_details.get('other_income',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>House Property Income</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(income_details.get('house_property_income',0))"/>-->
<!-- </tr>-->
<!-- <tr style="font-weight:bold; background:#f8f9fa;">-->
<!-- <td>Total Income</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(income_details.get('total_income',0))"/>-->
<!-- </tr>-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- &lt;!&ndash; Taxable Income &ndash;&gt;-->
<!-- <div style="page-break-inside: avoid; margin-bottom:20px;">-->
<!-- <h4 style="background:#f2f2f2; padding:5px; border-left:4px solid #007bff;">Taxable Income</h4>-->
<!-- <table class="table table-bordered table-sm" style="font-size: 13px;">-->
<!-- <tbody>-->
<!-- <tr>-->
<!-- <td>Taxable Income (Old Regime)</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(taxable_income.get('old_regime',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Taxable Income (New Regime)</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(taxable_income.get('new_regime',0))"/>-->
<!-- </tr>-->
<!-- <tr style="font-weight:bold; background:#f8f9fa;">-->
<!-- <td>Taxable Income (Current Regime)</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(taxable_income.get('current_regime',0))"/>-->
<!-- </tr>-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- &lt;!&ndash; Tax Regime Comparison &ndash;&gt;-->
<!-- <div style="page-break-inside: avoid; margin-bottom:20px;">-->
<!-- <h4 style="background:#f2f2f2; padding:5px; border-left:4px solid #6f42c1;">Tax Regime Comparison</h4>-->
<!-- <table class="table table-bordered table-sm" style="font-size: 13px;">-->
<!-- <thead style="background:#e9ecef;">-->
<!-- <tr>-->
<!-- <th>Particulars</th>-->
<!-- <th class="text-right">Old Regime (₹)</th>-->
<!-- <th class="text-right">New Regime (₹)</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- <tr>-->
<!-- <td>Total Tax Payable</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(comparison.get('old_regime_tax',0))"/>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(comparison.get('new_regime_tax',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Tax Savings</td>-->
<!-- <td class="text-right" colspan="2" t-esc="'{:,.2f}'.format(comparison.get('tax_savings',0))"/>-->
<!-- </tr>-->
<!-- <tr style="font-weight:bold; background:#f8f9fa;">-->
<!-- <td>Beneficial Regime</td>-->
<!-- <td class="text-center" colspan="2" t-esc="comparison.get('beneficial_regime','').capitalize() + ' Regime'"/>-->
<!-- </tr>-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- &lt;!&ndash; Tax Computation &ndash;&gt;-->
<!-- <div style="page-break-inside: avoid; margin-bottom:20px;">-->
<!-- <h4 style="background:#f2f2f2; padding:5px; border-left:4px solid #dc3545;">Tax Computation</h4>-->
<!-- <table class="table table-bordered table-sm" style="font-size: 13px;">-->
<!-- <tbody>-->
<!-- <tr>-->
<!-- <td>Taxable Income</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(tax_computation.get('taxable_income',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Slab Tax</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(tax_computation.get('slab_tax',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Rebate under Section 87A</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(tax_computation.get('rebate_87a',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Surcharge</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(tax_computation.get('surcharge',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Marginal Relief</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(tax_computation.get('marginal_relief',0))"/>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>Health &amp; Education Cess (4%)</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(tax_computation.get('cess_4pct',0))"/>-->
<!-- </tr>-->
<!-- <tr style="font-weight:bold; background:#f8f9fa;">-->
<!-- <td>Total Tax Payable</td>-->
<!-- <td class="text-right" t-esc="'{:,.2f}'.format(tax_computation.get('total_tax',0))"/>-->
<!-- </tr>-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- </main>-->
<!-- </t>-->
<!-- </template>-->
</odoo>

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<report
id="action_report_it_tax_statement"
model="emp.it.declaration"
string="IT Tax Statement"
report_type="qweb-pdf"
name="your_module_name.report_it_tax_statement"
file="your_module_name.report_it_tax_statement"
print_report_name="'IT_Tax_Statement_%s' % (object.employee_id.name)"
/>
</odoo>

View File

@ -1,65 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_payroll_period_user,payroll.period,model_payroll_period,,1,1,1,1
access_payroll_period_line_user,payroll.period.line,model_payroll_period_line,,1,1,1,1
access_it_investment_type,it.investment.type,model_it_investment_type,,1,1,1,1
access_past_employment_investment_type,past_employment.investment.type,model_past_employment_investment_type,,1,1,1,1
access_us80c_investment_type,us80c.investment.type,model_us80c_investment_type,,1,1,1,1
access_us80d_investment_type,us80d.investment.type,model_us80d_investment_type,,1,1,1,1
access_us10_investment_type,us10.investment.type,model_us10_investment_type,,1,1,1,1
access_us80g_investment_type,us80g.investment.type,model_us80g_investment_type,,1,1,1,1
access_chapter_via_investment_type,chapter.via.investment.type,model_chapter_via_investment_type,,1,1,1,1
access_us17_investment_type,us17.investment.type,model_us17_investment_type,,1,1,1,1
access_other_il_investment_type,other.il.investment.type,model_other_il_investment_type,,1,1,1,1
access_other_declaration_investment_type,other.declaration.investment.type,model_other_declaration_investment_type,,1,1,1,1
access_investment_costings,investment.costings,model_investment_costings,,1,1,1,1
access_past_employment_costing_type,past_employment.costing.type,model_past_employment_costing_type,,1,1,1,1
access_us80c_costing_type,us80c.costing.type,model_us80c_costing_type,,1,1,1,1
access_us80d_costing_type,us80d.costing.type,model_us80d_costing_type,,1,1,1,1
access_us10_costing_type,us10.costing.type,model_us10_costing_type,,1,1,1,1
access_us80g_costing_type,us80g.costing.type,model_us80g_costing_type,,1,1,1,1
access_chapter_via_costing_type,chapter.via.costing.type,model_chapter_via_costing_type,,1,1,1,1
access_us17_costing_type,us17.costing.type,model_us17_costing_type,,1,1,1,1
access_other_il_costing_type,other.il.costing.type,model_other_il_costing_type,,1,1,1,1
access_other_declaration_costing_type,other.declaration.costing.type,model_other_declaration_costing_type,,1,1,1,1
access_emp_it_declaration_user,emp.it.declarations,model_emp_it_declaration,base.group_user,1,1,1,1
access_children_education,access.children.education,model_children_education,base.group_user,1,1,1,1
access_children_education_costing,access.children.education.costing,model_children_education_costing,base.group_user,1,1,1,1
access_us80c_insurance_line,access.us80c.insurance.line,model_us80c_insurance_line,,1,1,1,1
access_employee_life_insurance,access.employee.life.insurance,model_employee_life_insurance,,1,1,1,1
access_nsc_declaration_line_user,nsc.declaration.line,model_nsc_declaration_line,base.group_user,1,1,1,1
access_nsc_entry_user,nsc.entry,model_nsc_entry,base.group_user,1,1,1,1
access_self_occupied_property_user,self.occupied.property,model_self_occupied_property,base.group_user,1,1,1,1
access_letout_house_property_user,access.letout.house.property.user,model_letout_house_property,base.group_user,1,1,1,1
access_nsc_interest_line_user,nsc.interest.line,model_nsc_interest_line,base.group_user,1,1,1,1
access_nsc_interest_entry_user,nsc.interest.entry,model_nsc_interest_entry,base.group_user,1,1,1,1
access_house_rent_declaration_user,access.house.rent.declaration.user,model_house_rent_declaration,base.group_user,1,1,1,1
access_it_tax_statement,it.tax.statement,model_it_tax_statement,base.group_user,1,0,0,0
access_it_tax_statement_wizard,it.tax.statement.wizard,model_it_tax_statement_wizard,base.group_user,1,0,0,0
access_it_tax_statement_manager,it.tax.statement,model_it_tax_statement,hr.group_hr_manager,1,1,1,1
access_it_tax_statement_wizard_manager,it.tax.statement.wizard,model_it_tax_statement_wizard,hr.group_hr_manager,1,1,1,1
access_it_slab_master,it.slab.master,model_it_slab_master,base.group_user,1,1,1,1
access_it_slab_master_rules,it.slab.master.rules,model_it_slab_master_rules,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_payroll_period_user payroll.period model_payroll_period 1 1 1 1
3 access_payroll_period_line_user payroll.period.line model_payroll_period_line 1 1 1 1
4 access_it_investment_type it.investment.type model_it_investment_type 1 1 1 1
5 access_past_employment_investment_type past_employment.investment.type model_past_employment_investment_type 1 1 1 1
6 access_us80c_investment_type us80c.investment.type model_us80c_investment_type 1 1 1 1
7 access_us80d_investment_type us80d.investment.type model_us80d_investment_type 1 1 1 1
8 access_us10_investment_type us10.investment.type model_us10_investment_type 1 1 1 1
9 access_us80g_investment_type us80g.investment.type model_us80g_investment_type 1 1 1 1
10 access_chapter_via_investment_type chapter.via.investment.type model_chapter_via_investment_type 1 1 1 1
11 access_us17_investment_type us17.investment.type model_us17_investment_type 1 1 1 1
12 access_other_il_investment_type other.il.investment.type model_other_il_investment_type 1 1 1 1
13 access_other_declaration_investment_type other.declaration.investment.type model_other_declaration_investment_type 1 1 1 1
14 access_investment_costings investment.costings model_investment_costings 1 1 1 1
15 access_past_employment_costing_type past_employment.costing.type model_past_employment_costing_type 1 1 1 1
16 access_us80c_costing_type us80c.costing.type model_us80c_costing_type 1 1 1 1
17 access_us80d_costing_type us80d.costing.type model_us80d_costing_type 1 1 1 1
18 access_us10_costing_type us10.costing.type model_us10_costing_type 1 1 1 1
19 access_us80g_costing_type us80g.costing.type model_us80g_costing_type 1 1 1 1
20 access_chapter_via_costing_type chapter.via.costing.type model_chapter_via_costing_type 1 1 1 1
21 access_us17_costing_type us17.costing.type model_us17_costing_type 1 1 1 1
22 access_other_il_costing_type other.il.costing.type model_other_il_costing_type 1 1 1 1
23 access_other_declaration_costing_type other.declaration.costing.type model_other_declaration_costing_type 1 1 1 1
24 access_emp_it_declaration_user emp.it.declarations model_emp_it_declaration base.group_user 1 1 1 1
25 access_children_education access.children.education model_children_education base.group_user 1 1 1 1
26 access_children_education_costing access.children.education.costing model_children_education_costing base.group_user 1 1 1 1
27 access_us80c_insurance_line access.us80c.insurance.line model_us80c_insurance_line 1 1 1 1
28 access_employee_life_insurance access.employee.life.insurance model_employee_life_insurance 1 1 1 1
29 access_nsc_declaration_line_user nsc.declaration.line model_nsc_declaration_line base.group_user 1 1 1 1
30 access_nsc_entry_user nsc.entry model_nsc_entry base.group_user 1 1 1 1
31 access_self_occupied_property_user self.occupied.property model_self_occupied_property base.group_user 1 1 1 1
32 access_letout_house_property_user access.letout.house.property.user model_letout_house_property base.group_user 1 1 1 1
33 access_nsc_interest_line_user nsc.interest.line model_nsc_interest_line base.group_user 1 1 1 1
34 access_nsc_interest_entry_user nsc.interest.entry model_nsc_interest_entry base.group_user 1 1 1 1
35 access_house_rent_declaration_user access.house.rent.declaration.user model_house_rent_declaration base.group_user 1 1 1 1
36 access_it_tax_statement it.tax.statement model_it_tax_statement base.group_user 1 0 0 0
37 access_it_tax_statement_wizard it.tax.statement.wizard model_it_tax_statement_wizard base.group_user 1 0 0 0
38 access_it_tax_statement_manager it.tax.statement model_it_tax_statement hr.group_hr_manager 1 1 1 1
39 access_it_tax_statement_wizard_manager it.tax.statement.wizard model_it_tax_statement_wizard hr.group_hr_manager 1 1 1 1
40 access_it_slab_master it.slab.master model_it_slab_master base.group_user 1 1 1 1
41 access_it_slab_master_rules it.slab.master.rules model_it_slab_master_rules base.group_user 1 1 1 1

View File

@ -1,355 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<record id="emp_it_declaration_list" model="ir.ui.view">
<field name="name">emp.it.declaration.list</field>
<field name="model">emp.it.declaration</field>
<field name="arch" type="xml">
<list>
<field name="period_id"/>
<field name="total_investment"/>
<field name="tax_regime"/>
</list>
</field>
</record>
<record id="view_emp_it_declaration_form" model="ir.ui.view">
<field name="name">emp.it.declarations.form</field>
<field name="model">emp.it.declaration</field>
<field name="arch" type="xml">
<form string="IT Declaration">
<sheet>
<div class="oe_title mb24">
<div class="o_row">
<field name="employee_id" widget="res_partner_many2one" placeholder="Employee Name..." readonly="costing_details_generated"/>
</div>
</div>
<group>
<group>
<field name="period_id" readonly="costing_details_generated"/>
</group>
<group>
<field name="total_investment"/>
<field name="costing_details_generated" invisible="1" force_save="1"/>
<field name="house_rent_costing_id"/>
</group>
</group>
<br/>
<br/>
<field name="tax_regime" nolabel="1" widget="radio" options="{'horizontal': true}"/>
<br/>
<button name="generate_declarations" type="object" class="btn-primary" string="Generate" confirm="Upon Confirming you won't be able to change the Period &amp; Employee" help="Generate Data to upload the declaration Costing" invisible="costing_details_generated"/>
<field name="is_section_open" invisible="1"/> <!-- Store toggle state -->
<group invisible="not costing_details_generated">
<!-- Styled full-width button -->
<div style="background-color: #1167A8; padding: 10px; color: white; font-weight: bold; display: flex; justify-content: space-between; align-items: center; cursor: pointer;">
<span><field name="display_period_label" readonly="1" nolabel="1" style="color: white; font-weight: bold;"/></span>
<button name="toggle_section_visibility"
type="object"
style="background: none; border: none; color: white; font-size: 16px;"
icon="fa-chevron-up"/>
</div>
</group>
<group>
<!-- Collapsible section -->
<div invisible="not is_section_open">
<notebook>
<page string="Total Investment Costing">
<group>
<group>
<field name="investment_costing_ids" nolabel="1">
<list editable="bottom" create="0" delete="0" edit="0">
<field name="investment_type_id"/>
<field name="amount"/>
</list>
</field>
</group>
<group>
</group>
</group>
</page>
</notebook>
</div>
</group>
<notebook invisible="not costing_details_generated">
<!-- <page name="investment_costings" string="Total Investment">-->
<!-- <field name="investment_costing_ids" readonly="1" force_save="1">-->
<!-- <list editable="bottom" create="0" delete="0" edit="0">-->
<!-- <field name="investment_type_id" width="50%"/>-->
<!-- <field name="amount" width="50%" class="text-start"/>-->
<!-- </list>-->
<!-- </field>-->
<!-- </page>-->
<page name="past_employment_costings" string="PAST EMPLOYMENT">
<field name="past_employment_costings" invisible="tax_regime != 'old'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
<field name="remarks" width="250px"/>
<field name="proof" width="120px"/>
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
<field name="past_employment_costings_new" invisible="tax_regime != 'new'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
<field name="remarks" width="250px"/>
<field name="proof" width="120px"/>
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
</page>
<page name="us_80c_costings" string="US 80C" invisible="tax_regime != 'old'">
<field name="us80c_costings">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
<field name="action_id" column_invisible="1"/>
<button name="open_action_wizard"
string="Action"
type="object"
icon="fa-external-link"
invisible="not action_id" width="60px"/>
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
<field name="remarks" width="200px"/>
<field name="proof" width="100px"/>
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
</page>
<page name="us_80d_costings" string="US 80D" invisible="tax_regime != 'old'">
<group>
<field name="us80d_selection_type" widget="radio" options="{'horizontal': true}" required="tax_regime == 'old' and costing_details_generated"/>
<field name="us80d_health_checkup"/>
</group>
<field name="us80d_costings" invisible="us80d_selection_type != 'self_family'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
<field name="remarks" width="250px"/>
<field name="proof" width="120px"/>
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
<field name="us80d_costings_parents" invisible="us80d_selection_type != 'self_family_parent'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
<field name="remarks" width="250px"/>
<field name="proof" width="120px"/>
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
<field name="us80d_costings_senior_parents" invisible="us80d_selection_type != 'self_family_senior_parent'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
<field name="remarks" width="250px"/>
<field name="proof" width="120px"/>
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
</page>
<page name="us_10_costing" string="US 10" invisible="tax_regime != 'old'">
<field name="us10_costings">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
<field name="remarks" width="250px"/>
<field name="proof" width="120px"/>
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
</page>
<page name="us_80g_costing" string="US 80G" invisible="tax_regime != 'old'">
<field name="us80g_costings">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
<field name="remarks" width="250px"/>
<field name="proof" width="120px"/>
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
</page>
<page name="chapter_via_costings" string="CHAPTER VIA">
<field name="chapter_via_costings" invisible="tax_regime != 'old'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
<field name="remarks" width="250px"/>
<field name="proof" width="120px"/>
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
<field name="chapter_via_costings_new" invisible="tax_regime != 'new'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
<field name="remarks" width="250px"/>
<field name="proof" width="120px"/>
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
</page>
<page name="us_17_costings" string="US 17" invisible="tax_regime != 'old'">
<field name="us17_costings">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
<field name="remarks" width="250px"/>
<field name="proof" width="120px"/>
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
</page>
<page name="house_rent_costings" string="HOUSE RENT" invisible="tax_regime != 'old'">
<!-- <field name="house_rent_costing_line_ids"/>-->
<field name="house_rent_costings" context="{
'default_costing_type': house_rent_costing_id
}">
<list string="House Rent Declarations">
<field name="hra_exemption_type"/>
<field name="rent_amount"/>
<field name="from_date"/>
<field name="to_date"/>
<field name="landlord_pan_status"/>
</list>
<form string="House Rent Declaration" class="o_form">
<sheet>
<group>
<field name="it_declaration_id" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="costing_type" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
</group>
<group>
<group>
<field name="hra_exemption_type" widget="radio"/>
<field name="landlord_pan_status" widget="radio"/>
</group>
</group>
<notebook>
<page string="Rent Details">
<group col="2">
<field name="rent_amount"/>
<field name="from_date"/>
<field name="to_date"/>
</group>
<field name="remarks" placeholder="Any additional information..."/>
</page>
<page string="Landlord Information">
<group col="2">
<field name="landlord_pan_no" invisible="landlord_pan_status == 'declaration'" required="landlord_pan_status == 'has_pan'"/>
<field name="landlord_name_address"/>
</group>
</page>
<page string="Proof Attachment">
<group>
<field name="attachment" filename="attachment_filename"/>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</page>
<page name="other_i_or_l_costings" string="OTHER INCOME/LOSS">
<field name="other_il_costings" invisible="tax_regime != 'old'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
<field name="action_id" column_invisible="1" force_save="1"/>
<button name="open_action_wizard"
string="Action"
type="object"
icon="fa-external-link"
invisible="not action_id" width="60px"/>
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
<field name="remarks" width="200px"/>
<field name="proof" width="100px"/>
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
<field name="other_il_costings_new" invisible="tax_regime != 'new'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
<field name="action_id" column_invisible="1" force_save="1"/>
<button name="open_action_wizard"
string="Action"
type="object"
icon="fa-external-link"
invisible="not action_id" width="60px"/>
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
<field name="remarks" width="200px"/>
<field name="proof" width="100px"/>
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
</page>
<page name="other_declaration_costings" string="Other Declarations" invisible="tax_regime != 'old'">
<field name="other_declaration_costings">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
<field name="remarks" width="250px"/>
<field name="proof" width="120px"/>
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_emp_it_declaration" model="ir.actions.act_window">
<field name="name">IT Declarations</field>
<field name="path">income-tax-declaration</field>
<field name="res_model">emp.it.declaration</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_it_declarations" name="IT Declarations"
parent="hr_payroll.menu_hr_payroll_root"
action="action_emp_it_declaration" sequence="99"/>
</data>
</odoo>

View File

@ -1,156 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<record id="view_it_investment_type_list" model="ir.ui.view">
<field name="name">it.investment.type.list</field>
<field name="model">it.investment.type</field>
<field name="arch" type="xml">
<list>
<field name="sequence" widget="handle"/>
<field name="investment_type"/>
<field name="active"/>
</list>
</field>
</record>
<record id="view_it_investment_type_form" model="ir.ui.view">
<field name="name">it.investment.type.form</field>
<field name="model">it.investment.type</field>
<field name="arch" type="xml">
<form string="Investment Type">
<sheet>
<group>
<field name="sequence" invisible="1"/>
<field name="investment_type"/>
<field name="active"/>
</group>
<notebook>
<page string="Past Employment" invisible="investment_type != 'past_employment'">
<field name="past_employment_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="tax_regime"/>
<field name="investment_code"/>
<field name="compute_method"/>
<field name="compute_code"/>
<field name="limit"/>
</list>
</field>
</page>
<page string="US 80C" invisible="investment_type != 'us_80c'">
<field name="us80c_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="tax_regime"/>
<field name="investment_code"/>
<field name="compute_method"/>
<field name="compute_code"/>
<field name="require_action"/>
<field name="action_id" readonly="not require_action" required="require_action"/>
<field name="limit"/>
</list>
</field>
</page>
<page string="US 80D" invisible="investment_type != 'us_80d'">
<field name="us80d_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="tax_regime"/>
<field name="for_family" string="Family"/>
<field name="for_parents" string="Parents"/>
<field name="for_senior_parent" string="Senior Parents"/>
<field name="limit"/>
</list>
</field>
</page>
<page string="US 10" invisible="investment_type != 'us_10'">
<field name="us10_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="tax_regime"/>
<field name="limit"/>
</list>
</field>
</page>
<page string="US 80G" invisible="investment_type != 'us_80g'">
<field name="us80g_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="tax_regime"/>
<field name="limit"/>
</list>
</field>
</page>
<page string="Chapter VIA" invisible="investment_type != 'chapter_via'">
<field name="chapter_via_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="tax_regime"/>
<field name="limit"/>
</list>
</field>
</page>
<page string="US 17" invisible="investment_type != 'us_17'">
<field name="us17_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="tax_regime"/>
<field name="limit"/>
</list>
</field>
</page>
<page string="Other Income or Loss" invisible="investment_type != 'other_i_or_l'">
<field name="other_il_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="tax_regime"/>
<field name="require_action"/>
<field name="action_id" readonly="not require_action" required="require_action"/>
<field name="limit"/>
</list>
</field>
</page>
<page string="Other Declaration" invisible="investment_type != 'other_declaration'">
<field name="other_declaration_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="tax_regime"/>
<field name="limit"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Action -->
<record id="action_it_investment_type" model="ir.actions.act_window">
<field name="name">Investment Types</field>
<field name="res_model">it.investment.type</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create the different Investment Types here.
</p>
</field>
</record>
<menuitem id="menu_it_investment_type" name="Investment Types"
parent="menu_it_payroll_declarations"
action="action_it_investment_type" sequence="20"/>
</data>
</odoo>

View File

@ -1,103 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- <menuitem id="menu_it_tax_statement_root" name="IT Tax Statement" parent="hr.menu_hr_root" sequence="100"/>-->
<record id="view_it_tax_statement_wizard_list" model="ir.ui.view">
<field name="name">it.tax.statement.wizard.list</field>
<field name="model">it.tax.statement.wizard</field>
<field name="arch" type="xml">
<list>
<field name="employee_id"/>
<field name="period_id"/>
<field name="period_line"/>
<field name="tax_regime"/>
</list>
</field>
</record>
<record id="view_it_tax_statement_wizard_form" model="ir.ui.view">
<field name="name">it.tax.statement.wizard.form</field>
<field name="model">it.tax.statement.wizard</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_generate_report"
string="Generate Tax Statement"
type="object"
class="oe_stat_button"
icon="fa-file-text"/>
</header>
<sheet>
<group>
<field name="employee_id" options="{'no_edit': True, 'no_create': True}"/>
<field name="contract_id" readonly="1" force_save="1" invisible="0"/>
</group>
<group>
<field name="taxpayer_age"/>
<field name="residential_status"/>
<field name="parent_age"/>
<field name="emp_doj" force_save="1" invisible="0" readonly="1"/>
</group>
<group>
<group>
<field name="period_id" options="{'no_edit': True, 'no_create': True, 'no_open': True}"/>
</group>
<group>
<field name="period_line" domain="[('period_id', '=', period_id),('to_date','&lt;',datetime.datetime.now()),('from_date','&gt;',emp_doj)]" options="{'no_edit': True, 'no_create': True, 'no_open': True}" invisible="not emp_doj"/>
<field name="period_line" domain="[('period_id', '=', period_id),('to_date','&lt;',datetime.datetime.now())]" options="{'no_edit': True, 'no_create': True, 'no_open': True}" invisible="emp_doj"/>
</group>
</group>
<group>
<field name="tax_regime" nolabel="1" widget="radio" options="{'horizontal': true}" on_change="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_it_tax_statement_wizard" model="ir.actions.act_window">
<field name="name">Generate Tax Statement</field>
<field name="res_model">it.tax.statement.wizard</field>
<field name="path">tax-statement</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new employment type
</p>
</field>
</record>
<menuitem id="menu_it_tax_statement_root" name="IT Tax Statement"
parent="hr_payroll.menu_hr_payroll_root"
action="action_it_tax_statement_wizard" sequence="99"/>
<record id="it_statement_paper_format" model="report.paperformat">
<field name="name">A4 - statement</field>
<field name="default" eval="True"/>
<field name="format">A4</field>
<field name="page_height">0</field>
<field name="page_width">0</field>
<field name="orientation">Portrait</field>
<field name="margin_top">10</field>
<field name="margin_bottom">10</field>
<field name="margin_left">7</field>
<field name="margin_right">7</field>
<field name="header_line" eval="False"/>
<field name="header_spacing">10</field>
<field name="dpi">90</field>
</record>
<record id="income_tax_statement_action_report" model="ir.actions.report">
<field name="name">Download Income Tax</field>
<field name="model">it.tax.statement.wizard</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">employee_it_declaration.generate_income_tax_statement_rpt</field>
<field name="report_file">employee_it_declaration.generate_income_tax_statement_rpt</field>
<field name="binding_model_id" ref="employee_it_declaration.model_it_tax_statement_wizard"/>
<field name="print_report_name">'INCOMETAX - %s' % (object.display_name)</field>
<field name="paperformat_id" ref="it_statement_paper_format"/>
<field name="binding_type">report</field>
</record>
</odoo>

View File

@ -1,49 +0,0 @@
<odoo>
<record id="view_payroll_period_form" model="ir.ui.view">
<field name="name">payroll.period.form</field>
<field name="model">payroll.period</field>
<field name="arch" type="xml">
<form string="Payroll Period">
<header>
<button name="action_generate_month_lines" type="object" string="Generate Periods" class="btn-primary"/>
</header>
<sheet>
<group>
<field name="from_date"/>
<field name="to_date"/>
<field name="name" readonly="1" force_save="1"/>
</group>
<field name="period_line_ids">
<list editable="bottom">
<field name="name"/>
<field name="from_date"/>
<field name="to_date"/>
</list>
</field>
</sheet>
</form>
</field>
</record>
<record id="view_payroll_period_tree" model="ir.ui.view">
<field name="name">payroll.period.tree</field>
<field name="model">payroll.period</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="from_date"/>
<field name="to_date"/>
</list>
</field>
</record>
<record id="action_payroll_period" model="ir.actions.act_window">
<field name="name">Payroll Periods</field>
<field name="res_model">payroll.period</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_it_payroll_declarations" name="Payroll" parent="hr_payroll.menu_hr_payroll_configuration"/>
<menuitem id="menu_payroll_period" name="Payroll Periods" parent="menu_it_payroll_declarations" action="action_payroll_period"/>
</odoo>

View File

@ -1,28 +0,0 @@
<odoo>
<template id="report_it_tax_statement">
<t t-call="web.html_container">
<t t-call="web.external_layout">
<div class="page">
<h2>IT Tax Statement</h2>
<p><strong>Employee:</strong> <t t-esc="doc.employee_id.name"/></p>
<p><strong>Period:</strong> <t t-esc="doc.period_id.name"/></p>
<p><strong>Tax Regime:</strong> <t t-esc="dict(doc._fields['tax_regime'].selection).get(doc.tax_regime)"/></p>
<table class="table table-sm">
<thead>
<tr><th>Section</th><th>Amount</th></tr>
</thead>
<tbody>
<t t-foreach="doc.investment_costing_ids" t-as="line">
<tr>
<td><t t-esc="line.investment_type_id.name"/></td>
<td><t t-esc="line.amount"/></td>
</tr>
</t>
</tbody>
</table>
<p><strong>Total:</strong> <t t-esc="sum(doc.investment_costing_ids.mapped('amount'))"/></p>
</div>
</t>
</t>
</template>
</odoo>

View File

@ -1,70 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Menu Items -->
<menuitem id="menu_it_slab_controller" name="Slab" parent="hr_payroll.menu_hr_payroll_configuration"/>
<!-- Window Action -->
<record id="action_it_slab_master" model="ir.actions.act_window">
<field name="name">Income Tax Slabs</field>
<field name="path">slab-master</field>
<field name="res_model">it.slab.master</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_it_slab_master_item"
name="Slab Master"
parent="menu_it_slab_controller"
action="action_it_slab_master"/>
<!-- list View -->
<record id="view_it_slab_master_list" model="ir.ui.view">
<field name="name">it.slab.master.list</field>
<field name="model">it.slab.master</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="regime"/>
<field name="age_category"/>
<field name="residence_type"/>
<field name="standard_deduction"/>
<field name="active"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="view_it_slab_master_form" model="ir.ui.view">
<field name="name">it.slab.master.form</field>
<field name="model">it.slab.master</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name"/>
<field name="regime"/>
<field name="age_category"/>
<field name="residence_type"/>
<field name="standard_deduction"/>
<field name="active"/>
</group>
<notebook>
<page string="Slab Rules">
<field name="rules">
<list editable="bottom">
<field name="min_income"/>
<field name="max_income"/>
<field name="tax_rate"/>
<field name="fixed_amount"/>
<field name="excess_threshold"/>
<field name="surcharge_rate"/>
<field name="cess_rate"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
</odoo>

View File

@ -1,6 +0,0 @@
from . import children_education_costing
from . import employee_life_insurance
from . import nsc_declaration
from . import self_occupied_property
from . import letout_house_property
from . import nsc_income_loss

View File

@ -1,48 +0,0 @@
from odoo import models, fields, api
class ChildrenEducation(models.Model):
_name = "children.education"
_description = "Children Education"
_rec_name = 'it_declaration_id'
it_declaration_id = fields.Many2one('emp.it.declaration')
children_ids = fields.One2many(
'children.education.costing',
'child_education_id',
string="Children",
)
total_count = fields.Integer(string="Total Tuition Fee", compute="_compute_total_count", store=True)
us80c_id = fields.Many2one('us80c.costing.type')
@api.depends('children_ids.tuition_fee')
def _compute_total_count(self):
for rec in self:
rec.total_count = sum(child.tuition_fee for child in rec.children_ids)
@api.model
def create(self, vals):
record = super().create(vals)
context = self._context or {}
if not record.children_ids:
record.children_ids = [
(0, 0, {'child_id': 'CHILD 1'}),
(0, 0, {'child_id': 'CHILD 2'}),
]
return record
# def write(self, vals):
# print(vals)
# self.us80c_id.declaration_amount = self.total_count if self.us80c_id.limit > self.total_count else self.us80c_id.limit
# return super().write(vals)
class ChildrenEducationCosting(models.Model):
_name = 'children.education.costing'
_description = "Children Education Costing"
child_id = fields.Char('Child ID')
name = fields.Char('Name of Child')
chile_class = fields.Char('Class / Grade')
organization = fields.Char('School / College')
tuition_fee = fields.Integer('Tuition Fee')
child_education_id = fields.Many2one('children.education', string="Education Record", ondelete='cascade')

View File

@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- children_education_views.xml -->
<odoo>
<record id="view_children_education_form" model="ir.ui.view">
<field name="name">children.education.form</field>
<field name="model">children.education</field>
<field name="arch" type="xml">
<form string="Children Education">
<sheet>
<group>
<field name="it_declaration_id" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="us80c_id" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="total_count" readonly="1"/>
</group>
<field name="children_ids" nolabel="1">
<list editable="bottom" create="0" delete="0">
<field name="child_id" readonly="1" force_save="1"/>
<field name="name"/>
<field name="chile_class"/>
<field name="organization"/>
<field name="tuition_fee"/>
</list>
</field>
</sheet>
</form>
</field>
</record>
<record id="view_children_education_list" model="ir.ui.view">
<field name="name">children.education.list</field>
<field name="model">children.education</field>
<field name="arch" type="xml">
<list>
<field name="it_declaration_id"/>
<field name="total_count"/>
</list>
</field>
</record>
<record id="action_children_education" model="ir.actions.act_window">
<field name="name">Children Education</field>
<field name="res_model">children.education</field>
<field name="view_mode">form</field>
</record>
</odoo>

View File

@ -1,52 +0,0 @@
from odoo import models, fields, api, _
class US80CInsuranceLine(models.Model):
_name = 'us80c.insurance.line'
_description = 'US80C Insurance Line'
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
us80c_id = fields.Many2one('us80c.costing.type', string="80C Costing Type")
life_insurance_ids = fields.One2many(
'employee.life.insurance',
'parent_id',
string="Life Insurance Entries"
)
total_premium_amount = fields.Float(string="Total Premium", compute='_compute_totals', store=True)
total_capital_sum_assured = fields.Float(string="Total Capital Sum Assured", compute='_compute_totals', store=True)
total_max_percentage = fields.Float(string="Total Max %", compute='_compute_totals', store=True)
total_exempt_amount = fields.Float(string="Total Exempt Amount", compute='_compute_totals', store=True)
@api.depends('life_insurance_ids')
def _compute_totals(self):
for rec in self:
rec.total_premium_amount = sum(line.premium_amount for line in rec.life_insurance_ids)
rec.total_capital_sum_assured = sum(line.capital_sum_assured for line in rec.life_insurance_ids)
rec.total_max_percentage = sum(line.max_percentage for line in rec.life_insurance_ids)
rec.total_exempt_amount = sum(line.exempt_amount for line in rec.life_insurance_ids)
class EmployeeLifeInsurance(models.Model):
_name = 'employee.life.insurance'
_description = 'Employee Life Insurance'
parent_id = fields.Many2one('us80c.insurance.line', string="Parent Line") # Link to parent
name_of_insurance_company = fields.Char(string="Name of Insurance Company")
insured_in_favour_of = fields.Selection([
('self', 'Self'),
('spouse', 'Spouse'),
('child', 'Child'),
('dependent', 'Dependent'),
], string="Insured in Favour of")
name_of_insured = fields.Char(string="Name of Insured")
policy_number = fields.Char(string="Policy Number")
premium_amount = fields.Float(string="Premium Amount")
payment_date = fields.Date(string="Payment Date")
capital_sum_assured = fields.Float(string="Capital Sum Assured")
policy_date = fields.Date(string="Policy Date")
max_percentage = fields.Float(string="Max Percentage")
exempt_amount = fields.Float(string="Exempt Amount")

View File

@ -1,57 +0,0 @@
<odoo>
<record id="view_us80c_insurance_line_form" model="ir.ui.view">
<field name="name">us80c.insurance.line.form</field>
<field name="model">us80c.insurance.line</field>
<field name="arch" type="xml">
<form string="US80C Insurance Line">
<group>
<field name="it_declaration_id" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="us80c_id" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
</group>
<field name="life_insurance_ids">
<list editable="bottom">
<field name="name_of_insurance_company" width="200px"/>
<field name="insured_in_favour_of" width="100px"/>
<field name="name_of_insured" width="150px"/>
<field name="policy_number" width="100px"/>
<field name="premium_amount" width="70px"/>
<field name="payment_date" width="70px"/>
<field name="capital_sum_assured" width="80px"/>
<field name="policy_date" width="70px"/>
<field name="max_percentage" width="70px"/>
<field name="exempt_amount" width="70px"/>
</list>
</field>
<group>
<group>
</group>
<group>
<field name="total_premium_amount" readonly="1"/>
<field name="total_capital_sum_assured" readonly="1"/>
<field name="total_max_percentage" readonly="1"/>
<field name="total_exempt_amount" readonly="1"/>
</group>
</group>
</form>
</field>
</record>
<record id="view_us80c_insurance_line_list" model="ir.ui.view">
<field name="name">us80c.insurance.line.list</field>
<field name="model">us80c.insurance.line</field>
<field name="arch" type="xml">
<list string="Insurance Lines">
<field name="it_declaration_id"/>
<field name="us80c_id"/>
</list>
</field>
</record>
<record id="action_us80c_insurance_line" model="ir.actions.act_window">
<field name="name">Life Insurance</field>
<field name="res_model">us80c.insurance.line</field>
<field name="view_mode">form</field>
</record>
</odoo>

View File

@ -1,43 +0,0 @@
from odoo import models, fields, api
import math
class LetoutHouseProperty(models.Model):
_name = 'letout.house.property'
_description = 'Letout House Property Details'
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
other_il_id = fields.Many2one('other.il.costing.type', string="Other Income/Loss Costing Type")
address = fields.Char(string="Address of Property")
loan_after_april_1999 = fields.Selection([('yes','Yes'),('no','No')],default='yes',
string="House Loan Taken After 01-Apr-1999 & Construction Completed Within 3 Years"
)
rent_received = fields.Integer(string="Rent Received Per Annum")
period_from = fields.Date(string="Period From")
period_to = fields.Date(string="Period To")
property_tax = fields.Integer(string="Less : Property Tax")
water_tax = fields.Integer(string="Less : Water Tax")
net_annual_value = fields.Integer(string="Net Annual Value of the Property", readonly=True)
deduction_for_repairs = fields.Integer(string="Less : Deduction for Repairs 30% if Net Annual Value of the Property", readonly=True)
interest_paid_to = fields.Integer(string="Less : Interest on Borrowed Capital Paid To")
income_loss = fields.Integer(string="Income / Loss on House Property", readonly=True)
lender_name = fields.Char(string="Lender Name")
lender_pan = fields.Char(string="Lender PAN")
@api.onchange('rent_received','property_tax','water_tax','interest_paid_to')
def onchange_property_water_income_tax(self):
for record in self:
net_annual_value = record.rent_received - record.property_tax - record.water_tax
deduction = net_annual_value * 0.30
income_loss = net_annual_value - deduction - record.interest_paid_to
print(net_annual_value)
print(deduction)
print(income_loss)
record.net_annual_value = net_annual_value
record.deduction_for_repairs = round(deduction)
record.income_loss = round(income_loss)

View File

@ -1,91 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_letout_house_property_form" model="ir.ui.view">
<field name="name">letout.house.property.form</field>
<field name="model">letout.house.property</field>
<field name="arch" type="xml">
<form string="Letout House Property">
<sheet>
<!-- Header Section -->
<group>
<group>
<field name="it_declaration_id" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
</group>
<group>
<field name="other_il_id" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
</group>
</group>
<!-- Property Information -->
<separator string="Property Information" colspan="2"/>
<group>
<field name="address" required="1"/>
</group>
<group>
<div class="oe_row">
<label for="loan_after_april_1999"
string="House Loan Taken After 01-Apr-1999 &amp; Construction Completed Within 3 Years &amp;nbsp;"/>
<field name="loan_after_april_1999" nolabel="1" widget="radio"
options="{'horizontal': true}" class="ml-2" required="1"/>
</div>
</group>
<group>
<!-- Rental and Period Info -->
<group>
<separator string="Rental and Period Info"/>
<field name="rent_received"/>
<field name="period_from" required="1"/>
<field name="period_to" required="1"/>
<br/>
<separator string="Lender Information"/>
<field name="lender_name"/>
<field name="lender_pan"/>
</group>
<!-- Deductions & Value -->
<group string="Deductions and Property Value">
<field name="property_tax"/>
<field name="water_tax"/>
<field name="net_annual_value" readonly="1" force_save="1"/>
<field name="deduction_for_repairs" readonly="1" force_save="1"/>
<field name="interest_paid_to"/>
<field name="income_loss" readonly="1" force_save="1"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_letout_house_property_list" model="ir.ui.view">
<field name="name">letout.house.property.list</field>
<field name="model">letout.house.property</field>
<field name="arch" type="xml">
<list string="Letout House Property">
<field name="address"/>
<field name="rent_received"/>
<field name="net_annual_value"/>
<field name="income_loss"/>
</list>
</field>
</record>
<record id="action_letout_house_property" model="ir.actions.act_window">
<field name="name">Letout House Property</field>
<field name="res_model">letout.house.property</field>
<field name="view_mode">form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a new Letout House Property entry
</p>
</field>
</record>
</odoo>

View File

@ -1,27 +0,0 @@
from odoo import models, fields, api
class NSCDeclarationLine(models.Model):
_name = 'nsc.declaration.line'
_description = 'NSC Declaration Line'
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration", required=True)
us80c_id = fields.Many2one('us80c.costing.type', string="80C Costing Type", required=True)
nsc_entry_ids = fields.One2many('nsc.entry', 'parent_id', string="NSC Entries")
total_nsc_amount = fields.Float(string="Total NSC Amount", compute="_compute_total_amount", store=True)
@api.depends('nsc_entry_ids.nsc_amount')
def _compute_total_amount(self):
for rec in self:
rec.total_nsc_amount = sum(entry.nsc_amount for entry in rec.nsc_entry_ids)
class NSCEntry(models.Model):
_name = 'nsc.entry'
_description = 'NSC Entry'
parent_id = fields.Many2one('nsc.declaration.line', string="NSC Declaration")
nsc_number = fields.Char(string="NSC Number")
nsc_amount = fields.Float(string="NSC Amount")
nsc_payment_date = fields.Date(string="NSC Payment Date")

View File

@ -1,52 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_nsc_declaration_line_form" model="ir.ui.view">
<field name="name">nsc.declaration.line.form</field>
<field name="model">nsc.declaration.line</field>
<field name="arch" type="xml">
<form string="NSC Declaration">
<sheet>
<group>
<field name="it_declaration_id" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="us80c_id" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
</group>
<field name="nsc_entry_ids">
<list editable="bottom">
<field name="nsc_number" width="500px"/>
<field name="nsc_amount"/>
<field name="nsc_payment_date"/>
</list>
</field>
<group>
<group>
</group>
<group>
<field name="total_nsc_amount" readonly="1"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_nsc_declaration_line_list" model="ir.ui.view">
<field name="name">nsc.declaration.line.list</field>
<field name="model">nsc.declaration.line</field>
<field name="arch" type="xml">
<list>
<field name="it_declaration_id"/>
<field name="us80c_id"/>
<field name="total_nsc_amount"/>
</list>
</field>
</record>
<record id="action_nsc_declaration_line" model="ir.actions.act_window">
<field name="name">NSC Declarations</field>
<field name="res_model">nsc.declaration.line</field>
<field name="view_mode">form</field>
</record>
</odoo>

View File

@ -1,32 +0,0 @@
from odoo import models, fields, api
class NSCInterestLine(models.Model):
_name = 'nsc.interest.line'
_description = 'NSC Interest Line'
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration", required=True)
other_il_id = fields.Many2one('other.il.costing.type', string="Other Income/Loss Costing Type")
nsc_entry_ids = fields.One2many('nsc.interest.entry', 'parent_id', string="NSC Entries")
total_nsc_amount = fields.Integer(string="Total NSC Amount", compute="_compute_total_amount", store=True)
total_nsc_interest_amount = fields.Integer(string="NSC Interest Amount",compute="_compute_total_amount", store=True)
@api.depends('nsc_entry_ids.nsc_amount')
def _compute_total_amount(self):
for rec in self:
rec.total_nsc_amount = sum(entry.nsc_amount for entry in rec.nsc_entry_ids)
rec.total_nsc_interest_amount = sum(entry.nsc_interest_amount for entry in rec.nsc_entry_ids)
class NSCEntry(models.Model):
_name = 'nsc.interest.entry'
_description = 'NSC Entry'
parent_id = fields.Many2one('nsc.interest.line', string="NSC Interest")
nsc_number = fields.Char(string="NSC Number")
nsc_amount = fields.Integer(string="NSC Amount")
nsc_payment_date = fields.Date(string="NSC Payment Date")
nsc_interest_amount = fields.Integer(string="NSC Interest Amount")

View File

@ -1,53 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_nsc_interest_line_form" model="ir.ui.view">
<field name="name">nsc.interest.line.form</field>
<field name="model">nsc.interest.line</field>
<field name="arch" type="xml">
<form string="National Saving Certificate">
<sheet>
<group>
<field name="it_declaration_id" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="other_il_id" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
</group>
<field name="nsc_entry_ids">
<list editable="bottom">
<field name="nsc_number" width="500px"/>
<field name="nsc_payment_date"/>
<field name="nsc_amount"/>
<field name="nsc_interest_amount"/>
</list>
</field>
<group>
<group>
</group>
<group>
<field name="total_nsc_amount" readonly="1" force_save="1"/>
<field name="total_nsc_interest_amount" readonly="1" force_save="1"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_nsc_interest_line_list" model="ir.ui.view">
<field name="name">nsc.interest.line.list</field>
<field name="model">nsc.interest.line</field>
<field name="arch" type="xml">
<list>
<field name="it_declaration_id"/>
<field name="other_il_id"/>
<field name="total_nsc_amount"/>
<field name="total_nsc_interest_amount"/>
</list>
</field>
</record>
<record id="action_nsc_interest_line" model="ir.actions.act_window">
<field name="name">NSC Interest</field>
<field name="res_model">nsc.interest.line</field>
<field name="view_mode">form</field>
</record>
</odoo>

View File

@ -1,28 +0,0 @@
from odoo import models, fields, api
class SelfOccupiedProperty(models.Model):
_name = 'self.occupied.property'
_description = 'Self Occupied House Property Details'
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
other_il_id = fields.Many2one('other.il.costing.type', string="Other Income/Loss Costing Type")
address = fields.Text(string="Address of Property")
loan_after_april_1999 = fields.Selection([('yes','Yes'),('no','No')],default='yes',
string="House Loan Taken After 01-Apr-1999 & Construction Completed Within 3 Years"
)
period_from = fields.Date(string="Period From")
period_to = fields.Date(string="Period To")
interest_paid_to = fields.Integer(string="Less: Interest on Borrowed Capital Paid To")
income_loss = fields.Integer(string="Income / Loss on House Property")
lender_name = fields.Char(string="Lender Name")
lender_pan = fields.Char(string="Lender PAN")
@api.onchange('interest_paid_to')
def onchange_interest_paid_to(self):
for rec in self:
rec.income_loss = -(rec.interest_paid_to)

View File

@ -1,80 +0,0 @@
<odoo>
<!-- Form View -->
<record id="view_self_occupied_property_form" model="ir.ui.view">
<field name="name">self.occupied.property.form</field>
<field name="model">self.occupied.property</field>
<field name="arch" type="xml">
<form string="Self Occupied House Property">
<sheet>
<!-- Header Section -->
<group>
<group>
<field name="it_declaration_id" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
</group>
<group>
<field name="other_il_id" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
</group>
</group>
<!-- Parent References -->
<!-- Property Info -->
<separator string="Property Information" colspan="2"/>
<group>
<field name="address"/>
</group>
<group>
<div class="oe_row">
<label for="loan_after_april_1999"
string="House Loan Taken After 01-Apr-1999 &amp; Construction Completed Within 3 Years &amp;nbsp; "/>
<field name="loan_after_april_1999" nolabel="1" widget="radio"
options="{'horizontal': true}" class="ml-2"/>
</div>
</group>
<group>
<!-- Period & Interest Info -->
<group string="Period &amp; Interest Details">
<field name="period_from"/>
<field name="period_to"/>
<field name="interest_paid_to"/>
</group>
<!-- Financial & Lender Info -->
<group string="Financial &amp; Lender Information">
<field name="income_loss" readonly="1" force_save="1"/>
<field name="lender_name"/>
<field name="lender_pan"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- list View -->
<record id="view_self_occupied_property_list" model="ir.ui.view">
<field name="name">self.occupied.property.list</field>
<field name="model">self.occupied.property</field>
<field name="arch" type="xml">
<list>
<field name="it_declaration_id"/>
<field name="address"/>
<field name="loan_after_april_1999"/>
<field name="period_from"/>
<field name="period_to"/>
<field name="interest_paid_to"/>
<field name="income_loss"/>
</list>
</field>
</record>
<!-- Action -->
<record id="action_self_occupied_property" model="ir.actions.act_window">
<field name="name">Self Occupied Properties</field>
<field name="res_model">self.occupied.property</field>
<field name="view_mode">form</field>
</record>
</odoo>

View File

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

View File

@ -1,65 +0,0 @@
from odoo import http, _
from odoo.http import request
from odoo.addons.hr_recruitment_extended.controllers.controllers import website_hr_recruitment_applications
from odoo.http import content_disposition
import logging
from odoo.tools import misc
_logger = logging.getLogger(__name__)
class website_hr_recruitment_applications_extended(website_hr_recruitment_applications):
@http.route(['/FTPROTECH/JoiningForm/<int:applicant_id>'], type='http', auth="public",
website=True)
def post_onboarding_form(self, applicant_id, **kwargs):
"""Renders the website form for applicants to submit additional details."""
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
if not applicant.exists():
return request.not_found()
if applicant and applicant.send_post_onboarding_form:
if applicant.post_onboarding_form_status == 'done':
return request.render("hr_recruitment_extended.thank_you_template", {
'applicant': applicant
})
else:
return request.render("hr_recruitment_extended.post_onboarding_form_template", {
'applicant': applicant
})
else:
return request.not_found()
@http.route(['/download/jod/<int:applicant_id>'], type='http', auth="public", cors='*', website=True)
def download_jod_form(self, applicant_id, **kwargs):
# Get the applicant record
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
if not applicant.exists():
return f"Error: Applicant with ID {applicant_id} not found"
# Business logic check
if not applicant.send_post_onboarding_form or applicant.post_onboarding_form_status != 'done':
return f"Error: Applicant {applicant_id} does not meet the criteria for download"
# Get the template
template = request.env.ref('hr_recruitment_extended.employee_joining_form_template')
if not template:
return "Error: Template not found"
try:
# Render the template to HTML for debugging
html = request.env['ir.qweb']._render(
template.id,
{
'docs': applicant,
'doc': applicant,
'time': misc.datetime,
'user': request.env.user,
}
)
# Return HTML for debugging
return html
except Exception as e:
return f"Error rendering template: {str(e)}"

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<odoo> <odoo>
<template id="emp_joining_form_template"> <template id="emp_joining_form_template">
<t t-call="web.basic_layout"> <t t-call="web.external_layout">
<main class="page" <main class="page"
style="margin: 0px; padding: 0px; font-size: 16px; font-family: 'Arial', sans-serif;"> style="margin: 0px; padding: 0px; font-size: 16px; font-family: 'Arial', sans-serif;">
<t t-foreach="docs" t-as="doc"> <t t-foreach="docs" t-as="doc">
@ -313,13 +313,4 @@
</t> </t>
</template> </template>
<!-- <template id="thank_you_template_inherit" inherit_id="hr_recruitment_exteded.thank_you_template" name="Thank You Template Extended">-->
<!-- <xpath expr="//div[@class='container mt-5 text-center']" position="inside">-->
<!-- <div t-if="applicant.post_onboarding_form_status == 'done'" style="margin-top: 20px;">-->
<!-- <a t-att-href="'/download/jod/%s' % applicant.id" class="btn btn-primary">Download JOD</a>-->
<!-- </div>-->
<!-- </xpath>-->
<!-- </template>-->
</odoo> </odoo>

View File

@ -61,116 +61,111 @@ class AttendanceReport(models.Model):
# Define the query # Define the query
query = """ query = """
WITH date_range AS ( WITH date_range AS (
SELECT generate_series( SELECT generate_series(
%s::date, %s::date,
%s::date, %s::date,
interval '1 day' interval '1 day'
)::date AS date )::date AS date
), ),
employee_dates AS ( employee_dates AS (
SELECT SELECT
emp.id AS employee_id, emp.id AS employee_id,
emp.name AS employee_name, emp.name AS employee_name,
dr.date, dr.date,
TO_CHAR(dr.date, 'Day') AS day_name, TO_CHAR(dr.date, 'Day') AS day_name,
EXTRACT(WEEK FROM dr.date) AS week_number, EXTRACT(WEEK FROM dr.date) AS week_number,
TO_CHAR(date_trunc('week', dr.date), 'MON DD') || ' - ' || TO_CHAR(date_trunc('week', dr.date), 'MON DD') || ' - ' ||
TO_CHAR(date_trunc('week', dr.date) + interval '6 days', 'MON DD') AS week_range, TO_CHAR(date_trunc('week', dr.date) + interval '6 days', 'MON DD') AS week_range,
dep.name->>'en_US' AS department dep.name->>'en_US' AS department
FROM FROM
hr_employee emp hr_employee emp
CROSS JOIN CROSS JOIN
date_range dr date_range dr
LEFT JOIN LEFT JOIN
hr_department dep ON emp.department_id = dep.id hr_department dep ON emp.department_id = dep.id
WHERE WHERE
emp.active = true emp.active = true
""" + (" AND " + " AND ".join(emp_date_conditions) if emp_date_conditions else "") + """ """ + (" AND " + " AND ".join(emp_date_conditions) if emp_date_conditions else "") + """
), ),
daily_checkins AS ( daily_checkins AS (
SELECT SELECT
emp.id, emp.id,
emp.name, emp.name,
DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') AS date, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') AS date,
at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS check_in, at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS check_in,
at.check_out AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS check_out, at.check_out AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS check_out,
at.worked_hours, at.worked_hours,
ROW_NUMBER() OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in) AS first_checkin_row, ROW_NUMBER() OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in) AS first_checkin_row,
ROW_NUMBER() OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in DESC) AS last_checkout_row, ROW_NUMBER() OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in DESC) AS last_checkout_row,
dep.name->>'en_US' AS department, dep.name->>'en_US' AS department
LEAD(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in) AS next_check_in FROM
FROM hr_attendance at
hr_attendance at LEFT JOIN
LEFT JOIN hr_employee emp ON at.employee_id = emp.id
hr_employee emp ON at.employee_id = emp.id LEFT JOIN
LEFT JOIN hr_department dep ON emp.department_id = dep.id
hr_department dep ON emp.department_id = dep.id WHERE
WHERE """ + " AND ".join(checkin_conditions) + """
""" + " AND ".join(checkin_conditions) + """ ),
), attendance_summary AS (
attendance_summary AS ( SELECT
SELECT id,
id, name,
name, date,
date, MAX(CASE WHEN first_checkin_row = 1 THEN check_in END) AS first_check_in,
MAX(CASE WHEN first_checkin_row = 1 THEN check_in END) AS first_check_in, MAX(CASE WHEN last_checkout_row = 1 THEN check_out END) AS last_check_out,
MAX(CASE WHEN last_checkout_row = 1 THEN check_out END) AS last_check_out, SUM(worked_hours) AS total_worked_hours,
SUM(worked_hours) AS total_worked_hours, department
-- 👇 Calculate total break time (sum of gaps between check_out and next check_in) FROM
SUM( daily_checkins
EXTRACT(EPOCH FROM (next_check_in - check_out)) / 3600 GROUP BY
) FILTER (WHERE next_check_in IS NOT NULL AND check_out IS NOT NULL) AS break_hours, id, name, date, department
department ),
FROM leave_data AS (
daily_checkins SELECT
GROUP BY hl.employee_id,
id, name, date, department hl.date_from AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS leave_start,
), hl.date_to AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS leave_end,
leave_data AS ( hlt.name->>'en_US' AS leave_type,
SELECT hl.request_unit_half AS is_half_day,
hl.employee_id, hl.request_date_from,
hl.date_from AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS leave_start, hl.request_date_to
hl.date_to AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS leave_end, FROM
hlt.name->>'en_US' AS leave_type, hr_leave hl
hl.request_unit_half AS is_half_day, JOIN
hl.request_date_from, hr_leave_type hlt ON hl.holiday_status_id = hlt.id
hl.request_date_to WHERE
FROM hl.state IN ('validate', 'confirm', 'validate1')
hr_leave hl AND (hl.date_from, hl.date_to) OVERLAPS (%s::timestamp, %s::timestamp)
JOIN )
hr_leave_type hlt ON hl.holiday_status_id = hlt.id SELECT
WHERE ed.employee_id AS id,
hl.state IN ('validate', 'confirm', 'validate1') ed.employee_name AS name,
AND (hl.date_from, hl.date_to) OVERLAPS (%s::timestamp, %s::timestamp) ed.date,
) 'Week ' || ed.week_number || ' (' || ed.week_range || ')' AS week_info,
SELECT TRIM(ed.day_name) AS day_name,
ed.employee_id AS id, COALESCE(ats.first_check_in, NULL) AS first_check_in,
ed.employee_name AS name, COALESCE(ats.last_check_out, NULL) AS last_check_out,
ed.date, COALESCE(ats.total_worked_hours, 0) AS total_worked_hours,
'Week ' || ed.week_number || ' (' || ed.week_range || ')' AS week_info, ed.department,
TRIM(ed.day_name) AS day_name, CASE
COALESCE(ats.first_check_in, NULL) AS first_check_in, WHEN ld.leave_type IS NOT NULL AND ld.is_half_day THEN 'on Half day ' || ld.leave_type
COALESCE(ats.last_check_out, NULL) AS last_check_out, WHEN ld.leave_type IS NOT NULL THEN 'on ' || ld.leave_type
COALESCE(ats.total_worked_hours, 0) AS total_worked_hours, WHEN ats.first_check_in IS NOT NULL THEN 'Present'
COALESCE(ats.break_hours, 0) AS total_break_hours, ELSE 'NA'
ed.department, END AS status
CASE FROM
WHEN ld.leave_type IS NOT NULL AND ld.is_half_day THEN 'on Half day ' || ld.leave_type employee_dates ed
WHEN ld.leave_type IS NOT NULL THEN 'on ' || ld.leave_type LEFT JOIN
WHEN ats.first_check_in IS NOT NULL THEN 'Present' attendance_summary ats ON ed.employee_id = ats.id AND ed.date = ats.date
ELSE 'NA' LEFT JOIN
END AS status leave_data ld ON ed.employee_id = ld.employee_id
FROM AND ed.date >= DATE(ld.leave_start)
employee_dates ed AND ed.date <= DATE(ld.leave_end)
LEFT JOIN ORDER BY
attendance_summary ats ON ed.employee_id = ats.id AND ed.date = ats.date ed.employee_id, ed.date;
LEFT JOIN
leave_data ld ON ed.employee_id = ld.employee_id
AND ed.date >= DATE(ld.leave_start)
AND ed.date <= DATE(ld.leave_end)
ORDER BY
ed.employee_id, ed.date;
""" """
# Combine all parameters in the correct order: # Combine all parameters in the correct order:
# 1. date_range params (start_date_str, end_date_str) # 1. date_range params (start_date_str, end_date_str)
# 2. employee_dates params (emp_date_params) # 2. employee_dates params (emp_date_params)
@ -201,7 +196,6 @@ ORDER BY
'check_in': r['first_check_in'], 'check_in': r['first_check_in'],
'check_out': r['last_check_out'], 'check_out': r['last_check_out'],
'worked_hours': float(r['total_worked_hours']) if r['total_worked_hours'] is not None else 0.0, 'worked_hours': float(r['total_worked_hours']) if r['total_worked_hours'] is not None else 0.0,
'break_hours': float(r['total_break_hours']) if r['total_break_hours'] is not None else 0.0,
'status': r['status'] 'status': r['status']
}) })
@ -219,6 +213,7 @@ ORDER BY
attendance_data = self.get_attendance_report(department_id, employee_id, start_date, end_date) attendance_data = self.get_attendance_report(department_id, employee_id, start_date, end_date)
if not attendance_data: if not attendance_data:
raise UserError("No data to export!") raise UserError("No data to export!")
# Create workbook and sheet # Create workbook and sheet
workbook = xlwt.Workbook(encoding='utf-8') workbook = xlwt.Workbook(encoding='utf-8')
sheet = workbook.add_sheet('Attendance Report') sheet = workbook.add_sheet('Attendance Report')
@ -286,26 +281,28 @@ ORDER BY
) )
# Set column widths (in units of 1/256 of a character width) # Set column widths (in units of 1/256 of a character width)
col_widths = [6000, 8000, 7000, 3000, 4000, 5000, 5000, 4000, 4000, 5000] col_widths = [6000, 8000, 7000, 3000, 4000, 5000, 5000, 4000, 5000]
for i, width in enumerate(col_widths): for i, width in enumerate(col_widths):
sheet.col(i).width = width sheet.col(i).width = width
# Write title # Write title
sheet.write_merge(0, 0, 0, 9, 'ATTENDANCE REPORT', title_style) sheet.write_merge(0, 0, 0, 8, 'ATTENDANCE REPORT', title_style)
# Write date range # Write date range
date_range = f"From: {start_date} To: {end_date}" date_range = f"From: {start_date} To: {end_date}"
sheet.write_merge(1, 1, 0, 9, date_range, xlwt.easyxf( sheet.write_merge(1, 1, 0, 8, date_range, xlwt.easyxf(
'font: italic on; align: horiz center' 'font: italic on; align: horiz center'
)) ))
# Write headers # Write headers
headers = [ headers = [
'Department', 'Employee Name','Week', 'Date', 'Day', 'Department', 'Employee Name','Week', 'Date', 'Day',
'Check-in', 'Check-out', 'Worked Hours', 'Break Hours', 'Status' 'Check-in', 'Check-out', 'Worked Hours', 'Status'
] ]
for col_num, header in enumerate(headers): for col_num, header in enumerate(headers):
sheet.write(2, col_num, header, header_style) sheet.write(2, col_num, header, header_style)
# Write data rows # Write data rows
current_employee = None current_employee = None
for row_num, record in enumerate(attendance_data, start=3): for row_num, record in enumerate(attendance_data, start=3):
@ -348,16 +345,11 @@ ORDER BY
else: else:
sheet.write(row_num, 7, str(record['worked_hours']), data_style) sheet.write(row_num, 7, str(record['worked_hours']), data_style)
# Break hours formatting sheet.write(row_num, 8, record['status'], status_style)
if isinstance(record['break_hours'], (float, int)):
sheet.write(row_num, 8, float(record['break_hours']), hours_style)
else:
sheet.write(row_num, 8, str(record['break_hours']), data_style)
sheet.write(row_num, 9, record['status'], status_style)
# Add freeze panes (headers will stay visible when scrolling) # Add freeze panes (headers will stay visible when scrolling)
sheet.set_panes_frozen(True) sheet.set_panes_frozen(True)
sheet.set_horz_split_pos(3) # After row 3 (headers) sheet.set_horz_split_pos(4) # After row 3 (headers)
sheet.set_vert_split_pos(0) # No vertical split sheet.set_vert_split_pos(0) # No vertical split
# Save to buffer # Save to buffer

View File

@ -206,7 +206,7 @@ export default class AttendanceReport extends Component {
} }
async generateReport() { async generateReport() {
debugger;
let { startDate, endDate, selectedEmployeeIds } = this.state; let { startDate, endDate, selectedEmployeeIds } = this.state;
startDate = $('#from_date').val() startDate = $('#from_date').val()
endDate = $('#to_date').val() endDate = $('#to_date').val()
@ -231,7 +231,7 @@ export default class AttendanceReport extends Component {
// Fetch the attendance data based on the date range and selected employees // Fetch the attendance data based on the date range and selected employees
// const attendanceData = await this.orm.searchRead('hr.attendance', domain, ['employee_id', 'check_in', 'check_out', 'worked_hours']); // const attendanceData = await this.orm.searchRead('hr.attendance', domain, ['employee_id', 'check_in', 'check_out', 'worked_hours']);
const attendanceData = await this.orm.call('attendance.report','get_attendance_report',[$('#dept').val(),$('#emp').val(),startDate,endDate]); const attendanceData = await this.orm.call('attendance.report','get_attendance_report',[$('#dept').val(),$('#emp').val(),startDate,endDate]);
debugger;
// Group data by employee_id // Group data by employee_id
const rawGroups = this.groupDataByEmployee(attendanceData); const rawGroups = this.groupDataByEmployee(attendanceData);

View File

@ -65,7 +65,6 @@
<th>Check In</th> <th>Check In</th>
<th>Check Out</th> <th>Check Out</th>
<th>Worked Hours</th> <th>Worked Hours</th>
<th>Break Hours</th>
<th>Status</th> <th>Status</th>
</tr> </tr>
</thead> </thead>
@ -79,16 +78,7 @@
<td><t t-esc="data.day_name"/></td> <td><t t-esc="data.day_name"/></td>
<td><t t-esc="data.check_in"/></td> <td><t t-esc="data.check_in"/></td>
<td><t t-esc="data.check_out"/></td> <td><t t-esc="data.check_out"/></td>
<td> <td><t t-esc="data.worked_hours"/></td>
<t t-set="hours" t-value="Math.floor(data.worked_hours)"/>
<t t-set="minutes" t-value="Math.round((data.worked_hours - hours) * 60)"/>
<t t-esc="hours"/>:<t t-esc="minutes >= 10 ? minutes : '0' + minutes"/>
</td>
<td>
<t t-set="hours" t-value="Math.floor(data.break_hours)"/>
<t t-set="minutes" t-value="Math.round((data.break_hours - hours) * 60)"/>
<t t-esc="hours"/>:<t t-esc="minutes >= 10 ? minutes : '0' + minutes"/>
</td>
<td><t t-esc="data.status"/></td> <td><t t-esc="data.status"/></td>
</tr> </tr>
</tbody> </tbody>

View File

@ -18,8 +18,12 @@
'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',

View File

@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import models

View File

@ -1,23 +0,0 @@
# -*- 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,
}

View File

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

View File

@ -1,97 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem name="Human Resources"
id="hr_menu_root"/>
<menuitem
id="menu_hr_main"
name="Employees"
parent="hr_menu_root"
sequence="0"/>
<menuitem
id="menu_hr_employee_user_hr"
name="Employees"
action="hr.open_view_employee_list_my"
parent="menu_hr_main"
sequence="1"/>
<menuitem
id="hr_menu_all_contracts_hr"
name="Contracts"
action="hr_contract.action_hr_contract"
parent="menu_hr_main"
sequence="30"/>
<menuitem
id="hr_menu_salary_attachments_hr"
name="Salary Attachments"
action="hr_payroll.hr_salary_attachment_action"
parent="menu_hr_main"
sequence="35"/>
<!-- ////////////////////////////////////////////////////////////////////////////// -->
<menuitem
id="menu_hr_pay"
name="Payslips"
parent="hr_menu_root"
sequence="1"
groups="hr_payroll.group_hr_payroll_user"/>
<menuitem
id="menu_hr_payslip_run_hr"
action="hr_payroll.action_hr_payslip_run_tree"
name="Batches"
sequence="2"
parent="menu_hr_pay"/>
<menuitem
id="menu_hr_payroll_employee_payslips_hr"
name="All Payslips"
parent="menu_hr_pay"
sequence="1"
action="hr_payroll.action_view_hr_payslip_month_form"
groups="hr_payroll.group_hr_payroll_user"/>
<!-- ////////////////////////////////////////////////////////////////////////////// -->
<menuitem
id="menu_hr_attendance_hr"
name="Work Management"
parent="hr_menu_root"
sequence="1"
groups="hr_payroll.group_hr_payroll_user"/>
<menuitem
id="menu_hr_attendance_view_attendances_hr"
name="Attendance" parent="menu_hr_attendance_hr"
sequence="5"
groups="hr_attendance.group_hr_attendance_officer"
action="hr_attendance.hr_attendance_action"/>
<menuitem
id="menu_open_department_leave_approve_hr"
name="Time Off"
parent="menu_hr_attendance_hr"
action="hr_holidays.hr_leave_action_action_approve_department"
sequence="55"/>
<!-- ////////////////////////////////////////////////////////////////////////////// -->
<menuitem
name="Recruitment"
id="menu_hr_recruitment_hr"
parent="hr_menu_root"
sequence="-1"/>
<menuitem
name="Applications"
parent="menu_hr_recruitment_hr"
id="menu_hr_recruitment_applications_hr"
action="hr_recruitment.crm_case_categ0_act_job"
sequence="3"/>
<menuitem
name="Candidates"
parent="menu_hr_recruitment_hr"
id="menu_hr_candidate_hr"
action="hr_recruitment.action_hr_candidate"
sequence="2"/>
</odoo>

View File

@ -456,8 +456,8 @@ class HrPayslip(models.Model):
'res_id': payslip.id 'res_id': payslip.id
}) })
# Send email to employees # Send email to employees
# if template: if template:
# template.send_mail(payslip.id, email_layout_xmlid='mail.mail_notification_light') template.send_mail(payslip.id, email_layout_xmlid='mail.mail_notification_light')
self.env['ir.attachment'].sudo().create(attachments_vals_list) self.env['ir.attachment'].sudo().create(attachments_vals_list)
def _filter_out_of_contracts_payslips(self): def _filter_out_of_contracts_payslips(self):
@ -1381,10 +1381,7 @@ class HrPayslip(models.Model):
def days_count(self): def days_count(self):
days = self.worked_days_line_ids.filtered(lambda x:x.work_entry_type_id.code == 'OUT').number_of_days
joining_date = self.contract_id.date_start 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) date_from = min(joining_date, self.date_from)
if joining_date > date_from: if joining_date > date_from:
@ -1403,7 +1400,7 @@ class HrPayslip(models.Model):
weekend_days_count = weekend_count weekend_days_count = weekend_count
else: else:
weekend_days_count = 0 weekend_days_count = 0
return weekend_days_count + days return weekend_days_count
def action_edit_payslip_lines(self): def action_edit_payslip_lines(self):
self.ensure_one() self.ensure_one()
@ -1852,40 +1849,3 @@ class HrPayslip(models.Model):
if 'stats' in sections: if 'stats' in sections:
result['stats'] = self._get_dashboard_stats() result['stats'] = self._get_dashboard_stats()
return result return result
def get_leave_balance(self):
employee = self.employee_id
if not employee:
return {'error': 'No employee linked to this user'}
leave_data = {}
leave_types = self.env['hr.leave.type'].search([
])
if not leave_types:
return []
for leave_type in leave_types:
allocations = self.env['hr.leave.allocation'].search([
('employee_id', '=', employee.id),
('holiday_status_id', '=', leave_type.id),
('state', '=', 'validate'),
])
taken_leaves = self.env['hr.leave'].search([
('employee_id', '=', employee.id),
('holiday_status_id', '=', leave_type.id),
('state', 'in', ['validate','validate1','confirm']),
])
total_allocated = sum(a.number_of_days for a in allocations)
total_taken = sum(l.number_of_days for l in taken_leaves)
remaining = total_allocated - total_taken
if remaining <= 0:
continue
leave_data[leave_type.name] = {
'name':leave_type.name,
'allocated': total_allocated,
'taken': total_taken,
'remaining': remaining,
}
return leave_data

View File

@ -44,21 +44,20 @@
<table style="width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 10px;"> <table style="width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 10px;">
<tr> <tr>
<td style="border: 1px solid #ccc; padding: 6px;"> <td style="border: 1px solid #ccc; padding: 6px;">
<u><b>Pay Summary</b></u><br/><br/>
<strong>Pay Period:</strong> <t style="padding: 6px;" t-esc="o.date_from"/> - <t t-esc="o.date_to"/><br/> <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"/> <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>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 <strong>Worked Days:</strong> <t style="padding: 6px;" t-esc="days"/> Days
</td> </td>
<t t-set="leave_data" t-value="o.get_leave_balance()"/> <t t-set="timeoff_data_table" t-value="o._get_employee_timeoff_data()"/>
<td t-if="leave_data" style="border: 1px solid #ccc; padding: 6px;"> <td t-if="timeoff_data_table" style="border: 1px solid #ccc; padding: 6px;">
<p><u><b>Leave Balance</b></u> <div t-foreach="timeoff_data_table" t-as="timeoff_data">
<div t-foreach="leave_data.values()" t-as="data"> <strong t-out="timeoff_data[0] + ':'"/>
<strong t-out="data['name'] + ':'"/> <t t-out="timeoff_data[1].get('remaining_leaves')"/> /
<t t-out="data['remaining']"/> <t t-out="timeoff_data[1].get('max_leaves')"/>
Days <t t-if="timeoff_data[1].get('request_unit') == 'hour'">Hours</t>
<t t-else="">Days</t>
</div> </div>
</p>
</td> </td>
</tr> </tr>
</table> </table>
@ -77,16 +76,12 @@
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code in ['BASIC','SPA','ALW']and l.amount &gt; 0)" t-as="l"> <div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code in ['BASIC','SPA','ALW']and l.amount &gt; 0)" t-as="l">
<t t-esc="l.name"/><br/> <t t-esc="l.name"/><br/>
</div> </div>
<br/>
<strong>Total Income</strong>
</td> </td>
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;"> <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.category_id.code in ['BASIC','SPA','ALW'] and l.amount &gt; 0)" t-as="l"> <div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code in ['BASIC','SPA','ALW'] and l.amount &gt; 0)" t-as="l">
<t t-esc="'%.2f' % l.amount"/><br/> <t t-esc="l.amount"/><br/>
<t t-set="income" t-value="income + l.amount"/> <t t-set="income" t-value="income + l.amount"/>
</div> </div>
<br/>
<strong><t t-esc="'%.2f' % income"/></strong>
</td> </td>
<td style="border: 1px solid #ccc; padding: 6px;"> <td style="border: 1px solid #ccc; padding: 6px;">
<t t-set="contribution" t-value="0"/> <t t-set="contribution" t-value="0"/>
@ -96,7 +91,7 @@
</td> </td>
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;"> <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.category_id.code in ['COMP','MA'] and l.amount &gt; 0)" t-as="l"> <div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code in ['COMP','MA'] and l.amount &gt; 0)" t-as="l">
<t t-esc="'%.2f' % l.amount"/><br/> <t t-esc="l.amount"/><br/>
<t t-set="contribution" t-value="contribution + l.amount"/> <t t-set="contribution" t-value="contribution + l.amount"/>
</div> </div>
</td> </td>
@ -112,33 +107,30 @@
</div> </div>
</td> </td>
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;"> <td style="border: 1px solid #ccc; padding: 6px; text-align: right;">
<strong><t t-esc="'%.2f' % (contribution + income)"/></strong><br/><br/><br/> <strong><t t-esc="contribution + income"/></strong><br/><br/><br/>
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code == 'DED')" t-as="l"> <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="'%.2f' % l.amount"/><br/> <t t-esc="l.amount"/><br/>
<t t-set="ded" t-value="ded + l.amount"/> <t t-set="ded" t-value="ded + l.amount"/>
</div> </div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td style="border: 1px solid #ccc; padding: 6px;" colspan="3"><strong>Gross Salary</strong></td> <td style="border: 1px solid #ccc; padding: 6px;"><strong>Gross Salary</strong></td>
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;" colspan="1"><strong><t t-esc="'%.2f' % income"/></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>
<tr> <tr>
<td style="border: 1px solid #ccc; padding: 6px;" colspan="3"><strong>Total Deduction</strong></td> <td class="net-salary" colspan="3" style="border: 1px solid #ccc; padding: 6px;"><strong>Net Salary:</strong></td>
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;" colspan="1"><strong><t t-esc="'%.2f' % ded"/></strong></td> <td class="net-salary" colspan="1" style="border: 1px solid #ccc; padding: 6px; text-align: right;">
</tr> <t t-esc="(contribution + income) + ded"/>
<tr>
<td class="net-salary" colspan="3" style="border: 1px solid #ccc; padding: 6px;font-size: 14px;"><strong>Net Salary:</strong></td>
<td class="net-salary" colspan="1" style="border: 1px solid #ccc; padding: 6px; text-align: right;font-size: 14px;">
<strong><t t-esc="'%.2f' % (income + ded)"/></strong>
</td> </td>
</tr> </tr>
</table> </table>
<div class="to-pay" style="margin-top: 20px;"> <div class="to-pay" style="margin-top: 20px;">
<p t-if="o.net_wage &gt;= 0"> <p t-if="o.net_wage &gt;= 0">
To pay <strong><span t-esc="'%.2f' % (income + ded)"/></strong> (<span style="padding-right: 5px;" t-esc="o.env.company.currency_id.amount_to_text(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> 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>

View File

@ -84,7 +84,7 @@
name="Contracts" name="Contracts"
action="hr_contract.action_hr_contract" action="hr_contract.action_hr_contract"
parent="menu_hr_payroll_employees_root" parent="menu_hr_payroll_employees_root"
sequence="90"/> sequence="30"/>
<menuitem <menuitem
id="hr_menu_salary_attachments" id="hr_menu_salary_attachments"

View File

@ -164,14 +164,13 @@ class website_hr_recruitment_applications(http.Controller):
@http.route(['/FTPROTECH/submit/<int:applicant_id>/JoinForm'], type='http', auth="public", @http.route(['/FTPROTECH/submit/<int:applicant_id>/JoinForm'], type='http', auth="public",
methods=['POST'], website=True, csrf=False) methods=['POST'], website=True, csrf=False)
def process_employee_joining_form(self,applicant_id,**post): def process_employee_joining_form(self,applicant_id,**post):
applicant = request.env['hr.applicant'].sudo().browse(applicant_id) applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
if not applicant.exists(): if not applicant.exists():
return request.not_found() # Return 404 if applicant doesn't exist return request.not_found() # Return 404 if applicant doesn't exist
if applicant.post_onboarding_form_status == 'done': if applicant.post_onboarding_form_status == 'done':
return request.render("hr_recruitment_extended.thank_you_template",{ return request.render("hr_recruitment_extended.thank_you_template")
'applicant': applicant
})
private_state_id = request.env['res.country.state'].sudo().browse(int(post.get('present_state', 0))) private_state_id = request.env['res.country.state'].sudo().browse(int(post.get('present_state', 0)))
permanent_state_id = request.env['res.country.state'].sudo().browse(int(post.get('permanent_state', 0))) permanent_state_id = request.env['res.country.state'].sudo().browse(int(post.get('permanent_state', 0)))
@ -281,41 +280,8 @@ class website_hr_recruitment_applications(http.Controller):
] ]
applicant.write(applicant_data) applicant.write(applicant_data)
template = request.env.ref('hr_recruitment_extended.email_template_post_onboarding_form_user_submit',
raise_if_not_found=False)
# Get HR managers with HR department
group = request.env.ref('hr.group_hr_manager')
users = request.env['res.users'].sudo().search([
('groups_id', 'in', group.ids),
('email', '!=', False),
('email', '!=', 'hr@ftprotech.com'),
('employee_id.department_id.name', '=', 'Human Resource')
])
# Extract emails and join them into a comma-separated string return request.render("hr_recruitment_extended.thank_you_template")
email_cc = ','.join([user.email for user in users])
# Prepare email values
email_values = {
'email_from': applicant.email_from,
'email_to': 'hr@ftprotech.com',
'email_cc': email_cc
}
# Debug: Print the email_cc value to verify
print(f"Email CC value: {email_values['email_cc']}")
# Send email
template.sudo().send_mail(
applicant.id,
email_values=email_values,
force_send=True
)
# Render thank you page
return request.render("hr_recruitment_extended.thank_you_template", {
'applicant': applicant
})
def safe_date_parse(self,date_str): def safe_date_parse(self,date_str):
try: try:

View File

@ -290,49 +290,6 @@
<field name="auto_delete" eval="True"/> <field name="auto_delete" eval="True"/>
</record> </record>
<record id="email_template_post_onboarding_form_user_submit" model="mail.template">
<field name="name">Joining Formalities Submission Notification</field>
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
<field name="email_from">{{ object.email_from }}</field>
<field name="email_to">hr@ftprotech.com</field>
<field name="subject">{{ object.candidate_id.partner_name or 'Applicant' }} JOD Submission</field>
<field name="description">
Notification sent by the applicants with joining formalities details.
</field>
<field name="body_html" type="html">
<div style="font-family: Arial, sans-serif; font-size: 14px; color: #333; padding: 20px; line-height: 1.6;">
<p>Dear
<strong>
<t>HR</t>
</strong>
,
</p>
<t t-set="applicant_name" t-value="object.candidate_id.partner_name or 'Applicant'"/>
<t t-if="object.employee_code">
<t t-set="employee_code" t-value="object.employee_code"/>
</t>
<p>
<t t-esc="applicant_name"/> has submitted the Joining Formalities (JOD) Form. Please click the link below to review the details.
</p>
<t t-set="base_url" t-value="object.env['ir.config_parameter'].sudo().get_param('web.base.url')"/>
<!-- FIXED LINE: Using proper string concatenation -->
<t t-set="form_url" t-value="base_url + '/odoo/hr.applicant/' + str(object.id)"/>
<p style="text-align: center; margin-top: 20px;">
<a t-att-href="form_url" target="_blank" style="background-color: #007bff; color: #fff; padding: 10px 20px; text-decoration: none; font-weight: bold; border-radius: 5px; display: inline-block;">
Open Application
</a>
</p>
<p>Best Regards,
<br/>
<strong>
<t t-esc="object.company_id.name or 'HR Team'">HR Team</t>
</strong>
</p>
</div>
</field>
</record>
<record id="email_template_post_onboarding_form" model="mail.template"> <record id="email_template_post_onboarding_form" model="mail.template">
<field name="name">Joining Formalities Notification</field> <field name="name">Joining Formalities Notification</field>
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/> <field name="model_id" ref="hr_recruitment.model_hr_applicant"/>

View File

@ -1,7 +1,7 @@
<odoo> <odoo>
<odoo> <odoo>
<template id="employee_joining_form_template"> <template id="employee_joining_form_template">
<t t-call="web.basic_layout"> <t t-call="web.external_layout">
<main class="page" <main class="page"
style="margin: 0px; padding: 0px; font-size: 16px; font-family: 'Arial', sans-serif;"> style="margin: 0px; padding: 0px; font-size: 16px; font-family: 'Arial', sans-serif;">
<t t-foreach="docs" t-as="doc"> <t t-foreach="docs" t-as="doc">

View File

@ -256,6 +256,8 @@ class HRJobRecruitment(models.Model):
rec.submission_status = 'zero' rec.submission_status = 'zero'
experience = fields.Many2one('candidate.experience', string="Experience")
@api.depends('application_ids.submitted_to_client') @api.depends('application_ids.submitted_to_client')
def _compute_no_of_submissions(self): def _compute_no_of_submissions(self):
counts = dict(self.env['hr.applicant']._read_group( counts = dict(self.env['hr.applicant']._read_group(

View File

@ -10,7 +10,7 @@ access_hr_job_recruitment_user,access.hr.job.recruitment.user,model_hr_job_recru
access_hr_job_recruitment_manager,access.hr.job.recruitment.manager,model_hr_job_recruitment,hr_recruitment.group_hr_recruitment_user,1,1,1,1 access_hr_job_recruitment_manager,access.hr.job.recruitment.manager,model_hr_job_recruitment,hr_recruitment.group_hr_recruitment_user,1,1,1,1
hr_recruitment.access_hr_candidate_interviewer,hr.candidate.interviewer,hr_recruitment.model_hr_candidate,hr_recruitment.group_hr_recruitment_interviewer,1,1,1,0 hr_recruitment.access_hr_candidate_interviewer,hr.candidate.interviewer,hr_recruitment.model_hr_candidate,hr_recruitment.group_hr_recruitment_interviewer,1,1,1,0
access_hr_candidate_hr,hr.candidate.hr,hr_recruitment.model_hr_candidate,hr.group_hr_manager,1,0,0,0
access_candidate_experience,access.candidate.experience.manager,model_candidate_experience,hr_recruitment.group_hr_recruitment_user,1,1,1,1 access_candidate_experience,access.candidate.experience.manager,model_candidate_experience,hr_recruitment.group_hr_recruitment_user,1,1,1,1
access_candidate_experience_user,access.candidate.experience.user,model_candidate_experience,base.group_user,1,0,0,0 access_candidate_experience_user,access.candidate.experience.user,model_candidate_experience,base.group_user,1,0,0,0
@ -23,13 +23,10 @@ access_employee_recruitment_attachments,employee.recruitment.attachments,model_e
hr_recruitment.access_hr_applicant_interviewer,hr.applicant.interviewer,hr_recruitment.model_hr_applicant,hr_recruitment.group_hr_recruitment_interviewer,1,1,1,0 hr_recruitment.access_hr_applicant_interviewer,hr.applicant.interviewer,hr_recruitment.model_hr_applicant,hr_recruitment.group_hr_recruitment_interviewer,1,1,1,0
hr_recruitment.access_hr_recruitment_stage_user,hr.recruitment.stage.user,hr_recruitment.model_hr_recruitment_stage,hr_recruitment.group_hr_recruitment_user,1,1,1,0 hr_recruitment.access_hr_recruitment_stage_user,hr.recruitment.stage.user,hr_recruitment.model_hr_recruitment_stage,hr_recruitment.group_hr_recruitment_user,1,1,1,0
access_hr_recruitment_stage_hr,hr.recruitment.stage.hr,hr_recruitment.model_hr_recruitment_stage,hr.group_hr_manager,1,0,0,0
access_application_stage_status,application.stage.status,model_application_stage_status,base.group_user,1,1,1,1 access_application_stage_status,application.stage.status,model_application_stage_status,base.group_user,1,1,1,1
access_ats_invite_mail_template_wizard,ats.invite.mail.template.wizard.user,hr_recruitment_extended.model_ats_invite_mail_template_wizard,,1,1,1,1 access_ats_invite_mail_template_wizard,ats.invite.mail.template.wizard.user,hr_recruitment_extended.model_ats_invite_mail_template_wizard,,1,1,1,1
access_client_submission_mails_template_wizard,client.submission.mails.template.wizard.user,hr_recruitment_extended.model_client_submission_mails_template_wizard,,1,1,1,1 access_client_submission_mails_template_wizard,client.submission.mails.template.wizard.user,hr_recruitment_extended.model_client_submission_mails_template_wizard,,1,1,1,1
access_hr_application_public,hr.applicant.public.access,hr_recruitment.model_hr_applicant,base.group_public,1,0,0,0
access_hr_application_group_hr,hr.applicant.hr.access,hr_recruitment.model_hr_applicant,hr.group_hr_manager,1,1,0,0
,,,,,,,
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
10 access_candidate_experience_user access_recruitment_attachments_user access.candidate.experience.user access.recruitment.attachments.user model_candidate_experience model_recruitment_attachments base.group_user 1 0 1 0 1 0 1
11 access_recruitment_attachments_user access_post_onboarding_attachment_wizard access.recruitment.attachments.user access.post.onboarding.attachment.wizard model_recruitment_attachments model_post_onboarding_attachment_wizard base.group_user 1 1 1 1
12 access_post_onboarding_attachment_wizard access_employee_recruitment_attachments access.post.onboarding.attachment.wizard employee.recruitment.attachments model_post_onboarding_attachment_wizard model_employee_recruitment_attachments base.group_user 1 1 1 1
13 access_employee_recruitment_attachments hr_recruitment.access_hr_applicant_interviewer employee.recruitment.attachments hr.applicant.interviewer model_employee_recruitment_attachments hr_recruitment.model_hr_applicant base.group_user hr_recruitment.group_hr_recruitment_interviewer 1 1 1 1 0
14 hr_recruitment.access_hr_applicant_interviewer hr_recruitment.access_hr_recruitment_stage_user hr.applicant.interviewer hr.recruitment.stage.user hr_recruitment.model_hr_applicant hr_recruitment.model_hr_recruitment_stage hr_recruitment.group_hr_recruitment_interviewer hr_recruitment.group_hr_recruitment_user 1 1 1 0
15 hr_recruitment.access_hr_recruitment_stage_user access_application_stage_status hr.recruitment.stage.user application.stage.status hr_recruitment.model_hr_recruitment_stage model_application_stage_status hr_recruitment.group_hr_recruitment_user base.group_user 1 1 1 0 1
16 access_hr_recruitment_stage_hr access_ats_invite_mail_template_wizard hr.recruitment.stage.hr ats.invite.mail.template.wizard.user hr_recruitment.model_hr_recruitment_stage hr_recruitment_extended.model_ats_invite_mail_template_wizard hr.group_hr_manager 1 0 1 0 1 0 1
23
24
25
26
27
28
29
30
31
32

View File

@ -465,13 +465,6 @@
<label>Permanent Address <label>Permanent Address
<span class="text-danger">*</span> <span class="text-danger">*</span>
</label> </label>
<!-- Checkbox to toggle same address -->
<div class="form-check d-inline-block ml-3">
<input type="checkbox" class="form-check-input permanent-address-checkbox" id="same_as_present"/>
<label class="form-check-label" for="same_as_present">Same as Present Address</label>
</div>
</h5> </h5>
<div class="mb-3"> <div class="mb-3">
<input type="text" class="form-control mb-2" name="permanent_street" <input type="text" class="form-control mb-2" name="permanent_street"
@ -1723,80 +1716,6 @@
let prevButtons = document.querySelectorAll(".prev-step"); let prevButtons = document.querySelectorAll(".prev-step");
let currentStep = 0; let currentStep = 0;
// Same as Present Address functionality
const sameAsPresentCheckbox = document.getElementById('same_as_present');
const form = document.getElementById('post_onboarding_form');
// Add event listener to the checkbox
sameAsPresentCheckbox.addEventListener('change', function() {
if (this.checked) {
// Get present address elements by name (since they don't have IDs)
const presentStreet = form.querySelector('input[name="present_street"]');
const presentStreet2 = form.querySelector('input[name="present_street2"]');
const presentCity = form.querySelector('input[name="present_city"]');
const presentZip = form.querySelector('input[name="present_zip"]');
// Get permanent address elements by name
const permanentStreet = form.querySelector('input[name="permanent_street"]');
const permanentStreet2 = form.querySelector('input[name="permanent_street2"]');
const permanentCity = form.querySelector('input[name="permanent_city"]');
const permanentZip = form.querySelector('input[name="permanent_zip"]');
// Copy values only if both elements exist
if (permanentStreet &amp;&amp; presentStreet) permanentStreet.value = presentStreet.value;
if (permanentStreet2 &amp;&amp; presentStreet2) permanentStreet2.value = presentStreet2.value;
if (permanentCity &amp;&amp; presentCity) permanentCity.value = presentCity.value;
if (permanentZip &amp;&amp; presentZip) permanentZip.value = presentZip.value;
// Handle state field
const presentStateContainer = document.getElementById('present_state_ids_container');
const permanentStateContainer = document.getElementById('permanent_state_ids_container');
// If state dropdowns exist, copy the selected value
const presentStateSelect = presentStateContainer ? presentStateContainer.querySelector('select') : null;
const permanentStateSelect = permanentStateContainer ? permanentStateContainer.querySelector('select') : null;
if (presentStateSelect &amp;&amp; permanentStateSelect) {
permanentStateSelect.value = presentStateSelect.value;
}
// Make permanent address fields readonly
[permanentStreet, permanentStreet2, permanentCity, permanentZip].forEach(field => {
if (field) {
field.readOnly = true;
field.classList.add('bg-light');
}
});
if (permanentStateSelect) {
permanentStateSelect.disabled = true;
permanentStateSelect.classList.add('bg-light');
}
} else {
// Make permanent address fields editable but keep the values
const permanentStreet = form.querySelector('input[name="permanent_street"]');
const permanentStreet2 = form.querySelector('input[name="permanent_street2"]');
const permanentCity = form.querySelector('input[name="permanent_city"]');
const permanentZip = form.querySelector('input[name="permanent_zip"]');
[permanentStreet, permanentStreet2, permanentCity, permanentZip].forEach(field => {
if (field) {
field.readOnly = false;
field.classList.remove('bg-light');
}
});
const permanentStateContainer = document.getElementById('permanent_state_ids_container');
const permanentStateSelect = permanentStateContainer ? permanentStateContainer.querySelector('select') : null;
if (permanentStateSelect) {
permanentStateSelect.disabled = false;
permanentStateSelect.classList.remove('bg-light');
}
}
});
function updateSteps() { function updateSteps() {
// Show/hide step content // Show/hide step content
steps.forEach((step, index) => { steps.forEach((step, index) => {
@ -1836,13 +1755,6 @@
// Next button logic // Next button logic
nextButtons.forEach(button => { nextButtons.forEach(button => {
button.addEventListener("click", function () { button.addEventListener("click", function () {
if (currentStep === 0) {
// Validate employer history fields before proceeding
if (!validateEmployerHistory()) {
return false; // Stop navigation
}
}
if (validateStep(currentStep)) { if (validateStep(currentStep)) {
if (currentStep &lt; steps.length - 1) { if (currentStep &lt; steps.length - 1) {
currentStep++; currentStep++;
@ -1851,181 +1763,6 @@
} }
}); });
}); });
function validateEmployerHistory() {
const employerTable = document.getElementById('previous_employer_table_body');
const rows = employerTable.querySelectorAll('tr');
let hasErrors = false;
rows.forEach(row => {
// Get the cells for this row
const companyNameInput = row.querySelector('input[name="company_name"]');
const designationInput = row.querySelector('input[name="designation"]');
const dojInput = row.querySelector('input[name="doj"]');
// Check if ANY field in this row has a value
const hasValues = Array.from(row.querySelectorAll('input')).some(input =>
input.value &amp;&amp; input.value.trim() !== ''
);
if (hasValues) {
// If any field has value, check required fields
if (!companyNameInput || !companyNameInput.value.trim()) {
companyNameInput.classList.add("is-invalid");
companyNameInput.required = true;
hasErrors = true;
} else {
companyNameInput.classList.remove("is-invalid");
}
if (!designationInput || !designationInput.value.trim()) {
if (designationInput) {
designationInput.classList.add("is-invalid");
designationInput.required = true;
hasErrors = true;
}
} else {
if (designationInput) designationInput.classList.remove("is-invalid");
}
if (!dojInput || !dojInput.value.trim()) {
if (dojInput) {
dojInput.classList.add("is-invalid");
dojInput.required = true;
hasErrors = true;
}
} else {
if (dojInput) dojInput.classList.remove("is-invalid");
}
} else {
// No values in this row, remove validation
if (companyNameInput) {
companyNameInput.classList.remove("is-invalid");
companyNameInput.required = false;
}
if (designationInput) {
designationInput.classList.remove("is-invalid");
designationInput.required = false;
}
if (dojInput) {
dojInput.classList.remove("is-invalid");
dojInput.required = false;
}
}
});
// Also check the "experience" radio buttons
const experienceRadios = document.querySelectorAll('input[name="experience"]');
const isExperienced = Array.from(experienceRadios).some(radio =>
radio.value === 'experienced' &amp;&amp; radio.checked
);
if (isExperienced) {
// If user selected "Experienced", check if at least one employer is filled
const anyEmployerFilled = Array.from(rows).some(row => {
return Array.from(row.querySelectorAll('input')).some(input =>
input.value &amp;&amp; input.value.trim() !== ''
);
});
if (!anyEmployerFilled) {
alert("Please fill at least one employer detail since you selected 'Experienced'");
hasErrors = true;
}
}
return !hasErrors;
}
// Add real-time validation as user types
const employerInputs = document.querySelectorAll('#previous_employer_table_body input');
employerInputs.forEach(input => {
input.addEventListener('input', function() {
// When user types in any employer field, validate that row
const row = this.closest('tr');
if (row) {
validateRow(row);
}
});
});
// Add change event to experience radio buttons
const experienceRadios = document.querySelectorAll('input[name="experience"]');
experienceRadios.forEach(radio => {
radio.addEventListener('change', function() {
if (this.value === 'experienced' &amp;&amp; this.checked) {
// Make first employer row required
const firstRow = document.querySelector('#previous_employer_table_body tr');
if (firstRow) {
const companyNameInput = firstRow.querySelector('input[name="company_name"]');
const designationInput = firstRow.querySelector('input[name="designation"]');
const dojInput = firstRow.querySelector('input[name="doj"]');
if (companyNameInput) companyNameInput.required = true;
if (designationInput) designationInput.required = true;
if (dojInput) dojInput.required = true;
}
} else if (this.value === 'fresher' &amp;&amp; this.checked) {
// Remove required from all employer fields
const allInputs = document.querySelectorAll('#previous_employer_table_body input');
allInputs.forEach(input => {
input.required = false;
input.classList.remove("is-invalid");
});
}
});
});
// Helper function to validate a single row
function validateRow(row) {
const companyNameInput = row.querySelector('input[name="company_name"]');
const designationInput = row.querySelector('input[name="designation"]');
const dojInput = row.querySelector('input[name="doj"]');
// Check if ANY field in this row has a value
const hasValues = Array.from(row.querySelectorAll('input')).some(input =>
input.value &amp;&amp; input.value.trim() !== ''
);
if (hasValues) {
// Set required attributes
if (companyNameInput) companyNameInput.required = true;
if (designationInput) designationInput.required = true;
if (dojInput) dojInput.required = true;
// Validate
if (companyNameInput &amp;&amp; !companyNameInput.value.trim()) {
companyNameInput.classList.add("is-invalid");
} else if (companyNameInput) {
companyNameInput.classList.remove("is-invalid");
}
if (designationInput &amp;&amp; !designationInput.value.trim()) {
designationInput.classList.add("is-invalid");
} else if (designationInput) {
designationInput.classList.remove("is-invalid");
}
if (dojInput &amp;&amp; !dojInput.value.trim()) {
dojInput.classList.add("is-invalid");
} else if (dojInput) {
dojInput.classList.remove("is-invalid");
}
} else {
// No values, remove requirements
if (companyNameInput) {
companyNameInput.required = false;
companyNameInput.classList.remove("is-invalid");
}
if (designationInput) {
designationInput.required = false;
designationInput.classList.remove("is-invalid");
}
if (dojInput) {
dojInput.required = false;
dojInput.classList.remove("is-invalid");
}
}
}
// Previous button logic // Previous button logic
prevButtons.forEach(button => { prevButtons.forEach(button => {
@ -2412,7 +2149,6 @@
<h2>Thank You for Your Submission</h2> <h2>Thank You for Your Submission</h2>
<p>Your form has been successfully submitted.</p> <p>Your form has been successfully submitted.</p>
<a href="/" class="btn btn-primary">Go Back to Home</a> <a href="/" class="btn btn-primary">Go Back to Home</a>
<a t-if="applicant and applicant.post_onboarding_form_status == 'done'" t-att-href="'/download/jod/%s' % applicant.id" class="btn btn-primary">Download JOD</a>
</div> </div>
</t> </t>
</template> </template>

View File

@ -0,0 +1,50 @@
# __manifest__.py
{
'name': 'Recruitment web app',
'version': '18.0',
'category': 'Tools',
"author": "FTPROTECH PVT LTD",
"website": "https://www.ftprotech.in/",
'summary': 'Extracts the information of candidates from the resumes and creates applications in recruitment.',
'depends': ['base', 'web', 'web_editor', 'sms', 'hr_recruitment', 'hr_recruitment_extended', 'base_setup','website_hr_recruitment_extended'],
'assets': {
'web.assets_frontend': [
'hr_recruitment_web_app/static/lib/ckeditor/ckeditor.js',
'hr_recruitment_web_app/static/src/js/ats.js',
'hr_recruitment_web_app/static/src/js/job_requests.js',
'hr_recruitment_web_app/static/src/js/applicants.js',
'hr_recruitment_web_app/static/src/css/candidate.css',
#
#
'hr_recruitment_web_app/static/src/css/colors.css',
'hr_recruitment_web_app/static/src/css/ats.css',
'hr_recruitment_web_app/static/src/css/list.css',
'hr_recruitment_web_app/static/src/css/content.css',
'hr_recruitment_web_app/static/src/css/applicants.css',
'hr_recruitment_web_app/static/src/css/jd.css',
# 'hr_recruitment_web_application/static/src/css/applicants_details.css',
# 'hr_recruitment_web_application/static/src/css/ats_candidate.css',
],
},
'data': [
"security/ir.model.access.csv",
'views/recruitmnet_doc_upload_wizard.xml',
'views/hr_candidate.xml',
'views/res_config_view.xml',
'views/main.xml',
'views/recruitment.xml',
'views/jd.xml',
'views/applicants.xml',
'views/candidate.xml',
],
'images': ['static/description/banner.png'],
'external_dependencies': {
'python': ['pytesseract', 'pdf2image', 'pypdf'],
},
'installable': True,
'application': True,
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
from . import recruitment_doc_upload_wizard
from . import res_config_settings

View File

@ -0,0 +1,216 @@
from odoo import models, fields, api
from pypdf import PdfReader
from datetime import datetime
import base64
import re
from dateutil.relativedelta import relativedelta
import logging
import requests
from io import BytesIO
from pdf2image import convert_from_bytes
from PIL import Image
import pytesseract
import json
# from docx import Document
# import binascii
from odoo.tools.mimetypes import guess_mimetype
_logger = logging.getLogger(__name__)
class RecruitmentDocUploadWizard(models.TransientModel):
_name = 'recruitment.doc.upload.wizard'
_description = 'Recruitment Document Upload Wizard'
# Define the fields in the wizard
name = fields.Char("Name")
json_data = fields.Text("Json Data")
file_name = fields.Char('File Name')
file_data = fields.Binary('File Data', required=True)
active = fields.Boolean(default=True)
mimetype = fields.Char(string="Type", readonly=True)
file_html_text = fields.Html()
def compute_mimetype(self):
for record in self:
record.mimetype = ''
if record.file_data:
try:
# Fix padding
padded_data = record.file_data + b'=' * (-len(record.file_data) % 4)
binary = base64.b64decode(padded_data)
record.mimetype = guess_mimetype(binary)
except Exception:
record.mimetype = 'Invalid base64'
def action_upload(self):
# Implement the logic for file upload here
# You can use the fields file_data, file_name, etc., to save the data in the desired model
pass
def action_fetch_json(self):
for record in self:
record.compute_mimetype()
if not record.file_data:
record.json_data = "No file content provided."
continue
binary = base64.b64decode(record.file_data)
file_type = record.mimetype
text_content = ""
try:
if file_type == "application/pdf":
try:
pdf_reader = PdfReader(BytesIO(binary))
for page in pdf_reader.pages:
page_text = page.extract_text()
text_content += page_text
if not text_content:
images = convert_from_bytes(binary, dpi=300)
extracted_text = []
for image in images:
text = pytesseract.image_to_string(image)
extracted_text.append(text)
text_content = "\n".join(extracted_text)
except Exception as e:
_logger.error("Error reading PDF: %s", str(e))
text_content = ""
elif file_type in ["image/png", "image/jpeg", "image/jpg"]:
try:
image = Image.open(BytesIO(binary))
text_content = pytesseract.image_to_string(image)
except Exception as e:
_logger.error("Error processing image: %s", str(e))
text_content = ""
elif file_type == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
try:
doc = Document(BytesIO(binary))
text_content = ''
for section in doc.sections:
header = section.header
for paragraph in header.paragraphs:
text_content += paragraph.text + '\n'
for paragraph in doc.paragraphs:
text_content += paragraph.text + '\n'
except Exception as e:
_logger.error("Error processing DOCX: %s", str(e))
text_content = ""
else:
_logger.error("Unsupported file type: %s", file_type)
record.json_data = f"Unsupported file type: {file_type}"
continue
record.file_html_text = text_content
json_response = record.get_json_from_model(text_content)
import pdb
pdb.set_trace()
if json_response and "choices" in json_response and len(json_response["choices"]) > 0:
message_content = json_response["choices"][0].get("message", {}).get("content", "")
if message_content:
match = re.search(r'```json\n(.*?)\n```', message_content, re.DOTALL)
if match:
clean_json_str = match.group(1).strip() # Extract JSON content
try:
parsed_json = json.loads(clean_json_str)
record.json_data = json.dumps(parsed_json, indent=4)
file_name = parsed_json.get("name", "")
if file_name:
self.write({'file_name': file_name})
except json.JSONDecodeError as e:
_logger.error("Error parsing JSON: %s", str(e))
record.json_data = "Error parsing JSON"
else:
_logger.error("No valid JSON found in the content")
record.json_data = "No valid JSON format found"
else:
_logger.error("No message content found in the response")
record.json_data = "No message content found"
else:
_logger.error("No valid response or choices in the API response")
record.json_data = "No valid JSON data received."
except Exception as e:
_logger.error("Unexpected error during OCR processing: %s", str(e))
record.json_data = "An unexpected error occurred during file processing."
_logger.info("Stored JSON data for file: %s", record.file_name)
def normalize_gender(self, gender):
if gender:
return gender.replace(" ", "").lower()
return gender
def normalize_marital_status(self, marital_status):
if marital_status:
return marital_status.replace(" ", "").lower()
return marital_status
def parse_experience(self, experience_str):
years = 0
months = 0
year_match = re.search(r'(\d+)\s*[\+]*\s*years?', experience_str, re.IGNORECASE)
month_match = re.search(r'(\d+)\s*months?', experience_str, re.IGNORECASE)
if year_match:
years = int(year_match.group(1))
if month_match:
months = int(month_match.group(1))
return years, months
def get_json_from_model(self, text_content):
print(text_content)
api_url = "https://api.together.xyz/v1/chat/completions"
together_api_key = self.env['ir.config_parameter'].sudo().get_param('hr_recruitment_web_app.together_api_key')
headers = {
'Authorization': 'Bearer %s' % together_api_key,
'Content-Type': 'application/json',
}
current_date = datetime.now()
previous_month_date = current_date - relativedelta(months=1)
previous_month_year = previous_month_date.strftime("%B %Y")
payload = {
"messages": [
{
"role": "system",
"content": "provide the json data from the above content for below fields---\n\nname-- particularly the full name of the candidate\nskills-- the skills of the candidate mentioned in the text specifically under skills section (add both soft and technical skills in this). \nemail -- the contact email of the candidate \nphone -- contact number of candidate usually a 10-12 number digits\ndegree-- the degree or qualification of the candidate mentioned in resume (only the name of the degrees or qualifications and not the whole details) "
f"\n experience in years and months in the format: Title of the experience (type of experience) (from month/year to month/year) -> years and months (If the end date is marked as 'present' or 'till now', assume today's date is {previous_month_year} and calculate the months also properly). \n"
"\n Total Experience (Non-Overlapping) : in years and months \n"
"\n location-- search for the location or place mentioned in the resume where the candidate belong to. \n gender-- the gender of the candidate if mentioned. \ndate_of_birth-- the date of birth of the candidate in the format-%d/%m/%Y \nmarital_status-- the marital status of the candidate \n languages-- the languages known by the candidate mentioned in resume(not technical languages but the spoken ones specifically mentioned under languages section)\n"
},
{
"role": "user",
"content": text_content
}
],
"model": "Qwen/Qwen2.5-72B-Instruct-Turbo",
}
try:
response = requests.post(api_url, json=payload, headers=headers)
if response.status_code == 200:
try:
json_response = response.json()
return json_response
except ValueError as e:
_logger.error("Error parsing JSON: %s", str(e))
return {}
else:
_logger.error("Error in API call: %s", response.text)
return {}
except Exception as e:
_logger.error("Exception during API call: %s", str(e))
return {}

View File

@ -0,0 +1,7 @@
from odoo import api, fields, models, _
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
together_api_key = fields.Char(config_parameter='hr_recruitment_web_app.together_api_key', string="Together API key")

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_recruitment_doc_upload_wizard_user,recruitment.doc.upload.wizard user,model_recruitment_doc_upload_wizard,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_recruitment_doc_upload_wizard_user recruitment.doc.upload.wizard user model_recruitment_doc_upload_wizard base.group_user 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -0,0 +1,563 @@
@import url('colors.css');
/* ========= application creation css ========= */
/* ===== Application Modal Styles ===== */
.application-creation-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.application-creation-modal.show {
display: flex;
opacity: 1;
}
.application-creation-modal .application-creation-content {
background-color: var(--white);
width: 85%;
max-width: 1200px;
border-radius: 8px;
box-shadow: 0 5px 20px var(--shadow-dark);
transform: translateY(-20px);
transition: transform 0.3s ease;
max-height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.application-creation-modal.show .application-creation-content {
transform: translateY(0);
}
.application-creation-modal .application-creation-header {
padding: 20px;
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue) 100%);
color: var(--white);
border-radius: 8px 8px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.application-creation-modal .header-icon-container {
display: flex;
align-items: center;
gap: 15px;
}
.application-creation-modal .header-icon {
font-size: 24px;
color: var(--white);
}
.application-creation-modal .application-creation-header h3 {
margin: 0;
font-size: 1.4rem;
font-weight: 600;
color: var(--white);
}
.application-creation-modal .application-creation-close {
font-size: 28px;
cursor: pointer;
transition: transform 0.2s;
color: var(--white);
background: none;
border: none;
}
.application-creation-modal .application-creation-close:hover {
transform: scale(1.2);
color: var(--gray-200);
}
.application-creation-modal .application-creation-body {
padding: 25px;
overflow-y: auto;
flex-grow: 1;
}
.application-creation-modal .form-section {
background-color: var(--white);
border-radius: 6px;
padding: 20px;
box-shadow: 0 2px 5px var(--shadow-color);
border-left: 4px solid var(--secondary-purple);
margin-bottom: 20px;
}
.application-creation-modal .section-title {
margin-top: 0;
margin-bottom: 20px;
color: var(--text-primary);
font-size: 18px;
display: flex;
align-items: center;
gap: 10px;
}
.application-creation-modal .section-title i {
color: var(--secondary-purple);
}
.application-creation-modal .form-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.application-creation-modal .form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.application-creation-modal .form-group label {
font-weight: 500;
color: var(--text-secondary);
font-size: 14px;
}
.application-creation-modal .form-input,
.application-creation-modal .form-select,
.application-creation-modal .form-textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s, box-shadow 0.3s;
background-color: var(--white);
color: var(--text-primary);
}
.application-creation-modal .form-input:focus,
.application-creation-modal .form-select:focus,
.application-creation-modal .form-textarea:focus {
border-color: var(--secondary-purple);
box-shadow: 0 0 0 3px var(--primary-blue-light);
outline: none;
}
/* Checkbox styles */
.application-creation-modal .same-as-current {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
}
.application-creation-modal .same-as-current input[type="checkbox"] {
width: auto;
}
/* Education entries */
.application-creation-modal .education-entries {
margin-bottom: 15px;
}
.application-creation-modal .education-entry {
position: relative;
padding: 15px;
background-color: var(--gray-50);
border-radius: 6px;
margin-bottom: 15px;
border: 1px solid var(--border-light);
}
.application-creation-modal .remove-education {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
color: var(--danger);
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
gap: 5px;
}
.application-creation-modal .remove-education:hover {
text-decoration: underline;
}
.application-creation-modal .add-education {
background-color: var(--primary-blue);
color: var(--white);
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 20px;
transition: background-color 0.3s;
}
.application-creation-modal .add-education:hover {
background-color: var(--primary-blue-dark);
}
/* Skills section */
.application-creation-modal .skills-container {
margin-top: 20px;
}
/* Upload area */
/* Resume Section */
.application-creation-modal .resume-upload-container {
display: flex;
gap: 20px;
}
.application-creation-modal .upload-area {
flex: 1;
border: 2px dashed var(--border-color);
border-radius: 6px;
padding: 30px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
max-width: 30%;
}
.application-creation-modal .upload-area:hover {
border-color: var(--primary-blue);
background-color: var(--gray-50);
}
.application-creation-modal .upload-icon {
font-size: 40px;
color: var(--primary-blue);
margin-bottom: 10px;
}
.application-creation-modal .upload-area h5 {
margin: 0 0 5px;
color: var(--text-primary);
}
.application-creation-modal .upload-area p {
margin: 0;
color: var(--text-muted);
font-size: 14px;
}
.application-creation-modal .resume-preview {
flex: 1;
border: 1px solid var(--border-light);
border-radius: 6px;
padding: 15px;
min-height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--gray-50);
}
.application-creation-modal .resume-preview-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: var(--text-muted);
height: 100%;
}
.application-creation-modal .resume-preview-placeholder i {
font-size: 40px;
margin-bottom: 10px;
color: var(--primary-blue);
}
.application-creation-modal .btn-danger {
background-color: var(--danger);
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
.application-creation-modal .btn-danger:hover {
background-color: #dc3545;
}
/* Additional attachments */
.application-creation-modal .additional-attachments {
margin-top: 20px;
}
.application-creation-modal .attachments-list {
margin: 10px 0;
}
.application-creation-modal .add-attachment {
background-color: transparent;
color: var(--primary-blue);
border: 1px solid var(--primary-blue);
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s;
}
.application-creation-modal .add-attachment:hover {
background-color: var(--primary-blue-light);
}
/* Form actions */
.application-creation-modal .form-actions {
position: sticky;
bottom: 0;
background: var(--white);
padding: 15px 25px;
border-top: 1px solid var(--border-light);
z-index: 100;
margin-top: auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.application-creation-modal .btn-cancel {
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: transform 0.2s, box-shadow 0.2s;
background-color: var(--gray-100);
color: var(--text-secondary);
border: 1px solid var(--border-color);
}
.application-creation-modal .btn-cancel:hover {
background-color: var(--gray-200);
}
.application-creation-modal .btn-application-primary {
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
align-items: center;
gap: 8px;
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue) 100%);
color: var(--white);
border: none;
}
.application-creation-modal .btn-application-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--shadow-color);
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue-dark) 100%);
}
.application-creation-modal .footer-right {
display: flex;
gap: 10px;
}
/* SELECT2 custom styles */
.application-creation-modal .select2-container {
z-index: 10000 !important;
width: 100% !important;
}
.application-creation-modal .select2-container--default .select2-selection--multiple,
.application-creation-modal .select2-container--default .select2-selection--single {
border: 1px solid var(--border-color) !important;
border-radius: 4px !important;
min-height: 40px;
background-color: var(--white);
padding: 6px 8px !important;
flex-wrap: wrap !important;
gap: 4px;
}
.application-creation-modal .select2-container--default .select2-selection--multiple .select2-selection__choice {
background-color: var(--primary-blue) !important;
border: none !important;
color: var(--white) !important;
padding: 2px 8px !important;
}
.application-creation-modal .select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
color: var(--white) !important;
}
.application-creation-modal .select2-dropdown {
border: 1px solid var(--border-color) !important;
box-shadow: 0 2px 5px var(--shadow-color) !important;
}
.application-creation-modal .select2-container--default .select2-selection__rendered {
display: flex !important;
align-items: center !important;
vertical-align: middle !important;
line-height: normal !important;
color: var(--text-primary);
}
.application-creation-modal .select2-container--default .select2-selection--multiple .select2-selection__rendered {
display: flex !important;
flex-wrap: wrap !important;
align-items: center !important;
width: 100% !important;
padding: 2px 5px !important;
overflow: visible !important;
box-sizing: border-box !important;
}
.application-creation-modal .select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: var(--primary-blue) !important;
color: var(--white) !important;
}
/* Scrollbar styling */
.application-creation-modal .application-creation-body::-webkit-scrollbar {
width: 8px;
}
.application-creation-modal .application-creation-body::-webkit-scrollbar-track {
background: var(--gray-100);
}
.application-creation-modal .application-creation-body::-webkit-scrollbar-thumb {
background: var(--primary-blue);
border-radius: 4px;
}
.application-creation-modal .application-creation-body::-webkit-scrollbar-thumb:hover {
background: var(--primary-blue-dark);
}
/* Responsive adjustments */
@media (max-width: 1024px) {
.application-creation-modal .application-creation-content {
width: 90%;
}
}
@media (max-width: 768px) {
.application-creation-modal .application-creation-content {
width: 95%;
height: 95vh;
}
.application-creation-modal .form-grid {
grid-template-columns: 1fr;
}
}
/* Special cases */
.application-creation-modal .marital-anniversary {
display: none;
}
.application-creation-modal #application-marital[value="married"] ~ .marital-anniversary {
display: flex;
}
.application-creation-modal .form-section.profile-section:first-child {
margin-top: 0;
}
.application-creation-modal .select2-dropdown {
border: 1px solid var(--border-color) !important;
box-shadow: 0 2px 5px var(--shadow-color) !important;
}
/* Base button styling */
.btn-stage {
position: relative;
min-width: 100px;
padding: 0.5rem 1rem;
margin: 0 2px;
border-radius: 4px;
font-size: 0.85rem;
font-weight: 500;
text-align: center;
transition: all 0.25s ease;
cursor: pointer;
border: 1px solid #ddd;
outline: none;
}
/* Current stage styling */
.btn-stage-current {
background-color: #0d6efd; /* Primary blue */
color: white;
border-color: #0d6efd;
box-shadow: 0 2px 5px rgba(13, 110, 253, 0.3);
font-weight: 600;
}
/* Other stage options */
.btn-stage-option {
background-color: white;
color: #555;
border-color: #ddd;
}
/* Hover effects */
.btn-stage-option:hover {
background-color: #f0f7ff; /* Very light blue */
border-color: #0d6efd;
color: #0d6efd;
}
.btn-stage-current:hover {
background-color: #0b5ed7; /* Slightly darker blue */
}
/* Active state */
.btn-stage:active {
transform: translateY(1px);
}
/* Focus state */
.btn-stage:focus {
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25);
}
/* Disabled state during loading */
.btn-stage.processing {
opacity: 0.7;
pointer-events: none;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.btn-stage {
min-width: 80px;
padding: 0.4rem 0.6rem;
font-size: 0.8rem;
margin: 2px;
}
}

View File

@ -0,0 +1,483 @@
/* ===== Modern Color Palette ===== */
:root {
/* Light Theme */
--primary-color: #4361ee;
--primary-light: #f0f4ff;
--primary-dark: #3a56d5;
--secondary-color: #6c757d;
--accent-color: #7209b7;
--bg-color: #f8fafc;
--surface-color: #ffffff;
--text-primary: #1e293b;
--text-secondary: #64748b;
--border-color: #e2e8f0;
--success-color: #10b981;
--warning-color: #f59e0b;
--error-color: #ef4444;
}
[data-theme="dark"] {
/* Dark Theme */
--primary-color: #5a7cff;
--primary-light: #1e293b;
--primary-dark: #4361ee;
--secondary-color: #94a3b8;
--accent-color: #9d4edd;
--bg-color: #0f172a;
--surface-color: #1e293b;
--text-primary: #f8fafc;
--text-secondary: #cbd5e1;
--border-color: #334155;
}
/* Add smooth transitions for theme switching */
body.ats-app {
transition: background-color 0.3s ease, color 0.3s ease;
}
/* ===== Theme Toggle Styles ===== */
.theme-toggle-container {
padding: 1rem;
margin-top: auto; /* Push to bottom */
border-top: 1px solid var(--border-color);
}
.theme-toggle {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.75rem 1rem;
border-radius: 8px;
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
}
.theme-toggle:hover {
background-color: var(--primary-light);
color: var(--primary-color);
}
.light-icon {
display: none;
color: #fbbf24; /* Amber color for sun */
}
.dark-icon {
color: #cbd5e1; /* Light gray for moon */
}
[data-theme="dark"] .light-icon {
display: block;
}
[data-theme="dark"] .dark-icon {
display: none;
}
.theme-text {
transition: opacity 0.2s ease;
}
/* Collapsed state styles */
.sidebar.collapsed .theme-text {
display: none;
}
.sidebar.collapsed .theme-toggle {
justify-content: center;
padding: 0.75rem 0;
}
.sidebar.collapsed .theme-toggle i {
margin-right: 0;
font-size: 1.25rem;
}
/* ===== Global Reset & Typography ===== */
body.ats-app {
margin: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
line-height: 1.5;
}
/* ===== Main Layout ===== */
.ats-app .layout-container {
display: flex;
flex: 1;
overflow: hidden;
}
/* ===== Header (Modern Design) ===== */
.ats-app .main-header {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 2rem;
background-color: var(--surface-color);
box-shadow: var(--shadow-sm);
z-index: 10;
border-bottom: 1px solid var(--border-color);
}
.ats-app .main-header img {
height: 36px;
width: auto;
}
.ats-app .main-header span {
font-size: 1.25rem;
font-weight: 600;
color: var(--primary-color);
letter-spacing: -0.5px;
}
/* ===== Sidebar (Modern Redesign) ===== */
.ats-app .sidebar {
background-color: var(--surface-color);
color: var(--text-secondary);
box-shadow: var(--shadow-md);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
display: flex;
flex-direction: column;
padding: 1.5rem 0;
position: relative;
border-right: 1px solid var(--border-color);
}
/* Sidebar States */
.ats-app .sidebar.expanded {
width: 280px;
min-width: 280px;
}
.ats-app .sidebar.collapsed {
width: 88px;
min-width: 88px;
padding: 1.5rem 0;
}
.ats-app .sidebar.collapsed .menu-item-text,
.ats-app .sidebar.collapsed .list-title,
.ats-app .sidebar.collapsed .main-header span {
opacity: 0;
width: 0;
height: 0;
overflow: hidden;
position: absolute;
}
/* ===== Navigation Menu (Improved UX) ===== */
.ats-app .menu-list {
list-style: none;
padding: 0;
margin: 1rem 0;
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.ats-app .list-title {
color: var(--text-secondary);
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05rem;
margin: 1.5rem 1.5rem 0.5rem;
transition: all 0.3s ease;
}
.ats-app .menu-list a {
display: flex;
align-items: center;
gap: 1rem;
color: var(--text-secondary);
text-decoration: none;
padding: 0.75rem 1.5rem;
border-radius: var(--radius-md);
transition: all 0.2s ease;
font-size: 0.95rem;
margin: 0 0.5rem;
position: relative;
}
.ats-app .menu-list a i {
font-size: 1.25rem;
min-width: 24px;
text-align: center;
}
/* Hover and Active State */
.ats-app .menu-list a:hover {
background-color: var(--primary-light);
color: var(--primary-color);
}
.ats-app .menu-list a.active {
background-color: var(--primary-light);
color: var(--primary-color);
font-weight: 500;
}
.ats-app .menu-list a.active::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background-color: var(--primary-color);
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
}
/* Toggle Button (Improved) */
.ats-app .toggle-btn {
position: absolute;
top: 1.5rem;
right: -12px;
background-color: var(--surface-color);
color: var(--primary-color);
border: 1px solid var(--border-color);
padding: 0.5rem;
cursor: pointer;
border-radius: 50%;
box-shadow: var(--shadow-sm);
z-index: 20;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: all 0.3s ease;
}
.ats-app .toggle-btn:hover {
background-color: var(--primary-color);
color: white;
transform: translateX(2px);
}
.ats-app .sidebar.collapsed .toggle-btn {
right: -12px;
transform: rotate(180deg);
}
.ats-app .sidebar.collapsed .toggle-btn:hover {
transform: rotate(180deg) translateX(2px);
}
/* ===== Main Content Area (Improved) ===== */
.ats-app .content-area {
flex: 1;
padding: 3px;
background-color: var(--bg-color);
overflow-y: auto;
min-width: 0;
scroll-behavior: smooth;
}
/* Content header */
.ats-app .content-header {
margin-bottom: 1.5rem;
}
.ats-app .content-header h1 {
font-size: 1.75rem;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 0.5rem;
}
/* ===== Responsive Design (Enhanced) ===== */
@media (max-width: 1024px) {
.ats-app .layout-container {
flex-direction: column;
}
.ats-app .sidebar {
width: 100%;
height: auto;
padding: 0;
border-right: none;
border-bottom: 1px solid var(--border-color);
}
.ats-app .sidebar.expanded {
height: auto;
}
.ats-app .sidebar.collapsed {
height: 60px;
overflow: hidden;
}
.ats-app .menu-list {
flex-direction: row;
flex-wrap: wrap;
margin: 0;
padding: 0.5rem;
gap: 0.25rem;
}
.ats-app .menu-list a {
flex-direction: column;
padding: 0.75rem;
gap: 0.25rem;
font-size: 0.75rem;
margin: 0;
}
.ats-app .menu-list a i {
font-size: 1.1rem;
}
.ats-app .menu-list a.active::before {
width: 100%;
height: 3px;
top: auto;
bottom: 0;
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.ats-app .list-title {
display: none;
}
.ats-app .toggle-btn {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
top: auto;
left: auto;
background-color: var(--primary-color);
color: white;
box-shadow: var(--shadow-lg);
width: 48px;
height: 48px;
font-size: 1.25rem;
}
.ats-app .content-area {
padding: 1.25rem;
}
}
/* Animation for smoother transitions */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
.ats-app .content-area > * {
animation: fadeIn 0.3s ease-out;
}
/* Menu list styles */
.menu-list {
list-style: none;
padding: 0;
margin: 0;
}
.menu-list li {
margin-bottom: 4px;
}
.menu-item {
display: flex;
align-items: center;
padding: 12px 16px;
color: var(--text-secondary);
text-decoration: none;
border-radius: 6px;
transition: all 0.2s ease;
}
.menu-item:hover {
background-color: var(--primary-light);
color: var(--primary-color);
}
.menu-item.active {
background-color: var(--primary-light);
color: var(--primary-color);
font-weight: 500;
}
.menu-icon {
font-size: 1.25rem;
min-width: 24px;
margin-right: 12px;
}
.menu-item-text {
transition: opacity 0.2s ease;
}
/* Collapsed state styles */
.sidebar.collapsed .menu-item {
justify-content: center;
padding: 12px 0;
}
.sidebar.collapsed .menu-icon {
margin-right: 0;
font-size: 1.4rem;
}
.sidebar.collapsed .menu-item-text {
display: none;
}
/* Active state indicator for collapsed */
.sidebar.collapsed .menu-item.active::after {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 24px;
background-color: var(--primary-color);
border-radius: 0 4px 4px 0;
}
/* Mobile responsive styles */
@media (max-width: 768px) {
.sidebar.collapsed .menu-list {
display: flex;
justify-content: space-around;
}
.sidebar.collapsed .menu-item {
flex-direction: column;
padding: 8px 4px;
font-size: 0.75rem;
}
.sidebar.collapsed .menu-icon {
font-size: 1.2rem;
margin-bottom: 4px;
}
.sidebar.collapsed .menu-item.active::after {
left: 50%;
top: auto;
bottom: 0;
transform: translateX(-50%);
width: 24px;
height: 3px;
}
}

View File

@ -0,0 +1,667 @@
@import url('colors.css');
/* ====== candidate creation template ======= */
/* ===== Candidate Form Styles ===== */
.candidate-form-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.candidate-form-modal.show {
display: flex;
opacity: 1;
}
.candidate-form-modal .candidate-form-content {
background-color: var(--white);
width: 90%;
max-width: 1400px;
border-radius: 8px;
box-shadow: 0 5px 20px var(--shadow-dark);
transform: translateY(-20px);
transition: transform 0.3s ease;
max-height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.candidate-form-modal.show .candidate-form-content {
transform: translateY(0);
}
.candidate-form-modal .candidate-form-header {
padding: 20px;
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue) 100%);
color: var(--white);
border-radius: 8px 8px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.candidate-form-modal .header-icon-container {
display: flex;
align-items: center;
gap: 15px;
}
.candidate-form-modal .header-icon {
font-size: 24px;
color: var(--white);
}
.candidate-form-modal .candidate-form-header h3 {
margin: 0;
font-size: 1.4rem;
font-weight: 600;
color: var(--white);
}
.candidate-form-modal .candidate-form-close {
font-size: 28px;
cursor: pointer;
transition: transform 0.2s;
color: var(--white);
background: none;
border: none;
}
.candidate-form-modal .candidate-form-close:hover {
transform: scale(1.2);
color: var(--gray-200);
}
.candidate-form-modal .candidate-form-body {
padding: 25px;
overflow-y: auto;
flex-grow: 1;
}
/* Header Section with Avatar */
.candidate-form-modal .header-section {
display: flex;
align-items: center;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border-light);
}
.candidate-form-modal .avatar-container {
display: flex;
align-items: center;
gap: 20px;
width: 100%;
}
.candidate-form-modal .candidate-avatar {
position: relative;
width: 120px;
height: 120px;
border-radius: 50%;
overflow: hidden;
border: 3px solid var(--primary-blue);
box-shadow: 0 3px 10px var(--shadow-color);
margin-right: 20px;
}
.candidate-form-modal .candidate-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.candidate-form-modal .avatar-upload {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
color: white;
text-align: center;
padding: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
.candidate-form-modal .avatar-upload:hover {
background-color: rgba(0, 0, 0, 0.7);
}
.candidate-form-modal .avatar-upload i {
margin-right: 5px;
}
.candidate-form-modal .avatar-upload input {
display: none;
}
.candidate-form-modal .basic-info {
flex-grow: 1;
}
/* Button Box Section */
.candidate-form-modal .button-box {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.candidate-form-modal .stat-button {
flex: 1;
min-width: 200px;
display: flex;
align-items: center;
gap: 10px;
padding: 12px 15px;
background-color: var(--gray-50);
border: 1px solid var(--border-light);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
}
.candidate-form-modal .stat-button:hover {
background-color: var(--gray-100);
transform: translateY(-2px);
box-shadow: 0 2px 5px var(--shadow-color);
}
.candidate-form-modal .stat-button i {
font-size: 24px;
color: var(--primary-blue);
}
.candidate-form-modal .stat-info {
display: flex;
flex-direction: column;
}
.candidate-form-modal .stat-value {
font-weight: 600;
color: var(--text-primary);
}
.candidate-form-modal .stat-text {
font-size: 12px;
color: var(--text-muted);
}
/* Form Sections */
.candidate-form-modal .form-section {
background-color: var(--white);
border-radius: 6px;
padding: 20px;
box-shadow: 0 2px 5px var(--shadow-color);
border-left: 4px solid var(--secondary-purple);
margin-bottom: 20px;
}
.candidate-form-modal .section-title {
margin-top: 0;
margin-bottom: 20px;
color: var(--text-primary);
font-size: 18px;
display: flex;
align-items: center;
gap: 10px;
}
.candidate-form-modal .section-title i {
color: var(--secondary-purple);
}
.candidate-form-modal .form-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.candidate-form-modal .form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.candidate-form-modal .form-group label {
font-weight: 500;
color: var(--text-secondary);
font-size: 14px;
}
.candidate-form-modal .form-input,
.candidate-form-modal .form-select,
.candidate-form-modal .form-textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s, box-shadow 0.3s;
background-color: var(--white);
color: var(--text-primary);
}
.candidate-form-modal .form-input:focus,
.candidate-form-modal .form-select:focus,
.candidate-form-modal .form-textarea:focus {
border-color: var(--secondary-purple);
box-shadow: 0 0 0 3px var(--primary-blue-light);
outline: none;
}
/* Resume Section */
.candidate-form-modal .resume-upload-container {
display: flex;
gap: 20px;
}
.candidate-form-modal .resume-upload-area {
flex: 1;
border: 2px dashed var(--border-color);
border-radius: 6px;
padding: 30px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
max-width: 30%;
}
.candidate-form-modal .resume-upload-area:hover {
border-color: var(--primary-blue);
background-color: var(--gray-50);
}
.candidate-form-modal .upload-icon {
font-size: 40px;
color: var(--primary-blue);
margin-bottom: 10px;
}
.candidate-form-modal .resume-upload-area h5 {
margin: 0 0 5px;
color: var(--text-primary);
}
.candidate-form-modal .resume-upload-area p {
margin: 0;
color: var(--text-muted);
font-size: 14px;
}
.candidate-form-modal .resume-preview {
flex: 1;
border: 1px solid var(--border-light);
border-radius: 6px;
padding: 15px;
min-height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--gray-50);
}
.candidate-form-modal .resume-preview-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: var(--text-muted);
height: 100%;
}
.candidate-form-modal .resume-preview-placeholder i {
font-size: 40px;
margin-bottom: 10px;
color: var(--primary-blue);
}
.candidate-form-modal .btn-danger {
background-color: var(--danger);
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
.candidate-form-modal .btn-danger:hover {
background-color: #dc3545;
}
/* Make sure dropzone doesn't interfere with child elements */
.candidate-form-modal .resume-upload-area > * {
pointer-events: none;
}
.candidate-form-modal .resume-upload-area input {
pointer-events: auto;
}
.candidate-form-modal #resume-iframe {
width: 100%;
height: 500px;
border: none;
}
.candidate-form-modal #resume-image {
max-width: 100%;
max-height: 500px;
object-fit: contain;
}
.candidate-form-modal #unsupported-format {
text-align: center;
color: var(--danger);
}
.candidate-form-modal #unsupported-format i {
font-size: 40px;
margin-bottom: 10px;
}
.candidate-form-modal #download-resume {
display: inline-flex;
align-items: center;
gap: 5px;
}
/* Skills Section */
.candidate-form-modal .skills-container {
margin-top: 15px;
}
.candidate-form-modal .skills-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
}
.candidate-form-modal .skill-tag {
background-color: var(--primary-blue);
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 14px;
display: flex;
align-items: center;
gap: 5px;
}
.candidate-form-modal .skill-tag i {
cursor: pointer;
font-size: 12px;
}
.candidate-form-modal .btn-add-skill {
background-color: transparent;
color: var(--primary-blue);
border: 1px solid var(--primary-blue);
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s;
}
.candidate-form-modal .btn-add-skill:hover {
background-color: var(--primary-blue-light);
}
/* Notebook Tabs */
.candidate-form-modal .notebook-tabs {
display: flex;
border-bottom: 1px solid var(--border-light);
margin-bottom: 20px;
}
.candidate-form-modal .tab {
padding: 10px 20px;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.3s;
}
.candidate-form-modal .tab.active {
border-bottom-color: var(--primary-blue);
color: var(--primary-blue);
font-weight: 600;
}
.candidate-form-modal .tab:hover:not(.active) {
background-color: var(--gray-50);
}
.candidate-form-modal .notebook-content {
margin-top: 15px;
}
.candidate-form-modal .tab-content {
display: none;
}
.candidate-form-modal .tab-content.active {
display: block;
}
/* Sub-sections in notebook */
.candidate-form-modal .sub-section {
margin-bottom: 30px;
}
.candidate-form-modal .sub-section-title {
display: flex;
align-items: center;
gap: 10px;
color: var(--text-primary);
font-size: 16px;
margin-bottom: 15px;
}
.candidate-form-modal .sub-section-title i {
color: var(--secondary-purple);
}
/* Form Actions */
.candidate-form-modal .form-actions {
position: sticky;
bottom: 0;
background: var(--white);
padding: 15px 25px;
border-top: 1px solid var(--border-light);
z-index: 100;
margin-top: auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.candidate-form-modal .btn-cancel {
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: transform 0.2s, box-shadow 0.2s;
background-color: var(--gray-100);
color: var(--text-secondary);
border: 1px solid var(--border-color);
}
.candidate-form-modal .btn-cancel:hover {
background-color: var(--gray-200);
}
.candidate-form-modal .btn-candidate-primary {
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
align-items: center;
gap: 8px;
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue) 100%);
color: var(--white);
border: none;
}
.candidate-form-modal .btn-candidate-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--shadow-color);
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue-dark) 100%);
}
.candidate-form-modal .footer-right {
display: flex;
gap: 10px;
}
.candidate-form-modal .select2-container {
z-index: 10000 !important;
width: 100% !important;
}
.candidate-form-modal .select2-container--default .select2-selection--multiple,
.candidate-form-modal .select2-container--default .select2-selection--single {
border: 1px solid var(--border-color) !important;
border-radius: 4px !important;
min-height: 40px;
background-color: var(--white);
padding: 5px 5px 0 5px !important;
}
.candidate-form-modal .select2-container--default .select2-selection--multiple .select2-selection__choice {
background-color: var(--primary-blue) !important;
border: none !important;
color: var(--white) !important;
padding: 2px 8px !important;
}
.candidate-form-modal .select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
color: var(--white) !important;
}
.application-creation-modal .select2-dropdown {
border: 1px solid var(--border-color) !important;
box-shadow: 0 2px 5px var(--shadow-color) !important;
}
.candidate-form-modal .select2-container--default .select2-selection__rendered {
display: flex !important;
align-items: center !important;
vertical-align: middle !important;
line-height: normal !important;
color: var(--text-primary);
padding: 2px 5px !important;
}
.candidate-form-modal .select2-container--default .select2-selection--multiple .select2-selection__rendered {
display: flex !important;
flex-wrap: wrap !important;
align-items: center !important;
width: 100% !important;
padding: 2px 5px !important;
overflow: visible !important;
box-sizing: border-box !important;
}
.candidate-form-modal .select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: var(--primary-blue) !important;
color: var(--white) !important;
}
/* Scrollbar styling */
.candidate-form-modal .candidate-form-body::-webkit-scrollbar {
width: 8px;
}
.candidate-form-modal .candidate-form-body::-webkit-scrollbar-track {
background: var(--gray-100);
}
.candidate-form-modal .candidate-form-body::-webkit-scrollbar-thumb {
background: var(--primary-blue);
border-radius: 4px;
}
.candidate-form-modal .candidate-form-body::-webkit-scrollbar-thumb:hover {
background: var(--primary-blue-dark);
}
.candidate-form-modal .btn-candidate-primary {
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
align-items: center;
gap: 8px;
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue) 100%);
color: var(--white);
border: none;
}
.candidate-form-modal .btn-candidate-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--shadow-color);
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue-dark) 100%);
}
/* Responsive adjustments */
@media (max-width: 1024px) {
.candidate-form-modal .candidate-form-content {
width: 95%;
}
.candidate-form-modal .resume-upload-container {
flex-direction: column;
}
}
@media (max-width: 768px) {
.candidate-form-modal .candidate-form-content {
width: 98%;
height: 95vh;
}
.candidate-form-modal .form-grid {
grid-template-columns: 1fr;
}
.candidate-form-modal .avatar-container {
flex-direction: column;
text-align: center;
}
.candidate-form-modal .button-box {
justify-content: center;
}
.candidate-form-modal .stat-button {
min-width: 150px;
}
}

View File

@ -0,0 +1,66 @@
:root {
/* Primary Colors */
--primary-blue: #3498db;
--primary-blue-dark: #2980b9;
--primary-blue-light: #e8f0ff;
/* Secondary Colors */
--secondary-purple: #6f42c1;
--secondary-green: #28a745;
--secondary-red: #dc3545;
--secondary-yellow: #ffc107;
/* Grayscale */
--white: #ffffff;
--gray-100: #f8f9fa;
--gray-200: #e9ecef;
--gray-300: #dee2e6;
--gray-400: #ced4da;
--gray-500: #adb5bd;
--gray-600: #6c757d;
--gray-700: #495057;
--gray-800: #343a40;
--gray-900: #212529;
--black: #000000;
/* Semantic Colors */
--success: #28a745;
--info: #17a2b8;
--warning: #ffc107;
--danger: #dc3545;
/* Background Colors */
--body-bg: #f5f6fa;
--sidebar-bg: #FAFCFF;
--create-model-bg: #FAFCFF;
--content-bg: #E3E9EF;
--active-search-bg: #ffffff;
--active-search-hover-bg: #f0f0f0;
--add-btn-bg: #3498db;
--add-btn-hover-bg: #2980b9;
--side-panel-bg: #FAFCFF;
--side-panel-item-hover: #f0f8ff;
--side-panel-item-selected: #d7eaff;
/* Text Colors */
--text-primary: #2f3542;
--text-secondary: #4B5865;
--text-muted: #6c757d;
--sidebar-text: #0F1419;
--active-search-text: #333;
--add-btn-color: #ffffff;
/* Border Colors */
--border-color: #dcdde1;
--border-light: #e0e0e0;
/* Shadow Colors */
--shadow-color: rgba(0, 0, 0, 0.1);
--shadow-dark: rgba(0, 0, 0, 0.2);
/* Status Colors */
--status-new: #3498db;
--status-interview: #f39c12;
--status-hired: #2ecc71;
--status-rejected: #e74c3c;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,708 @@
@import url('colors.css');
/* ===== Job List View Styling ===== */
.ats-list-container {
display: flex;
flex-direction: column;
height: 99%;
width: 100%;
border: 1px solid var(--border-color);
border-radius: 6px;
overflow: hidden;
box-shadow: 0 2px 8px var(--shadow-color);
background-color: var(--body-bg);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.ats-list-container .ats-list-search {
flex: 0 0 auto;
padding: 12px;
background-color: var(--gray-100);
border-bottom: 1px solid var(--border-light);
}
.ats-list-container .ats-list-search input {
width: 100%;
padding: 10px 14px;
font-size: 14px;
border: 1px solid var(--gray-300);
border-radius: 6px;
outline: none;
transition: border-color 0.3s ease;
background-color: var(--white);
color: var(--text-primary);
}
.ats-list-container .ats-list-search input:focus {
border-color: var(--primary-blue);
}
.ats-list-container .ats-list-body {
flex: 1;
display: flex;
height: calc(100vh - 70px); /* header + search box approx height */
overflow: hidden;
position: relative;
}
.ats-list-container .ats-actions-header {
padding: 10px 15px;
background-color: var(--content-bg);
border-radius: 6px;
border-left: 4px solid var(--primary-blue);
display: flex;
align-items: center;
}
.ats-list-container .ats-actions-header .section-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.ats-list-container .ats-actions-header .btn {
font-size: 14px;
font-weight: 500;
padding: 6px 15px;
border-radius: 4px;
transition: all 0.3s ease;
}
.ats-list-container .ats-actions-header #activeRecords {
margin-right: auto;
background-color: var(--active-search-bg);
color: var(--active-search-text);
border: 1px solid var(--gray-300);
padding: 10px 20px;
border-radius: 6px;
font-weight: 500;
box-shadow: 0 2px 4px var(--shadow-color);
transition: all 0.3s ease-in-out;
cursor: pointer;
}
.ats-list-container .job-actions-header #activeRecords:hover {
background-color: var(--active-search-hover-bg);
color: var(--active-search-text);
box-shadow: 0 4px 8px var(--shadow-dark);
}
/* Button Styles */
.ats-list-container .ats-actions-header .add-create-btn {
margin-left: auto;
background-color: var(--add-btn-bg);
color: var(--add-btn-color);
border: none;
padding: 10px 20px;
border-radius: 6px;
font-weight: bold;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 2px 6px rgba(52, 152, 219, 0.2);
transition: all 0.3s ease-in-out;
cursor: pointer;
}
.ats-list-container .ats-actions-header .add-create-btn:hover {
background-color: var(--add-btn-hover-bg);
transform: scale(1.05);
box-shadow: 0 6px 12px rgba(41, 128, 185, 0.3);
}
.ats-list-container .ats-actions-header .add-create-btn .plus-icon {
font-size: 20px;
font-weight: bold;
line-height: 1;
display: inline-block;
transition: transform 0.3s ease;
}
.ats-list-container .ats-actions-header .add-create-btn:hover .plus-icon {
transform: scale(1.3);
}
/* ===== Job List Panel ===== */
.ats-list-container .ats-list-left {
width: 30%;
padding: 0;
overflow-y: auto;
background-color: var(--content-bg);
position: relative;
max-height: 100%;
display: flex;
flex-direction: column;
}
.ats-list-container .ats-list-left ul {
list-style: none;
padding: 12px 12px 20px 12px;
margin: 0;
flex: 1;
overflow-y: auto;
}
/* ===== Kanban View (Full Screen) ===== */
.ats-list-container:not(.ats-selected) .ats-list-left {
width: 100%;
display: flex;
flex-direction: column;
}
.ats-list-container:not(.ats-selected) .ats-list-left ul {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
grid-auto-rows: 1fr; /* Equal height rows */
grid-gap: 16px;
padding: 16px;
overflow-y: auto;
width: 100%;
box-sizing: border-box;
align-content: start; /* Align items to the top */
}
.ats-list-container:not(.ats-selected) .ats-item {
padding: 16px;
margin-bottom: 0;
border: 1px solid var(--gray-200);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
background-color: var(--white);
box-shadow: 0 2px 6px var(--shadow-color);
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
height: auto; /* Fill the grid cell */
min-height: 150; /* Minimum height */
}
.ats-list-container:not(.ats-selected) .ats-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 6px;
height: 100%;
background-color: var(--primary-blue);
transform: scaleY(0);
transition: transform 0.3s ease;
}
.ats-list-container:not(.ats-selected) .ats-item:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px var(--shadow-dark);
border-color: var(--primary-blue);
}
.ats-list-container:not(.ats-selected) .ats-item:hover::before {
transform: scaleY(1);
}
.ats-list-container:not(.ats-selected) .ats-item .ats-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
padding-right: 20px;
}
.ats-list-container:not(.ats-selected) .ats-item .ats-meta {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 12px;
display: flex;
align-items: center;
}
.ats-list-container:not(.ats-selected) .ats-item .ats-meta i {
margin-right: 6px;
color: var(--primary-blue);
}
.ats-list-container:not(.ats-selected) .ats-item .ats-badges {
margin-top: auto;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.ats-list-container:not(.ats-selected) .ats-item .job-badge {
margin-right: 0;
padding: 4px 10px;
font-size: 12px;
border-radius: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 24px;
}
.ats-list-container:not(.ats-selected) .ats-item .badge-primary {
background-color: var(--primary-blue);
}
.ats-list-container:not(.ats-selected) .ats-item .badge-warning {
background-color: var(--warning);
color: var(--black);
}
.ats-list-container:not(.ats-selected) .ats-item .badge-success {
background-color: var(--success);
}
.ats-list-container:not(.ats-selected) .ats-item .badge-danger {
background-color: var(--danger);
}
/* ===== List View (When Job Selected) ===== */
.ats-list-container.ats-selected .ats-item {
padding: 10px 12px;
margin-bottom: 8px;
border: 1px solid var(--gray-200);
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s, box-shadow 0.2s;
background-color: var(--sidebar-bg);
}
.ats-list-container.ats-selected .ats-item:hover {
background-color: var(--side-panel-item-hover);
box-shadow: 0 1px 4px var(--shadow-color);
}
.ats-list-container.ats-selected .ats-item.selected {
background-color: var(--side-panel-item-selected);
border: 1px solid var(--primary-blue);
}
/* ===== Job Detail Panel ===== */
.ats-list-container .ats-detail {
width: 70%;
padding: 20px;
overflow-y: auto;
overflow-x: hidden;
background-color: var(--content-bg);
color: var(--text-primary);
position: relative;
flex: 1;
transition: all 0.3s ease;
}
.ats-list-container .ats-detail h3 {
margin-top: 0;
font-size: 20px;
color: var(--primary-blue);
}
.ats-list-container .ats-detail p {
margin: 8px 0;
line-height: 1.6;
color: var(--text-secondary);
}
.ats-list-container .ats-detail em {
color: var(--text-muted);
}
/* ===== Panel Controls ===== */
.ats-list-container .panel-controls {
position: sticky;
top: 0;
background: var(--white);
z-index: 100;
padding: 8px 0;
display: flex;
justify-content: flex-end;
}
.ats-list-container .panel-controls button {
background-color: var(--gray-200);
border: none;
color: var(--text-primary);
font-size: 14px;
margin-left: 5px;
padding: 2px 6px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.ats-list-container .panel-controls button:hover {
background-color: var(--gray-300);
}
/* ======Job stats====== */
.ats-list-container .job-stats-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: var(--content-bg);
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 10;
}
.ats-list-container .job-stats-values {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.ats-list-container .close-stats {
background: none;
border: none;
font-size: 18px;
color: var(--gray-600);
cursor: pointer;
padding: 4px 8px;
transition: color 0.3s;
}
.ats-list-container .close-stats:hover {
color: var(--gray-900);
}
.ats-list-container .badge {
padding: 6px 10px;
border-radius: 14px;
font-size: 12px;
font-weight: 600;
color: var(--white);
}
.ats-list-container .stat-toggle.crossed {
text-decoration: line-through;
opacity: 0.6;
cursor: pointer;
}
.ats-list-container .stat-toggle {
cursor: pointer;
margin-right: 8px;
}
.ats-list-container .job-badges {
margin-top: 6px;
}
.ats-list-container .job-badge {
margin-right: 6px;
padding: 4px 10px;
font-size: 12px;
border-radius: 12px;
display: inline-block;
}
.ats-list-container .badge-primary { background-color: var(--primary-blue); }
.ats-list-container .badge-warning { background-color: var(--warning); color: var(--black); }
.ats-list-container .badge-success { background-color: var(--success); }
.ats-list-container .badge-danger { background-color: var(--danger); }
/* ===== Sidebar Toggle ===== */
.ats-list-container .ats-list-left {
width: 30%;
min-width: 300px;
transition: all 0.3s ease;
position: relative;
overflow-x: hidden;
}
.ats-list-container .ats-list-left.collapsed {
width: 5%;
min-width: 5%;
padding: 0;
border-right: none;
}
.ats-list-container .ats-detail {
width: 70%;
transition: all 0.3s ease;
}
.ats-list-container .ats-list-left.collapsed + .ats-detail {
width: 95%;
}
/* Toggle Button */
.ats-list-container .ats-list-toggle-btn {
position: absolute;
top: 50%;
right: -12px;
transform: translateY(-50%);
background-color: var(--gray-100);
color: var(--text-primary);
border: none;
padding: 0.5rem 0.7rem;
cursor: pointer;
border-radius: 0 6px 6px 0;
box-shadow: 0 2px 5px var(--shadow-color);
z-index: 20;
display: flex;
align-items: center;
justify-content: center;
transition: right 0.3s ease;
}
/* Button in collapsed state */
.ats-list-container .ats-list-left.collapsed .ats-list-toggle-btn {
right: -12px;
transform: translateY(-50%);
}
.ats-list-container .ats-list-toggle-btn:hover {
background-color: var(--gray-200);
}
/* Hide content in collapsed state */
.ats-list-container .ats-list-left.collapsed > *:not(.job-list-toggle-btn) {
display: none;
}
/* ===== Layout States ===== */
/* Initial state: job list takes full width */
.ats-list-container:not(.ats-selected) .ats-list-left {
width: 100%;
}
.ats-list-container:not(.ats-selected) .ats-detail {
display: none;
}
/* When a job is selected */
.ats-list-container.ats-selected .ats-list-left {
width: 30%;
}
.ats-list-container.ats-selected .ats-detail {
width: 70%;
display: block;
overflow: hidden;
}
/* When sidebar is collapsed */
.ats-list-container.ats-selected .ats-list-left.collapsed {
width: 5%;
min-width: 5%;
}
.ats-list-container.ats-selected .ats-list-left.collapsed + .ats-detail {
width: 95%;
}
.ats-list-container:not(.ats-selected) .ats-list-toggle-btn {
display: none;
}
.ats-list-container.ats-selected .ats-list-toggle-btn {
display: flex;
}
/* Close button styling */
.close-detail {
position: absolute;
top: 10px;
right: 10px;
z-index: 100;
background: transparent;
border: none;
font-size: 1.5rem;
color: var(--gray-600);
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s ease;
}
.close-detail:hover {
background-color: var(--gray-200);
color: var(--gray-900);
}
/* Transitions for smooth resizing */
.ats-list-container .ats-list-left,
.ats-list-container .ats-detail {
transition: width 0.3s ease;
}
/* ===== Responsive Adjustments ===== */
@media (max-width: 768px) {
.ats-list-container:not(.ats-selected) .ats-list-left ul {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
grid-gap: 12px;
padding: 12px;
}
.ats-list-container:not(.ats-selected) .ats-item {
padding: 12px;
min-height: 160px;
}
.ats-list-container.ats-selected .ats-list-left {
width: 40%;
}
.ats-list-container.ats-selected .ats-detail {
width: 60%;
}
.ats-list-container.ats-selected .ats-list-left.collapsed {
width: 10%;
min-width: 10%;
}
.ats-list-container.ats-selected .ats-list-left.collapsed + .ats-detail {
width: 90%;
}
}
/* ===== Applicant List Styling ===== */
.ats-list-container .ats-list {
list-style: none;
padding: 12px 12px 20px 12px;
margin: 0;
flex: 1;
overflow-y: auto;
}
.ats-list-container .ats-item-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.ats-list-container .ats-info {
flex: 1;
}
.ats-list-container .ats-avatar {
margin-left: 12px;
flex-shrink: 0;
}
.ats-list-container .ats-item-image {
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
}
.ats-list-container .ats-item-initials {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: var(--primary-blue);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 18px;
}
/* ===== Applicant Modal Styling ===== */
.applicant-detail-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
}
.applicant-detail-modal.active {
display: flex;
align-items: center;
justify-content: center;
}
.modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.applicant-modal-content {
position: relative;
background-color: var(--white);
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
z-index: 1001;
}
.applicant-close-modal {
position: absolute;
top: 10px;
right: 15px;
font-size: 24px;
font-weight: bold;
color: var(--gray-600);
cursor: pointer;
z-index: 1002;
}
.applicant-close-modal:hover {
color: var(--gray-900);
}
.applicant-status-ribbon {
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: 8px 15px;
background-color: var(--primary-blue);
color: var(--white);
font-weight: 600;
border-radius: 8px 8px 0 0;
}
.modal-applicant-container {
display: flex;
padding: 50px 20px 20px;
}
.modal-applicant-left {
flex: 0 0 auto;
margin-right: 20px;
}
.modal-applicant-image {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 3px solid var(--primary-blue);
}
.modal-applicant-initials {
width: 120px;
height: 120px;
border-radius: 50%;
background-color: var(--primary-blue);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 48px;
border: 3px solid var(--primary-blue);
}
.modal-applicant-right {
flex: 1;
}
.modal-applicant-name {
margin-top: 0;
color: var(--text-primary);
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
}
.modal-applicant-details {
margin-top: 15px;
}
.detail-row {
display: flex;
margin-bottom: 10px;
}
.detail-label {
flex: 0 0 140px;
font-weight: 600;
color: var(--text-secondary);
}
.detail-value {
flex: 1;
color: var(--text-primary);
}
.recruiter-info {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid var(--border-color);
}
.recruiter-avatar {
display: flex;
align-items: center;
}
.recruiter-image {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
margin-right: 10px;
}
.recruiter-initials {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--gray-300);
color: var(--text-primary);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 10px;
}
.recruiter-tooltip {
font-weight: 500;
color: var(--text-primary);
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

@ -0,0 +1,850 @@
/** @odoo-module **/
function initApplicantsPage() {
console.log("Applicants Page Loaded");
const applicantDetailArea = document.getElementById("applicants-detail");
const container = document.querySelector('.ats-list-container');
const toggleBtn = document.getElementById("applicants-list-sidebar-toggle-btn");
const sidebar = document.getElementById("applicants-list-panel");
// Fix: Use correct class for applicant items - using both classes
document.querySelectorAll(".ats-item.applicants-item").forEach(item => {
item.addEventListener("click", function() {
console.log("Applicant item clicked"); // Add this for debugging
document.querySelectorAll(".ats-item.applicants-item.selected").forEach(el => el.classList.remove("selected"));
this.classList.add("selected");
applicantDetailArea.style.display = 'block';
container.classList.add('ats-selected');
sidebar.classList.remove('collapsed');
toggleBtn.style.display = 'flex';
const applicantId = this.dataset.id;
console.log("Applicant ID:", applicantId); // Add this for debugging
// Show loading state
if (applicantDetailArea) {
applicantDetailArea.innerHTML = '<div class="text-center p-5"><i class="fa fa-spinner fa-spin fa-2x"></i><p class="mt-2">Loading applicant details...</p></div>';
}
fetch(`/myATS/applicant/detail/${applicantId}`, {
headers: { "X-Requested-With": "XMLHttpRequest" }
})
.then(res => {
if (!res.ok) throw new Error('Network response was not ok');
return res.text();
})
.then(html => {
console.log("Response received"); // Add this for debugging
if (applicantDetailArea) {
applicantDetailArea.innerHTML = html;
initApplicantDetailEdit(); // Initialize edit functionality
// Add close button functionality
const closeBtn = applicantDetailArea.querySelector('.close-detail');
if (closeBtn) {
closeBtn.addEventListener('click', function() {
applicantDetailArea.style.display = 'none';
container.classList.remove('ats-selected');
document.querySelectorAll(".ats-item.applicants-item.selected").forEach(el => el.classList.remove("selected"));
});
}
}
})
.catch(error => {
console.error('Error loading applicant details:', error);
if (applicantDetailArea) {
applicantDetailArea.innerHTML = '<div class="alert alert-danger">Error loading applicant details. Please try again.</div>';
}
});
});
});
// Search functionality - use correct ID
const search = document.getElementById("applicants-search");
if (search) {
search.addEventListener("input", function() {
const query = this.value.toLowerCase();
let visibleCount = 0;
// Also fix this selector to use both classes
document.querySelectorAll(".ats-item.applicants-item").forEach(item => {
const match = item.textContent.toLowerCase().includes(query);
item.style.display = match ? "" : "none";
if (match) visibleCount++;
});
const countElement = document.getElementById("active-records-count");
if (countElement) {
countElement.textContent = visibleCount;
}
});
}
// Sidebar Toggle
if (toggleBtn && sidebar) {
toggleBtn.addEventListener("click", function(e) {
e.stopPropagation();
sidebar.classList.toggle("collapsed");
});
}
// Applicant Modal Handling
const modal = document.querySelector('.applicant-detail-modal');
if (modal) {
const closeModal = modal.querySelector('.applicant-close-modal');
// Event delegation for image clicks - use correct classes
document.addEventListener('click', function(e) {
const img = e.target.closest('.ats-item-image, .ats-item-initials');
if (!img) return;
const applicantItem = img.closest('.ats-item.applicants-item');
if (!applicantItem) return;
e.preventDefault();
e.stopImmediatePropagation();
// Get applicant data with null checks
const applicantId = applicantItem.dataset.id;
const nameElement = applicantItem.querySelector('.ats-title');
const jobElement = applicantItem.querySelector('#job_request');
const stageElement = applicantItem.querySelector('#applicant_stage');
const createDate = applicantItem.querySelector('#create_date');
const applicantEmail = applicantItem.querySelector('#applicant_email');
const applicantPhone = applicantItem.querySelector('#applicant_phone');
const alternatePhone = applicantItem.querySelector('#alternate_phone');
const recruiterName = applicantItem.querySelector('#recruiter_name');
const recruiterImage = applicantItem.querySelector('#recruiter_image');
const recruiterUserId = applicantItem.querySelector('#recruiter_id');
const name = nameElement ? nameElement.textContent : 'N/A';
const job = jobElement ? jobElement.textContent : 'N/A';
const stage = stageElement ? stageElement.textContent : 'N/A';
const date = createDate ? createDate.textContent : 'N/A';
const email = applicantEmail ? applicantEmail.textContent : 'N/A';
const phone = applicantPhone ? applicantPhone.textContent : 'N/A';
const altPhone = alternatePhone ? alternatePhone.textContent : 'N/A';
const recruiter = recruiterName ? recruiterName.textContent : 'N/A';
const recruiterId = recruiterUserId ? recruiterUserId.textContent: null;
const photoSrc = img.classList.contains('ats-item-image') ? img.src : null;
const initials = img.classList.contains('ats-item-initials') ? img.textContent : null;
// Populate modal with applicant data
modal.querySelector('.modal-applicant-name').textContent = name;
modal.querySelector('.modal-applicant-job').textContent = job;
modal.querySelector('.modal-applicant-stage').textContent = stage;
modal.querySelector('.modal-applicant-date').textContent = date;
modal.querySelector('.modal-applicant-email').textContent = email;
modal.querySelector('.modal-applicant-phone').textContent = phone;
modal.querySelector('.modal-applicant-altphone').textContent = altPhone;
// Handle applicant image
if (photoSrc) {
const modalImg = modal.querySelector('.modal-applicant-image');
modalImg.src = photoSrc;
modalImg.style.display = 'block';
modal.querySelector('.modal-applicant-initials').style.display = 'none';
} else {
modal.querySelector('.modal-applicant-initials').textContent = initials;
modal.querySelector('.modal-applicant-initials').style.display = 'flex';
modal.querySelector('.modal-applicant-image').style.display = 'none';
}
// Handle recruiter info
const recruiterImageEl = modal.querySelector('.recruiter-image');
const recruiterInitialsEl = modal.querySelector('.recruiter-initials');
const recruiterTooltip = modal.querySelector('.recruiter-tooltip');
if (recruiterImage && recruiterId) {
const recruiterSrc = `/web/image/res.users/${recruiterId}/image_128`;
recruiterImageEl.src = recruiterSrc;
recruiterImageEl.style.display = 'block';
recruiterInitialsEl.style.display = 'none';
recruiterSrc.onerror = function () {
recruiterImageEl.style.display = 'none';
};
} else if (recruiterName) {
recruiterInitialsEl.textContent = recruiterName.textContent.charAt(0).toUpperCase();
recruiterInitialsEl.style.display = 'flex';
recruiterImageEl.style.display = 'none';
}
recruiterTooltip.textContent = recruiter;
// Set status ribbon class based on stage - use correct selector
const statusRibbon = modal.querySelector('.applicant-status-ribbon');
if (statusRibbon) {
// Remove all existing status classes
statusRibbon.classList.remove('new', 'interview', 'hired', 'rejected');
// Add appropriate class based on stage
if (stage.toLowerCase().includes('interview')) {
statusRibbon.classList.add('interview');
} else if (stage.toLowerCase().includes('hired')) {
statusRibbon.classList.add('hired');
} else if (stage.toLowerCase().includes('reject')) {
statusRibbon.classList.add('rejected');
} else {
statusRibbon.classList.add('new');
}
}
// Show modal
modal.style.display = 'flex';
setTimeout(() => {
modal.classList.add('show');
}, 10);
document.body.style.overflow = 'hidden';
});
// Close modal handlers
closeModal.addEventListener('click', function() {
modal.classList.remove('show');
setTimeout(() => {
modal.style.display = 'none';
}, 300);
document.body.style.overflow = '';
});
modal.addEventListener('click', function(e) {
if (e.target === modal) {
modal.classList.remove('show');
document.body.style.overflow = '';
}
});
// Close with ESC key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && modal.classList.contains('show')) {
modal.classList.remove('show');
document.body.style.overflow = '';
}
});
}
const createApplication = document.getElementById('add-application-create-btn');
const applicantModal = document.getElementById('application-creation-modal');
const closeModal = document.querySelectorAll('.application-creation-close, .btn-cancel');
if (createApplication) {
createApplication.addEventListener('click', function(e) {
e.preventDefault();
applicantModal.style.display = 'flex';
setTimeout(() => {
applicantModal.classList.add('show');
}, 10);
document.body.style.overflow = 'hidden';
setTimeout(() => {
initSelect2();
initResumeUploadHandlers();
}, 100);
setTimeout(createApplicationForm, 100);
});
}
if (closeModal) {
closeModal.forEach(btn => {
btn.addEventListener('click', function() {
applicantModal.classList.remove('show');
setTimeout(() => {
applicantModal.style.display = 'none';
}, 300);
document.body.style.overflow = '';
});
});
}
// Close modal when clicking outside of it
// applicantModal.addEventListener('click', function(e) {
// if (e.target === applicantModal) {
// applicantModal.classList.remove('show');
// setTimeout(() => {
// applicantModal.style.display = 'none';
// }, 300);
// document.body.style.overflow = '';
// }
// });
// File Upload Handling
const resumeUpload = document.getElementById('resume-upload');
const resumeDropzone = document.getElementById('resume-dropzone');
const resumePreview = document.getElementById('resume-preview');
const resumePlaceholder = document.querySelector('.resume-preview-placeholder');
const resumeIframe = document.getElementById('resume-iframe');
const resumeImage = document.getElementById('resume-image');
const unsupportedFormat = document.getElementById('unsupported-format');
const downloadResume = document.getElementById('download-resume');
const attachmentsList = document.querySelector('.attachments-list');
const addAttachmentBtn = document.querySelector('.add-attachment');
function initResumeUploadHandlers() {
// Create remove button
const removeResumeBtn = document.createElement('button');
removeResumeBtn.innerHTML = '<i class="fas fa-trash"></i> Remove Resume';
removeResumeBtn.className = 'btn btn-danger btn-sm mt-2';
removeResumeBtn.style.display = 'none';
resumePreview.appendChild(removeResumeBtn);
// Handle remove resume
removeResumeBtn.addEventListener('click', function() {
resetResumePreview();
});
function resetResumePreview() {
// Clear file input
resumeUpload.value = '';
currentResumeFile = null;
// Reset preview
resumePlaceholder.style.display = 'flex';
resumeIframe.style.display = 'none';
resumeImage.style.display = 'none';
unsupportedFormat.style.display = 'none';
removeResumeBtn.style.display = 'none';
// Reset iframe/src to prevent memory leaks
if (resumeIframe.src) {
URL.revokeObjectURL(resumeIframe.src);
resumeIframe.src = '';
}
if (resumeImage.src) {
URL.revokeObjectURL(resumeImage.src);
resumeImage.src = '';
}
if (downloadResume.href) {
URL.revokeObjectURL(downloadResume.href);
downloadResume.href = '#';
}
}
// Handle click on dropzone
resumeDropzone.addEventListener('click', function(e) {
if (e.target === this || e.target.classList.contains('upload-icon') ||
e.target.tagName === 'H5' || e.target.tagName === 'P') {
resumeUpload.click();
}
});
// Handle drag and drop
resumeDropzone.addEventListener('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
this.classList.add('dragover');
this.style.borderColor = '#3498db';
this.style.backgroundColor = 'rgba(52, 152, 219, 0.1)';
});
resumeDropzone.addEventListener('dragleave', function(e) {
e.preventDefault();
e.stopPropagation();
this.classList.remove('dragover');
this.style.borderColor = '';
this.style.backgroundColor = '';
});
resumeDropzone.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
this.classList.remove('dragover');
this.style.borderColor = '';
this.style.backgroundColor = '';
if (e.dataTransfer.files.length) {
const file = e.dataTransfer.files[0];
handleResumeFile(file);
}
});
// Handle file selection from the regular input
resumeUpload.addEventListener('change', function(e) {
if (this.files.length) {
handleResumeFile(this.files[0]);
}
});
function handleResumeFile(file) {
const validTypes = [
'application/pdf',
'application/msword',
'application/wps-office.docx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'image/jpeg',
'image/png',
'text/plain'
];
if (!validTypes.includes(file.type)) {
alert('Please upload a valid file type (PDF, Word, Image, or Text)');
return;
}
currentResumeFile = file;
// Hide placeholder
resumePlaceholder.style.display = 'none';
// Set up download link
const fileURL = URL.createObjectURL(file);
downloadResume.href = fileURL;
downloadResume.download = file.name;
removeResumeBtn.style.display = 'block';
// Check file type and show appropriate preview
if (file.type === 'application/pdf') {
// PDF preview
resumeIframe.src = fileURL;
resumeIframe.style.display = 'block';
resumeImage.style.display = 'none';
unsupportedFormat.style.display = 'none';
} else if (file.type.match('image.*')) {
// Image preview
resumeImage.src = fileURL;
resumeImage.style.display = 'block';
resumeIframe.style.display = 'none';
unsupportedFormat.style.display = 'none';
} else {
// Unsupported format for preview
unsupportedFormat.style.display = 'flex';
resumeIframe.style.display = 'none';
resumeImage.style.display = 'none';
}
// Update the actual resume-upload input
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
resumeUpload.files = dataTransfer.files;
}
}
if (addAttachmentBtn) {
addAttachmentBtn.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.accept = '.pdf,.doc,.docx,.jpg,.png';
input.onchange = (e) => {
if (e.target.files.length) {
handleFiles(e.target.files, true);
}
};
input.click();
});
}
function createApplicationForm() {
const anniversaryField = document.getElementById('marital-anniversary-field');
const maritalStatus = document.getElementById('application-marital');
maritalStatus.addEventListener('change', () => {
if (maritalStatus.value !== 'married') {
document.getElementById('marital-anniversary-field').style.display = 'none';
document.getElementById('application-anniversary').setAttribute('disabled', 'disabled');
} else {
document.getElementById('marital-anniversary-field').style.display = 'block';
document.getElementById('application-anniversary').removeAttribute('disabled');
}
});
}
function initSelect2() {
// Check if Select2 is already initialized
const applicantSkills = document.getElementById('application-skills');
if (applicantSkills) {
$(applicantSkills).select2({
placeholder: 'Select skills',
allowClear: true,
dropdownParent: $('.application-creation-modal'),
width: '100%',
escapeMarkup: function(m) { return m; }
});
}
const applicantPosition = document.getElementById('application-position');
if (applicantPosition) {
$(applicantPosition).select2({
placeholder: 'Select Job',
allowClear: false,
dropdownParent: $('.application-creation-modal'),
width: '100%',
escapeMarkup: function(m) { return m; }
});
}
const applicantCandidate = document.getElementById('application-candidate');
if (applicantCandidate) {
$(applicantCandidate).select2({
placeholder: 'Select Candidate',
allowClear: true,
dropdownParent: $('.application-creation-modal'),
width: '100%',
templateResult: formatCandidate,
templateSelection: formatCandidateSelection,
escapeMarkup: function(m) { return m; }
}).on('change', function() {
const selectedOption = $(this).find('option:selected');
if (selectedOption.val()) {
// Populate fields from candidate data
$('#application-email').val(selectedOption.data('email') || '');
$('#application-phone').val(selectedOption.data('phone') || '');
$('#application-alt-phone').val(selectedOption.data('altPhone') || '');
$('#application-linkedin').val(selectedOption.data('linkedin') || '');
// For skills
let skillIds = selectedOption.data('skillIds');
// Convert to array if it's a string
if (typeof skillIds === 'string') {
try {
skillIds = JSON.parse(skillIds);
} catch (e) {
skillIds = [];
}
}
// Ensure skillIds is an array
skillIds = Array.isArray(skillIds) ? skillIds : [];
// Set the values in Select2 and trigger change
$('#application-skills').val(skillIds).trigger('change');
} else {
$('#application-email').val('');
$('#application-phone').val('');
$('#application-alt-phone').val('');
$('#application-linkedin').val('');
// For skills
let skillIds = [];
// Ensure skillIds is an array
skillIds = Array.isArray(skillIds) ? skillIds : [];
$('#application-skills').val(skillIds).trigger('change');
}
});
}
}
function formatCandidate(candidate) {
if (!candidate.id) {
return candidate.text;
}
var imageUrl = $(candidate.element).data('image') || '/web/static/img/placeholder.png';
var $candidate = $(
'<span style="display: flex; align-items: center;">' +
'<img class="user-avatar" src="' + imageUrl + '" style="width:24px;height:24px;border-radius:50%;margin-right:8px;object-fit:cover;" onerror="this.src=\'/web/static/img/placeholder.png\'" />' +
'<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">' + candidate.text + '</span>' +
'</span>'
);
return $candidate;
}
function formatCandidateSelection(candidate) {
if (!candidate.id) {
return candidate.text;
}
var imageUrl = $(candidate.element).data('image') || '/web/static/img/placeholder.png';
var $candidate = $(
'<span style="display: flex; align-items: center; width: 100%;">' +
'<img class="user-avatar" src="' + imageUrl + '" style="width:24px;height:24px;border-radius:50%;margin-right:8px;object-fit:cover;" onerror="this.src=\'/web/static/img/placeholder.png\'" />' +
'<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-grow: 1;">' + candidate.text + '</span>' +
'</span>'
);
return $candidate;
}
function handleFiles(files, isAdditional = false) {
Array.from(files).forEach(file => {
if (!file.type.match('application/pdf|application/msword|application/vnd.openxmlformats-officedocument.wordprocessingml.document|image.*')) {
alert('Only PDF, DOC, DOCX, JPG, and PNG files are allowed');
return;
}
if (file.size > 10 * 1024 * 1024) { // 10MB limit
alert('File size must be less than 10MB');
return;
}
if (isAdditional) {
addAttachmentToList(file);
} else {
updateResumeUpload(file);
}
});
}
function updateResumeUpload(file) {
// You can preview or process the resume file here
const dropzone = document.getElementById('resume-dropzone');
dropzone.innerHTML = `
<i class="fas fa-check-circle upload-icon" style="color:var(--success)"></i>
<h5>${file.name}</h5>
<p>${(file.size / 1024 / 1024).toFixed(2)} MB</p>
<button type="button" class="btn-remove remove-resume">
<i class="fas fa-times"></i> Remove
</button>
<input type="file" id="resume-upload" name="resume" class="file-input" style="display:none"/>
`;
// Re-attach event listeners
document.querySelector('.remove-resume').addEventListener('click', () => {
resetResumeUpload();
});
}
function resetResumeUpload() {
const dropzone = document.getElementById('resume-dropzone');
dropzone.innerHTML = `
<i class="fas fa-cloud-upload-alt upload-icon"></i>
<h5>Upload Resume</h5>
<p>Drag & drop your resume here or click to browse</p>
<input type="file" id="resume-upload" name="resume" accept=".pdf,.doc,.docx" class="file-input"/>
`;
// Re-attach event listeners
setupFileUpload();
}
function addAttachmentToList(file) {
const attachmentItem = document.createElement('div');
attachmentItem.className = 'attachment-item';
attachmentItem.innerHTML = `
<div class="attachment-info">
<div class="attachment-header">
<span class="attachment-title">${file.name}</span>
<span class="attachment-size">${(file.size / 1024 / 1024).toFixed(2)} MB</span>
</div>
<div class="attachment-actions">
<button type="button" class="btn-remove remove-attachment">
<i class="fas fa-times"></i> Remove
</button>
</div>
</div>
`;
attachmentsList.appendChild(attachmentItem);
attachmentItem.querySelector('.remove-attachment').addEventListener('click', () => {
attachmentItem.remove();
});
}
function setupFileUpload() {
// Re-initialize event listeners if needed
const resumeDropzone = document.getElementById('resume-dropzone');
const resumeUpload = document.getElementById('resume-upload');
if (resumeDropzone && resumeUpload) {
resumeDropzone.addEventListener('click', () => {
resumeUpload.click();
});
}
}
const uploadBtn = document.getElementById('upload-resume')
if (uploadBtn) {
uploadBtn.addEventListener('click', function (e) {
e.preventDefault();
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.pdf,.doc,.docx,.txt';
fileInput.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
// Show loading state
const button = document.getElementById('upload-resume');
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing Resume...';
button.disabled = true;
try {
const formData = new FormData();
formData.append('file', file);
formData.append('type', 'applicant');
const response = await fetch('/resume/upload', {
method: 'POST',
body: formData,
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
populateApplicationForm(result);
} catch (error) {
console.error('Error parsing Resume:', error);
showNotification('Failed to parse Resume. Please try again or enter manually.', 'danger');
} finally {
button.innerHTML = originalText;
button.disabled = false;
}
};
fileInput.click();
});
}
function populateApplicationForm(resumeData) {
// Add this CSS class definition dynamically
const style = document.createElement('style');
style.textContent = `
.populated-field {
background-color: #f5f5f5 !important;
transition: background-color 0.3s ease;
}
`;
document.head.appendChild(style);
// Helper function to set value with visual feedback
function setValueWithFeedback(element, value) {
if (element && value) {
element.value = value;
element.classList.add('populated-field');
return true;
}
return false;
}
// Helper function for select elements
function setSelectValueWithFeedback(selectElement, value, compareAsText = false) {
if (!selectElement || !value) return false;
for (let i = 0; i < selectElement.options.length; i++) {
const option = selectElement.options[i];
const match = compareAsText
? option.text.toLowerCase().includes(value.toLowerCase())
: option.value.toLowerCase() === value.toLowerCase();
if (match) {
selectElement.value = option.value;
selectElement.classList.add('populated-field');
return true;
}
}
return false;
}
// Section 1: Basic Information
if (resumeData.personal_info) {
const personal = resumeData.personal_info;
if (resumeData.candidate_id) {
let candidateId = resumeData.candidate_id;
$('#application-candidate').val(candidateId).trigger('change')
.addClass('populated-field');
} else {
setValueWithFeedback(document.getElementById('application-fullname'), personal.name);
setValueWithFeedback(document.getElementById('application-email'), personal.email);
setValueWithFeedback(document.getElementById('application-phone'), personal.phone);
setValueWithFeedback(document.getElementById('application-linkedin'), personal.linkedin);
// Skills
if (resumeData.skills?.length) {
const skillValues = resumeData.skills.map(skill => skill.id);
$('#application-skills').val(skillValues).trigger('change')
.addClass('populated-field');
}
}
setSelectValueWithFeedback(
document.getElementById('application-gender'),
personal.gender
);
setValueWithFeedback(document.getElementById('application-dob'), personal.dob);
}
// Professional Information
if (resumeData.professional_info) {
const prof = resumeData.professional_info;
setValueWithFeedback(document.getElementById('application-current-org'), prof.current_company);
setValueWithFeedback(
document.getElementById('application-current-location'),
prof.current_location?.city
);
setValueWithFeedback(document.getElementById('application-notice-period'), prof.notice_period);
if (prof.notice_period) {
setSelectValueWithFeedback(
document.getElementById('application-notice-negotiable'),
'yes'
);
}
setSelectValueWithFeedback(
document.getElementById('application-holding-offer'),
prof.holding_offer ? 'yes' : 'no'
);
setValueWithFeedback(document.getElementById('application-total-exp'), prof.total_experience);
}
// Salary Information
setValueWithFeedback(document.getElementById('application-current-ctc'), resumeData.current_ctc);
setValueWithFeedback(document.getElementById('application-expected-salary'), resumeData.expected_salary);
// Address Information
if (resumeData.contact_info?.current_address) {
const addr = resumeData.contact_info.current_address;
setValueWithFeedback(document.getElementById('application-current-street'), addr.street);
setValueWithFeedback(document.getElementById('application-current-city'), addr.city);
setSelectValueWithFeedback(
document.getElementById('application-current-state'),
addr.state,
true // Compare as text
);
setSelectValueWithFeedback(
document.getElementById('application-current-country'),
addr.country,
true // Compare as text
);
setValueWithFeedback(document.getElementById('application-current-zip'), addr.zip);
}
// Show notification
showNotification('Resume uploaded and fields populated successfully!', 'success');
}
// Helper function to show notifications
function showNotification(message, type = 'info') {
// Check if notification container exists, create if not
let notificationContainer = document.getElementById('notification-container');
if (!notificationContainer) {
notificationContainer = document.createElement('div');
notificationContainer.id = 'notification-container';
notificationContainer.style.position = 'fixed';
notificationContainer.style.top = '20px';
notificationContainer.style.right = '20px';
notificationContainer.style.zIndex = '9999';
document.body.appendChild(notificationContainer);
}
// Create notification element
const notification = document.createElement('div');
notification.className = `alert alert-${type} alert-dismissible fade show`;
notification.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
// Add to container
notificationContainer.appendChild(notification);
// Auto remove after 5 seconds
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
notification.remove();
}, 300);
}, 5000);
}
}
function scrollToTarget(targetElement, offset = 100) {
const targetPosition = targetElement.getBoundingClientRect().top + window.pageYOffset - offset;
// First try scrolling the container
const container = document.getElementById('ats-details-container');
if (container && container.scrollHeight > container.clientHeight) {
const containerTop = container.getBoundingClientRect().top;
const scrollPosition = targetPosition - containerTop - offset;
container.scrollTo({
top: scrollPosition,
behavior: 'smooth'
});
} else {
// Fallback to window scrolling
window.scrollTo({
top: targetPosition,
behavior: 'smooth'
});
}
}
function initApplicantDetailEdit() {
// Recruiter photo toggle
const recruiterTrigger = document.getElementById('recruiter-photo-trigger');
const recruiterInfo = document.getElementById('recruiter-info');
if (recruiterTrigger && recruiterInfo) {
recruiterTrigger.addEventListener('click', function() {
recruiterInfo.classList.toggle('show');
});
}
// Improved smooth scroll navigation
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId);
if (targetElement) {
scrollToTarget(targetElement);
// Highlight effect
targetElement.style.boxShadow = '0 0 0 3px rgba(13, 110, 253, 0.5)';
targetElement.style.transition = 'box-shadow 0.3s ease';
setTimeout(() => {
targetElement.style.boxShadow = 'none';
}, 2000);
}
});
});
}
// Initialize the page when DOM is ready
document.addEventListener('DOMContentLoaded', initApplicantsPage);

View File

@ -0,0 +1,229 @@
document.addEventListener("DOMContentLoaded", function () {
// Initialize application
initTheme();
initSidebar();
initNavigation();
// Load default page based on URL hash or default to jobs
});
/**
* Initialize theme functionality
*/
function initTheme() {
const themeToggle = document.getElementById('themeToggle');
const htmlElement = document.documentElement;
// Get saved theme or use system preference
const getPreferredTheme = () => {
const storedTheme = localStorage.getItem('theme');
if (storedTheme) return storedTheme;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};
// Apply theme
const applyTheme = (theme) => {
htmlElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
updateThemeButton(theme);
};
// Update toggle button state
const updateThemeButton = (theme) => {
const themeText = theme === 'dark' ? 'Light Mode' : 'Dark Mode';
if (document.querySelector('.theme-text')) {
document.querySelector('.theme-text').textContent = themeText;
}
};
// Initialize with preferred theme
applyTheme(getPreferredTheme());
// Toggle theme on button click
if (themeToggle) {
themeToggle.addEventListener('click', () => {
const currentTheme = htmlElement.getAttribute('data-theme');
applyTheme(currentTheme === 'dark' ? 'light' : 'dark');
});
}
document.querySelectorAll('.btn-stage').forEach(button => {
button.addEventListener('click', function() {
const container = this.closest('.stage-selector');
const buttons = container.querySelectorAll('.btn-stage');
const applicantId = container.dataset.applicantId;
const newStageId = this.dataset.stageId;
// Visual feedback
buttons.forEach(btn => {
btn.classList.remove('btn-stage-current');
btn.classList.add('btn-stage-option');
});
this.classList.remove('btn-stage-option');
this.classList.add('btn-stage-current', 'processing');
// AJAX call to update stage
fetch('/update_stage', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': getCSRFToken()
},
body: JSON.stringify({
applicant_id: applicantId,
stage_id: newStageId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('Stage updated successfully!', 'success');
} else {
throw new Error(data.error || 'Update failed');
}
})
.catch(error => {
console.error("Error:", error);
showNotification('Failed to update stage', 'danger');
// Revert visual state if needed
})
.finally(() => {
this.classList.remove('processing');
});
});
});
function getCSRFToken() {
return document.querySelector('meta[name="csrf-token"]').content;
}
function showNotification(message, type) {
// Your notification implementation
}
// Watch for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (!localStorage.getItem('theme')) {
applyTheme(e.matches ? 'dark' : 'light');
}
});
}
/**
* Initialize sidebar functionality
*/
function initSidebar() {
const sidebar = document.getElementById('sidebar');
const toggleBtn = document.getElementById('sidebar-toggle-btn');
if (!sidebar || !toggleBtn) return;
// Toggle sidebar state
const toggleSidebar = () => {
sidebar.classList.toggle('collapsed');
sidebar.classList.toggle('expanded');
// Update toggle button icon
const icon = toggleBtn.querySelector('i');
icon.textContent = sidebar.classList.contains('collapsed') ? '>>' : '<<';
// Store preference
localStorage.setItem('sidebarCollapsed', sidebar.classList.contains('collapsed'));
};
// Initialize sidebar state
const sidebarCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
if (sidebarCollapsed) {
sidebar.classList.add('collapsed');
sidebar.classList.remove('expanded');
toggleBtn.querySelector('i').textContent = '>>';
}
// Add click event
toggleBtn.addEventListener('click', toggleSidebar);
}
/**
* Initialize page navigation
*/
function initNavigation() {
// Highlight current menu item based on hash
const highlightActiveMenu = () => {
const hash = window.location.hash;
document.querySelectorAll('.menu-list a').forEach(link => {
link.classList.toggle('active', link.getAttribute('href') === hash);
});
};
// Handle page loading
const loadPage = async (hash) => {
if (!hash) return;
try {
const contentArea = document.getElementById('main-content');
const jobDetailArea = document.getElementById('job-detail');
const page = hash.substring(1); // Remove #
// Highlight active menu
highlightActiveMenu();
// Load page content
const res = await fetch(`/myATS/page/${page}`, {
headers: { "X-Requested-With": "XMLHttpRequest" }
});
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
const html = await res.text();
contentArea.innerHTML = html;
if (jobDetailArea) jobDetailArea.innerHTML = "";
// Initialize page-specific JS
initPageScripts(page);
} catch (error) {
console.error('Error loading page:', error);
document.getElementById('main-content').innerHTML = `
<div class="alert alert-danger">
Error loading page: ${error.message}
</div>
`;
}
};
// Initialize page-specific scripts
const initPageScripts = (page) => {
switch (page) {
case 'jobs':
case 'job_requests':
if (typeof initJobListPage === 'function') initJobListPage();
break;
case 'applicants':
if (typeof initApplicantsPage === 'function') initApplicantsPage();
break;
case 'candidates':
if (typeof initCandidatesPage === 'function') initCandidatesPage();
break;
}
};
// Set up navigation events
document.querySelectorAll('.menu-list a[data-page]').forEach(link => {
link.addEventListener('click', async (e) => {
e.preventDefault();
const hash = link.getAttribute('href');
window.location.hash = hash;
await loadPage(hash);
});
});
// Handle hash changes
window.addEventListener('hashchange', () => {
loadPage(window.location.hash);
});
// Initial highlight
highlightActiveMenu();
}

View File

@ -0,0 +1,608 @@
/** @odoo-module **/
function initCandidatesPage() {
console.log("candidates Page Loaded");
const candidateDetailArea = document.getElementById("candidates-detail");
const container = document.querySelector('.ats-list-container'); // Added this line
const toggleBtn = document.getElementById("candidates-list-sidebar-toggle-btn"); // Added this line
const sidebar = document.getElementById("candidates-list-panel"); // Added this line
document.querySelectorAll(".ats-item.candidates-item").forEach(item => {
item.addEventListener("click", function() {
document.querySelectorAll(".candidates-item.selected").forEach(el => el.classList.remove("selected"));
this.classList.add("selected");
// Show the detail panel and add necessary classes
candidateDetailArea.style.display = 'block'; // Added this line
container.classList.add('ats-selected'); // Added this line
sidebar.classList.remove('collapsed'); // Added this line
toggleBtn.style.display = 'flex'; // Added this line
const candidateId = this.dataset.id;
console.log("Candidate ID:", candidateId); // Added for debugging
// Show loading state
if (candidateDetailArea) {
candidateDetailArea.innerHTML = '<div class="text-center p-5"><i class="fa fa-spinner fa-spin fa-2x"></i><p class="mt-2">Loading candidate details...</p></div>';
}
fetch(`/myATS/candidate/detail/${candidateId}`, {
headers: { "X-Requested-With": "XMLHttpRequest" }
})
.then(res => {
if (!res.ok) throw new Error('Network response was not ok');
return res.text();
})
.then(html => {
console.log("Response received"); // Added for debugging
if (candidateDetailArea) {
candidateDetailArea.innerHTML = html;
// Add close button functionality
const closeBtn = candidateDetailArea.querySelector('.close-detail');
if (closeBtn) {
closeBtn.addEventListener('click', function() {
candidateDetailArea.style.display = 'none';
container.classList.remove('ats-selected');
document.querySelectorAll(".ats-item.candidates-item.selected").forEach(el => el.classList.remove("selected"));
});
}
}
})
.catch(error => {
console.error('Error loading candidate details:', error);
if (candidateDetailArea) {
candidateDetailArea.innerHTML = '<div class="alert alert-danger">Error loading candidate details. Please try again.</div>';
}
});
});
});
// Search functionality
const search = document.getElementById("candidates-search");
if (search) {
search.addEventListener("input", function() {
const query = this.value.toLowerCase();
let visibleCount = 0;
document.querySelectorAll(".ats-item.candidates-item").forEach(item => {
const match = item.textContent.toLowerCase().includes(query);
item.style.display = match ? "" : "none";
if (match) visibleCount++;
});
const countElement = document.getElementById("active-records-count");
if (countElement) {
countElement.textContent = visibleCount;
}
});
}
// Sidebar Toggle
if (toggleBtn && sidebar) { // Added this check
toggleBtn.addEventListener("click", function(e) {
e.stopPropagation();
sidebar.classList.toggle("collapsed");
});
}
// Rest of your code remains the same...
const createCandidate = document.getElementById('add-candidate-create-btn');
const candidateModal = document.getElementById('candidate-form-modal');
const form = document.getElementById('candidate-form');
const closeModal = document.querySelectorAll('.candidate-form-close, .btn-cancel');
const avatarUpload = document.getElementById('avatar-upload');
const candidateImage = document.getElementById('candidate-image');
const avatarUploadIcon = document.querySelector('.avatar-upload i');
if (avatarUpload && candidateImage) {
// Make the entire avatar clickable
candidateImage.parentElement.style.cursor = 'pointer';
// Handle click on avatar
candidateImage.parentElement.addEventListener('click', function(e) {
if (e.target !== avatarUpload && e.target !== avatarUploadIcon) {
avatarUpload.click();
}
});
// Handle file selection
avatarUpload.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file && file.type.match('image.*')) {
const reader = new FileReader();
reader.onload = function(e) {
candidateImage.src = e.target.result;
};
reader.readAsDataURL(file);
}
});
}
// Resume Upload Elements
const resumeUpload = document.getElementById('resume-upload');
const resumeDropzone = document.getElementById('resume-dropzone');
const resumePreview = document.getElementById('resume-preview');
const resumePlaceholder = document.querySelector('.resume-preview-placeholder');
const resumeIframe = document.getElementById('resume-iframe');
const resumeImage = document.getElementById('resume-image');
const unsupportedFormat = document.getElementById('unsupported-format');
const downloadResume = document.getElementById('download-resume');
const uploadResumeBtn = document.getElementById('upload-applicant-resume');
let currentResumeFile = null;
const saveBtn = document.getElementById('save-candidate');
if (saveBtn) {
saveBtn.addEventListener('click', function (e) {
e.preventDefault();
createNewCandidate(form, candidateModal);
});
}
if (createCandidate) {
createCandidate.addEventListener('click', function(e) {
e.preventDefault();
candidateModal.style.display = 'flex';
setTimeout(() => {
candidateModal.classList.add('show');
}, 10);
document.body.style.overflow = 'hidden';
setTimeout(() => {
initSelect2();
initResumeUploadHandlers();
}, 100);
});
}
if (closeModal) {
closeModal.forEach(btn => {
btn.addEventListener('click', function() {
candidateModal.classList.remove('show');
setTimeout(() => {
candidateModal.style.display = 'none';
}, 300);
document.body.style.overflow = '';
});
});
}
// candidateModal.addEventListener('click', function(e) {
// if (e.target === candidateModal) {
// candidateModal.classList.remove('show');
// setTimeout(() => {
// candidateModal.style.display = 'none';
// }, 300);
// document.body.style.overflow = '';
// }
// });
function formatUserOption(user) {
if (!user.id) return user.text;
var imageUrl = $(user.element).data('image') || '/web/static/img/placeholder.png';
var $container = $(
'<span style="display: flex; align-items: center;">' +
'<img class="user-avatar" src="' + imageUrl + '" style="width:24px;height:24px;border-radius:50%;margin-right:8px;object-fit:cover;" onerror="this.src=\'/web/static/img/placeholder.png\'" />' +
'<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">' + user.text + '</span>' +
'</span>'
);
return $container;
}
function formatUserSelection(user) {
if (!user.id) return user.text;
var imageUrl = $(user.element).data('image') || '/web/static/img/placeholder.png';
var $container = $(
'<span style="display: flex; align-items: center; width: 100%;">' +
'<img class="user-avatar" src="' + imageUrl + '" style="width:24px;height:24px;border-radius:50%;margin-right:8px;object-fit:cover;" onerror="this.src=\'/web/static/img/placeholder.png\'" />' +
'<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-grow: 1;">' + user.text + '</span>' +
'</span>'
);
return $container;
}
function initSelect2() {
const candidateSkills = document.getElementById('candidate-skills');
if (candidateSkills) {
$(candidateSkills).select2({
placeholder: 'Select skills',
allowClear: true,
dropdownParent: $('.candidate-form-modal'),
width: '100%',
escapeMarkup: function(m) { return m; }
});
}
const managerSelect = document.getElementById('manager');
if (managerSelect) {
$(managerSelect).select2({
placeholder: 'Select Manager',
allowClear: true,
templateResult: formatUserOption,
templateSelection: formatUserSelection,
escapeMarkup: function(m) { return m; }
});
}
}
function initResumeUploadHandlers() {
// Create remove button
const removeResumeBtn = document.createElement('button');
removeResumeBtn.innerHTML = '<i class="fas fa-trash"></i> Remove Resume';
removeResumeBtn.className = 'btn btn-danger btn-sm mt-2';
removeResumeBtn.style.display = 'none';
resumePreview.appendChild(removeResumeBtn);
// Handle remove resume
removeResumeBtn.addEventListener('click', function() {
resetResumePreview();
});
function resetResumePreview() {
// Clear file input
resumeUpload.value = '';
currentResumeFile = null;
// Reset preview
resumePlaceholder.style.display = 'flex';
resumeIframe.style.display = 'none';
resumeImage.style.display = 'none';
unsupportedFormat.style.display = 'none';
removeResumeBtn.style.display = 'none';
// Reset iframe/src to prevent memory leaks
if (resumeIframe.src) {
URL.revokeObjectURL(resumeIframe.src);
resumeIframe.src = '';
}
if (resumeImage.src) {
URL.revokeObjectURL(resumeImage.src);
resumeImage.src = '';
}
if (downloadResume.href) {
URL.revokeObjectURL(downloadResume.href);
downloadResume.href = '#';
}
}
// Unified upload handler for both preview and parsing
uploadResumeBtn.addEventListener('click', function(e) {
e.preventDefault();
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.pdf,.doc,.docx,.txt,.jpg,.jpeg,.png';
fileInput.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
// First handle the preview
handleResumeFile(file);
// Then try to parse the resume if it's a parseable type
if (file.type.match(/pdf|msword|openxmlformats|text/)) {
// Show loading state
const button = uploadResumeBtn;
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing Resume...';
button.disabled = true;
try {
const formData = new FormData();
formData.append('file', file);
formData.append('type', 'candidate');
const response = await fetch('/resume/upload', {
method: 'POST',
body: formData,
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
populateCandidateForm(result);
} catch (error) {
console.error('Error parsing Resume:', error);
showNotification('Failed to parse Resume. Please try again or enter manually.', 'danger');
} finally {
button.innerHTML = '<i class="fas fa-upload"></i> Upload Resume';
button.disabled = false;
}
}
};
fileInput.click();
});
// Handle click on dropzone
resumeDropzone.addEventListener('click', function(e) {
if (e.target === this || e.target.classList.contains('upload-icon') ||
e.target.tagName === 'H5' || e.target.tagName === 'P') {
resumeUpload.click();
}
});
// Handle drag and drop
resumeDropzone.addEventListener('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
this.classList.add('dragover');
this.style.borderColor = '#3498db';
this.style.backgroundColor = 'rgba(52, 152, 219, 0.1)';
});
resumeDropzone.addEventListener('dragleave', function(e) {
e.preventDefault();
e.stopPropagation();
this.classList.remove('dragover');
this.style.borderColor = '';
this.style.backgroundColor = '';
});
resumeDropzone.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
this.classList.remove('dragover');
this.style.borderColor = '';
this.style.backgroundColor = '';
if (e.dataTransfer.files.length) {
const file = e.dataTransfer.files[0];
handleResumeFile(file);
}
});
// Handle file selection from the regular input
resumeUpload.addEventListener('change', function(e) {
if (this.files.length) {
handleResumeFile(this.files[0]);
}
});
function handleResumeFile(file) {
const validTypes = [
'application/pdf',
'application/msword',
'application/wps-office.docx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'image/jpeg',
'image/png',
'text/plain'
];
if (!validTypes.includes(file.type)) {
alert('Please upload a valid file type (PDF, Word, Image, or Text)');
return;
}
currentResumeFile = file;
// Hide placeholder
resumePlaceholder.style.display = 'none';
// Set up download link
const fileURL = URL.createObjectURL(file);
downloadResume.href = fileURL;
downloadResume.download = file.name;
removeResumeBtn.style.display = 'block';
// Check file type and show appropriate preview
if (file.type === 'application/pdf') {
// PDF preview
resumeIframe.src = fileURL;
resumeIframe.style.display = 'block';
resumeImage.style.display = 'none';
unsupportedFormat.style.display = 'none';
} else if (file.type.match('image.*')) {
// Image preview
resumeImage.src = fileURL;
resumeImage.style.display = 'block';
resumeIframe.style.display = 'none';
unsupportedFormat.style.display = 'none';
} else {
// Unsupported format for preview
unsupportedFormat.style.display = 'flex';
resumeIframe.style.display = 'none';
resumeImage.style.display = 'none';
}
// Update the actual resume-upload input
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
resumeUpload.files = dataTransfer.files;
}
}
function populateCandidateForm(resumeData) {
// Add CSS for visual feedback
const style = document.createElement('style');
style.textContent = `
.populated-field {
background-color: #f5f5f5 !important;
transition: background-color 0.3s ease;
}
.select2-populated .select2-selection--multiple {
background-color: #f5f5f5 !important;
}
.select2-populated .select2-selection__choice {
background-color: #e8f5e9 !important;
border-color: #c8e6c9 !important;
}
`;
document.head.appendChild(style);
// Helper function to set value with visual feedback
function setValueWithFeedback(elementId, value) {
const element = document.getElementById(elementId);
if (element && value) {
element.value = value;
element.classList.add('populated-field');
// Special handling for Select2 if this element uses it
if ($(element).hasClass('select2-hidden-accessible')) {
$(element).next('.select2-container')
.find('.select2-selection')
.addClass('populated-field');
}
return true;
}
return false;
}
// Section 1: Basic Information
if (resumeData.personal_info) {
const personal = resumeData.personal_info;
// Set values with visual feedback
setValueWithFeedback('partner-name', personal.name);
setValueWithFeedback('email', personal.email);
setValueWithFeedback('phone', personal.phone);
setValueWithFeedback('linkedin', personal.linkedin);
// If any of these fields are Select2 elements, ensure they get styled
['partner-name', 'email', 'phone', 'linkedin'].forEach(id => {
const el = document.getElementById(id);
if (el && el.value && $(el).hasClass('select2-hidden-accessible')) {
$(el).next('.select2-container')
.find('.select2-selection')
.addClass('populated-field');
}
});
}
// Skills - Handle Select2 with background color change
if (resumeData.skills?.length) {
const skillValues = resumeData.skills.map(skill => skill.id);
$('#candidate-skills')
.val(skillValues)
.trigger('change')
.addClass('populated-field');
// Add class to Select2 container for styling
$('#candidate-skills').next('.select2-container')
.find('.select2-selection--multiple')
.addClass('select2-populated');
}
// Show notification
showNotification('Resume uploaded and fields populated successfully!', 'success');
}
function showNotification(message, type) {
// Check if notification container exists, create if not
let notificationContainer = document.getElementById('notification-container');
if (!notificationContainer) {
notificationContainer = document.createElement('div');
notificationContainer.id = 'notification-container';
notificationContainer.style.position = 'fixed';
notificationContainer.style.top = '20px';
notificationContainer.style.right = '20px';
notificationContainer.style.zIndex = '9999';
document.body.appendChild(notificationContainer);
}
// Create notification element
const notification = document.createElement('div');
notification.className = `alert alert-${type} alert-dismissible fade show`;
notification.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
// Add to container
notificationContainer.appendChild(notification);
// Auto remove after 5 seconds
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
notification.remove();
}, 300);
}, 5000);
}
}
function createNewCandidate(form, modal) {
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const formData = new FormData();
// Basic Information
formData.append('sequence', document.getElementById('candidate-sequence').value);
formData.append('partner_name', document.getElementById('partner-name').value);
// Add image file if selected
const avatarUpload = document.getElementById('avatar-upload');
if (avatarUpload.files.length > 0) {
formData.append('image_1920', avatarUpload.files[0]);
}
// Contact Information
formData.append('email', document.getElementById('email').value);
formData.append('phone', document.getElementById('phone').value);
formData.append('mobile', document.getElementById('alternate-phone').value);
formData.append('linkedin_profile', document.getElementById('linkedin').value);
formData.append('type_id', document.getElementById('type').value);
formData.append('user_id', document.getElementById('manager').value);
formData.append('availability', document.getElementById('availability').value);
// Skills
const skillOptions = document.getElementById('candidate-skills').selectedOptions;
for (let i = 0; i < skillOptions.length; i++) {
formData.append('skill_ids', skillOptions[i].value);
}
// Add resume file if selected
const resumeUpload = document.getElementById('resume-upload');
if (resumeUpload.files.length > 0) {
formData.append('resume_file', resumeUpload.files[0]);
}
// Additional fields
formData.append('active', 'true');
formData.append('color', '0');
fetch('/myATS/candidate/create', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
}
})
.then(async (response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || response.statusText);
}
return response.json();
})
.then(data => {
if (data.success) {
modal.classList.remove('show');
document.body.style.overflow = '';
form.reset();
alert('Candidate created successfully!');
// Refresh the candidates list
fetch('/myATS/page/candidates', {
headers: { "X-Requested-With": "XMLHttpRequest" }
})
.then(res => res.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newCandidate = doc.querySelector('.candidates-list-left');
const existingCandidatesList = document.querySelector('.candidates-list-left');
if (newCandidate && existingCandidatesList) {
existingCandidatesList.innerHTML = newCandidate.innerHTML;
initCandidatesPage();
}
});
} else {
throw new Error(data.error || 'Failed to save Candidate');
}
})
.catch(error => {
console.error('Error:', error);
alert("Error saving changes: " + error.message);
});
}
// Initialize the page when DOM is ready
document.addEventListener('DOMContentLoaded', initCandidatesPage);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More