"""Recalculate an XLSX through a temporary LibreOffice import/export, without macros.""" import argparse import json import os import shutil import subprocess import tempfile import zipfile from pathlib import Path from openpyxl import load_workbook from office.soffice import run_soffice def inspect_workbook(path): formulas = load_workbook(path, data_only=False) values = load_workbook(path, data_only=True) try: formula_cells = set() errors = {} for sheet in formulas: if not hasattr(sheet, "iter_rows"): continue for row in sheet.iter_rows(): for cell in row: location = f"{sheet.title}!{cell.coordinate}" if cell.data_type == "f": formula_cells.add(location) cached = values[sheet.title][cell.coordinate] if cached.data_type == "e": errors.setdefault(str(cached.value), []).append(location) return formula_cells, errors finally: formulas.close() values.close() def recalc(filename, timeout=120): source = Path(filename).expanduser().resolve() if not source.is_file() or source.suffix.lower() != ".xlsx": return {"error": "Input must be an existing .xlsx file; use Excel for macro-enabled workbooks"} try: with zipfile.ZipFile(source) as archive: if any(name.startswith("xl/externalLinks/") for name in archive.namelist()): return {"error": "External workbook links require validation in Excel; original file was not modified"} original_formulas, _ = inspect_workbook(source) with tempfile.TemporaryDirectory(prefix="xlsx-recalc-", dir=source.parent, ignore_cleanup_errors=True) as temporary: root = Path(temporary) input_dir, output_dir = root / "input", root / "output" input_dir.mkdir() output_dir.mkdir() candidate = input_dir / source.name shutil.copy2(source, candidate) result = run_soffice([ "--headless", "--norestore", "--convert-to", "xlsx:Calc MS Excel 2007 XML", "--outdir", str(output_dir), str(candidate), ], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout) recalculated = output_dir / source.name if result.returncode or not recalculated.is_file(): return {"error": "LibreOffice did not export a recalculated workbook", "details": result.stderr or result.stdout} formula_cells, errors = inspect_workbook(recalculated) if original_formulas != formula_cells: return {"error": "Formula locations changed during conversion; original file was not modified"} error_count = sum(len(locations) for locations in errors.values()) report = { "status": "errors_found" if error_count else "success", "total_formulas": len(formula_cells), "total_errors": error_count, "error_summary": { error: {"count": len(locations), "locations": locations[:100]} for error, locations in errors.items() }, } if not errors: os.replace(recalculated, source) return report except (OSError, subprocess.SubprocessError, zipfile.BadZipFile, ValueError) as exc: return {"error": str(exc)} if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("input") parser.add_argument("timeout", nargs="?", type=int, default=120) args = parser.parse_args() report = recalc(args.input, args.timeout) print(json.dumps(report, ensure_ascii=False, indent=2)) raise SystemExit(1 if "error" in report else 0)