Report FIX

This commit is contained in:
raman 2025-04-07 16:55:50 +05:30
parent 1d6d5289e3
commit c220870ad5
6 changed files with 451 additions and 2 deletions

View File

@ -12,7 +12,6 @@
'website': "https://www.ftprotech.com", 'website': "https://www.ftprotech.com",
# Categories can be used to filter modules in modules listing # 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 # for the full list
'category': 'Human Resources/Attendances', 'category': 'Human Resources/Attendances',
'version': '0.1', 'version': '0.1',
@ -26,8 +25,16 @@
'security/security.xml', 'security/security.xml',
'data/cron.xml', 'data/cron.xml',
'data/sequence.xml', 'data/sequence.xml',
'views/hr_attendance.xml',
'views/on_duty_form.xml', 'views/on_duty_form.xml',
'views/hr_attendance.xml',
'views/day_attendance_report.xml',
], ],
'assets': {
'web.assets_backend': [
'hr_attendance_extended/static/src/xml/attendance_report.xml',
'hr_attendance_extended/static/src/js/attendance_report.js',
]
}
} }

View File

@ -1,2 +1,4 @@
from . import hr_attendance from . import hr_attendance
from . import hr_attendance
from . import hr_attendance_report
from . import on_duty_form from . import on_duty_form

View File

@ -0,0 +1,146 @@
from odoo import models, fields, api
from datetime import datetime, timedelta
import xlwt
from io import BytesIO
import base64
from odoo.exceptions import UserError
def convert_to_date(date_string):
# Use strptime to parse the date string in 'dd/mm/yyyy' format
return datetime.strptime(date_string, '%Y/%m/%d')
class AttendanceReport(models.Model):
_name = 'attendance.report'
_description = 'Attendance Report'
@api.model
def get_attendance_report(self, employee_id, start_date, end_date):
# Ensure start_date and end_date are in the correct format (datetime)
if employee_id == '-':
employee_id = False
else:
employee_id = int(employee_id)
if isinstance(start_date, str):
start_date = datetime.strptime(start_date, '%d/%m/%Y')
if isinstance(end_date, str):
end_date = datetime.strptime(end_date, '%d/%m/%Y')
# Convert the dates to 'YYYY-MM-DD' format for PostgreSQL
start_date_str = start_date.strftime('%Y-%m-%d')
end_date_str = end_date.strftime('%Y-%m-%d')
# Define the base where condition
if employee_id:
case = """WHERE emp.id = %s AND at.check_in >= %s AND at.check_out <= %s"""
params = (employee_id, start_date_str, end_date_str)
else:
case = """WHERE at.check_in >= %s AND at.check_out <= %s"""
params = (start_date_str, end_date_str)
# Define the query with improved date handling
query = """
WITH daily_checkins AS (
SELECT
emp.id,
emp.name,
DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') AS date,
at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS check_in,
at.check_out AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS check_out,
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
FROM
hr_attendance at
LEFT JOIN
hr_employee emp ON at.employee_id = emp.id
""" + case + """
)
SELECT
id,
name,
date,
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
FROM
daily_checkins
GROUP BY
id, name, date
ORDER BY
id, date;
"""
# Execute the query with parameters
self.env.cr.execute(query, params)
rows = self.env.cr.dictfetchall()
data = []
a = 0
for r in rows:
a += 1
# Calculate worked hours in Python, but here it's better done in the query itself.
worked_hours = r['last_check_out'] - r['first_check_in'] if r['first_check_in'] and r[
'last_check_out'] else 0
data.append({
'id': a,
'employee_id': r['id'],
'employee_name': r['name'],
'date': r['date'],
'check_in': r['first_check_in'],
'check_out': r['last_check_out'],
'worked_hours': worked_hours,
})
return data
@api.model
def export_to_excel(self, employee_id, start_date, end_date):
# Fetch the attendance data (replace with your logic to fetch attendance data)
attendance_data = self.get_attendance_report(employee_id, start_date, end_date)
if not attendance_data:
raise UserError("No data to export!")
# Create an Excel workbook and a sheet
workbook = xlwt.Workbook()
sheet = workbook.add_sheet('Attendance Report')
# Define the column headers
headers = ['Employee Name', 'Check-in', 'Check-out', 'Worked Hours']
# Write headers to the first row
for col_num, header in enumerate(headers):
sheet.write(0, col_num, header)
# Write the attendance data to the sheet
for row_num, record in enumerate(attendance_data, start=1):
sheet.write(row_num, 0, record['employee_name'])
sheet.write(row_num, 1, record['check_in'].strftime("%Y-%m-%d %H:%M:%S"))
sheet.write(row_num, 2, record['check_out'].strftime("%Y-%m-%d %H:%M:%S"))
if isinstance(record['worked_hours'], timedelta):
hours = record['worked_hours'].seconds // 3600
minutes = (record['worked_hours'].seconds % 3600) // 60
# Format as "X hours Y minutes"
worked_hours_str = f"{record['worked_hours'].days * 24 + hours} hours {minutes} minutes"
sheet.write(row_num, 3, worked_hours_str)
else:
sheet.write(row_num, 3, record['worked_hours'])
# Save the workbook to a BytesIO buffer
output = BytesIO()
workbook.save(output)
# Convert the output to base64 for saving in Odoo
file_data = base64.b64encode(output.getvalue())
# Create an attachment record to save the Excel file in Odoo
attachment = self.env['ir.attachment'].create({
'name': 'attendance_report.xls',
'type': 'binary',
'datas': file_data,
'mimetype': 'application/vnd.ms-excel',
})
# Return the attachment's URL to allow downloading in the Odoo UI
return '/web/content/%d/%s' % (attachment.id, attachment.name),

