test #1
|
|
@ -12,7 +12,6 @@
|
|||
'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/Attendances',
|
||||
'version': '0.1',
|
||||
|
|
@ -25,7 +24,20 @@
|
|||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/cron.xml',
|
||||
'views/hr_attendance.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',
|
||||
|
||||
],
|
||||
'web.assets_frontend': [
|
||||
'web/static/lib/jquery/jquery.js',
|
||||
'hr_attendance_extended/static/src/js/jquery-ui.min.js',
|
||||
'hr_attendance_extended/static/src/js/jquery-ui.min.css',
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
from . import hr_attendance
|
||||
from . import hr_attendance
|
||||
from . import hr_attendance_report
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
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),
|
||||
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Component, xml, useState, onMounted } 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
|
||||
});
|
||||
|
||||
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);
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,91 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="attendance_report_template">
|
||||
<script t-att-src="'/web/static/lib/jquery/jquery.js'"></script>
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue