Spaces:
Sleeping
Sleeping
Sagar Patel
Finalize i18n, AI coach, receipt scanning, and UI polish for hackathon submission
edd2d73 | """Business analytics helpers for VoiceLedger.""" | |
| from __future__ import annotations | |
| from datetime import date | |
| from pathlib import Path | |
| import pandas as pd | |
| from voiceledger.ledger.customers import get_customer_balances | |
| from voiceledger.ledger.database import get_transactions | |
| from voiceledger.ledger.inventory import get_inventory | |
| from voiceledger.ledger.settings import get_business_settings | |
| DEFAULT_LOW_STOCK_THRESHOLD = 5 | |
| def calculate_daily_sales(db_path: str | Path | None = None, report_date: date | None = None) -> float: | |
| """Return total sales for the selected day.""" | |
| transactions = _transactions_for_date(db_path, report_date) | |
| return _sum_amounts(transactions, ["sale"]) | |
| def calculate_daily_expenses(db_path: str | Path | None = None, report_date: date | None = None) -> float: | |
| """Return total expenses for the selected day.""" | |
| transactions = _transactions_for_date(db_path, report_date) | |
| return _sum_amounts(transactions, ["expense", "inventory_purchase"]) | |
| def calculate_net_profit(db_path: str | Path | None = None, report_date: date | None = None) -> float: | |
| """Return sales minus expenses for the selected day.""" | |
| sales = calculate_daily_sales(db_path=db_path, report_date=report_date) | |
| expenses = calculate_daily_expenses(db_path=db_path, report_date=report_date) | |
| return round(sales - expenses, 2) | |
| def top_selling_items( | |
| db_path: str | Path | None = None, | |
| report_date: date | None = None, | |
| limit: int = 5, | |
| ) -> pd.DataFrame: | |
| """Return top selling items by sold quantity for the selected day.""" | |
| transactions = _transactions_for_date(db_path, report_date) | |
| if transactions.empty: | |
| return _empty_top_items_frame() | |
| sales = transactions[ | |
| (transactions["transaction_type"] == "sale") | |
| & transactions["item"].notna() | |
| & transactions["quantity"].notna() | |
| ].copy() | |
| if sales.empty: | |
| return _empty_top_items_frame() | |
| sales["quantity"] = pd.to_numeric(sales["quantity"], errors="coerce").fillna(0) | |
| sales["amount"] = pd.to_numeric(sales["amount"], errors="coerce").fillna(0) | |
| grouped = ( | |
| sales.groupby("item", as_index=False) | |
| .agg(quantity_sold=("quantity", "sum"), sales_amount=("amount", "sum")) | |
| .sort_values(["quantity_sold", "sales_amount"], ascending=[False, False]) | |
| .head(limit) | |
| ) | |
| return grouped.reset_index(drop=True) | |
| def outstanding_credit(db_path: str | Path | None = None) -> float: | |
| """Return total outstanding customer credit.""" | |
| balances = get_customer_balances(db_path) | |
| if balances.empty or "outstanding_balance" not in balances: | |
| return 0.0 | |
| total = pd.to_numeric(balances["outstanding_balance"], errors="coerce").fillna(0).sum() | |
| return round(float(total), 2) | |
| def low_stock_items( | |
| db_path: str | Path | None = None, | |
| threshold: float = DEFAULT_LOW_STOCK_THRESHOLD, | |
| ) -> pd.DataFrame: | |
| """Return inventory rows where current stock is below the threshold.""" | |
| inventory = get_inventory(db_path) | |
| if inventory.empty or "current_stock" not in inventory: | |
| return pd.DataFrame(columns=["item", "current_stock"]) | |
| stock = inventory.copy() | |
| stock["current_stock"] = pd.to_numeric(stock["current_stock"], errors="coerce").fillna(0) | |
| low_stock = stock[stock["current_stock"] < threshold] | |
| return low_stock.sort_values(["current_stock", "item"], ascending=[True, True]).reset_index(drop=True) | |
| def get_business_summary(db_path: str | Path | None = None, report_date: date | None = None) -> str: | |
| """Return a text summary of today's business performance for LLM analysis.""" | |
| settings = get_business_settings(db_path) | |
| currency = settings["currency_symbol"] | |
| sales = calculate_daily_sales(db_path, report_date) | |
| expenses = calculate_daily_expenses(db_path, report_date) | |
| profit = calculate_net_profit(db_path, report_date) | |
| credit = outstanding_credit(db_path) | |
| top_items = top_selling_items(db_path, report_date, limit=3) | |
| threshold = float(settings.get("low_stock_threshold", DEFAULT_LOW_STOCK_THRESHOLD)) | |
| low_stock = low_stock_items(db_path, threshold=threshold) | |
| summary = [ | |
| f"Business: {settings['business_name']}", | |
| f"Date: {report_date or date.today()}", | |
| f"Total Sales: {currency}{sales}", | |
| f"Total Expenses: {currency}{expenses}", | |
| f"Net Profit: {currency}{profit}", | |
| f"Outstanding Customer Credit: {currency}{credit}", | |
| ] | |
| if not top_items.empty: | |
| items_str = ", ".join([f"{row['item']} ({row['quantity_sold']} sold)" for _, row in top_items.iterrows()]) | |
| summary.append(f"Top Selling Items: {items_str}") | |
| if not low_stock.empty: | |
| low_stock_str = ", ".join([f"{row['item']} ({row['current_stock']} left)" for _, row in low_stock.iterrows()]) | |
| summary.append(f"Low Stock Alerts: {low_stock_str}") | |
| return "\n".join(summary) | |
| def _transactions_for_date(db_path: str | Path | None, report_date: date | None) -> pd.DataFrame: | |
| """Return transactions for the selected local date.""" | |
| selected_date = report_date or date.today() | |
| transactions = get_transactions(db_path) | |
| if transactions.empty or "created_at" not in transactions: | |
| return transactions | |
| created_dates = pd.to_datetime(transactions["created_at"], errors="coerce").dt.date | |
| return transactions.loc[created_dates == selected_date].copy() | |
| def _sum_amounts(transactions: pd.DataFrame, transaction_types: list[str]) -> float: | |
| """Sum transaction amounts for selected transaction types.""" | |
| if transactions.empty or "transaction_type" not in transactions or "amount" not in transactions: | |
| return 0.0 | |
| filtered = transactions[transactions["transaction_type"].isin(transaction_types)] | |
| if filtered.empty: | |
| return 0.0 | |
| total = pd.to_numeric(filtered["amount"], errors="coerce").fillna(0).sum() | |
| return round(float(total), 2) | |
| def _empty_top_items_frame() -> pd.DataFrame: | |
| """Return an empty top selling items frame with stable columns.""" | |
| return pd.DataFrame(columns=["item", "quantity_sold", "sales_amount"]) | |