VyaparFlow / app /services /inventory_service.py
Dipan04's picture
Add application file
32d42b3
"""
inventory_service.py
--------------------
Stage 6: Inventory Service for Notiflow
Tracks stock movements as a delta log in the Inventory Excel sheet.
Each event appends one row recording what changed, by how much,
and in which direction (in / out).
Design: delta-log (not current-stock snapshot)
- Every inventory change is a new row
- Current stock for an item = sum of all deltas for that item
- This keeps the history intact and avoids row-update complexity
Directions:
"out" β€” stock leaves (order fulfilled)
"in" β€” stock arrives (return accepted, restock)
"""
import logging
from datetime import datetime, timezone
from app.utils.excel_writer import append_row, read_sheet
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def deduct_stock(item: str, quantity: int | float, reference_id: str, note: str = "") -> dict:
"""
Record a stock deduction (items going out β€” e.g. an order is fulfilled).
Args:
item: Name of the inventory item.
quantity: Number of units being deducted.
reference_id: ID of the triggering record (e.g. order_id, invoice_id).
note: Optional human-readable note.
Returns:
The inventory movement record that was persisted.
Example:
>>> deduct_stock("kurti", 3, "ORD-20240115-0001", "order fulfilled")
{
"timestamp": "...",
"item": "kurti",
"change": 3,
"direction": "out",
"reference_id": "ORD-20240115-0001",
"note": "order fulfilled"
}
"""
if quantity is None or quantity <= 0:
logger.warning("deduct_stock called with invalid quantity: %s", quantity)
return {}
record = {
"timestamp": _now_iso(),
"item": item,
"change": quantity,
"direction": "out",
"reference_id": reference_id,
"note": note or "stock deducted",
}
append_row("Inventory", record)
logger.info("Stock deducted: %s Γ— %s (ref: %s)", quantity, item, reference_id)
return record
def add_stock(item: str, quantity: int | float, reference_id: str, note: str = "") -> dict:
"""
Record a stock addition (items coming in β€” e.g. a return is accepted).
Args:
item: Name of the inventory item.
quantity: Number of units being added.
reference_id: ID of the triggering record (e.g. return_id).
note: Optional human-readable note.
Returns:
The inventory movement record that was persisted.
"""
if quantity is None or quantity <= 0:
logger.warning("add_stock called with invalid quantity: %s", quantity)
return {}
record = {
"timestamp": _now_iso(),
"item": item,
"change": quantity,
"direction": "in",
"reference_id": reference_id,
"note": note or "stock added",
}
append_row("Inventory", record)
logger.info("Stock added: %s Γ— %s (ref: %s)", quantity, item, reference_id)
return record
def get_stock_level(item: str) -> int | float:
"""
Calculate the current stock level for an item by summing all deltas.
Args:
item: Name of the inventory item (case-insensitive match).
Returns:
Net stock level (int or float). Returns 0 if no records found.
Example:
>>> get_stock_level("kurti")
47
"""
df = read_sheet("Inventory")
if df.empty or "item" not in df.columns:
return 0
item_rows = df[df["item"].str.lower() == item.lower()]
if item_rows.empty:
return 0
total = 0
for _, row in item_rows.iterrows():
change = row.get("change", 0) or 0
direction = row.get("direction", "out")
if direction == "in":
total += change
else:
total -= change
return max(total, 0) # Stock can't go below 0 in the display