Notiflow / utils /excel_writer.py
Dipan04's picture
bina soke kam kia
0bb062b
"""
utils/excel_writer.py
---------------------
Excel persistence utility for Notiflow. (FIX 4 β€” config-driven paths)
All file paths come from app/config.py β€” no hardcoded paths in this module.
Single-file, multi-sheet store: DATA_FILE (default: data/notiflow_data.xlsx)
Sheets and their column schemas:
Orders β€” order records
Ledger β€” payment and credit entries
Returns β€” return / exchange requests
Inventory β€” stock movement delta log
Invoices β€” generated invoice records
Public API
----------
append_row(sheet_name, record) β€” append one row, atomic save
append_rows(sheet_name, records) β€” append many rows, one save
read_sheet(sheet_name) -> DataFrame β€” read sheet into pandas DataFrame
"""
from __future__ import annotations
import logging
from pathlib import Path
import pandas as pd
from openpyxl import load_workbook, Workbook
from app.config import DATA_FILE # FIX 4: single source of truth
logger = logging.getLogger(__name__)
EXCEL_FILE = Path(DATA_FILE) # re-export for modules that imported it directly
# ---------------------------------------------------------------------------
# Canonical column schemas
# ---------------------------------------------------------------------------
SHEET_SCHEMAS: dict[str, list[str]] = {
"Orders": [
"order_id", "timestamp", "customer", "item", "quantity", "status"
],
"Ledger": [
"entry_id", "timestamp", "type", "customer", "item",
"quantity", "amount", "payment_type", "status"
],
"Returns": [
"return_id", "timestamp", "customer", "item", "reason", "status"
],
"Inventory": [
"timestamp", "item", "change", "direction", "reference_id", "note"
],
"Invoices": [
"invoice_id", "timestamp", "order_id", "customer",
"item", "quantity", "unit_price", "total_amount", "status"
],
}
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _ensure_file() -> None:
"""Create the Excel file with all sheets if it does not exist."""
if EXCEL_FILE.exists():
return
EXCEL_FILE.parent.mkdir(parents=True, exist_ok=True)
logger.info("Creating new Excel file: %s", EXCEL_FILE)
wb = Workbook()
if "Sheet" in wb.sheetnames:
del wb["Sheet"]
for sheet_name, columns in SHEET_SCHEMAS.items():
ws = wb.create_sheet(title=sheet_name)
ws.append(columns)
wb.save(EXCEL_FILE)
logger.info("Excel file created with sheets: %s", list(SHEET_SCHEMAS.keys()))
def _ensure_sheet(wb: Workbook, sheet_name: str) -> None:
if sheet_name not in wb.sheetnames:
ws = wb.create_sheet(title=sheet_name)
columns = SHEET_SCHEMAS.get(sheet_name, [])
if columns:
ws.append(columns)
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def append_row(sheet_name: str, record: dict) -> None:
"""
Append a single record to the named Excel sheet.
Missing columns default to None. Extra keys in record are ignored.
File and sheet are created automatically if they don't exist.
Args:
sheet_name: e.g. "Orders", "Ledger"
record: Dict of column β†’ value pairs.
Raises:
ValueError: Unknown sheet_name.
"""
if sheet_name not in SHEET_SCHEMAS:
raise ValueError(
f"Unknown sheet '{sheet_name}'. Valid: {list(SHEET_SCHEMAS)}"
)
_ensure_file()
wb = load_workbook(EXCEL_FILE)
_ensure_sheet(wb, sheet_name)
ws = wb[sheet_name]
columns = SHEET_SCHEMAS[sheet_name]
ws.append([record.get(col) for col in columns])
wb.save(EXCEL_FILE)
logger.debug("Row appended to '%s': %s", sheet_name, record)
def append_rows(sheet_name: str, records: list[dict]) -> None:
"""Append multiple records in one file open/save cycle."""
if not records:
return
if sheet_name not in SHEET_SCHEMAS:
raise ValueError(f"Unknown sheet '{sheet_name}'.")
_ensure_file()
wb = load_workbook(EXCEL_FILE)
_ensure_sheet(wb, sheet_name)
ws = wb[sheet_name]
columns = SHEET_SCHEMAS[sheet_name]
for record in records:
ws.append([record.get(col) for col in columns])
wb.save(EXCEL_FILE)
logger.debug("Appended %d rows to '%s'", len(records), sheet_name)
def read_sheet(sheet_name: str) -> pd.DataFrame:
"""
Read a sheet into a DataFrame.
Returns an empty DataFrame (with correct columns) if the file or
sheet does not exist yet.
"""
columns = SHEET_SCHEMAS.get(sheet_name, [])
if not EXCEL_FILE.exists():
return pd.DataFrame(columns=columns)
try:
return pd.read_excel(EXCEL_FILE, sheet_name=sheet_name)
except Exception as exc:
logger.warning("Could not read sheet '%s': %s", sheet_name, exc)
return pd.DataFrame(columns=columns)