This commit is contained in:
raman 2025-04-28 09:56:09 +05:30
parent af98a28428
commit b34dcf6d5b
3 changed files with 373 additions and 209 deletions

View File

@ -1,50 +1,64 @@
/** @odoo-module **/
import { useEffect, useState, Component ,onMounted, useRef} from "@odoo/owl";
import { Component, useEffect, useState, onMounted, useRef, onWillStart } from "@odoo/owl";
import { registry } from "@web/core/registry";
const actionRegistry = registry.category("actions");
import { useService } from "@web/core/utils/hooks";
import { ActivityMenu } from "@hr_attendance/components/attendance_menu/attendance_menu";
import { patch } from "@web/core/utils/patch";
import { loadJS } from "@web/core/assets";
export class NetflixProfileContainer extends Component {
static template = 'employee_profile_template'; // The template for the profile
static template = 'employee_profile_template';
static props = ["*"];
setup() {
super.setup(...arguments);
debugger;
this.orm = useService("orm");
this.effect = useService("effect");
this.log_in_out = useRef("log_in_out")
this.action = useService("action");
this.action = useService("action");
this.chart = null;
// Initialize state for storing employee data
this.state = useState({
login_employee: {
name: '',
image_1920: '',
job_id: null,
current_company_exp: null,
doj:'',
employee_id:null,
doj: '',
employee_id: null,
birthday: '',
attendance_state:null,
attendance_state: null,
mobile_phone: '',
work_email: '',
private_street: '',
department_id:''
department_id: ''
},
attendance_lines:[],
leaves:[]
attendance_lines: [],
leaves: []
});
});
onMounted(() => {
this.fetchEmployeeData();
});
onWillStart(() => loadJS(["/web/static/lib/Chart/Chart.js"]));
onMounted(async () => {
await this.fetchEmployeeData();
// Make sure Chart is loaded and canvas is available
await loadJS(["/web/static/lib/Chart/Chart.js"]);
if (typeof Chart === 'undefined') {
console.error("Chart.js failed to load");
return;
}
const canvas = document.querySelector("#attendanceChart");
if (!canvas) {
console.warn("Canvas element not found. Skipping chart render.");
return;
}
});
}
hr_timesheets() {
this.action.doAction({
name: "Timesheets",
@ -52,52 +66,131 @@ export class NetflixProfileContainer extends Component {
res_model: 'account.analytic.line',
view_mode: 'list,form',
views: [[false, 'list'], [false, 'form']],
context: {
'search_default_month': true,
},
domain: [['employee_id.user_id','=', this.props.action.context.user_id]],
context: { 'search_default_month': true },
domain: [['employee_id.user_id', '=', this.props.action.context.user_id]],
target: 'current'
})
});
}
attendance_sign_in_out() {
if (this.state.login_employee.attendance_state == 'checked_out') {
this.state.login_employee.attendance_state = 'checked_in'
}
else{
if (this.state.login_employee.attendance_state == 'checked_in') {
this.state.login_employee.attendance_state = 'checked_out'
}
}
this.update_attendance()
hr_payslip() {
this.action.doAction({
name: "Employee Payslips",
type: 'ir.actions.act_window',
res_model: 'hr.payslip',
view_mode: 'list,form,calendar',
views: [[false, 'list'],[false, 'form']],
domain: [['employee_id','=', this.props.action.context.user_id]],
target: 'current'
});
}
async update_attendance() {
var self = this;
var result = await this.orm.call('hr.employee', 'attendance_manual',[[this.props.action.context.user_id]])
if (result) {
var attendance_state = this.state.login_employee.attendance_state;
var message = ''
if (attendance_state == 'checked_in'){
message = 'Checked In'
this.env.bus.trigger('signin_signout', {
mode: "checked_in",
});
}
else if (attendance_state == 'checked_out'){
message = 'Checked Out'
this.env.bus.trigger('signin_signout', {
mode: false,
});
}
this.effect.add({
message: ("Successfully " + message),
type: 'rainbow_man',
fadeout: "fast",
async hr_contract() {
if(this.isHrManager){
this.action.doAction({
name: _t("Contracts"),
type: 'ir.actions.act_window',
res_model: 'hr.contract',
view_mode: 'tree,form,calendar',
views: [[false, 'list'],[false, 'form']],
context: {
'search_default_employee_id': this.props.action.context.user_id,
},
target: 'current'
})
}
}
add_attendance() {
renderChart() {
const labels = this.state.attendance_lines.map(r => r.create_date);
const data = this.state.attendance_lines.map(r => parseFloat(r.worked_hours));
const ctx = document.querySelector("#attendanceChart");
if (!ctx) {
console.warn("Canvas context not found for chart.");
return;
}
if (this.chart) {
this.chart.destroy();
}
this.chart = new Chart(ctx, {
type: "bar", // Base type
data: {
labels,
datasets: [
{
type: 'bar',
label: "Hours Worked (Bar)",
data,
backgroundColor: "rgba(54, 162, 235, 0.6)",
borderColor: "rgba(54, 162, 235, 1)",
borderWidth: 1
},
{
type: 'line',
label: "Hours Worked (Line)",
data,
backgroundColor: "rgba(255, 99, 132, 0.2)",
borderColor: "rgba(255, 99, 132, 1)",
borderWidth: 2,
tension: 0.3,
fill: false,
pointRadius: 4,
pointHoverRadius: 6
}
]
},
options: {
responsive: true,
plugins: {
legend: { display: true },
tooltip: { mode: 'index', intersect: false }
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: "Hours"
}
},
x: {
title: {
display: true,
text: "Date"
},
reverse: true
}
}
}
});
}
async attendance_sign_in_out() {
const result = await this.orm.call('hr.employee', 'attendance_manual', [[this.props.action.context.user_id]]);
if (result) {
let current = this.state.login_employee.attendance_state;
this.state.login_employee.attendance_state = (current === 'checked_in') ? 'checked_out' : 'checked_in';
let mode = this.state.login_employee.attendance_state;
let message = mode === 'checked_in' ? 'Checked In' : 'Checked Out';
this.env.bus.trigger('signin_signout', {
mode: mode === 'checked_in' ? "checked_in" : false,
});
this.effect.add({
message: "Successfully " + message,
type: 'rainbow_man',
fadeout: "fast"
});
}
}
add_attendance() {
this.action.doAction({
name: ("Attendances"),
name: "Attendances",
type: 'ir.actions.act_window',
res_model: 'hr.attendance',
view_mode: 'form',
@ -105,9 +198,10 @@ export class NetflixProfileContainer extends Component {
target: 'new'
});
}
add_leave() {
this.action.doAction({
name: ("Leave Request"),
name: "Leave Request",
type: 'ir.actions.act_window',
res_model: 'hr.leave',
view_mode: 'form',
@ -116,95 +210,147 @@ export class NetflixProfileContainer extends Component {
});
}
// Method to fetch employee data from hr.employee model
async fetchEmployeeData() {
console.log(this.props.action.context.user_id)
try {
const userId = this.props.action.context.user_id;
const employeeData = await this.orm.call("hr.employee", 'get_user_employee_details');
if (!employeeData.length) return;
const employeeData = await this.orm.call(
"hr.employee",'get_user_employee_details');
const attendanceLines = await this.orm.searchRead(
'hr.attendance',
[['employee_id', '=', employeeData[0].id]],
['create_date', 'check_in', 'check_out', 'worked_hours']);
const Leaves = await this.orm.searchRead('hr.leave',[['employee_id', '=', employeeData[0].id]],
['request_date_from', 'request_date_to', 'state','holiday_status_id']);
const employee = employeeData[0];
Leaves.forEach(line => {
// Extract the 'type' from 'holiday_status_id' and assign it
line['type'] = line['holiday_status_id'][1];
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
// Change state and set color based on the state
if (line['state'] === 'confirm') {
line['state'] = 'To Approve';
line['color'] = 'orange';
} else if (line['state'] === 'validate1') {
line['state'] = 'Second Approval';
line['color'] = '#7CFC00';
} else if (line['state'] === 'validate') {
line['state'] = 'Approved';
line['color'] = 'green';
} else if (line['state'] === 'cancel') {
line['state'] = 'Cancelled';
line['color'] = 'red';
} else {
line['state'] = 'Refused';
line['color'] = 'red';
}
});
// Format as Odoo-compatible strings: 'YYYY-MM-DD'
const startDateStr = startOfMonth.toISOString().split('T')[0];
const endDateStr = endOfMonth.toISOString().split('T')[0];
const attendanceLines = await this.orm.searchRead(
'hr.attendance',
[
['employee_id', '=', employee.id],
['create_date', '>=', startDateStr],
['create_date', '<=', endDateStr]
],
['create_date', 'check_in', 'check_out', 'worked_hours']
);
if (employeeData.length > 0) {
const employee = employeeData[0];
attendanceLines.forEach(line => {
let createDate = new Date(line.create_date);
line.create_date = createDate.toLocaleDateString('en-IN', { timeZone: 'Asia/Kolkata' }); // Format as 'YYYY-MM-DD'
let checkIn = new Date(line.check_in + 'Z');
line.check_in = checkIn.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', timeZone:'Asia/Kolkata'}); // Format as 'HH:MM'
let checkOut = new Date(line.check_out + 'Z');
line.check_out = checkOut.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', timeZone:'Asia/Kolkata'}); // Format as 'HH:MM'
line.worked_hours = line.worked_hours.toFixed(2);
});
this.state.attendance_lines = attendanceLines,
this.state.leaves = Leaves
const leaves = await this.orm.searchRead(
'hr.leave',
[['employee_id', '=', employee.id]],
['request_date_from', 'request_date_to', 'state', 'holiday_status_id']
);
this.state.login_employee = {
name: employee.name,
image_1920: employee.image_1920,
doj:employee.doj,
job_id: employee.job_id,
employee_id:employee.employee_id,
current_company_exp: employee.current_company_exp,
attendance_state:employee.attendance_state,
birthday: employee.birthday,
mobile_phone: employee.mobile_phone,
work_email: employee.work_email,
private_street: employee.private_street,
department_id:employee.department_id[1]
};
}
const groupedLines = {};
attendanceLines.forEach(line => {
const createDate = new Date(line.create_date);
const dateStr = createDate.toLocaleDateString('en-IN', { timeZone: 'Asia/Kolkata' });
const workedHours = parseFloat(line.worked_hours);
const checkIn = new Date(line.check_in + 'Z');
const checkOut = new Date(line.check_out + 'Z');
const checkInStr = checkIn.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', timeZone: 'Asia/Kolkata' });
const checkOutStr = checkOut.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', timeZone: 'Asia/Kolkata' });
if (!groupedLines[dateStr]) {
groupedLines[dateStr] = {
create_date: dateStr,
worked_hours: workedHours,
check_in: checkInStr,
check_out: checkOutStr,
earliestCheckIn: checkIn,
latestCheckOut: checkOut
};
} else {
groupedLines[dateStr].worked_hours += workedHours;
// Update earliest check-in
if (checkIn < groupedLines[dateStr].earliestCheckIn) {
groupedLines[dateStr].check_in = checkInStr;
groupedLines[dateStr].earliestCheckIn = checkIn;
}
// Update latest check-out
if (checkOut > groupedLines[dateStr].latestCheckOut) {
groupedLines[dateStr].check_out = checkOutStr;
groupedLines[dateStr].latestCheckOut = checkOut;
}
}
});
// Format final result
const groupedAttendance = Object.values(groupedLines).map(line => ({
create_date: line.create_date,
worked_hours: line.worked_hours.toFixed(2),
check_in: line.check_in,
check_out: line.check_out
}));
// Assign to state
this.state.attendance_lines = groupedAttendance;
this.renderChart();
leaves.forEach(line => {
line.type = line.holiday_status_id[1];
switch (line.state) {
case 'confirm':
line.state = 'To Approve';
line.color = 'orange';
break;
case 'validate1':
line.state = 'Second Approval';
line.color = '#7CFC00';
break;
case 'validate':
line.state = 'Approved';
line.color = 'green';
break;
case 'cancel':
line.state = 'Cancelled';
line.color = 'red';
break;
default:
line.state = 'Refused';
line.color = 'red';
break;
}
});
this.state.login_employee = {
name: employee.name,
image_1920: employee.image_1920,
doj: employee.doj,
job_id: employee.job_id,
employee_id: employee.employee_id,
current_company_exp: employee.current_company_exp,
attendance_state: employee.attendance_state,
birthday: employee.birthday,
mobile_phone: employee.mobile_phone,
work_email: employee.work_email,
private_street: employee.private_street,
department_id: employee.department_id[1]
};
this.state.leaves = leaves;
} catch (error) {
console.error('Error fetching employee data:', error);
console.error("Error fetching employee data:", error);
}
}
}
registry.category("actions").add("NetflixProfileContainer", NetflixProfileContainer)
actionRegistry.add("NetflixProfileContainer", NetflixProfileContainer);
// Patch the attendance menu to update icon on signin/signout
patch(ActivityMenu.prototype, {
setup() {
super.setup();
var self = this
onMounted(() => {
this.env.bus.addEventListener('signin_signout', ({
detail
}) => {
if (detail.mode == 'checked_in') {
self.state.checkedIn = detail.mode
} else {
self.state.checkedIn = false
}
})
})
},
})
this.env.bus.addEventListener('signin_signout', ({ detail }) => {
this.state.checkedIn = detail.mode === "checked_in" ? true : false;
});
});
}
});

