Add xlsxwriter-based Excel generation scripts with openpyxl implementation

- Created create_excel_xlsxwriter.py and update_excel_xlsxwriter.py
- Uses openpyxl exclusively to preserve Excel formatting and formulas
- Updated server.js to use new xlsxwriter scripts for form submissions
- Maintains all original functionality while ensuring proper Excel file handling

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
andrei
2025-09-22 13:53:06 +00:00
commit 0e2e1bddba
842 changed files with 316330 additions and 0 deletions

326
create_excel_clean.py Executable file
View File

@@ -0,0 +1,326 @@
#!/usr/bin/env python3
"""
Cross-platform Excel generation script using openpyxl.
This version ensures clean Excel files without SharePoint/OneDrive metadata.
"""
import json
import os
import datetime
from pathlib import Path
from dateutil.relativedelta import relativedelta
import openpyxl
from openpyxl.workbook import Workbook
from openpyxl.utils import get_column_letter
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
import tempfile
import shutil
def create_excel_from_template():
"""
Create an Excel file from template with all placeholders replaced.
Uses openpyxl for maximum cross-platform compatibility.
"""
# Define paths
script_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(script_dir, 'config.json')
template_dir = os.path.join(script_dir, 'template')
# Try to find the template with either naming convention
possible_templates = [
'cleaned_template.xlsx', # Prefer cleaned template
'Footprints AI for {store_name} - Retail Media Business Case Calculations.xlsx',
'Footprints AI for store_name - Retail Media Business Case Calculations.xlsx'
]
template_path = None
for template_name in possible_templates:
full_path = os.path.join(template_dir, template_name)
if os.path.exists(full_path):
template_path = full_path
print(f"Found template: {template_name}")
break
if not template_path:
print(f"Error: No template found in {template_dir}")
return False
output_dir = os.path.join(script_dir, 'output')
os.makedirs(output_dir, exist_ok=True)
# Read config.json
try:
with open(config_path, 'r') as f:
config = json.load(f)
user_data = config.get('user_data', {})
store_name = user_data.get('store_name', 'Your Store')
starting_date = user_data.get('starting_date', '')
duration = user_data.get('duration', 36)
if not store_name:
store_name = "Your Store"
print(f"Processing for store: {store_name}")
# Calculate years array
years = calculate_years(starting_date, duration)
calculated_years = years
print(f"Years in the period: {years}")
except Exception as e:
print(f"Error reading config file: {e}")
return False
# Determine year range for filename
year_range = ""
if years and len(years) > 0:
if len(years) == 1:
year_range = f"{years[0]}"
else:
year_range = f"{years[0]}-{years[-1]}"
else:
year_range = f"{datetime.datetime.now().year}"
# Create output filename
output_filename = f"Footprints AI for {store_name} - Retail Media Business Case Calculations {year_range}.xlsx"
output_path = os.path.join(output_dir, output_filename)
try:
# Load template with data_only=False to preserve formulas
print("Loading template...")
wb = openpyxl.load_workbook(template_path, data_only=False, keep_vba=False)
# Build mapping of placeholder patterns to actual values
placeholder_patterns = [
('{store_name}', store_name),
('store_name', store_name)
]
# Step 1: Create sheet name mappings
print("Processing sheet names...")
sheet_name_mappings = {}
sheets_to_rename = []
for sheet in wb.worksheets:
old_title = sheet.title
new_title = old_title
for placeholder, replacement in placeholder_patterns:
if placeholder in new_title:
new_title = new_title.replace(placeholder, replacement)
if old_title != new_title:
sheet_name_mappings[old_title] = new_title
sheet_name_mappings[f"'{old_title}'"] = f"'{new_title}'"
sheets_to_rename.append((sheet, new_title))
print(f" Will rename: '{old_title}' -> '{new_title}'")
# Step 2: Update all formulas and values
print("Updating formulas and cell values...")
total_updates = 0
for sheet in wb.worksheets:
if 'Variables' in sheet.title:
continue
updates_in_sheet = 0
for row in sheet.iter_rows():
for cell in row:
try:
# Handle formulas
if hasattr(cell, '_value') and isinstance(cell._value, str) and cell._value.startswith('='):
original = cell._value
updated = original
# Update sheet references
for old_ref, new_ref in sheet_name_mappings.items():
updated = updated.replace(old_ref, new_ref)
# Update placeholders
for placeholder, replacement in placeholder_patterns:
updated = updated.replace(placeholder, replacement)
if updated != original:
cell._value = updated
updates_in_sheet += 1
# Handle regular text values
elif cell.value and isinstance(cell.value, str):
original = cell.value
updated = original
for placeholder, replacement in placeholder_patterns:
updated = updated.replace(placeholder, replacement)
if updated != original:
cell.value = updated
updates_in_sheet += 1
except Exception as e:
# Skip cells that cause issues
continue
if updates_in_sheet > 0:
print(f" {sheet.title}: {updates_in_sheet} updates")
total_updates += updates_in_sheet
print(f"Total updates: {total_updates}")
# Step 3: Rename sheets
print("Renaming sheets...")
for sheet, new_title in sheets_to_rename:
old_title = sheet.title
sheet.title = new_title
print(f" Renamed: '{old_title}' -> '{new_title}'")
# Hide forecast sheets not in calculated years
if "Forecast" in new_title:
try:
sheet_year = int(new_title.split()[0])
if sheet_year not in calculated_years:
sheet.sheet_state = 'hidden'
print(f" Hidden sheet '{new_title}' (year {sheet_year} not in range)")
except (ValueError, IndexError):
pass
# Step 4: Update Variables sheet
print("Updating Variables sheet...")
if 'Variables' in wb.sheetnames:
update_variables_sheet(wb['Variables'], user_data)
# Step 5: Save as a clean Excel file
print(f"Saving clean Excel file to: {output_path}")
# Create a temporary file first
with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as tmp:
tmp_path = tmp.name
# Save to temporary file
wb.save(tmp_path)
# Re-open and save again to ensure clean structure
wb_clean = openpyxl.load_workbook(tmp_path, data_only=False)
wb_clean.save(output_path)
wb_clean.close()
# Clean up temporary file
os.unlink(tmp_path)
print(f"✓ Excel file created successfully: {output_filename}")
return True
except Exception as e:
print(f"Error creating Excel file: {e}")
import traceback
traceback.print_exc()
return False
def update_variables_sheet(sheet, user_data):
"""
Update the Variables sheet with values from config.json
"""
cell_mappings = {
'B2': user_data.get('store_name', ''),
'B31': user_data.get('starting_date', ''),
'B32': user_data.get('duration', 36),
'B37': user_data.get('open_days_per_month', 0),
# Store types
'H37': user_data.get('convenience_store_type', {}).get('stores_number', 0),
'C37': user_data.get('convenience_store_type', {}).get('monthly_transactions', 0),
'I37': 1 if user_data.get('convenience_store_type', {}).get('has_digital_screens', False) else 0,
'J37': user_data.get('convenience_store_type', {}).get('screen_count', 0),
'K37': user_data.get('convenience_store_type', {}).get('screen_percentage', 0),
'M37': 1 if user_data.get('convenience_store_type', {}).get('has_in_store_radio', False) else 0,
'N37': user_data.get('convenience_store_type', {}).get('radio_percentage', 0),
'H38': user_data.get('minimarket_store_type', {}).get('stores_number', 0),
'C38': user_data.get('minimarket_store_type', {}).get('monthly_transactions', 0),
'I38': 1 if user_data.get('minimarket_store_type', {}).get('has_digital_screens', False) else 0,
'J38': user_data.get('minimarket_store_type', {}).get('screen_count', 0),
'K38': user_data.get('minimarket_store_type', {}).get('screen_percentage', 0),
'M38': 1 if user_data.get('minimarket_store_type', {}).get('has_in_store_radio', False) else 0,
'N38': user_data.get('minimarket_store_type', {}).get('radio_percentage', 0),
'H39': user_data.get('supermarket_store_type', {}).get('stores_number', 0),
'C39': user_data.get('supermarket_store_type', {}).get('monthly_transactions', 0),
'I39': 1 if user_data.get('supermarket_store_type', {}).get('has_digital_screens', False) else 0,
'J39': user_data.get('supermarket_store_type', {}).get('screen_count', 0),
'K39': user_data.get('supermarket_store_type', {}).get('screen_percentage', 0),
'M39': 1 if user_data.get('supermarket_store_type', {}).get('has_in_store_radio', False) else 0,
'N39': user_data.get('supermarket_store_type', {}).get('radio_percentage', 0),
'H40': user_data.get('hypermarket_store_type', {}).get('stores_number', 0),
'C40': user_data.get('hypermarket_store_type', {}).get('monthly_transactions', 0),
'I40': 1 if user_data.get('hypermarket_store_type', {}).get('has_digital_screens', False) else 0,
'J40': user_data.get('hypermarket_store_type', {}).get('screen_count', 0),
'K40': user_data.get('hypermarket_store_type', {}).get('screen_percentage', 0),
'M40': 1 if user_data.get('hypermarket_store_type', {}).get('has_in_store_radio', False) else 0,
'N40': user_data.get('hypermarket_store_type', {}).get('radio_percentage', 0),
# Channels
'B43': user_data.get('website_visitors', 0),
'B44': user_data.get('app_users', 0),
'B45': user_data.get('loyalty_users', 0),
'B49': user_data.get('facebook_followers', 0),
'B50': user_data.get('instagram_followers', 0),
'B51': user_data.get('google_views', 0),
'B52': user_data.get('email_subscribers', 0),
'B53': user_data.get('sms_users', 0),
'B54': user_data.get('whatsapp_contacts', 0)
}
for cell_ref, value in cell_mappings.items():
try:
sheet[cell_ref].value = value
print(f" Updated {cell_ref} = {value}")
except Exception as e:
print(f" Warning: Could not update {cell_ref}: {e}")
def calculate_years(starting_date, duration):
"""
Calculate an array of years that appear in the period.
"""
default_years = [datetime.datetime.now().year]
if not starting_date:
return default_years
try:
# Parse date - support multiple formats
if '/' in str(starting_date):
day, month, year = map(int, str(starting_date).split('/'))
elif '.' in str(starting_date):
day, month, year = map(int, str(starting_date).split('.'))
elif '-' in str(starting_date):
# ISO format (yyyy-mm-dd)
date_parts = str(starting_date).split('-')
if len(date_parts) == 3:
year, month, day = map(int, date_parts)
else:
return default_years
else:
return default_years
start_date = datetime.datetime(year, month, day)
end_date = start_date + relativedelta(months=duration-1)
years_set = set()
years_set.add(start_date.year)
years_set.add(end_date.year)
for y in range(start_date.year + 1, end_date.year):
years_set.add(y)
return sorted(list(years_set))
except Exception as e:
print(f"Error calculating years: {e}")
return default_years
if __name__ == "__main__":
create_excel_from_template()