Natwar commited on
Commit
bdba181
Β·
verified Β·
1 Parent(s): cb24c49

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +246 -207
app.py CHANGED
@@ -1,268 +1,307 @@
1
  import gradio as gr
2
  import pandas as pd
3
  import tempfile
4
- import os
5
- from datetime import datetime
 
 
 
 
 
6
 
7
- def process_invoice_files(file1, file2, file3, bookings_file, start_date, end_date):
8
- """
9
- Process uploaded CSV files and generate final invoice
 
 
 
 
 
 
 
 
 
10
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  try:
12
- # Check which employee files are uploaded (filter out None values)
13
  employee_files = [f for f in [file1, file2, file3] if f is not None]
14
-
15
  if not employee_files:
16
- return None, "❗ **Error:** At least one Employee CSV (1.csv, 2.csv, or 3.csv) must be uploaded."
17
-
18
  if bookings_file is None:
19
- return None, "❗ **Error:** Bookings CSV is required and must be uploaded."
20
-
21
- print(f"βœ… Files loaded successfully:")
22
- print(f"Employee files: {len(employee_files)}")
23
- print(f"Bookings file: {'βœ“' if bookings_file else 'βœ—'}")
24
-
25
- # Step 1: Combine available employee CSVs
26
- employee_dfs = []
27
- for i, ef in enumerate(employee_files):
28
- df = pd.read_csv(ef.name)
29
- employee_dfs.append(df)
30
- print(f"Employee CSV {i+1}: {len(df)} rows")
31
 
 
32
  all_employee_data = pd.concat(employee_dfs, ignore_index=True)
33
-
34
- # Step 2: Load bookings CSV
35
  pdf_bookings = pd.read_csv(bookings_file.name)
36
- print(f"Bookings CSV: {len(pdf_bookings)} rows")
37
 
38
- # Step 3: Convert dates - FIXED DATE FORMAT PARSING
39
- # Handle both DD/MM/YY and DD/MM/YYYY formats
40
- all_employee_data['Date'] = pd.to_datetime(all_employee_data['Date'], dayfirst=True, format='mixed')
41
- pdf_bookings['Date'] = pd.to_datetime(pdf_bookings['Date'], dayfirst=True, format='mixed')
42
 
43
- # Step 4: Filter by selected date range
44
  start_date_dt = pd.to_datetime(start_date)
45
  end_date_dt = pd.to_datetime(end_date)
46
 
47
- filtered_data = all_employee_data[
48
- (all_employee_data['Date'] >= start_date_dt) &
49
- (all_employee_data['Date'] <= end_date_dt)
50
- ].copy()
51
-
52
- filtered_bookings = pdf_bookings[
53
- (pdf_bookings['Date'] >= start_date_dt) &
54
- (pdf_bookings['Date'] <= end_date_dt)
55
- ].copy()
56
-
57
- print(f"πŸ“… Filtered records ({start_date} to {end_date}): {len(filtered_data)} employee records")
58
 
59
- # Step 5: Standardize employee names (keep as in expected output)
60
- name_mapping = {
61
- 'Marisa Felipa': 'Marisa Felipa', # Keep original as shown in expected output
62
- 'Sumaya Parvin': 'Sumaya Parvin', # Keep original as shown in expected output
63
- }
64
- filtered_data['Name'] = filtered_data['Name'].replace(name_mapping)
65
-
66
- # Step 6: Extract tips from bookings
67
  def extract_employee_name(team_string):
68
- if pd.isna(team_string):
69
- return None
70
- if ',' in team_string:
71
- team_string = team_string.split(',')[0].strip()
72
- if '(' in team_string:
73
- return team_string.split('(')[0].strip()
74
- return team_string.strip()
75
 
76
  filtered_bookings['Employee_Name'] = filtered_bookings['Teams Assigned (without IDs)'].apply(extract_employee_name)
77
-
78
- # Find tips (Β£10 from Morgan Barnes booking for Pelumi Oluwatobi)
79
  tips_summary = filtered_bookings.groupby('Employee_Name')['Tip'].sum().reset_index()
80
  tips_with_amount = tips_summary[tips_summary['Tip'] > 0]
81
 
82
- print(f"πŸ’° Total tips found: Β£{tips_summary['Tip'].sum():.2f}")
83
  if not tips_with_amount.empty:
84
- print(f"Tip recipients: {tips_with_amount[['Employee_Name', 'Tip']].values.tolist()}")
85
-
86
- # Step 7: Add tip as separate rows (matching your manual process)
87
- if not tips_with_amount.empty:
88
- tip_rows = []
89
- for idx, row in tips_with_amount.iterrows():
90
- tip_rows.append({
91
- 'Date': None,
92
- 'Team': 'TIP',
93
- 'Name': row['Employee_Name'],
94
- 'Hourly Rate': 0,
95
- 'Hours Worked': 0,
96
- 'Total': row['Tip'] # Put tip in Total column (as per your manual process)
97
- })
98
-
99
  tip_df = pd.DataFrame(tip_rows)
100
  final_data = pd.concat([filtered_data, tip_df], ignore_index=True)
101
  else:
102
  final_data = filtered_data.copy()
103
 
104
- print(f"πŸ“Š Total records after adding tips: {len(final_data)}")
105
-
106
- # Step 8: Simple aggregation by Name (exact replication of your process)
107
- report = final_data.groupby('Name').agg({
108
- 'Hourly Rate': 'mean',
109
- 'Hours Worked': 'sum',
110
- 'Total': 'sum'
111
- }).reset_index()
112
-
113
- # Step 9: Format to match expected output exactly
114
- report = report.rename(columns={
115
- 'Name': 'Row Labels',
116
- 'Hourly Rate': 'Average of Hourly Rate',
117
- 'Hours Worked': 'Sum of Hours Worked',
118
- 'Total': 'Sum of Total'
119
- })
120
-
121
- # Round values to match expected precision
122
  report['Average of Hourly Rate'] = report['Average of Hourly Rate'].round(8)
123
  report['Sum of Hours Worked'] = report['Sum of Hours Worked'].round(2)
124
  report['Sum of Total'] = report['Sum of Total'].round(2)
125
 
126
- # Step 10: Add Grand Total
127
- if report['Sum of Hours Worked'].sum() > 0:
128
- grand_total = pd.DataFrame({
129
- 'Row Labels': ['Grand Total'],
130
- 'Average of Hourly Rate': [report['Sum of Total'].sum() / report['Sum of Hours Worked'].sum()],
131
- 'Sum of Hours Worked': [report['Sum of Hours Worked'].sum()],
132
- 'Sum of Total': [report['Sum of Total'].sum()]
133
- })
134
- else:
135
- grand_total = pd.DataFrame({
136
- 'Row Labels': ['Grand Total'],
137
- 'Average of Hourly Rate': [0],
138
- 'Sum of Hours Worked': [0],
139
- 'Sum of Total': [0]
140
- })
141
-
142
  final_invoice = pd.concat([report, grand_total], ignore_index=True)
143
 
144
- # Step 11: Save to temporary file for download
145
- temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.csv', mode='w')
146
- final_invoice.to_csv(temp_file.name, index=False)
147
- temp_file.close()
148
-
149
- summary_text = f"""
150
- πŸ“Š **Processing Summary:**
151
- - Employee CSVs processed: {len(employee_files)} out of 3 possible files
152
- - Total employees: {len(report)}
153
- - Total hours worked: {report['Sum of Hours Worked'].sum():.2f}
154
- - Total amount (including tips): Β£{report['Sum of Total'].sum():.2f}
155
- - Date range: {start_date} to {end_date}
156
- - Records processed: {len(filtered_data)}
157
- - Tips included: Β£{tips_summary['Tip'].sum():.2f}
158
 
