A production-grade Flask application that adds a March 2026 column to Indian financial statements, preserving all 1,230+ Excel formulas with correct column reference shifting.
finprojector_dev/
│
├── app.py ← App factory (create_app)
├── config.py ← All config: sheet layout, defaults, paths
│
├── routes/
│ ├── main.py ← Serves the HTML page (GET /)
│ └── api.py ← REST endpoints (POST /api/generate)
│
├── core/
│ ├── projector.py ← Pure business logic (no Flask)
│ └── validator.py ← Upload sanity checks
│
├── templates/
│ └── index.html ← Frontend UI (NSVR & Associates LLP)
│
├── tests/
│ └── test_projector.py ← 17 unit tests (pytest)
│
├── uploads/ ← Temp upload dir (git-ignored)
├── outputs/ ← Temp output dir (git-ignored)
│
├── requirements.txt
├── Procfile ← Railway / Heroku
├── nixpacks.toml ← LibreOffice install on Railway
├── .env.example
└── .gitignore
# Python 3.9+
python --version
# LibreOffice (needed to convert .xls → .xlsx)
# Windows: https://www.libreoffice.org/download
# Linux:
sudo apt install libreoffice
# Mac:
brew install --cask libreofficegit clone <your-repo>
cd finprojector_dev
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # Edit SECRET_KEYpython app.py
# → http://localhost:5000python -m pytest tests/ -v
# → 17 passedAccepts multipart form data, returns .xlsx file.
| Field | Type | Default | Description |
|---|---|---|---|
file |
File | required | .xls or .xlsx upload |
new_header |
string | As at 31 March, 2026 |
2026 column header text |
output_name |
string | FS-FY_2025-26_Draft.xlsx |
Download filename |
col_content |
string | copy2025 |
copy2025 or blank |
title_update |
string | yes |
yes or no |
Success: Returns .xlsx binary (HTTP 200)
Error: Returns { "error": "..." } (HTTP 400/422/500)
Returns { "status": "ok", "version": "v8" }
- Push this folder to a GitHub repo
- Go to railway.app → New Project → GitHub repo
- Railway reads
nixpacks.toml→ installs LibreOffice automatically - Reads
Procfile→ starts gunicorn - Set
SECRET_KEYin Railway environment variables - Done — live URL in ~2 minutes
- Push to GitHub
- New Web Service → connect repo
- Build:
pip install -r requirements.txt - Add package
libreofficein Environment - Start:
gunicorn app:create_app() --bind 0.0.0.0:$PORT --timeout 120
FROM python:3.11-slim
RUN apt-get update && apt-get install -y libreoffice --no-install-recommends && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["gunicorn", "app:create_app()", "--bind", "0.0.0.0:8080", "--timeout", "120"]docker build -t finprojector .
docker run -p 8080:8080 -e SECRET_KEY=mysecret finprojectorThe core problem: .xls files store formula results (numbers), not formula
strings. To recover the actual formulas (e.g. =SUM(D9:D12)), we must convert
.xls → .xlsx using LibreOffice — this is the critical step that makes
everything else possible.
1. _ensure_xlsx() LibreOffice: .xls → .xlsx (recovers 1,230 formulas)
2. _process_financial() For each of 11 financial sheets:
a. Save merged cell ranges
b. Unmerge all
c. Shift cells right-to-left from insert column
d. shift_formula() updates every ref: D→E, SUM(D9:D12)→SUM(E9:E12)
e. Re-merge with shifted column numbers
f. Write 2026 column (header + formulas + optional values)
3. _process_support() Fix cross-sheet refs in WDV, RATIO WORKING, TB, COMPUTATION
4. _apply_chain_patches() Fix 8 merged header range formulas
5. wb.save()
The regex handles Sheet!A1:B2 ranges as a single unit — both A1 and B2
belong to Sheet, so neither leaks into same-sheet shifting. This fixed the
bug where TB!C102:C106 became TB!C102:D106.
Edit config.py:
SHEET_CONFIG = {
...
'My New Sheet': {'insert': 4}, # 1-indexed column to insert 2026 col
}That's it — the projector handles the rest automatically.