import subprocess
import sys
# Install openpyxl if not available
try:
import openpyxl
except ImportError:
subprocess.check_call([sys.executable, "-m", "pip", "install", "openpyxl"])
import openpyxl
# Install additional dependencies for PDF generation
try:
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import inch
from reportlab.lib.colors import HexColor
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
except ImportError:
subprocess.check_call([sys.executable, "-m", "pip", "install", "reportlab"])
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import inch
from reportlab.lib.colors import HexColor
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
import gradio as gr
import pandas as pd
import tempfile
from datetime import datetime, timedelta
import openpyxl
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from openpyxl.drawing.image import Image as ExcelImage
import random
import base64
from io import BytesIO
import os
import zipfile
def generate_random_invoice_number():
"""Generate random 6-digit invoice number"""
return str(random.randint(100000, 999999))
def create_professional_individual_invoice(workbook, employee_data, start_date, end_date):
"""Create individual invoice sheet with professional Glimmr template formatting and logo"""
# --- PASTE YOUR CLEANED BASE64 STRING HERE ---
GLIMMR_LOGO_BASE64 = """
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
"""
ws = workbook.create_sheet(title=employee_data['Row Labels'])
column_widths = {'A': 25, 'B': 15, 'C': 15, 'D': 15, 'E': 15, 'F': 12, 'G': 12, 'H': 12, 'I': 12}
for col, width in column_widths.items():
ws.column_dimensions[col].width = width
try:
if GLIMMR_LOGO_BASE64.strip() and "PASTE" not in GLIMMR_LOGO_BASE64 and "YOUR BASE64" not in GLIMMR_LOGO_BASE64:
logo_data = base64.b64decode(GLIMMR_LOGO_BASE64.strip())
logo_stream = BytesIO(logo_data)
img = ExcelImage(logo_stream)
img.width = 200
img.height = 100
ws.add_image(img, 'A1')
except Exception as e:
print(f"Logo embedding failed: {e}")
# Company name
ws['A6'] = 'Glimmr Ltd'
ws['A6'].font = Font(bold=True, size=16)
# --- CORRECTED HEADER STRUCTURE TO MATCH ORIGINAL ---
# Main header spanning wider (extended one cell left)
ws.merge_cells('E2:I2')
ws['E2'] = 'CLEANER INVOICE'
ws['E2'].font = Font(bold=True, size=14, color='FFFFFF')
ws['E2'].alignment = Alignment(horizontal='center', vertical='center')
ws['E2'].fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
# Invoice # header (left side)
ws.merge_cells('E6:F6')
ws['E6'] = 'Invoice #'
ws['E6'].font = Font(bold=True, color='FFFFFF')
ws['E6'].fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
ws['E6'].alignment = Alignment(horizontal='center', vertical='center')
# Date header (right side)
ws.merge_cells('G6:I6')
ws['G6'] = 'Date'
ws['G6'].font = Font(bold=True, color='FFFFFF')
ws['G6'].fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
ws['G6'].alignment = Alignment(horizontal='center', vertical='center')
# Invoice # value (below header)
ws.merge_cells('E7:F7')
ws['E7'] = generate_random_invoice_number()
ws['E7'].alignment = Alignment(horizontal='center', vertical='center')
# Date value (below header)
ws.merge_cells('G7:I7')
ws['G7'] = datetime.now().strftime('%d/%m/%y')
ws['G7'].alignment = Alignment(horizontal='center', vertical='center')
# Company address
ws['A8'] = '108 Westbourne Terrace'
ws['A9'] = 'London'
ws['A10'] = 'W2 6QJ'
ws['A11'] = 'support@glimmr.co.uk'
ws['A12'] = '020 8158 8505'
# Payee information
ws['A14'] = "Payee's Name"
ws['A14'].font = Font(bold=True)
ws['A15'] = employee_data['Row Labels']
ws['A15'].font = Font(size=11)
# Payment period header
ws.merge_cells('A20:D20')
ws['A20'] = 'Payment Period'
ws['A20'].font = Font(bold=True, color='FFFFFF')
ws['A20'].fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
ws['A20'].alignment = Alignment(horizontal='center')
# Table headers - adjusted positions
ws['E20'] = 'Hours'
ws['F20'] = '£ / Hour'
ws.merge_cells('G20:I20')
ws['G20'] = 'Amount'
for cell in ['E20', 'F20', 'G20']:
ws[cell].font = Font(bold=True, color='FFFFFF')
ws[cell].fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
ws[cell].alignment = Alignment(horizontal='center')
# Payment period data with center alignment
ws.merge_cells('A21:D21')
ws['A21'] = f"{start_date} to {end_date}"
ws['A21'].alignment = Alignment(horizontal='center')
ws['E21'] = employee_data['Sum of Hours Worked']
ws['F21'] = f"£ {employee_data['Average of Hourly Rate']:.2f}"
ws.merge_cells('G21:I21')
ws['G21'] = f"£ {employee_data['Sum of Total']:.2f}"
# Total sections - adjusted positions
ws.merge_cells('F24:G24')
ws['F24'] = 'Total Labor'
ws['F24'].font = Font(bold=True)
ws.merge_cells('H24:I24')
ws['H24'] = f"£ {employee_data['Sum of Total']:.2f}"
ws.merge_cells('F26:G26')
ws['F26'] = 'Tax (Withholding)'
# Payment due date and total
due_date = (datetime.strptime(end_date, '%Y-%m-%d') + timedelta(days=1)).strftime('%d/%m/%Y')
ws['A27'] = f'Payment is due on {due_date}'
ws.merge_cells('F27:G27')
ws['F27'] = 'TOTAL'
ws['F27'].font = Font(bold=True)
ws.merge_cells('H27:I27')
ws['H27'] = f"£ {employee_data['Sum of Total']:.2f}"
ws['H27'].font = Font(bold=True)
# Comments section
ws['A28'] = 'Comments or Special Instructions:'
ws['A28'].font = Font(bold=True)
ws['A29'] = 'Payments to be made every other Saturday (or as close to these days).'
ws['A30'] = 'Please let us know immediately if there are any issues.'
# Apply borders - updated ranges
thin_border = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin'))
# Border for payment period table
for row in range(20, 22):
for col in range(1, 10):
ws.cell(row=row, column=col).border = thin_border
# Border for header section
for row in range(6, 8):
for col_letter in ['E', 'F', 'G', 'H', 'I']:
ws[f'{col_letter}{row}'].border = thin_border
def create_individual_pdf_invoice(employee_data, start_date, end_date, output_dir):
"""Create individual PDF invoice for each cleaner"""
GLIMMR_LOGO_BASE64 = """
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
"""
clean_name = "".join(c for c in employee_data['Row Labels'] if c.isalnum() or c in (' ', '-', '_')).rstrip()
clean_name = clean_name.replace(' ', '_')
start_dt = datetime.strptime(start_date, '%Y-%m-%d')
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
date_period = f"{start_dt.strftime('%d%b%Y')}-{end_dt.strftime('%d%b%Y')}"
filename = f"{clean_name}_{date_period}_Glimmr_Invoice.pdf"
filepath = os.path.join(output_dir, filename)
doc = SimpleDocTemplate(filepath, pagesize=A4, rightMargin=72, leftMargin=72, topMargin=72, bottomMargin=18)
story = []
try:
if GLIMMR_LOGO_BASE64.strip():
logo_data = base64.b64decode(GLIMMR_LOGO_BASE64.strip())
logo_stream = BytesIO(logo_data)
logo_img = Image(logo_stream)
logo_img.drawWidth = 2*inch
logo_img.drawHeight = 1*inch
story.append(logo_img)
story.append(Spacer(1, 12))
except Exception as e:
print(f"PDF Logo embedding failed: {e}")
styles = getSampleStyleSheet()
title_style = ParagraphStyle('CustomTitle', parent=styles['Heading1'], fontSize=18, spaceAfter=30, textColor=HexColor('#4472C4'), alignment=1)
header_style = ParagraphStyle('CustomHeader', parent=styles['Heading2'], fontSize=14, spaceAfter=12, textColor=HexColor('#4472C4'))
normal_style = styles['Normal']
story.append(Paragraph('Glimmr Ltd', title_style))
address_text = "108 Westbourne Terrace
London
W2 6QJ
support@glimmr.co.uk
020 8158 8505"
story.append(Paragraph(address_text, normal_style))
story.append(Spacer(1, 20))
invoice_number = generate_random_invoice_number()
current_date = datetime.now().strftime('%d/%m/%Y')
invoice_header_data = [['CLEANER INVOICE', ''], ['Invoice #: ' + invoice_number, 'Date: ' + current_date]]
invoice_header_table = Table(invoice_header_data, colWidths=[3*inch, 3*inch])
invoice_header_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), HexColor('#4472C4')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 14),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 1), (-1, -1), 12),
('BOTTOMPADDING', (0, 0), (-1, -1), 12),
('GRID', (0, 0), (-1, -1), 1, colors.black)
]))
story.append(invoice_header_table)
story.append(Spacer(1, 20))
story.append(Paragraph('Payee\'s Name:', header_style))
story.append(Paragraph(employee_data['Row Labels'], normal_style))
story.append(Spacer(1, 20))
due_date = (datetime.strptime(end_date, '%Y-%m-%d') + timedelta(days=1)).strftime('%d/%m/%Y')
payment_data = [
['Payment Period', 'Hours', '£ / Hour', 'Amount'],
[f"{start_date} to {end_date}",
str(employee_data['Sum of Hours Worked']),
f"£ {employee_data['Average of Hourly Rate']:.2f}",
f"£ {employee_data['Sum of Total']:.2f}"]
]
payment_table = Table(payment_data, colWidths=[2*inch, 1*inch, 1*inch, 1.5*inch])
payment_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), HexColor('#4472C4')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -1), 10),
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
('GRID', (0, 0), (-1, -1), 1, colors.black)
]))
story.append(payment_table)
story.append(Spacer(1, 20))
total_data = [
['Total Labor', f"£ {employee_data['Sum of Total']:.2f}"],
['Tax (Withholding)', '£ 0.00'],
['TOTAL', f"£ {employee_data['Sum of Total']:.2f}"]
]
total_table = Table(total_data, colWidths=[3*inch, 2*inch])
total_table.setStyle(TableStyle([
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 1), 'Helvetica'),
('FONTNAME', (0, 2), (-1, 2), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 12),
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
('GRID', (0, 0), (-1, -1), 1, colors.black),
('BACKGROUND', (0, 2), (-1, 2), colors.lightgrey)
]))
story.append(total_table)
story.append(Spacer(1, 20))
story.append(Paragraph(f'Payment is due on {due_date}', normal_style))
story.append(Spacer(1, 20))
story.append(Paragraph('Comments or Special Instructions:', header_style))
story.append(Paragraph('Payments to be made every other Saturday (or as close to these days).', normal_style))
story.append(Paragraph('Please let us know immediately if there are any issues.', normal_style))
doc.build(story)
return filepath
def create_invoices_from_template_with_logo(report_data, start_date, end_date):
wb = openpyxl.Workbook()
wb.remove(wb.active)
# Create Payments sheet
payments_ws = wb.create_sheet(title="Payments")
payments_ws['C1'] = 'DO NOT ALTER THIS TEMPLATE'
payments_ws['C1'].font = Font(bold=True)
headers = ['First Name', 'Last Name', 'Stripe Name', 'Reference', 'Amount (GBP)', 'Remarks']
for col, header in enumerate(headers, 1):
cell = payments_ws.cell(row=2, column=col, value=header)
cell.font = Font(bold=True)
for row_idx, employee in enumerate(report_data[report_data['Row Labels'] != 'Grand Total'].itertuples(), 3):
name_parts = employee[1].split(' ', 1)
first_name = name_parts[0] if name_parts else ''
last_name = name_parts[1] if len(name_parts) > 1 else ''
payments_ws.cell(row=row_idx, column=1, value=first_name)
payments_ws.cell(row=row_idx, column=2, value=last_name)
payments_ws.cell(row=row_idx, column=3, value=employee[1])
payments_ws.cell(row=row_idx, column=4, value='')
payments_ws.cell(row=row_idx, column=5, value=employee[4]) # Sum of Total
payments_ws.cell(row=row_idx, column=6, value='')
# Create Pivot sheet
pivot_ws = wb.create_sheet(title="Pivot")
pivot_ws['A1'] = f"{start_date} to {end_date}"
pivot_ws['A1'].font = Font(bold=True)
due_date = (datetime.strptime(end_date, '%Y-%m-%d') + timedelta(days=1)).strftime('%d/%m/%Y')
pivot_ws['A2'] = f"Payment is due on {due_date}"
# MODIFIED: Updated header name as requested
pivot_headers = ['Name', 'Fixed Hourly Rate(without TIP)', 'Average of Hourly Rate', 'Sum of Hours Worked', 'Sum of Total']
for col, header in enumerate(pivot_headers, 1):
cell = pivot_ws.cell(row=3, column=col, value=header)
cell.font = Font(bold=True)
for row_idx, employee in enumerate(report_data.itertuples(index=False), 4):
# The tuple index corresponds to the column order in the final_invoice DataFrame
# [0]: Row Labels, [1]: Avg Rate, [2]: Sum Hours, [3]: Sum Total, [4]: Fixed Rate
pivot_ws.cell(row=row_idx, column=1, value=employee[0]) # Name
pivot_ws.cell(row=row_idx, column=2, value=employee[4]) # Fixed Hourly Rate(without TIP)
pivot_ws.cell(row=row_idx, column=3, value=employee[1]) # Average of Hourly Rate
pivot_ws.cell(row=row_idx, column=4, value=employee[2]) # Sum of Hours Worked
pivot_ws.cell(row=row_idx, column=5, value=employee[3]) # Sum of Total
# Create individual invoice sheets
individual_employees = report_data[report_data['Row Labels'] != 'Grand Total']
for _, employee in individual_employees.iterrows():
create_professional_individual_invoice(wb, employee, start_date, end_date)
return wb
def process_invoice_files_with_professional_excel(file1, file2, file3, bookings_file, start_date, end_date):
try:
employee_files = [f for f in [file1, file2, file3] if f is not None]
if not employee_files:
return None, None, "❗ **Error:** At least one Employee CSV must be uploaded."
if bookings_file is None:
return None, None, "❗ **Error:** Bookings CSV is required."
employee_dfs = [pd.read_csv(ef.name) for ef in employee_files]
all_employee_data = pd.concat(employee_dfs, ignore_index=True)
# Add the new Fixed Hour Rate column
all_employee_data['Fixed Hour Rate'] = all_employee_data['Hourly Rate']
pdf_bookings = pd.read_csv(bookings_file.name)
all_employee_data['Date'] = pd.to_datetime(all_employee_data['Date'], dayfirst=True, errors='coerce')
pdf_bookings['Date'] = pd.to_datetime(pdf_bookings['Date'], dayfirst=True, errors='coerce')
start_date_dt = pd.to_datetime(start_date)
end_date_dt = pd.to_datetime(end_date)
filtered_data = all_employee_data[(all_employee_data['Date'] >= start_date_dt) & (all_employee_data['Date'] <= end_date_dt)].copy()
filtered_bookings = pdf_bookings[(pdf_bookings['Date'] >= start_date_dt) & (pdf_bookings['Date'] <= end_date_dt)].copy()
def extract_employee_name(team_string):
if pd.isna(team_string): return None
name = team_string.split(',')[0].strip()
return name.split('(')[0].strip()
filtered_bookings['Employee_Name'] = filtered_bookings['Teams Assigned (without IDs)'].apply(extract_employee_name)
tips_summary = filtered_bookings.groupby('Employee_Name')['Tip'].sum().reset_index()
tips_with_amount = tips_summary[tips_summary['Tip'] > 0]
if not tips_with_amount.empty:
# Add Fixed Hour Rate to tip rows as 0
tip_rows = [{'Name': row['Employee_Name'], 'Hourly Rate': 0, 'Hours Worked': 0, 'Total': row['Tip'], 'Fixed Hour Rate': 0} for _, row in tips_with_amount.iterrows()]
tip_df = pd.DataFrame(tip_rows)
final_data = pd.concat([filtered_data, tip_df], ignore_index=True)
else:
final_data = filtered_data.copy()
# Update aggregation to include Fixed Hour Rate
report = final_data.groupby('Name').agg({
'Hourly Rate': 'mean',
'Hours Worked': 'sum',
'Total': 'sum',
'Fixed Hour Rate': 'first' # Use 'first' to get the original rate
}).reset_index()
# MODIFIED: Update column renaming as requested
report = report.rename(columns={
'Name': 'Row Labels',
'Hourly Rate': 'Average of Hourly Rate',
'Hours Worked': 'Sum of Hours Worked',
'Total': 'Sum of Total',
'Fixed Hour Rate': 'Fixed Hourly Rate(without TIP)'
})
report['Average of Hourly Rate'] = report['Average of Hourly Rate'].round(8)
report['Sum of Hours Worked'] = report['Sum of Hours Worked'].round(2)
report['Sum of Total'] = report['Sum of Total'].round(2)
total_hours = report['Sum of Hours Worked'].sum()
total_sum = report['Sum of Total'].sum()
# MODIFIED: Update Grand Total DataFrame to use new column name
grand_total = pd.DataFrame({
'Row Labels': ['Grand Total'],
'Average of Hourly Rate': [total_sum / total_hours if total_hours > 0 else 0],
'Sum of Hours Worked': [total_hours],
'Sum of Total': [total_sum],
'Fixed Hourly Rate(without TIP)': [None]
})
final_invoice = pd.concat([report, grand_total], ignore_index=True)
# Reorder columns for the create_invoices function to process correctly
# The order here determines the order in `itertuples`
final_invoice = final_invoice[['Row Labels', 'Average of Hourly Rate', 'Sum of Hours Worked', 'Sum of Total', 'Fixed Hourly Rate(without TIP)']]
wb = create_invoices_from_template_with_logo(final_invoice, start_date, end_date)
with tempfile.NamedTemporaryFile(delete=False, suffix='.xlsx') as tmp:
wb.save(tmp.name)
temp_excel_file_path = tmp.name
# Create PDF files for individual cleaners
pdf_temp_dir = tempfile.mkdtemp()
individual_employees = final_invoice[final_invoice['Row Labels'] != 'Grand Total']
pdf_files = []
for _, employee in individual_employees.iterrows():
pdf_path = create_individual_pdf_invoice(employee, start_date, end_date, pdf_temp_dir)
pdf_files.append(pdf_path)
# Create a ZIP file containing all PDFs
zip_temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.zip')
with zipfile.ZipFile(zip_temp_file.name, 'w') as zipf:
for pdf_file in pdf_files:
zipf.write(pdf_file, os.path.basename(pdf_file))
individual_employees_count = len(final_invoice[final_invoice['Row Labels'] != 'Grand Total'])
summary_text = f"🎉 **Professional Invoice Generated Successfully!**\n\n- ✅ **Individual Professional Invoices** for each employee ({individual_employees_count} Excel sheets)\n- ✅ **Individual PDF Invoices** for each cleaner ({individual_employees_count} PDFs)\n- ✅ **Payments Summary & Pivot Data** sheets included\n- ✅ **Glimmr Logo & Branding** applied\n\n**Summary for {start_date} to {end_date}:**\n- Total hours: {total_hours:.2f}\n- Total amount: £{total_sum:.2f}\n\n**Files Generated:**\n- Excel file with all data and individual sheets\n- ZIP file containing individual PDF invoices for each cleaner"
return temp_excel_file_path, zip_temp_file.name, summary_text
except Exception as e:
return None, None, f"❌ **An error occurred:**\n\n{str(e)}\n\nPlease check your input files and date formats (YYYY-MM-DD)."
with gr.Blocks(title="Complete Professional Invoice Generator", theme=gr.themes.Soft()) as interface:
gr.Markdown("# 🧾 Complete Professional Invoice Generator for Launch27 Data")
with gr.Row():
with gr.Column(scale=2):
gr.Markdown("### 📁 Upload CSV Files")
file1 = gr.File(label="Upload 1.csv (Optional Employee Data)", file_types=[".csv"])
file2 = gr.File(label="Upload 2.csv (Optional Employee Data)", file_types=[".csv"])
file3 = gr.File(label="Upload 3.csv (Optional Employee Data)", file_types=[".csv"])
bookings_file = gr.File(label="Upload Bookings CSV (Required)", file_types=[".csv"])
with gr.Column(scale=1):
gr.Markdown("### 📅 Select Date Range")
start_date = gr.Textbox(label="Start Date (YYYY-MM-DD)", value="2025-08-09")
end_date = gr.Textbox(label="End Date (YYYY-MM-DD)", value="2025-08-22")
process_btn = gr.Button("🚀 Generate Professional Invoice Excel & PDFs", variant="primary", size="lg")
gr.Markdown("---")
gr.Markdown("### 📋 Results")
with gr.Row():
summary_output = gr.Markdown(label="Summary")
with gr.Row():
download_excel_file = gr.File(label="📥 Download Professional Invoice Excel (.xlsx)")
download_pdf_zip = gr.File(label="📥 Download Individual PDF Invoices (.zip)")
process_btn.click(
fn=process_invoice_files_with_professional_excel,
inputs=[file1, file2, file3, bookings_file, start_date, end_date],
outputs=[download_excel_file, download_pdf_zip, summary_output]
)
with gr.Accordion("📖 How to Use", open=False):
gr.Markdown("""
1. **Upload Files**: Upload 1-3 employee CSVs and the required bookings CSV.
2. **Select Dates**: Enter the date range in YYYY-MM-DD format.
3. **Generate**: Click the button.
4. **Download**: Your complete Excel file and ZIP of individual PDF invoices will appear for download.
**New Features:**
- Individual PDF invoices for each cleaner with the naming convention: `Cleaner_Name_Dateperiod_Glimmr_Invoice.pdf`
- All PDF files are packaged in a convenient ZIP file for download
- Professional PDF formatting matching the Excel invoice design
""")
if __name__ == "__main__":
interface.launch(debug=True)