#!/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()