159
- βœ… **Final invoice generated successfully!**
 
 
160
 
161
- **Expected Values (with all 3 files):**
162
- - Should match Final-Invoice.csv exactly
163
- - Pelumi Oluwatobi: Β£303.25 (includes Β£10 tip)
164
- - Grand Total: Β£3,877.91
165
- """
166
 
167
- return temp_file.name, summary_text
168
 
169
  except Exception as e:
170
- error_msg = f"❌ **Error processing files:** {str(e)}"
171
- print(error_msg)
172
- return None, error_msg
173
 
174
- # Create Gradio Interface
175
- with gr.Blocks(title="Invoice Generator", theme=gr.themes.Soft()) as interface:
176
-
177
- gr.Markdown("# 🧾 Invoice Generator for Launch27 Data")
178
- gr.Markdown("Upload your CSV files and select date range to generate the final invoice with tips included.")
179
 
 
 
180
  with gr.Row():
181
- with gr.Column():
182
  gr.Markdown("### πŸ“ Upload CSV Files")
183
- gr.Markdown("**Employee Data Files (Upload 1, 2, or 3 files):**")
184
- file1 = gr.File(label="Upload 1.csv (Employee Data)", file_types=[".csv"])
185
- file2 = gr.File(label="Upload 2.csv (Employee Data)", file_types=[".csv"])
186
- file3 = gr.File(label="Upload 3.csv (Employee Data)", file_types=[".csv"])
187
-
188
- gr.Markdown("**Required File:**")
189
  bookings_file = gr.File(label="Upload Bookings CSV (Required)", file_types=[".csv"])
190
-
191
- with gr.Column():
192
  gr.Markdown("### πŸ“… Select Date Range")
193
- start_date = gr.Textbox(
194
- label="Start Date (YYYY-MM-DD)",
195
- value="2025-08-09",
196
- placeholder="2025-08-09"
197
- )
198
- end_date = gr.Textbox(
199
- label="End Date (YYYY-MM-DD)",
200
- value="2025-08-22",
201
- placeholder="2025-08-22"
202
- )
203
-
204
- process_btn = gr.Button("πŸš€ Generate Invoice", variant="primary", size="lg")
205
 
 
206
  gr.Markdown("### πŸ“‹ Results")
207
-
208
  with gr.Row():
209
- with gr.Column():
210
- summary_output = gr.Markdown(label="Summary")
211
 
212
- with gr.Column():
213
- download_file = gr.File(label="πŸ“₯ Download Final Invoice CSV")
214
-
215
- # Event handlers
216
  process_btn.click(
217
- fn=process_invoice_files,
218
  inputs=[file1, file2, file3, bookings_file, start_date, end_date],
219
  outputs=[download_file, summary_output]
220
  )
221
 
222
- # Example section
223
  with gr.Accordion("πŸ“– How to Use", open=False):
224
  gr.Markdown("""