View File

@ -74,6 +74,7 @@
</p>
</div>
</div>
<div class="stat-att" t-on-click="attendance_sign_in_out">
<t t-if="this.state.login_employee.attendance_state == 'checked_out'">
<div class="stat-atticon">
@ -114,7 +115,26 @@
</h2>
</div>
</div>
<div class="stat-card" t-on-click="hr_payslip">
<div class="stat-icon">
<i class="fa fa-id-card">
</i>
</div>
<div class="stat-content">
<p>
Payslips
</p>
<h2>
<t t-esc="this.state.login_employee['emp_timesheets']" />
</h2>
</div>
</div>
</div>
<div class="container" >
<div style="width: 100%; max-width: 600px; display: block; margin-left: 0;">
<canvas id="attendanceChart" width="600" height="300"></canvas>
</div>
</div>
<div class="tables-section">
<!-- Attendance Table -->
<div class="table-card">
@ -147,7 +167,8 @@
</tr>
</thead>
<tbody>
<tr t-foreach="this.state.attendance_lines" t-as="line" t-key="line['id']">
<tr t-foreach="state.attendance_lines" t-as="line" t-key="line.create_date">
<td t-esc="line.create_date">
</td>
<td t-esc="line['check_in']">

View File

@ -172,39 +172,39 @@ class BiometricDeviceDetails(models.Model):
if conn:
conn.disable_device()
self.get_all_users()
# self.action_set_timezone()
user = conn.get_users()
# get All Fingerprints
fingers = conn.get_templates()
for use in user:
for finger in fingers:
if finger.uid == use.uid:
templates = conn.get_user_template(uid=use.uid,
temp_id=finger.fid,
user_id=use.user_id)
hex_data = templates.template.hex()
# Convert hex data to binary
binary_data = binascii.unhexlify(hex_data)
base64_data = base64.b64encode(binary_data).decode(
'utf-8')
employee = self.env['hr.employee'].search(
[('device_id_num', '=', use.user_id),('company_id', '=', self.env.company.id)],limit=1)
employee.device_ids |= self
if str(finger.fid) in employee.fingerprint_ids.mapped(
'finger_id'):
employee.fingerprint_ids.search(
[('finger_id', '=', finger.fid)]).update({
'finger_template': base64_data,
})
else:
employee.fingerprint_ids.create({
'finger_template': base64_data,
'finger_id': finger.fid,
'employee_bio_id': employee.id,
'filename': f'{employee.name}-finger-{finger.fid}'
})
# fingers = conn.get_templates()
# for use in user:
# for finger in fingers:
# if finger.uid == use.uid:
# templates = conn.get_user_template(uid=use.uid,
# temp_id=finger.fid,
# user_id=use.user_id)
# hex_data = templates.template.hex()
# # Convert hex data to binary
# binary_data = binascii.unhexlify(hex_data)
# base64_data = base64.b64encode(binary_data).decode(
# 'utf-8')
# employee = self.env['hr.employee'].search(
# [('device_id_num', '=', use.user_id),('company_id', '=', self.env.company.id)],limit=1)
# employee.device_ids |= self
# if str(finger.fid) in employee.fingerprint_ids.mapped(
# 'finger_id'):
# employee.fingerprint_ids.search(
# [('finger_id', '=', finger.fid)]).update({
# 'finger_template': base64_data,
# })
# else:
# employee.fingerprint_ids.create({
# 'finger_template': base64_data,
# 'finger_id': finger.fid,
# 'employee_bio_id': employee.id,
# 'filename': f'{employee.name}-finger-{finger.fid}'
# })
# get all attendances
print(help(zk.get_attendance))
attendance = conn.get_attendance()
print(attendance)
if attendance:
filtered_attendance = []
@ -240,53 +240,50 @@ class BiometricDeviceDetails(models.Model):
for uid in user:
if uid.user_id == each.user_id:
get_user_id = self.env['hr.employee'].search(
[('device_id_num', '=', each.user_id), ('company_id', '=', self.env.company.id)],limit=1)
check_in_today = hr_attendance.search([(
'employee_id', '=', get_user_id.id),
('check_in', '!=', False),('check_out', '=',False)])
from datetime import timedelta
employee = self.env['hr.employee'].search([
('device_id_num', '=', each.user_id),
('company_id', '=', self.env.company.id)
], limit=1)
# Define the tolerance (10 minutes)
tolerance = timedelta(minutes=10)
if not employee:
continue
# Convert the atten_time string to a datetime object
# Calculate the lower and upper bounds with the tolerance
# Convert atten_time to datetime
atten_time_obj = datetime.datetime.strptime(atten_time, "%Y-%m-%d %H:%M:%S")
# Calculate the lower and upper bounds with the tolerance
lower_bound = atten_time_obj - tolerance
upper_bound = atten_time_obj + tolerance
# Find existing attendance for today
start_of_day = atten_time_obj.replace(hour=0, minute=0, second=0, microsecond=0)
end_of_day = atten_time_obj.replace(hour=23, minute=59, second=59, microsecond=999999)
# Ensure the 'check_in' and 'check_out' fields are datetime objects and compare them
next_in = hr_attendance.search([
('employee_id', '=', get_user_id.id),
('check_in', '>=', lower_bound),
('check_in', '<=', upper_bound)
])
attendance_today = self.env['hr.attendance'].search([
('employee_id', '=', employee.id),
('check_in', '>=', start_of_day),
('check_in', '<=', end_of_day)
], limit=1)
next_out = hr_attendance.search([
('employee_id', '=', get_user_id.id),
('check_out', '>=', lower_bound),
('check_out', '<=', upper_bound)
])
if get_user_id:
if self.display_name == 'IN' and not check_in_today:
if next_in:
continue
hr_attendance.create({
'employee_id':get_user_id.id,
'check_in': atten_time,
# IN logic
if self.display_name == 'IN':
if not attendance_today:
# No attendance yet, create new with check_in only
self.env['hr.attendance'].create({
'employee_id': employee.id,
'check_in': atten_time_obj,
})
get_user_id.attendance_state = 'checked_in'
elif check_in_today and self.display_name != 'IN':
if fields.Datetime.to_string(check_in_today.check_in) > atten_time or next_out:
continue
check_in_today.write({
'check_out': atten_time,
})
get_user_id.attendance_state = 'checked_out'
employee.attendance_state = 'checked_in'
else:
attendance_today.check_out = False
employee.attendance_state = 'checked_in'
continue
# OUT logic
elif self.display_name != 'IN':
if attendance_today:
# Only update checkout if it's not set or is earlier than current atten_time
if not attendance_today.check_out or attendance_today.check_out < atten_time_obj:
attendance_today.write({
'check_out': atten_time_obj,
})
employee.attendance_state = 'checked_out'
else:
pass
@ -296,7 +293,7 @@ class BiometricDeviceDetails(models.Model):
('company_id', '=', self.env.company.id)])
if not duplicate_atten_ids:
zk_attendance.create({
'employee_id': get_user_id.id,
'employee_id': employee.id,
'device_id_num': each.user_id,
'attendance_type': str(1),
'punch_type': '0' if self.display_name == 'IN' else '1',
@ -566,7 +563,7 @@ class BiometricDeviceDetails(models.Model):
"group_id: %s\n"
"user_id: %s\n"
"Here is the debugging information:\n%s\n"
"Try Restarting the device")
"Try Reqing the device")
% (candidate_uid, employee.name, privilege, password,
group_id, str(candidate_uid), e))
conn.enable_device()
@ -750,4 +747,4 @@ class ZKBioAttendance(Thread):
new_cr.commit()
if self.conn.get_attendance():
self.record.with_env(new_env).action_download_attendance()