From b34dcf6d5badc72a1bee01c6ab968dc19be73cca Mon Sep 17 00:00:00 2001
From: raman
Date: Mon, 28 Apr 2025 09:56:09 +0530
Subject: [PATCH] FIX BUG
---
.../static/src/js/profile_component.js | 416 ++++++++++++------
.../src/xml/employee_profile_template.xml | 23 +-
.../models/biometric_device_details.py | 143 +++---
3 files changed, 373 insertions(+), 209 deletions(-)
diff --git a/addons_extensions/hr_emp_dashboard/static/src/js/profile_component.js b/addons_extensions/hr_emp_dashboard/static/src/js/profile_component.js
index 23b5c1ebb..9eab0c9e9 100644
--- a/addons_extensions/hr_emp_dashboard/static/src/js/profile_component.js
+++ b/addons_extensions/hr_emp_dashboard/static/src/js/profile_component.js
@@ -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;
+ });
+ });
+ }
+});
diff --git a/addons_extensions/hr_emp_dashboard/static/src/xml/employee_profile_template.xml b/addons_extensions/hr_emp_dashboard/static/src/xml/employee_profile_template.xml
index 8dfeec8d4..b3fbcc562 100644
--- a/addons_extensions/hr_emp_dashboard/static/src/xml/employee_profile_template.xml
+++ b/addons_extensions/hr_emp_dashboard/static/src/xml/employee_profile_template.xml
@@ -74,6 +74,7 @@
+
+
+
@@ -147,7 +167,8 @@
-
+
+
|
|
diff --git a/third_party_addons/hr_biometric_attendance/models/biometric_device_details.py b/third_party_addons/hr_biometric_attendance/models/biometric_device_details.py
index aabee507b..2629846cd 100644
--- a/third_party_addons/hr_biometric_attendance/models/biometric_device_details.py
+++ b/third_party_addons/hr_biometric_attendance/models/biometric_device_details.py
@@ -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()
-
+
|