Fix bugs payroll export
This commit is contained in:
@@ -4358,20 +4358,31 @@ def payroll_dashboard():
|
|||||||
employee_names = {}
|
employee_names = {}
|
||||||
if working_hours_data:
|
if working_hours_data:
|
||||||
try:
|
try:
|
||||||
# Try to get employee names from your employee table if it exists
|
# Use the same SQL approach as attendance report - JOIN with CAST
|
||||||
employee_ids = list(working_hours_data['employees'].keys())
|
employee_ids = list(working_hours_data['employees'].keys())
|
||||||
# This assumes you have an employee table - modify as needed
|
if employee_ids:
|
||||||
employee_query = db.session.execute(text("""
|
# Build a query similar to attendance report
|
||||||
SELECT id, CONCAT(firstName, ' ', lastName) as full_name
|
placeholders = ','.join([f"'{emp_id}'" for emp_id in employee_ids])
|
||||||
FROM employee
|
employee_query = db.session.execute(text(f"""
|
||||||
WHERE id IN :employee_ids
|
SELECT
|
||||||
"""), {'employee_ids': tuple(employee_ids)})
|
ad.employee_id,
|
||||||
|
CONCAT(e.firstName, ' ', e.lastName) as full_name
|
||||||
|
FROM attendance_data ad
|
||||||
|
LEFT JOIN employee e ON CAST(ad.employee_id AS UNSIGNED) = e.id
|
||||||
|
WHERE ad.employee_id IN ({placeholders})
|
||||||
|
GROUP BY ad.employee_id, e.firstName, e.lastName
|
||||||
|
"""))
|
||||||
|
|
||||||
for row in employee_query:
|
for row in employee_query:
|
||||||
|
if row[1]: # Only add if we got a name
|
||||||
employee_names[str(row[0])] = row[1]
|
employee_names[str(row[0])] = row[1]
|
||||||
|
|
||||||
|
print(f"📊 Retrieved names for {len(employee_names)} employees using CAST method")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Could not load employee names: {e}")
|
print(f"⚠️ Could not load employee names: {e}")
|
||||||
|
import traceback
|
||||||
|
print(f"⚠️ Traceback: {traceback.format_exc()}")
|
||||||
# Continue without names - will use employee IDs
|
# Continue without names - will use employee IDs
|
||||||
|
|
||||||
# Get selected project name for display
|
# Get selected project name for display
|
||||||
@@ -4467,39 +4478,41 @@ def export_payroll_excel():
|
|||||||
|
|
||||||
print(f"📊 Exporting {len(attendance_records)} attendance records to Excel")
|
print(f"📊 Exporting {len(attendance_records)} attendance records to Excel")
|
||||||
|
|
||||||
# Get employee names
|
|
||||||
employee_names = {}
|
|
||||||
try:
|
|
||||||
employee_ids = list(set(str(record.employee_id) for record in attendance_records))
|
|
||||||
employee_query = db.session.execute(text("""
|
|
||||||
SELECT id, CONCAT(firstName, ' ', lastName) as full_name
|
|
||||||
FROM employee
|
|
||||||
WHERE id IN :employee_ids
|
|
||||||
"""), {'employee_ids': tuple(employee_ids)})
|
|
||||||
|
|
||||||
for row in employee_query:
|
|
||||||
employee_names[str(row[0])] = row[1]
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Could not load employee names for export: {e}")
|
|
||||||
|
|
||||||
# Get project name for filename
|
|
||||||
project_name = ''
|
|
||||||
if project_filter:
|
|
||||||
try:
|
|
||||||
project = Project.query.get(int(project_filter))
|
|
||||||
if project:
|
|
||||||
project_name = f"_{project.name.replace(' ', '_')}"
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Error getting project name for filename: {e}")
|
|
||||||
|
|
||||||
# Create Excel exporter
|
# Create Excel exporter
|
||||||
exporter = PayrollExcelExporter(
|
exporter = PayrollExcelExporter(
|
||||||
company_name=os.environ.get('COMPANY_NAME', 'Your Company'),
|
company_name=os.environ.get('COMPANY_NAME', 'Your Company'),
|
||||||
contract_name=os.environ.get('CONTRACT_NAME', 'Default Contract')
|
contract_name=os.environ.get('CONTRACT_NAME', 'Default Contract')
|
||||||
)
|
)
|
||||||
|
|
||||||
# Generate Excel file
|
# Get employee names using the same method as dashboard
|
||||||
|
employee_names = {}
|
||||||
|
try:
|
||||||
|
employee_ids = list(set(str(record.employee_id) for record in attendance_records))
|
||||||
|
if employee_ids:
|
||||||
|
# Use the same SQL approach as attendance report - JOIN with CAST
|
||||||
|
placeholders = ','.join([f"'{emp_id}'" for emp_id in employee_ids])
|
||||||
|
employee_query = db.session.execute(text(f"""
|
||||||
|
SELECT
|
||||||
|
ad.employee_id,
|
||||||
|
CONCAT(e.firstName, ' ', e.lastName) as full_name
|
||||||
|
FROM attendance_data ad
|
||||||
|
LEFT JOIN employee e ON CAST(ad.employee_id AS UNSIGNED) = e.id
|
||||||
|
WHERE ad.employee_id IN ({placeholders})
|
||||||
|
GROUP BY ad.employee_id, e.firstName, e.lastName
|
||||||
|
"""))
|
||||||
|
|
||||||
|
for row in employee_query:
|
||||||
|
if row[1]: # Only add if we got a name
|
||||||
|
employee_names[str(row[0])] = row[1]
|
||||||
|
|
||||||
|
print(f"📊 Retrieved names for {len(employee_names)} employees for export using CAST method")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Could not load employee names for export: {e}")
|
||||||
|
import traceback
|
||||||
|
print(f"⚠️ Traceback: {traceback.format_exc()}")
|
||||||
|
|
||||||
|
# Generate Excel file with employee names
|
||||||
if report_type == 'detailed':
|
if report_type == 'detailed':
|
||||||
excel_file = exporter.create_detailed_hours_report(
|
excel_file = exporter.create_detailed_hours_report(
|
||||||
start_date, end_date, attendance_records, employee_names, include_travel_time
|
start_date, end_date, attendance_records, employee_names, include_travel_time
|
||||||
@@ -4511,6 +4524,16 @@ def export_payroll_excel():
|
|||||||
)
|
)
|
||||||
filename_prefix = 'payroll_report'
|
filename_prefix = 'payroll_report'
|
||||||
|
|
||||||
|
# Get project name for filename
|
||||||
|
project_name = ''
|
||||||
|
if project_filter:
|
||||||
|
try:
|
||||||
|
project = Project.query.get(int(project_filter))
|
||||||
|
if project:
|
||||||
|
project_name = f"_{project.name.replace(' ', '_')}"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Error getting project name for filename: {e}")
|
||||||
|
|
||||||
if excel_file:
|
if excel_file:
|
||||||
# Generate filename with timestamp and project name
|
# Generate filename with timestamp and project name
|
||||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
@@ -4571,7 +4594,6 @@ def calculate_working_hours_api():
|
|||||||
employee_id = data.get('employee_id')
|
employee_id = data.get('employee_id')
|
||||||
date_from = data.get('date_from')
|
date_from = data.get('date_from')
|
||||||
date_to = data.get('date_to')
|
date_to = data.get('date_to')
|
||||||
include_travel_time = data.get('include_travel_time', True)
|
|
||||||
|
|
||||||
if not all([employee_id, date_from, date_to]):
|
if not all([employee_id, date_from, date_to]):
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -4597,7 +4619,7 @@ def calculate_working_hours_api():
|
|||||||
|
|
||||||
attendance_records = query.all()
|
attendance_records = query.all()
|
||||||
|
|
||||||
# Calculate working hours
|
# Calculate working hours using single check-in calculator
|
||||||
calculator = SingleCheckInCalculator()
|
calculator = SingleCheckInCalculator()
|
||||||
hours_data = calculator.calculate_employee_hours(
|
hours_data = calculator.calculate_employee_hours(
|
||||||
str(employee_id), start_date, end_date, attendance_records
|
str(employee_id), start_date, end_date, attendance_records
|
||||||
|
|||||||
@@ -105,6 +105,59 @@ class PayrollExcelExporter:
|
|||||||
for attr, value in style_dict.items():
|
for attr, value in style_dict.items():
|
||||||
setattr(cell, attr, value)
|
setattr(cell, attr, value)
|
||||||
|
|
||||||
|
def _get_employee_names(self, attendance_records: List[Dict]) -> Dict[str, str]:
|
||||||
|
"""Get employee names - fallback method if names not provided"""
|
||||||
|
print("⚠️ Excel exporter falling back to internal name lookup - this should not normally happen")
|
||||||
|
return {} # Return empty dict - names should be provided by the route
|
||||||
|
|
||||||
|
def _get_employee_names(self, attendance_records: List[Dict]) -> Dict[str, str]:
|
||||||
|
"""Get employee names using the same CAST method as attendance report"""
|
||||||
|
employee_names = {}
|
||||||
|
try:
|
||||||
|
from flask import current_app
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
# Get unique employee IDs from attendance records
|
||||||
|
employee_ids = []
|
||||||
|
for record in attendance_records:
|
||||||
|
if hasattr(record, '__dict__'):
|
||||||
|
emp_id = str(record.employee_id)
|
||||||
|
else:
|
||||||
|
emp_id = str(record['employee_id'])
|
||||||
|
|
||||||
|
if emp_id not in employee_ids:
|
||||||
|
employee_ids.append(emp_id)
|
||||||
|
|
||||||
|
if employee_ids:
|
||||||
|
# Use the same SQL approach as attendance report - JOIN with CAST
|
||||||
|
placeholders = ','.join([f"'{emp_id}'" for emp_id in employee_ids])
|
||||||
|
|
||||||
|
# Import db from current app context
|
||||||
|
from app import db
|
||||||
|
|
||||||
|
employee_query = db.session.execute(text(f"""
|
||||||
|
SELECT
|
||||||
|
ad.employee_id,
|
||||||
|
CONCAT(e.firstName, ' ', e.lastName) as full_name
|
||||||
|
FROM attendance_data ad
|
||||||
|
LEFT JOIN employee e ON CAST(ad.employee_id AS UNSIGNED) = e.id
|
||||||
|
WHERE ad.employee_id IN ({placeholders})
|
||||||
|
GROUP BY ad.employee_id, e.firstName, e.lastName
|
||||||
|
"""))
|
||||||
|
|
||||||
|
for row in employee_query:
|
||||||
|
if row[1]: # Only add if we got a name
|
||||||
|
employee_names[str(row[0])] = row[1]
|
||||||
|
|
||||||
|
print(f"📊 Excel exporter retrieved names for {len(employee_names)} employees using CAST method")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Excel exporter could not load employee names: {e}")
|
||||||
|
import traceback
|
||||||
|
print(f"⚠️ Traceback: {traceback.format_exc()}")
|
||||||
|
|
||||||
|
return employee_names
|
||||||
|
|
||||||
@log_database_operations('payroll_excel_export')
|
@log_database_operations('payroll_excel_export')
|
||||||
def create_payroll_report(self, start_date: datetime, end_date: datetime,
|
def create_payroll_report(self, start_date: datetime, end_date: datetime,
|
||||||
attendance_records: List[Dict], employee_names: Dict[str, str] = None,
|
attendance_records: List[Dict], employee_names: Dict[str, str] = None,
|
||||||
@@ -125,6 +178,10 @@ class PayrollExcelExporter:
|
|||||||
try:
|
try:
|
||||||
print(f"📊 Creating payroll report from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
|
print(f"📊 Creating payroll report from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
|
||||||
|
|
||||||
|
# Get employee names using the same method as payroll routes
|
||||||
|
if employee_names is None:
|
||||||
|
employee_names = self._get_employee_names(attendance_records)
|
||||||
|
|
||||||
# Calculate working hours
|
# Calculate working hours
|
||||||
calculator = SingleCheckInCalculator()
|
calculator = SingleCheckInCalculator()
|
||||||
hours_data = calculator.calculate_all_employees_hours(start_date, end_date, attendance_records)
|
hours_data = calculator.calculate_all_employees_hours(start_date, end_date, attendance_records)
|
||||||
@@ -360,6 +417,10 @@ class PayrollExcelExporter:
|
|||||||
try:
|
try:
|
||||||
print(f"📊 Creating detailed hours report from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
|
print(f"📊 Creating detailed hours report from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
|
||||||
|
|
||||||
|
# Get employee names using the same method as payroll routes
|
||||||
|
if employee_names is None:
|
||||||
|
employee_names = self._get_employee_names(attendance_records)
|
||||||
|
|
||||||
# Calculate working hours
|
# Calculate working hours
|
||||||
calculator = SingleCheckInCalculator()
|
calculator = SingleCheckInCalculator()
|
||||||
hours_data = calculator.calculate_all_employees_hours(start_date, end_date, attendance_records)
|
hours_data = calculator.calculate_all_employees_hours(start_date, end_date, attendance_records)
|
||||||
|
|||||||
@@ -198,31 +198,31 @@ class SingleCheckInCalculator:
|
|||||||
Calculate hours for a single day from check-in records
|
Calculate hours for a single day from check-in records
|
||||||
|
|
||||||
Logic:
|
Logic:
|
||||||
- Pair consecutive check-ins as work periods
|
- Must have even number of records (complete pairs)
|
||||||
- 1st = start, 2nd = end, 3rd = start new period, 4th = end, etc.
|
- 1st = check-in, 2nd = check-out, 3rd = check-in, 4th = check-out, etc.
|
||||||
- Single record = miss punch
|
- Any odd number of records = miss punch
|
||||||
- Validate reasonable work periods
|
- Any invalid work period = miss punch
|
||||||
|
|
||||||
Returns: (hours, is_miss_punch)
|
Returns: (hours, is_miss_punch)
|
||||||
"""
|
"""
|
||||||
if not day_records:
|
if not day_records:
|
||||||
return 0.0, False
|
return 0.0, False
|
||||||
|
|
||||||
if len(day_records) == 1:
|
# Must have even number of records for complete pairs
|
||||||
print(f"⚠️ Single check-in found - miss punch")
|
if len(day_records) % 2 != 0:
|
||||||
return 0.0, True # Single check-in = miss punch
|
print(f"⚠️ Odd number of records ({len(day_records)}) - miss punch (incomplete pairs)")
|
||||||
|
return 0.0, True # Odd number = miss punch
|
||||||
|
|
||||||
# Sort records by time
|
# Sort records by time
|
||||||
sorted_records = sorted(day_records, key=lambda r: r.timestamp)
|
sorted_records = sorted(day_records, key=lambda r: r.timestamp)
|
||||||
print(f"📝 Processing {len(sorted_records)} records for the day")
|
print(f"📝 Processing {len(sorted_records)} records for the day (must be complete pairs)")
|
||||||
|
|
||||||
# Create work periods from consecutive check-ins
|
# Create work periods from consecutive check-ins (must be pairs)
|
||||||
work_periods = []
|
work_periods = []
|
||||||
for i in range(0, len(sorted_records) - 1, 2):
|
for i in range(0, len(sorted_records), 2):
|
||||||
start_record = sorted_records[i]
|
start_record = sorted_records[i]
|
||||||
end_record = sorted_records[i + 1] if i + 1 < len(sorted_records) else None
|
end_record = sorted_records[i + 1] # We know this exists because we checked even count
|
||||||
|
|
||||||
if end_record:
|
|
||||||
period = WorkPeriod(start_record, end_record)
|
period = WorkPeriod(start_record, end_record)
|
||||||
|
|
||||||
# Validate work period duration
|
# Validate work period duration
|
||||||
@@ -230,20 +230,17 @@ class SingleCheckInCalculator:
|
|||||||
work_periods.append(period)
|
work_periods.append(period)
|
||||||
print(f"✅ Valid work period: {start_record.check_in_time} - {end_record.check_in_time} = {period.duration_minutes/60:.2f} hours")
|
print(f"✅ Valid work period: {start_record.check_in_time} - {end_record.check_in_time} = {period.duration_minutes/60:.2f} hours")
|
||||||
else:
|
else:
|
||||||
print(f"⚠️ Invalid work period: {period.duration_minutes/60:.2f} hours (too long or negative)")
|
print(f"⚠️ Invalid work period: {period.duration_minutes/60:.2f} hours - miss punch (invalid duration)")
|
||||||
return 0.0, True # Invalid period = miss punch
|
return 0.0, True # Invalid period = miss punch
|
||||||
else:
|
|
||||||
print(f"⚠️ Unpaired check-in at {start_record.check_in_time}")
|
|
||||||
return 0.0, True # Unpaired record = miss punch
|
|
||||||
|
|
||||||
# Calculate total hours
|
# If we got here, all periods are valid
|
||||||
total_minutes = sum(period.duration_minutes for period in work_periods)
|
total_minutes = sum(period.duration_minutes for period in work_periods)
|
||||||
total_hours = total_minutes / 60.0
|
total_hours = total_minutes / 60.0
|
||||||
|
|
||||||
# Round to nearest quarter hour
|
# Round to nearest quarter hour
|
||||||
rounded_hours = round(total_hours * 4) / 4
|
rounded_hours = round(total_hours * 4) / 4
|
||||||
|
|
||||||
print(f"📊 Daily total: {rounded_hours:.2f} hours from {len(work_periods)} work periods")
|
print(f"📊 Daily total: {rounded_hours:.2f} hours from {len(work_periods)} complete work periods")
|
||||||
|
|
||||||
return rounded_hours, False
|
return rounded_hours, False
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user