Files
bussines_case_automation/update_excel.py
2025-09-12 14:02:53 +03:00

322 lines
15 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
from zipfile import ZipFile, ZIP_DEFLATED
def update_excel_variables(excel_path):
"""
Update the Variables sheet in the Excel file with values from config.json
and hide forecast sheets that aren't in the calculated years array
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),
# 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),
# Convert boolean to 1/0 for has_digital_screens
'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),
# Convert boolean to 1/0 for has_in_store_radio
'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),
# Convert boolean to 1/0 for has_digital_screens
'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),
# Convert boolean to 1/0 for has_in_store_radio
'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),
# Convert boolean to 1/0 for has_digital_screens
'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),
# Convert boolean to 1/0 for has_in_store_radio
'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:
# Force the value to be set, even if the cell is protected or has data validation
cell = sheet[cell_ref]
cell.value = value
print(f"Updated {cell_ref} with value: {value}")
except Exception as e:
print(f"Error updating cell {cell_ref}: {e}")
# Save the workbook with variables updated
print("Saving workbook with updated variables...")
wb.save(excel_path)
# Get the calculated years array from config
starting_date = user_data.get('starting_date', '')
duration = user_data.get('duration', 36)
calculated_years = []
# Import datetime at the module level to avoid scope issues
import datetime
from dateutil.relativedelta import relativedelta
# Calculate years array based on starting_date and duration
try:
# Try to parse the date, supporting both dd/mm/yyyy and dd.mm.yyyy formats
if starting_date:
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):
# Handle ISO format (yyyy-mm-dd)
date_parts = str(starting_date).split('-')
if len(date_parts) == 3:
year, month, day = map(int, date_parts)
else:
# Default to current date if format is not recognized
current_date = datetime.datetime.now()
year, month, day = current_date.year, current_date.month, current_date.day
elif isinstance(starting_date, datetime.datetime):
day, month, year = starting_date.day, starting_date.month, starting_date.year
else:
# Default to current date if format is not recognized
current_date = datetime.datetime.now()
year, month, day = current_date.year, current_date.month, current_date.day
# Create datetime object for starting date
start_date = datetime.datetime(year, month, day)
# Calculate end date (starting date + duration months - 1 day)
end_date = start_date + relativedelta(months=duration-1)
# Create a set of years (to avoid duplicates)
years_set = set()
# Add starting year
years_set.add(start_date.year)
# Add ending year
years_set.add(end_date.year)
# If there are years in between, add those too
for y in range(start_date.year + 1, end_date.year):
years_set.add(y)
# Convert set to sorted list
calculated_years = sorted(list(years_set))
print(f"Calculated years for sheet visibility: {calculated_years}")
else:
# Default to current year if no starting date
calculated_years = [datetime.datetime.now().year]
except Exception as e:
print(f"Error calculating years for sheet visibility: {e}")
calculated_years = [datetime.datetime.now().year]
# 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}'")
# Check if this is a forecast sheet and if its year is in the calculated years
# Forecast sheets have names like "2025 Forecast {store_name}"
if "Forecast" in new_sheet_name:
# Extract the year from the sheet name
try:
sheet_year = int(new_sheet_name.split()[0])
# Hide the sheet if its year is not in the calculated years
if sheet_year not in calculated_years:
sheet.sheet_state = 'hidden'
print(f"Hiding sheet '{new_sheet_name}' as year {sheet_year} is not in calculated years {calculated_years}")
except Exception as e:
print(f"Error extracting year from sheet name '{new_sheet_name}': {e}")
# Save the workbook with renamed and hidden sheets
wb.save(excel_path)
# Use direct XML modification to replace all instances of {store_name} in formulas
print("Using direct XML modification to update all formulas...")
update_excel_with_direct_xml(excel_path, store_name)
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_excel_with_direct_xml(excel_path, store_name):
"""
Update all references to {store_name} in the Excel file by directly modifying XML
Args:
excel_path: Path to the Excel file
store_name: The store name to replace {store_name} with
Returns:
bool: True if successful, False otherwise
"""
try:
print(f"Using direct XML modification to replace '{{store_name}}' with '{store_name}'...")
# Create a temporary file for modification
temp_dir = os.path.dirname(os.path.abspath(excel_path))
temp_file = os.path.join(temp_dir, f"_temp_{os.path.basename(excel_path)}")
# Make a copy of the original file
import shutil
shutil.copy2(excel_path, temp_file)
# Count of replacements
total_replacements = 0
# Process the Excel file - use a safer approach
# First read all files from the zip
files_data = {}
with ZipFile(excel_path, 'r') as zip_ref:
for item in zip_ref.infolist():
files_data[item.filename] = (zip_ref.read(item.filename), item)
# Modify the content
for filename, (content, item) in files_data.items():
# Only modify XML files that might contain formulas or text
if filename.endswith('.xml') or filename.endswith('.rels'):
# Skip sheet8.xml which is the Variables sheet (based on common Excel structure)
if 'sheet8.xml' in filename:
print(f"Skipping Variables sheet: {filename}")
continue
# Convert to string for text replacement
try:
text_content = content.decode('utf-8')
# Check if this file contains our placeholder
if '{store_name}' in text_content:
# Count occurrences before replacement
occurrences = text_content.count('{store_name}')
total_replacements += occurrences
# Replace all instances of {store_name} with the actual store name
modified_content = text_content.replace('{store_name}', store_name)
# Convert back to bytes
files_data[filename] = (modified_content.encode('utf-8'), item)
print(f"Replaced {occurrences} instances of '{{store_name}}' in {filename}")
except UnicodeDecodeError:
# Not a text file, leave as is
pass
# Write the modified zip file
with ZipFile(temp_file, 'w', ZIP_DEFLATED) as zip_out:
for filename, (content, item) in files_data.items():
zip_out.writestr(filename, content)
# Replace the original file with the modified one
shutil.move(temp_file, excel_path)
print(f"Total replacements: {total_replacements}")
return True
except Exception as e:
print(f"Error updating Excel file with direct XML modification: {e}")
import traceback
traceback.print_exc()
return False
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")