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:
331
create_excel_v2.py
Normal file
331
create_excel_v2.py
Normal 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()
|
||||
Reference in New Issue
Block a user