Compare commits
52 Commits
feature/od
...
ats_web
| Author | SHA1 | Date |
|---|---|---|
|
|
b479fee7c9 | |
|
|
672551cd0a | |
|
|
0b93dd1a87 | |
|
|
e88f7da488 | |
|
|
79d14a2023 | |
|
|
0193fe04fb | |
|
|
b10e096540 | |
|
|
a8df849cc3 | |
|
|
d154a96b2b | |
|
|
8c6b7669a3 | |
|
|
d56eaaa306 | |
|
|
8c4e5d82da | |
|
|
6d9933c2a1 | |
|
|
2084217fbc | |
|
|
2aef56be49 | |
|
|
4194fbfecf | |
|
|
95a50af0f3 | |
|
|
a64a2f8016 | |
|
|
4f4b7e0c36 | |
|
|
6b97411909 | |
|
|
2896e9e83b | |
|
|
785a7da404 | |
|
|
c21f46bf33 | |
|
|
d4f31b5af5 | |
|
|
1e9485bb5a | |
|
|
541272a7c1 | |
|
|
5b0d3f4a3e | |
|
|
b195382233 | |
|
|
709e17ad7b | |
|
|
b9c7d43541 | |
|
|
3207b47f72 | |
|
|
5fdb26226c | |
|
|
640ddcdaa5 | |
|
|
6053b90162 | |
|
|
54fafbb1e9 | |
|
|
f9985e9c58 | |
|
|
c5c89709e4 | |
|
|
871ad34f7a | |
|
|
7188c475d5 | |
|
|
e650a1f1bf | |
|
|
d9f2183c2b | |
|
|
19f54e5ba0 | |
|
|
3e6eb7799b | |
|
|
4a2026faae | |
|
|
5cdbdd75b0 | |
|
|
f7c05c7b2e | |
|
|
6273e2fa96 | |
|
|
7600729b8b | |
|
|
25aa25c6f5 | |
|
|
a0961f338b | |
|
|
196f7fb371 | |
|
|
e7a8762880 |
|
|
@ -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
|
||||
})
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import maintenance_equipment
|
||||
from . import res_company
|
||||
|
|
@ -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
|
||||
})
|
||||
|
|
@ -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
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import custom_qrcode_generator
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
Binary file not shown.
|
Before Width: | Height: | Size: 9.2 KiB |
|
|
@ -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>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import equipment_label_layout
|
||||
|
|
@ -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)
|
||||
|
|
@ -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>
|
||||
|
|
@ -45,7 +45,7 @@ class HrPayslipRun(models.Model):
|
|||
'attendance_days': attendance_days,
|
||||
'leave_days': leave_days,
|
||||
'lop_days': lop_days,
|
||||
'doj':contract.date_start,
|
||||
'doj':employee.doj,
|
||||
'birthday':employee.birthday,
|
||||
'bank': employee.bank_account_id.display_name if employee.bank_account_id else '-',
|
||||
'sick_leave_balance': leave_balances.get('LEAVE110', 0),
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
from . import models
|
||||
from . import wizards
|
||||
|
|
@ -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'
|
||||
],
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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'))
|
||||
|
|
@ -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},
|
||||
)
|
||||
|
|
@ -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}
|
||||
|
|
@ -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")
|
||||
|
|
@ -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}"
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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')"/>-->
|
||||
|
||||
<!-- <!– Report Title –>-->
|
||||
<!-- <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>-->
|
||||
|
||||
<!-- <!– Employee Info –>-->
|
||||
<!-- <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>-->
|
||||
|
||||
<!-- <!– Head of Income –>-->
|
||||
<!-- <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>-->
|
||||
|
||||
<!-- <!– Deductions –>-->
|
||||
<!-- <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>-->
|
||||
|
||||
<!-- <!– Income Details –>-->
|
||||
<!-- <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>-->
|
||||
|
||||
<!-- <!– Taxable Income –>-->
|
||||
<!-- <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>-->
|
||||
|
||||
<!-- <!– Tax Regime Comparison –>-->
|
||||
<!-- <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>-->
|
||||
|
||||
<!-- <!– Tax Computation –>-->
|
||||
<!-- <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 & 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,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 & 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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','<',datetime.datetime.now()),('from_date','>',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','<',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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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')
|
||||
|
|
@ -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>
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
|
|
@ -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 & Construction Completed Within 3 Years &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>
|
||||
|
|
@ -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")
|
||||
|
|
@ -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>
|
||||
|
|
@ -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")
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
|
|
@ -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 & Construction Completed Within 3 Years &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 & Interest Details">
|
||||
<field name="period_from"/>
|
||||
<field name="period_to"/>
|
||||
<field name="interest_paid_to"/>
|
||||
</group>
|
||||
|
||||
<!-- Financial & Lender Info -->
|
||||
|
||||
<group string="Financial & 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>
|
||||
|
|
@ -1 +1 @@
|
|||
from . import models, controllers
|
||||
from . import models
|
||||
|
|
@ -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)}"
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<template id="emp_joining_form_template">
|
||||
<t t-call="web.basic_layout">
|
||||
<t t-call="web.external_layout">
|
||||
<main class="page"
|
||||
style="margin: 0px; padding: 0px; font-size: 16px; font-family: 'Arial', sans-serif;">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
|
|
@ -313,13 +313,4 @@
|
|||
</t>
|
||||
</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>
|
||||
|
|
@ -97,8 +97,7 @@ daily_checkins AS (
|
|||
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 DESC) AS last_checkout_row,
|
||||
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
|
||||
dep.name->>'en_US' AS department
|
||||
FROM
|
||||
hr_attendance at
|
||||
LEFT JOIN
|
||||
|
|
@ -116,10 +115,6 @@ attendance_summary AS (
|
|||
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,
|
||||
SUM(worked_hours) AS total_worked_hours,
|
||||
-- 👇 Calculate total break time (sum of gaps between check_out and next check_in)
|
||||
SUM(
|
||||
EXTRACT(EPOCH FROM (next_check_in - check_out)) / 3600
|
||||
) FILTER (WHERE next_check_in IS NOT NULL AND check_out IS NOT NULL) AS break_hours,
|
||||
department
|
||||
FROM
|
||||
daily_checkins
|
||||
|
|
@ -152,7 +147,6 @@ SELECT
|
|||
COALESCE(ats.first_check_in, NULL) AS first_check_in,
|
||||
COALESCE(ats.last_check_out, NULL) AS last_check_out,
|
||||
COALESCE(ats.total_worked_hours, 0) AS total_worked_hours,
|
||||
COALESCE(ats.break_hours, 0) AS total_break_hours,
|
||||
ed.department,
|
||||
CASE
|
||||
WHEN ld.leave_type IS NOT NULL AND ld.is_half_day THEN 'on Half day ' || ld.leave_type
|
||||
|
|
@ -171,6 +165,7 @@ LEFT JOIN
|
|||
ORDER BY
|
||||
ed.employee_id, ed.date;
|
||||
"""
|
||||
|
||||
# Combine all parameters in the correct order:
|
||||
# 1. date_range params (start_date_str, end_date_str)
|
||||
# 2. employee_dates params (emp_date_params)
|
||||
|
|
@ -201,7 +196,6 @@ ORDER BY
|
|||
'check_in': r['first_check_in'],
|
||||
'check_out': r['last_check_out'],
|
||||
'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']
|
||||
})
|
||||
|
||||
|
|
@ -219,6 +213,7 @@ ORDER BY
|
|||
attendance_data = self.get_attendance_report(department_id, employee_id, start_date, end_date)
|
||||
if not attendance_data:
|
||||
raise UserError("No data to export!")
|
||||
|
||||
# Create workbook and sheet
|
||||
workbook = xlwt.Workbook(encoding='utf-8')
|
||||
sheet = workbook.add_sheet('Attendance Report')
|
||||
|
|
@ -286,26 +281,28 @@ ORDER BY
|
|||
)
|
||||
|
||||
# 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):
|
||||
sheet.col(i).width = width
|
||||
|
||||
# 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
|
||||
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'
|
||||
))
|
||||
|
||||
# Write headers
|
||||
headers = [
|
||||
'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):
|
||||
sheet.write(2, col_num, header, header_style)
|
||||
|
||||
# Write data rows
|
||||
current_employee = None
|
||||
for row_num, record in enumerate(attendance_data, start=3):
|
||||
|
|
@ -348,16 +345,11 @@ ORDER BY
|
|||
else:
|
||||
sheet.write(row_num, 7, str(record['worked_hours']), data_style)
|
||||
|
||||
# Break hours formatting
|
||||
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, 8, record['status'], status_style)
|
||||
|
||||
sheet.write(row_num, 9, record['status'], status_style)
|
||||
# Add freeze panes (headers will stay visible when scrolling)
|
||||
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
|
||||
|
||||
# Save to buffer
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ export default class AttendanceReport extends Component {
|
|||
}
|
||||
|
||||
async generateReport() {
|
||||
debugger;
|
||||
|
||||
let { startDate, endDate, selectedEmployeeIds } = this.state;
|
||||
startDate = $('#from_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
|
||||
// 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]);
|
||||
debugger;
|
||||
|
||||
// Group data by employee_id
|
||||
const rawGroups = this.groupDataByEmployee(attendanceData);
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@
|
|||
<th>Check In</th>
|
||||
<th>Check Out</th>
|
||||
<th>Worked Hours</th>
|
||||
<th>Break Hours</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -79,16 +78,7 @@
|
|||
<td><t t-esc="data.day_name"/></td>
|
||||
<td><t t-esc="data.check_in"/></td>
|
||||
<td><t t-esc="data.check_out"/></td>
|
||||
<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.worked_hours"/></td>
|
||||
<td><t t-esc="data.status"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -18,8 +18,12 @@
|
|||
'version': '0.1',
|
||||
|
||||
# any module necessary for this one to work correctly
|
||||
|
||||
'depends': ['base','hr','account','mail','hr_skills', 'hr_contract'],
|
||||
|
||||
|
||||
|
||||
|
||||
# always loaded
|
||||
'data': [
|
||||
'security/security.xml',
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -456,8 +456,8 @@ class HrPayslip(models.Model):
|
|||
'res_id': payslip.id
|
||||
})
|
||||
# Send email to employees
|
||||
# if template:
|
||||
# template.send_mail(payslip.id, email_layout_xmlid='mail.mail_notification_light')
|
||||
if template:
|
||||
template.send_mail(payslip.id, email_layout_xmlid='mail.mail_notification_light')
|
||||
self.env['ir.attachment'].sudo().create(attachments_vals_list)
|
||||
|
||||
def _filter_out_of_contracts_payslips(self):
|
||||
|
|
@ -1381,10 +1381,7 @@ class HrPayslip(models.Model):
|
|||
|
||||
|
||||
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
|
||||
if not joining_date or joining_date == self.date_from:
|
||||
return 0
|
||||
date_from = min(joining_date, self.date_from)
|
||||
if joining_date > date_from:
|
||||
|
||||
|
|
@ -1403,7 +1400,7 @@ class HrPayslip(models.Model):
|
|||
weekend_days_count = weekend_count
|
||||
else:
|
||||
weekend_days_count = 0
|
||||
return weekend_days_count + days
|
||||
return weekend_days_count
|
||||
|
||||
def action_edit_payslip_lines(self):
|
||||
self.ensure_one()
|
||||
|
|
@ -1852,40 +1849,3 @@ class HrPayslip(models.Model):
|
|||
if 'stats' in sections:
|
||||
result['stats'] = self._get_dashboard_stats()
|
||||
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
|
||||
|
|
|
|||
|
|
@ -44,21 +44,20 @@
|
|||
<table style="width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 10px;">
|
||||
<tr>
|
||||
<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/>
|
||||
<t t-set="days" t-value="(o.date_to - o.date_from).days + 1"/>
|
||||
<strong>Number of Days:</strong> <t style="padding: 6px;" t-esc="days"/> Days<br/>
|
||||
<strong>Worked Days:</strong> <t style="padding: 6px;" t-esc="days"/> Days
|
||||
</td>
|
||||
<t t-set="leave_data" t-value="o.get_leave_balance()"/>
|
||||
<td t-if="leave_data" style="border: 1px solid #ccc; padding: 6px;">
|
||||
<p><u><b>Leave Balance</b></u>
|
||||
<div t-foreach="leave_data.values()" t-as="data">
|
||||
<strong t-out="data['name'] + ':'"/>
|
||||
<t t-out="data['remaining']"/>
|
||||
Days
|
||||
<t t-set="timeoff_data_table" t-value="o._get_employee_timeoff_data()"/>
|
||||
<td t-if="timeoff_data_table" style="border: 1px solid #ccc; padding: 6px;">
|
||||
<div t-foreach="timeoff_data_table" t-as="timeoff_data">
|
||||
<strong t-out="timeoff_data[0] + ':'"/>
|
||||
<t t-out="timeoff_data[1].get('remaining_leaves')"/> /
|
||||
<t t-out="timeoff_data[1].get('max_leaves')"/>
|
||||
<t t-if="timeoff_data[1].get('request_unit') == 'hour'">Hours</t>
|
||||
<t t-else="">Days</t>
|
||||
</div>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</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 > 0)" t-as="l">
|
||||
<t t-esc="l.name"/><br/>
|
||||
</div>
|
||||
<br/>
|
||||
<strong>Total Income</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;">
|
||||
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code in ['BASIC','SPA','ALW'] and l.amount > 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"/>
|
||||
</div>
|
||||
<br/>
|
||||
<strong><t t-esc="'%.2f' % income"/></strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;">
|
||||
<t t-set="contribution" t-value="0"/>
|
||||
|
|
@ -96,7 +91,7 @@
|
|||
</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;">
|
||||
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code in ['COMP','MA'] and l.amount > 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"/>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -112,33 +107,30 @@
|
|||
</div>
|
||||
</td>
|
||||
<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">
|
||||
<t t-esc="'%.2f' % l.amount"/><br/>
|
||||
<t t-esc="l.amount"/><br/>
|
||||
<t t-set="ded" t-value="ded + l.amount"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;" colspan="3"><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;"><strong>Gross Salary</strong></td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;"><strong><t t-esc="income"/></strong></td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;"><strong>Total Deduction</strong></td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;"><strong><t t-esc="ded"/></strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;" colspan="3"><strong>Total Deduction</strong></td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;" colspan="1"><strong><t t-esc="'%.2f' % ded"/></strong></td>
|
||||
</tr>
|
||||
<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 class="net-salary" colspan="3" style="border: 1px solid #ccc; padding: 6px;"><strong>Net Salary:</strong></td>
|
||||
<td class="net-salary" colspan="1" style="border: 1px solid #ccc; padding: 6px; text-align: right;">
|
||||
<t t-esc="(contribution + income) + ded"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="to-pay" style="margin-top: 20px;">
|
||||
<p t-if="o.net_wage >= 0">
|
||||
To pay <strong><span t-esc="'%.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>
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@
|
|||
name="Contracts"
|
||||
action="hr_contract.action_hr_contract"
|
||||
parent="menu_hr_payroll_employees_root"
|
||||
sequence="90"/>
|
||||
sequence="30"/>
|
||||
|
||||
<menuitem
|
||||
id="hr_menu_salary_attachments"
|
||||
|
|
|
|||
|
|
@ -164,14 +164,13 @@ class website_hr_recruitment_applications(http.Controller):
|
|||
@http.route(['/FTPROTECH/submit/<int:applicant_id>/JoinForm'], type='http', auth="public",
|
||||
methods=['POST'], website=True, csrf=False)
|
||||
def process_employee_joining_form(self,applicant_id,**post):
|
||||
|
||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||
if not applicant.exists():
|
||||
return request.not_found() # Return 404 if applicant doesn't exist
|
||||
|
||||
if applicant.post_onboarding_form_status == 'done':
|
||||
return request.render("hr_recruitment_extended.thank_you_template",{
|
||||
'applicant': applicant
|
||||
})
|
||||
return request.render("hr_recruitment_extended.thank_you_template")
|
||||
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)))
|
||||
|
|
@ -281,41 +280,8 @@ class website_hr_recruitment_applications(http.Controller):
|
|||
]
|
||||
|
||||
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
|
||||
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
|
||||
})
|
||||
return request.render("hr_recruitment_extended.thank_you_template")
|
||||
|
||||
def safe_date_parse(self,date_str):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -290,49 +290,6 @@
|
|||
<field name="auto_delete" eval="True"/>
|
||||
</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">
|
||||
<field name="name">Joining Formalities Notification</field>
|
||||
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<odoo>
|
||||
<odoo>
|
||||
<template id="employee_joining_form_template">
|
||||
<t t-call="web.basic_layout">
|
||||
<t t-call="web.external_layout">
|
||||
<main class="page"
|
||||
style="margin: 0px; padding: 0px; font-size: 16px; font-family: 'Arial', sans-serif;">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
|
|
|
|||
|
|
@ -256,6 +256,8 @@ class HRJobRecruitment(models.Model):
|
|||
rec.submission_status = 'zero'
|
||||
|
||||
|
||||
experience = fields.Many2one('candidate.experience', string="Experience")
|
||||
|
||||
@api.depends('application_ids.submitted_to_client')
|
||||
def _compute_no_of_submissions(self):
|
||||
counts = dict(self.env['hr.applicant']._read_group(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
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_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_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_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_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
|
||||
,,,,,,,
|
||||
|
|
|
@ -465,13 +465,6 @@
|
|||
<label>Permanent Address
|
||||
<span class="text-danger">*</span>
|
||||
</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>
|
||||
<div class="mb-3">
|
||||
<input type="text" class="form-control mb-2" name="permanent_street"
|
||||
|
|
@ -1723,80 +1716,6 @@
|
|||
let prevButtons = document.querySelectorAll(".prev-step");
|
||||
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 && presentStreet) permanentStreet.value = presentStreet.value;
|
||||
if (permanentStreet2 && presentStreet2) permanentStreet2.value = presentStreet2.value;
|
||||
if (permanentCity && presentCity) permanentCity.value = presentCity.value;
|
||||
if (permanentZip && 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 && 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() {
|
||||
// Show/hide step content
|
||||
steps.forEach((step, index) => {
|
||||
|
|
@ -1836,13 +1755,6 @@
|
|||
// Next button logic
|
||||
nextButtons.forEach(button => {
|
||||
button.addEventListener("click", function () {
|
||||
if (currentStep === 0) {
|
||||
// Validate employer history fields before proceeding
|
||||
if (!validateEmployerHistory()) {
|
||||
return false; // Stop navigation
|
||||
}
|
||||
}
|
||||
|
||||
if (validateStep(currentStep)) {
|
||||
if (currentStep < steps.length - 1) {
|
||||
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 && 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' && 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 && 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' && 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' && 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 && 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 && !companyNameInput.value.trim()) {
|
||||
companyNameInput.classList.add("is-invalid");
|
||||
} else if (companyNameInput) {
|
||||
companyNameInput.classList.remove("is-invalid");
|
||||
}
|
||||
|
||||
if (designationInput && !designationInput.value.trim()) {
|
||||
designationInput.classList.add("is-invalid");
|
||||
} else if (designationInput) {
|
||||
designationInput.classList.remove("is-invalid");
|
||||
}
|
||||
|
||||
if (dojInput && !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
|
||||
prevButtons.forEach(button => {
|
||||
|
|
@ -2412,7 +2149,6 @@
|
|||
<h2>Thank You for Your Submission</h2>
|
||||
<p>Your form has been successfully submitted.</p>
|
||||
<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>
|
||||
</t>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import web_recruitment
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,2 @@
|
|||
from . import recruitment_doc_upload_wizard
|
||||
from . import res_config_settings
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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 |
|
|
@ -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);
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
@ -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
7
addons_extensions/hr_recruitment_web_app/static/src/libs/ckeditor/build/ckeditor.js
vendored
Normal file
7
addons_extensions/hr_recruitment_web_app/static/src/libs/ckeditor/build/ckeditor.js
vendored
Normal file
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
Loading…
Reference in New Issue