225
- **Step-by-step instructions:**
226
-
227
- 1. **Upload Files**:
228
- - **Employee Data**: Upload 1, 2, or 3 CSV files (1.csv, 2.csv, 3.csv)
229
- - **Bookings**: Upload bookings CSV file (REQUIRED)
230
-
231
- 2. **Select Dates**: Enter your bi-weekly date range in YYYY-MM-DD format:
232
- - Start Date: First day of the period (e.g., 2025-08-09)
233
- - End Date: Last day of the period (e.g., 2025-08-22)
234
-
235
- 3. **Generate**: Click "Generate Invoice" button
236
-
237
- 4. **Download**: Download the final invoice CSV file
238
-
239
- **Features:**
240
- - βœ… **Flexible file upload**: Works with 1, 2, or 3 employee CSV files
241
- - βœ… **Handles DD/MM/YYYY date format** automatically
242
- - βœ… **Merges multiple employee CSV files**
243
- - βœ… **Filters data by date range**
244
- - βœ… **Includes tips from bookings** (Β£10 from Morgan Barnes)
245
- - βœ… **Generates pivot table format**
246
- - βœ… **Matches expected Final-Invoice.csv exactly**
247
-
248
- **Expected Output (with all 3 files):**
249
- - 20 employees total
250
- - 284 total hours
251
- - Β£3,877.91 total (including Β£10 tip)
252
- - Pelumi Oluwatobi: Β£303.25 (Β£293.25 + Β£10 tip)
253
-
254
- **Note:** At least one employee CSV file and the bookings CSV file are required.
255
  """)
256
 
257
- # Footer
258
- gr.Markdown("---")
259
- gr.Markdown("πŸ”§ Built for Launch27 invoice processing | Works with 1-3 employee CSV files + bookings CSV")
260
-
261
- # Launch the interface
262
  if __name__ == "__main__":
263
- interface.launch(
264
- share=True, # Create shareable link
265
- debug=True, # Enable debug mode
266
- server_name="0.0.0.0", # Allow external access
267
- server_port=7860 # Default Gradio port
268
- )
 
1
  import gradio as gr
2
  import pandas as pd
3
  import tempfile
4
+ from datetime import datetime, timedelta
5
+ import openpyxl
6
+ from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
7
+ from openpyxl.drawing.image import Image as ExcelImage
8
+ import random
9
+ import base64
10
+ from io import BytesIO
11
 
12
+
13
+ def generate_random_invoice_number():
14
+ """Generate random 6-digit invoice number"""
15
+ return str(random.randint(100000, 999999))
16
+
17
+
18
+ def create_professional_individual_invoice(workbook, employee_data, start_date, end_date):
19
+ """Create individual invoice sheet with professional Glimmr template formatting and logo"""
20
+
21
+ # --- PASTE YOUR CLEANED BASE64 STRING HERE ---
22
+ GLIMMR_LOGO_BASE64 = """
23
+ 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
24
  """
25
+
26
+ ws = workbook.create_sheet(title=employee_data['Row Labels'])
27
+
28
+ column_widths = {'A': 25, 'B': 15, 'C': 15, 'D': 15, 'E': 15, 'F': 12, 'G': 12, 'H': 12, 'I': 12}
29
+ for col, width in column_widths.items():
30
+ ws.column_dimensions[col].width = width
31
+
32
+ try:
33
+ if GLIMMR_LOGO_BASE64.strip() and "PASTE" not in GLIMMR_LOGO_BASE64 and "YOUR BASE64" not in GLIMMR_LOGO_BASE64:
34
+ logo_data = base64.b64decode(GLIMMR_LOGO_BASE64.strip())
35
+ logo_stream = BytesIO(logo_data)
36
+ img = ExcelImage(logo_stream)
37
+ img.width = 200
38
+ img.height = 100
39
+ ws.add_image(img, 'A1')
40
+ except Exception as e:
41
+ print(f"Logo embedding failed: {e}")
42
+
43
+ # Company name
44
+ ws['A6'] = 'Glimmr Ltd'
45
+ ws['A6'].font = Font(bold=True, size=16)
46
+
47
+ # --- CORRECTED HEADER STRUCTURE TO MATCH ORIGINAL ---
48
+ # Main header spanning wider (extended one cell left)
49
+ ws.merge_cells('E2:I2')
50
+ ws['E2'] = 'CLEANER INVOICE'
51
+ ws['E2'].font = Font(bold=True, size=14, color='FFFFFF')
52
+ ws['E2'].alignment = Alignment(horizontal='center', vertical='center')
53
+ ws['E2'].fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
54
+
55
+ # Invoice # header (left side)
56
+ ws.merge_cells('E6:F6')
57
+ ws['E6'] = 'Invoice #'
58
+ ws['E6'].font = Font(bold=True, color='FFFFFF')
59
+ ws['E6'].fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
60
+ ws['E6'].alignment = Alignment(horizontal='center', vertical='center')
61
+
62
+ # Date header (right side)
63
+ ws.merge_cells('G6:I6')
64
+ ws['G6'] = 'Date'
65
+ ws['G6'].font = Font(bold=True, color='FFFFFF')
66
+ ws['G6'].fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
67
+ ws['G6'].alignment = Alignment(horizontal='center', vertical='center')
68
+
69
+ # Invoice # value (below header)
70
+ ws.merge_cells('E7:F7')
71
+ ws['E7'] = generate_random_invoice_number()
72
+ ws['E7'].alignment = Alignment(horizontal='center', vertical='center')
73
+
74
+ # Date value (below header)
75
+ ws.merge_cells('G7:I7')
76
+ ws['G7'] = datetime.now().strftime('%d/%m/%y')
77
+ ws['G7'].alignment = Alignment(horizontal='center', vertical='center')
78
+
79
+ # Company address
80
+ ws['A8'] = '108 Westbourne Terrace'
81
+ ws['A9'] = 'London'
82
+ ws['A10'] = 'W2 6QJ'
83
+ ws['A11'] = 'support@glimmr.co.uk'
84
+ ws['A12'] = '020 8158 8505'
85
+
86
+ # Payee information
87
+ ws['A14'] = "Payee's Name"
88
+ ws['A14'].font = Font(bold=True)
89
+ ws['A15'] = employee_data['Row Labels']
90
+ ws['A15'].font = Font(size=11)
91
+
92
+ # Payment period header
93
+ ws.merge_cells('A20:D20')
94
+ ws['A20'] = 'Payment Period'
95
+ ws['A20'].font = Font(bold=True, color='FFFFFF')
96
+ ws['A20'].fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
97
+ ws['A20'].alignment = Alignment(horizontal='center')
98
+
99
+ # Table headers - adjusted positions
100
+ ws['E20'] = 'Hours'
101
+ ws['F20'] = 'Β£ / Hour'
102
+ ws.merge_cells('G20:I20')
103
+ ws['G20'] = 'Amount'
104
+
105
+ for cell in ['E20', 'F20', 'G20']:
106
+ ws[cell].font = Font(bold=True, color='FFFFFF')
107
+ ws[cell].fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
108
+ ws[cell].alignment = Alignment(horizontal='center')
109
+
110
+ # Payment period data with center alignment
111
+ ws.merge_cells('A21:D21')
112
+ ws['A21'] = f"{start_date} to {end_date}"
113
+ ws['A21'].alignment = Alignment(horizontal='center')
114
+ ws['E21'] = employee_data['Sum of Hours Worked']
115
+ ws['F21'] = f"Β£ {employee_data['Average of Hourly Rate']:.2f}"
116
+ ws.merge_cells('G21:I21')
117
+ ws['G21'] = f"Β£ {employee_data['Sum of Total']:.2f}"
118
+
119
+ # Total sections - adjusted positions
120
+ ws.merge_cells('F24:G24')
121
+ ws['F24'] = 'Total Labor'
122
+ ws['F24'].font = Font(bold=True)
123
+ ws.merge_cells('H24:I24')
124
+ ws['H24'] = f"Β£ {employee_data['Sum of Total']:.2f}"
125
+
126
+ ws.merge_cells('F26:G26')
127
+ ws['F26'] = 'Tax (Withholding)'
128
+
129
+ # Payment due date and total
130
+ due_date = (datetime.strptime(end_date, '%Y-%m-%d') + timedelta(days=1)).strftime('%d/%m/%Y')
131
+ ws['A27'] = f'Payment is due on {due_date}'
132
+ ws.merge_cells('F27:G27')
133
+ ws['F27'] = 'TOTAL'
134
+ ws['F27'].font = Font(bold=True)
135
+ ws.merge_cells('H27:I27')
136
+ ws['H27'] = f"Β£ {employee_data['Sum of Total']:.2f}"
137
+ ws['H27'].font = Font(bold=True)
138
+
139
+ # Comments section
140
+ ws['A28'] = 'Comments or Special Instructions:'
141
+ ws['A28'].font = Font(bold=True)
142
+ ws['A29'] = 'Payments to be made every other Saturday (or as close to these days).'
143
+ ws['A30'] = 'Please let us know immediately if there are any issues.'
144
+
145
+ # Apply borders - updated ranges
146
+ thin_border = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin'))
147
+
148
+ # Border for payment period table
149
+ for row in range(20, 22):
150
+ for col in range(1, 10):
151
+ ws.cell(row=row, column=col).border = thin_border
152
+
153
+ # Border for header section
154
+ for row in range(6, 8):
155
+ for col_letter in ['E', 'F', 'G', 'H', 'I']:
156
+ ws[f'{col_letter}{row}'].border = thin_border
157
+
158
+
159
+ def create_invoices_from_template_with_logo(report_data, start_date, end_date):
160
+ wb = openpyxl.Workbook()
161
+ wb.remove(wb.active)
162
+
163
+ # Create Payments sheet
164
+ payments_ws = wb.create_sheet(title="Payments")
165
+ payments_ws['C1'] = 'DO NOT ALTER THIS TEMPLATE'
166
+ payments_ws['C1'].font = Font(bold=True)
167
+ headers = ['First Name', 'Last Name', 'Stripe Name', 'Reference', 'Amount (GBP)', 'Remarks']
168
+ for col, header in enumerate(headers, 1):
169
+ cell = payments_ws.cell(row=2, column=col, value=header)
170
+ cell.font = Font(bold=True)
171
+
172
+ for row_idx, employee in enumerate(report_data[report_data['Row Labels'] != 'Grand Total'].itertuples(), 3):
173
+ name_parts = employee._1.split(' ', 1)
174
+ first_name = name_parts[0] if name_parts else ''
175
+ last_name = name_parts[1] if len(name_parts) > 1 else ''
176
+ payments_ws.cell(row=row_idx, column=1, value=first_name)
177
+ payments_ws.cell(row=row_idx, column=2, value=last_name)
178
+ payments_ws.cell(row=row_idx, column=3, value=employee._1)
179
+ payments_ws.cell(row=row_idx, column=4, value='')
180
+ payments_ws.cell(row=row_idx, column=5, value=employee._4)
181
+ payments_ws.cell(row=row_idx, column=6, value='')
182
+
183
+ # Create Pivot sheet
184
+ pivot_ws = wb.create_sheet(title="Pivot")
185
+ pivot_ws['A1'] = f"{start_date} to {end_date}"
186
+ pivot_ws['A1'].font = Font(bold=True)
187
+ due_date = (datetime.strptime(end_date, '%Y-%m-%d') + timedelta(days=1)).strftime('%d/%m/%Y')
188
+ pivot_ws['A2'] = f"Payment is due on {due_date}"
189
+ pivot_headers = ['Name', 'Average of Hourly Rate', 'Sum of Hours Worked', 'Sum of Total']
190
+ for col, header in enumerate(pivot_headers, 1):
191
+ cell = pivot_ws.cell(row=3, column=col, value=header)
192
+ cell.font = Font(bold=True)
193
+
194
+ for row_idx, employee in enumerate(report_data.itertuples(), 4):
195
+ pivot_ws.cell(row=row_idx, column=1, value=employee._1)
196
+ pivot_ws.cell(row=row_idx, column=2, value=employee._2)
197
+ pivot_ws.cell(row=row_idx, column=3, value=employee._3)
198
+ pivot_ws.cell(row=row_idx, column=4, value=employee._4)
199
+
200
+ # Create individual invoice sheets
201
+ individual_employees = report_data[report_data['Row Labels'] != 'Grand Total']
202
+ for _, employee in individual_employees.iterrows():
203
+ create_professional_individual_invoice(wb, employee, start_date, end_date)
204
+
205
+ return wb
206
+
207
+
208
+ def process_invoice_files_with_professional_excel(file1, file2, file3, bookings_file, start_date, end_date):
209
  try:
 
210
  employee_files = [f for f in [file1, file2, file3] if f is not None]
 
211
  if not employee_files:
212
+ return None, "❗ **Error:** At least one Employee CSV must be uploaded."
 
213
  if bookings_file is None:
214
+ return None, "❗ **Error:** Bookings CSV is required."
 
 
 
 
 
 
 
 
 
 
 
215
 
216
+ employee_dfs = [pd.read_csv(ef.name) for ef in employee_files]
217
  all_employee_data = pd.concat(employee_dfs, ignore_index=True)
 
 
218
  pdf_bookings = pd.read_csv(bookings_file.name)
 
219
 
220
+ all_employee_data['Date'] = pd.to_datetime(all_employee_data['Date'], dayfirst=True, errors='coerce')
221
+ pdf_bookings['Date'] = pd.to_datetime(pdf_bookings['Date'], dayfirst=True, errors='coerce')
 
 
222
 
 
223
  start_date_dt = pd.to_datetime(start_date)
224
  end_date_dt = pd.to_datetime(end_date)
225
 
226
+ filtered_data = all_employee_data[(all_employee_data['Date'] >= start_date_dt) & (all_employee_data['Date'] <= end_date_dt)].copy()
227
+ filtered_bookings = pdf_bookings[(pdf_bookings['Date'] >= start_date_dt) & (pdf_bookings['Date'] <= end_date_dt)].copy()
 
 
 
 
 
 
 
 
 
228
 
 
 
 
 
 
 
 
 
229
  def extract_employee_name(team_string):
230
+ if pd.isna(team_string): return None
231
+ name = team_string.split(',')[0].strip()
232
+ return name.split('(')[0].strip()
 
 
 
 
233
 
234
  filtered_bookings['Employee_Name'] = filtered_bookings['Teams Assigned (without IDs)'].apply(extract_employee_name)
 
 
235
  tips_summary = filtered_bookings.groupby('Employee_Name')['Tip'].sum().reset_index()
236
  tips_with_amount = tips_summary[tips_summary['Tip'] > 0]
237
 
 
238
  if not tips_with_amount.empty:
239
+ tip_rows = [{'Name': row['Employee_Name'], 'Hourly Rate': 0, 'Hours Worked': 0, 'Total': row['Tip']} for _, row in tips_with_amount.iterrows()]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  tip_df = pd.DataFrame(tip_rows)
241
  final_data = pd.concat([filtered_data, tip_df], ignore_index=True)
242
  else:
243
  final_data = filtered_data.copy()
244
 
245
+ report = final_data.groupby('Name').agg({'Hourly Rate': 'mean', 'Hours Worked': 'sum', 'Total': 'sum'}).reset_index()
246
+ report = report.rename(columns={'Name': 'Row Labels', 'Hourly Rate': 'Average of Hourly Rate', 'Hours Worked': 'Sum of Hours Worked', 'Total': 'Sum of Total'})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  report['Average of Hourly Rate'] = report['Average of Hourly Rate'].round(8)
248
  report['Sum of Hours Worked'] = report['Sum of Hours Worked'].round(2)
249
  report['Sum of Total'] = report['Sum of Total'].round(2)
250
 
251
+ total_hours = report['Sum of Hours Worked'].sum()
252
+ total_sum = report['Sum of Total'].sum()
253
+ 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]})
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  final_invoice = pd.concat([report, grand_total], ignore_index=True)
255
 
256
+ wb = create_invoices_from_template_with_logo(final_invoice, start_date, end_date)
 
 
 
 
 
 
 
 
 
 
 
 
 
257
 
258
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.xlsx') as tmp:
259
+ wb.save(tmp.name)
260
+ temp_file_path = tmp.name
261
 
262
+ individual_employees_count = len(report[report['Row Labels'] != 'Grand Total'])
263
+ summary_text = f"πŸŽ‰ **Professional Invoice Generated Successfully!**\n\n- βœ… **Individual Professional Invoices** for each employee ({individual_employees_count} sheets)\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}"
 
 
 
264
 
265
+ return temp_file_path, summary_text
266
 
267
  except Exception as e:
268
+ return None, f"❌ **An error occurred:**\n\n``````\n\nPlease check your input files and date formats (YYYY-MM-DD)."
 
 
269
 
 
 
 
 
 
270
 
271
+ with gr.Blocks(title="Complete Professional Invoice Generator", theme=gr.themes.Soft()) as interface:
272
+ gr.Markdown("# 🧾 Complete Professional Invoice Generator for Launch27 Data")
273
  with gr.Row():
274
+ with gr.Column(scale=2):
275
  gr.Markdown("### πŸ“ Upload CSV Files")
276
+ file1 = gr.File(label="Upload 1.csv (Optional Employee Data)", file_types=[".csv"])
277
+ file2 = gr.File(label="Upload 2.csv (Optional Employee Data)", file_types=[".csv"])
278
+ file3 = gr.File(label="Upload 3.csv (Optional Employee Data)", file_types=[".csv"])
 
 
 
279
  bookings_file = gr.File(label="Upload Bookings CSV (Required)", file_types=[".csv"])
280
+ with gr.Column(scale=1):
 
281
  gr.Markdown("### πŸ“… Select Date Range")
282
+ start_date = gr.Textbox(label="Start Date (YYYY-MM-DD)", value="2025-08-09")
283
+ end_date = gr.Textbox(label="End Date (YYYY-MM-DD)", value="2025-08-22")
284
+ process_btn = gr.Button("πŸš€ Generate Professional Invoice Excel", variant="primary", size="lg")
 
 
 
 
 
 
 
 
 
285
 
286
+ gr.Markdown("---")
287
  gr.Markdown("### πŸ“‹ Results")
 
288
  with gr.Row():
289
+ summary_output = gr.Markdown(label="Summary")
290
+ download_file = gr.File(label="πŸ“₯ Download Professional Invoice Excel (.xlsx)")
291
 
 
 
 
 
292
  process_btn.click(
293
+ fn=process_invoice_files_with_professional_excel,
294
  inputs=[file1, file2, file3, bookings_file, start_date, end_date],
295
  outputs=[download_file, summary_output]
296
  )
297
 
 
298
  with gr.Accordion("πŸ“– How to Use", open=False):
299
  gr.Markdown("""
300
+ 1. **Upload Files**: Upload 1-3 employee CSVs and the required bookings CSV.
301
+ 2. **Select Dates**: Enter the date range in YYYY-MM-DD format.
302
+ 3. **Generate**: Click the button.
303
+ 4. **Download**: Your complete Excel file will appear for download.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  """)
305
 
 
 
 
 
 
306
  if __name__ == "__main__":
307
+ interface.launch(debug=True)