- 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>
331 lines
13 KiB
Python
331 lines
13 KiB
Python
#!/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() |