View File

@ -0,0 +1,200 @@
import { useService } from "@web/core/utils/hooks";
import { loadJS, loadCSS } from "@web/core/assets";
import { Component, xml, useState, onMounted,onWillStart } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { getOrigin } from "@web/core/utils/urls";
export default class AttendanceReport extends Component {
static props = ['*'];
static template = 'attendance_report_template';
setup() {
super.setup(...arguments);
this.orm = useService("orm");
this.state = useState({
startDate: "", // To store the start date parameter
endDate: "",
attendanceData: [], // Initialized as an empty array
groupedData: [], // To store the grouped attendance data by employee_id
employeeIDS: [] // List of employee IDs to bind with select dropdown
});
onWillStart(async () => {
try {
await Promise.all([
loadJS('https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.js'),
loadJS('https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.14.1/jquery-ui.min.js'),
loadCSS('https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.14.1/themes/base/jquery-ui.min.css')
])
} catch (error) {
throw error;
}
});
onMounted( () => {
this.loademployeeIDS();
});
}
async loademployeeIDS() {
try {
const employee = await this.orm.searchRead('hr.employee', [], ['id', 'display_name']);
this.state.employeeIDS = employee;
this.initializeSelect2();
this.render();// Initialize Select2 after data is loaded
this.reload();
} catch (error) {
console.error("Error loading employeeIDS:", error);
}
}
// Initialize Select2 with error handling and ensuring it's initialized only once
initializeSelect2() {
const employeeIDS = this.state.employeeIDS;
// Ensure the <select> element is initialized only once
const $empSelect = $('#emp');
const from_date = $("#from_date").datepicker({
dateFormat: "dd/mm/yy", // Date format
showAnim: "slideDown", // Animation
changeMonth: true, // Allow month selection
changeYear: true, // Allow year selection
yearRange: "2010:2030", // Year range
// ... other options
});
const to_date = $("#to_date").datepicker({
dateFormat: "dd/mm/yy", // Date format
showAnim: "slideDown", // Animation
changeMonth: true, // Allow month selection
changeYear: true, // Allow year selection
yearRange: "2010:2030", // Year range
// ... other options
});
// Debugging the employeeIDS array to verify its structure
console.log("employeeIDS:", employeeIDS);
// Check if employeeIDS is an array and has the necessary properties
if (Array.isArray(employeeIDS) && employeeIDS.length > 0) {
// Clear the current options (if any)
$empSelect.empty();
// Add options for each employee
employeeIDS.forEach(emp => {
$empSelect.append(
`<option value="${emp.id}">${emp.display_name}</option>`
);
});
$empSelect.append(
`<option value="-">All</option>`
);
// Initialize the select with the 'multiple' attribute for multi-select
// $empSelect.attr('multiple', 'multiple');
// Enable tagging (you can manually add tags as well)
$empSelect.on('change', (ev) => {
const selectedEmployeeIds = $(ev.target).val();
console.log('Selected Employee IDs: ', selectedEmployeeIds);
const selectedEmployees = employeeIDS.filter(emp => selectedEmployeeIds.includes(emp.id.toString()));
console.log('Selected Employees: ', selectedEmployees);
});
} else {
console.error("Invalid employee data format:", employeeIDS);
}
}
// Method called when a date is selected in the input fields
async ExportToExcel() {
var startdate = $('#from_date').val()
var enddate = $('#to_date').val()
let domain = [
['check_in', '>=', startdate],
['check_in', '<=', enddate],
];
// If employee(s) are selected, filter the data by employee_id
if ($('#emp').val() && $('#emp').val().length > 0 && $('#emp').val() !== '-') {
domain.push(['employee_id', '=', parseInt($('#emp').val())]);
}
try {
debugger;
// Fetch the attendance data based on the date range and selected employees
const URL = await this.orm.call('attendance.report', 'export_to_excel', [$('#emp').val(), startdate, enddate]);
window.open(getOrigin()+URL, '_blank');
} catch (error) {
console.error("Error generating report:", error);
}
}
async generateReport() {
let { startDate, endDate, selectedEmployeeIds } = this.state;
startDate = $('#from_date').val()
endDate = $('#to_date').val()
if (!startDate || !endDate) {
alert("Please specify both start and end dates!");
return;
}
// Build the domain for the search query
let domain = [
['check_in', '>=', startDate],
['check_in', '<=', endDate],
];
// If employee(s) are selected, filter the data by employee_id
if ($('#emp').val() && $('#emp').val().length > 0 && $('#emp').val() !== '-') {
domain.push(['employee_id', '=', parseInt($('#emp').val())]);
}
try {
// 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',[$('#emp').val(),startDate,endDate]);
// Group data by employee_id
const groupedData = this.groupDataByEmployee(attendanceData);
// Update state with the fetched and grouped data
this.state.attendanceData = attendanceData;
this.state.groupedData = groupedData;
} catch (error) {
console.error("Error generating report:", error);
}
}
// Helper function to group data by employee_id
groupDataByEmployee(data) {
const grouped = {};
data.forEach(record => {
const employeeId = record.employee_id; // employee_id[1] is the name
if (employeeId) {
if (!grouped[employeeId]) {
grouped[employeeId] = [];
}
grouped[employeeId].push(record);
}
});
// Convert the grouped data into an array to be used in t-foreach
return Object.values(grouped);
}
}
// Register the action in the actions registry
registry.category("actions").add("AttendanceReport", AttendanceReport);

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="attendance_report_template">
<!-- <script t-att-src="'/hr_attendance_extended/static/src/js/jquery-ui.min.js'"></script>-->
<!-- <link rel="stylesheet" type="text/css" href="/hr_attendance_extended/static/src/js/jquery-ui.min.css"/>-->
<style>
.ui-datepicker {
background-color: #f0f0f0;
}
.ui-state-highlight {
background-color: #ffcc00;
}
</style>
<div class="header pt-5" style="text-align:center">
<h1>Attendance Report</h1>
<div class="navbar navbar-expand-lg container">
<h4 class="p-3 text-nowrap">Employee </h4>
<div class="input-group input-group-lg">
<select type="text" id="emp" class="form-control" />
</div>
<h4 class="p-3 text-nowrap"> From Date</h4>
<div class="input-group input-group-lg">
<input type="text" id="from_date" class="form-control" value="DD/MM/YYYY"/>
</div>
<h4 class="p-3 text-nowrap"> To Date</h4>
<div class="input-group input-group-lg">
<input type="text" id="to_date" class="form-control" value="DD/MM/YYYY"/>
</div>
</div>
<button class="btn btn-outline-success" t-on-click="generateReport" >Generate Report</button>
<button class="btn btn-outline-success" t-on-click="ExportToExcel">Export Report</button>
<div t-if="this.state.groupedData.length > 0" style="max-height: 800px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;">
<div t-foreach="this.state.groupedData" t-as="group" t-key="group[0].employee_id">
<div class="employee-group">
<h3 style="text-align:left" class="p-2">
Employee:
<t t-if="group[0].employee_id">
<t t-esc="group[0].employee_name"/>
</t>
<t t-else="">
<span>Unknown Employee</span>
</t>
</h3>
<!-- Scrollable Container for the Table -->
<div class="scrollable-table-container">
<table class="table table-hover">
<thead>
<tr>
<th>Date</th>
<th>Employee</th>
<th>Check In</th>
<th>Check Out</th>
<th>Worked Hours</th>
</tr>
</thead>
<tbody>
<tr t-foreach="group" t-as="data" t-key="data.id">
<td><t t-esc="data.date"/></td>
<td>
<t t-if="data.employee_id">
<t t-esc="data.employee_name"/>
</t>
<t t-else="">
<span>Unknown Employee</span>
</t>
</td>
<td><t t-esc="data.check_in"/></td>
<td><t t-esc="data.check_out"/></td>
<td><t t-esc="data.worked_hours"/></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div t-else="">
<p>No data available for the selected date range.</p>
</div>
</div>
</t>
</templates>

View File

@ -0,0 +1,7 @@
<odoo>
<record id="action_attendance_report_s" model="ir.actions.client">
<field name="name">Attendance Report</field>
<field name="tag">AttendanceReport</field>
</record>
<menuitem action="action_attendance_report_s" id="menu_hr_attendance_day" groups="hr_attendance.group_hr_attendance_officer" parent="hr_attendance_extended.menu_attendance_attendance"/>
</odoo>