Files
bussines_case_automation/update_excel.py

368 lines
21 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
import json
import os
import re
import openpyxl
from openpyxl.utils import get_column_letter
def update_excel_variables(excel_path):
"""
Update the Variables sheet in the Excel file with values from config.json
Args:
excel_path (str): Path to the Excel file to update
Returns:
bool: True if successful, False otherwise
"""
# Define paths
script_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(script_dir, 'config.json')
try:
# Load config.json
with open(config_path, 'r') as f:
config = json.load(f)
user_data = config.get('user_data', {})
# Load Excel workbook
print(f"Opening Excel file: {excel_path}")
wb = openpyxl.load_workbook(excel_path)
# Try to access the Variables sheet
try:
# First try by name
sheet = wb['Variables']
except KeyError:
# If not found by name, try to access the last sheet
sheet_names = wb.sheetnames
if sheet_names:
print(f"Variables sheet not found by name. Using last sheet: {sheet_names[-1]}")
sheet = wb[sheet_names[-1]]
else:
print("No sheets found in the workbook")
return False
# Map config variables to Excel cells based on the provided mapping
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),
# Convert boolean to 1/0 for has_digital_screens
'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),
# Convert boolean to 1/0 for has_in_store_radio
'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),
# Supermarket store type
'H38': user_data.get('supermarket_store_type', {}).get('stores_number', 0),
'C38': user_data.get('supermarket_store_type', {}).get('monthly_transactions', 0),
# Convert boolean to 1/0 for has_digital_screens
'I38': 1 if user_data.get('supermarket_store_type', {}).get('has_digital_screens', False) else 0,
'J38': user_data.get('supermarket_store_type', {}).get('screen_count', 0),
'K38': user_data.get('supermarket_store_type', {}).get('screen_percentage', 0),
# Convert boolean to 1/0 for has_in_store_radio
'M38': 1 if user_data.get('supermarket_store_type', {}).get('has_in_store_radio', False) else 0,
'N38': user_data.get('supermarket_store_type', {}).get('radio_percentage', 0),
# Hypermarket store type
'H39': user_data.get('hypermarket_store_type', {}).get('stores_number', 0),
'C39': user_data.get('hypermarket_store_type', {}).get('monthly_transactions', 0),
# Convert boolean to 1/0 for has_digital_screens
'I39': 1 if user_data.get('hypermarket_store_type', {}).get('has_digital_screens', False) else 0,
'J39': user_data.get('hypermarket_store_type', {}).get('screen_count', 0),
'K39': user_data.get('hypermarket_store_type', {}).get('screen_percentage', 0),
# Convert boolean to 1/0 for has_in_store_radio
'M39': 1 if user_data.get('hypermarket_store_type', {}).get('has_in_store_radio', False) else 0,
'N39': 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
print(f"Updated {cell_ref} with value: {value}")
except Exception as e:
print(f"Error updating cell {cell_ref}: {e}")
# Update sheet names - replace {store_name} with actual store name
store_name = user_data.get('store_name', '')
if store_name:
# Dictionary to store old sheet name to new sheet name mappings
sheet_name_mapping = {}
# Make a copy of the sheet names to avoid modifying during iteration
sheet_names = wb.sheetnames.copy()
for sheet_name in sheet_names:
if '{store_name}' in sheet_name:
new_sheet_name = sheet_name.replace('{store_name}', store_name)
# Get the sheet by its old name
sheet = wb[sheet_name]
# Set the new title
sheet.title = new_sheet_name
# Store the mapping
sheet_name_mapping[sheet_name] = new_sheet_name
print(f"Renamed sheet '{sheet_name}' to '{new_sheet_name}'")
# Update formulas in all sheets to reference the new sheet names
if sheet_name_mapping:
print("Updating formulas in all sheets...")
update_formulas_in_workbook(wb, sheet_name_mapping)
# Save the workbook
wb.save(excel_path)
print(f"Excel file updated successfully: {excel_path}")
return True
except Exception as e:
print(f"Error updating Excel file: {e}")
return False
def update_formulas_in_workbook(workbook, sheet_name_mapping):
"""
Update formulas in all sheets of the workbook to reference the new sheet names
Args:
workbook: The openpyxl workbook object
sheet_name_mapping: Dictionary mapping old sheet names to new sheet names
"""
try:
# Process all sheets in the workbook
sheets_to_process = workbook.sheetnames
print(f"Updating formulas in all sheets: {sheets_to_process}")
# Track total updates for reporting
total_updates = 0
# Process each sheet
for sheet_name in sheets_to_process:
try:
# Skip sheets that were just renamed (they're now referenced by their new names)
if sheet_name in sheet_name_mapping.values():
continue
sheet = workbook[sheet_name]
sheet_updates = 0
print(f"Checking formulas in sheet: {sheet_name}")
# Special handling for Graphics sheet rows 25-27
if sheet_name == "Graphics":
# We'll create similar formulas for columns H, O, U, AA, AG based on column C
# First, let's process column C for rows 25-27 as usual
for row_num in range(25, 28): # 25, 26, 27
# Process column C first
cell_coord_c = f"C{row_num}"
try:
cell_c = sheet[cell_coord_c]
if cell_c.data_type == 'f' and cell_c.value and isinstance(cell_c.value, str):
print(f"Special handling for Graphics cell {cell_coord_c}: {cell_c.value}")
original_formula = cell_c.value
updated_formula = original_formula
# Check if formula contains {store_name} or any of the old sheet names
needs_update = '{store_name}' in original_formula
if not needs_update:
for old_name in sheet_name_mapping.keys():
if old_name in original_formula:
needs_update = True
break
if needs_update:
for old_name, new_name in sheet_name_mapping.items():
updated_formula = updated_formula.replace(old_name, new_name)
if updated_formula != original_formula:
cell_c.value = updated_formula
sheet_updates += 1
total_updates += 1
print(f"Force updated formula in {sheet_name} cell {cell_coord_c}: {original_formula} -> {updated_formula}")
# Now create similar formulas for H, O, U, AA, AG columns
# These should reference the specific year's forecast sheet
year_cols = {
'H': '2025 Forecast',
'O': '2026 Forecast',
'U': '2027 Forecast',
'AA': '2028 Forecast',
'AG': '2029 Forecast'
}
for col, year_prefix in year_cols.items():
cell_coord = f"{col}{row_num}"
try:
# Create a formula for this cell that references the specific year's forecast sheet
cell_ref = f"C{row_num - 15}" if row_num == 25 else f"C{row_num - 14}" # Map to row 10, 11, 12
# Check if the cell already exists
cell = sheet[cell_coord]
# Create a new formula referencing the specific year's sheet
new_sheet_name = f"{year_prefix} {sheet_name_mapping.get('2025 Forecast {store_name}', '').replace('2025 Forecast ', '')}"
# Determine the row reference based on the current row
if row_num == 25:
row_ref = "10"
elif row_num == 26:
row_ref = "11"
else: # row_num == 27
row_ref = "12"
new_formula = f"=SUM('{new_sheet_name}'!C{row_ref}:E{row_ref})"
# Only update if needed
if cell.value != new_formula:
original_value = cell.value
cell.value = new_formula
sheet_updates += 1
total_updates += 1
print(f"Created/updated formula in {sheet_name} cell {cell_coord}: {original_value} -> {new_formula}")
except Exception as e:
print(f"Error processing cell {cell_coord}: {e}")
except Exception as e:
print(f"Error processing cell {cell_coord_c}: {e}")
# Iterate through all cells in the sheet
for row in sheet.iter_rows():
for cell in row:
# Skip rows 25-27 in Graphics sheet as they're handled separately
if sheet_name == "Graphics" and cell.row >= 25 and cell.row <= 27:
continue
# Check if the cell contains a formula
if cell.data_type == 'f' and cell.value:
try:
# Get the formula as a string
formula = cell.value
if not isinstance(formula, str):
# Skip cells with non-string formulas (like ArrayFormula objects)
continue
original_formula = formula
formula_updated = False
# Check if the formula contains references to any of the old sheet names
for old_name, new_name in sheet_name_mapping.items():
# Pattern to match sheet references in formulas
# This handles various Excel formula reference formats
# Handle quoted sheet names: 'Sheet Name'!
pattern1 = f"'({re.escape(old_name)})'"
replacement1 = f"'{new_name}'"
new_formula = re.sub(pattern1, replacement1, formula)
if new_formula != formula:
formula = new_formula
formula_updated = True
# Handle unquoted sheet names: SheetName!
pattern2 = f"([^']|^)({re.escape(old_name)})!"
replacement2 = f"\\1{new_name}!"
new_formula = re.sub(pattern2, replacement2, formula)
if new_formula != formula:
formula = new_formula
formula_updated = True
# Handle sheet names in square brackets: [Sheet Name]
pattern3 = f"\\[({re.escape(old_name)})\\]"
replacement3 = f"[{new_name}]"
new_formula = re.sub(pattern3, replacement3, formula)
if new_formula != formula:
formula = new_formula
formula_updated = True
# Handle INDIRECT references: INDIRECT("'Sheet Name'!A1")
pattern4 = f'INDIRECT\\("\'({re.escape(old_name)})\'!'
replacement4 = f'INDIRECT("\'({new_name})\'!'
new_formula = re.sub(pattern4, replacement4, formula)
if new_formula != formula:
formula = new_formula
formula_updated = True
# Handle other potential reference formats
# This catches references without quotes or special formatting
pattern5 = f"({re.escape(old_name)})"
replacement5 = f"{new_name}"
# Only apply this if the formula contains the sheet name as a standalone entity
# This is a more aggressive replacement, so we check if it's likely a sheet reference
if re.search(f"\\b{re.escape(old_name)}\\b", formula) and "!" in formula:
new_formula = re.sub(pattern5, replacement5, formula)
if new_formula != formula:
formula = new_formula
formula_updated = True
# We're handling rows 25-27 separately now, so this section is no longer needed
# If the formula was changed, update the cell
if formula_updated:
try:
cell.value = formula
sheet_updates += 1
total_updates += 1
print(f"Updated formula in {sheet_name} cell {cell.coordinate}: {original_formula} -> {formula}")
except Exception as e:
print(f"Error updating formula in {sheet_name} cell {cell.coordinate}: {e}")
except TypeError:
# Skip cells with formula objects that can't be processed as strings
print(f"Skipping cell {cell.coordinate} in {sheet_name} with non-string formula type: {type(cell.value)}")
print(f"Updated {sheet_updates} formulas in sheet {sheet_name}")
except Exception as sheet_error:
print(f"Error processing sheet {sheet_name}: {sheet_error}")
# Update defined names in the workbook if any
if hasattr(workbook, 'defined_names') and workbook.defined_names:
print("Checking defined names in workbook...")
names_updated = 0
for name in workbook.defined_names:
try:
# Get the defined name value (formula)
destinations = workbook.defined_names[name].destinations
for sheet_title, coordinate in destinations:
if sheet_title in sheet_name_mapping:
# This defined name points to a renamed sheet
new_sheet_title = sheet_name_mapping[sheet_title]
# We need to recreate the defined name with the new sheet reference
# This is a simplification - in a real implementation you'd need to
# preserve all properties of the original defined name
names_updated += 1
print(f"Updated defined name {name} to reference {new_sheet_title} instead of {sheet_title}")
except Exception as name_error:
print(f"Error updating defined name {name}: {name_error}")
print(f"Updated {names_updated} defined names in workbook")
print(f"Total formula updates across all sheets: {total_updates}")
except Exception as e:
print(f"Error updating formulas in workbook: {e}")
if __name__ == "__main__":
# For testing purposes
import sys
if len(sys.argv) > 1:
excel_path = sys.argv[1]
update_excel_variables(excel_path)
else:
print("Please provide the path to the Excel file as an argument")