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

331
create_excel_v2.py Normal file
View File

@@ -0,0 +1,331 @@
#!/usr/bin/env python3
"""
Improved Excel creation script that processes templates in memory
to prevent external link issues in Excel.
"""
import json
import os
import datetime
from pathlib import Path
from dateutil.relativedelta import relativedelta
import openpyxl
from openpyxl.utils import get_column_letter
def create_excel_from_template():
"""
Create an Excel file from template with all placeholders replaced in memory
before saving to prevent external link issues.
"""
# Define paths
script_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(script_dir, 'config.json')
# Check for both possible template names
template_dir = os.path.join(script_dir, 'template')
# Try to find the template with either naming convention
possible_templates = [
'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')
# Ensure output directory exists
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 # For sheet visibility later
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:
# STAGE 1: Load template and replace all placeholders in memory
print("Loading template in memory...")
wb = openpyxl.load_workbook(template_path, data_only=False)
# Build mapping of placeholder patterns to actual values
# Support both {store_name} and store_name formats
placeholder_patterns = [
('{store_name}', store_name),
('store_name', store_name) # New format without curly braces
]
# STAGE 2: Replace placeholders in sheet names first
print("Replacing placeholders in sheet names...")
sheet_name_mappings = {}
for sheet in wb.worksheets:
old_title = sheet.title
new_title = old_title
# Replace all placeholder patterns in sheet name
for placeholder, replacement in placeholder_patterns:
if placeholder in new_title:
new_title = new_title.replace(placeholder, replacement)
print(f" Sheet name: '{old_title}' -> '{new_title}'")
if old_title != new_title:
# Store the mapping for formula updates
sheet_name_mappings[old_title] = new_title
# Also store with quotes for formula references
sheet_name_mappings[f"'{old_title}'"] = f"'{new_title}'"
# STAGE 3: Update all formulas and cell values BEFORE renaming sheets
print("Updating formulas and cell values...")
total_replacements = 0
for sheet in wb.worksheets:
sheet_name = sheet.title
replacements_in_sheet = 0
# Skip Variables sheet to avoid issues
if 'Variables' in sheet_name:
continue
for row in sheet.iter_rows():
for cell in row:
# Handle formulas
if cell.data_type == 'f' and cell.value:
original_formula = str(cell.value)
new_formula = original_formula
# First replace sheet references
for old_ref, new_ref in sheet_name_mappings.items():
if old_ref in new_formula:
new_formula = new_formula.replace(old_ref, new_ref)
# Then replace any remaining placeholders
for placeholder, replacement in placeholder_patterns:
if placeholder in new_formula:
new_formula = new_formula.replace(placeholder, replacement)
if new_formula != original_formula:
cell.value = new_formula
replacements_in_sheet += 1
# Handle text values
elif cell.value and isinstance(cell.value, str):
original_value = str(cell.value)
new_value = original_value
for placeholder, replacement in placeholder_patterns:
if placeholder in new_value:
new_value = new_value.replace(placeholder, replacement)
if new_value != original_value:
cell.value = new_value
replacements_in_sheet += 1
if replacements_in_sheet > 0:
print(f" {sheet_name}: {replacements_in_sheet} replacements")
total_replacements += replacements_in_sheet
print(f"Total replacements: {total_replacements}")
# STAGE 4: Now rename the sheets (after formulas are updated)
print("Renaming sheets...")
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.title = new_title
print(f" Renamed: '{old_title}' -> '{new_title}'")
# Check if this is a forecast sheet and hide if needed
if "Forecast" in new_title:
try:
# Extract year from sheet name
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
# STAGE 5: Update Variables sheet with config values
print("Updating Variables sheet...")
if 'Variables' in wb.sheetnames:
update_variables_sheet(wb['Variables'], user_data)
# STAGE 6: Save the fully processed workbook
print(f"Saving to: {output_path}")
wb.save(output_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
"""
# Map config variables to Excel cells
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),
# Convenience store type
'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),
# Minimarket store type
'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),
# Supermarket store type
'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),
# Hypermarket store type
'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),
# On-site channels
'B43': user_data.get('website_visitors', 0),
'B44': user_data.get('app_users', 0),
'B45': user_data.get('loyalty_users', 0),
# Off-site channels
'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)
}
# Update the cells
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
# Create datetime object
start_date = datetime.datetime(year, month, day)
# Calculate end date
end_date = start_date + relativedelta(months=duration-1)
# Create set of years
years_set = set()
years_set.add(start_date.year)
years_set.add(end_date.year)
# Add any years in between
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()