update FlowChart
This commit is contained in:
@@ -413,14 +413,24 @@ async def daily_flow(
|
||||
(DAMAGE/WASTE/LOSS, reported separately for the stacked red segment).
|
||||
balance = running sum of EVERY signed movement (transfers/found/stock
|
||||
counts included), seeded with the opening balance before `date_from`.
|
||||
|
||||
Each day also carries a `breakdown` list — the ledger rows behind that
|
||||
day's aggregates, grouped by (category, transaction type, specs, brand,
|
||||
warehouse) — so the chart can subdivide categories into tonal shades and
|
||||
the tooltip can itemize them. When a single material is selected, the
|
||||
payload also carries that material's canonical `unit`.
|
||||
"""
|
||||
def _with_joins(sel):
|
||||
return (
|
||||
def _with_joins(sel, *, include_brand: bool = False):
|
||||
# ترتیب joinها مهم است: برند فقط بعد از join شدن inventory قابل ارجاع است
|
||||
stmt = (
|
||||
sel.select_from(InventoryTransaction)
|
||||
.join(Warehouse, Warehouse.id == InventoryTransaction.warehouse_id)
|
||||
.join(InventoryItem, InventoryItem.id == InventoryTransaction.inventory_id)
|
||||
.join(Material, Material.id == InventoryItem.material_id)
|
||||
)
|
||||
if include_brand:
|
||||
stmt = stmt.outerjoin(Brand, Brand.id == InventoryItem.brand_id)
|
||||
return stmt
|
||||
|
||||
scope_conditions = []
|
||||
if project_id is not None:
|
||||
@@ -437,18 +447,38 @@ async def daily_flow(
|
||||
if specs_hash:
|
||||
what_conditions.append(InventoryItem.specs_hash == specs_hash)
|
||||
|
||||
day_expr = func.date_trunc("day", InventoryTransaction.transaction_date).label("day")
|
||||
bucket_rows = (
|
||||
# «روز» بر مبنای تقویم تهران است؛ بدون این تبدیل، تراکنشهای بامداد
|
||||
# (۰۰:۰۰ تا ۰۳:۳۰ تهران) به روز قبلِ UTC میافتادند.
|
||||
day_expr = func.date_trunc(
|
||||
"day", func.timezone("Asia/Tehran", InventoryTransaction.transaction_date)
|
||||
).label("day")
|
||||
# ریزتراکنشها بهازای (نوع، مشخصات، برند، انبار) گروه میشوند تا هم تجمیع
|
||||
# روزانه و هم شکست چندبعدی نمودار از یک کوئری واحد بیایند (بدون تکرار رکورد).
|
||||
detail_rows = (
|
||||
await db.execute(
|
||||
_with_joins(
|
||||
select(day_expr, InventoryTransaction.transaction_type, func.sum(InventoryTransaction.quantity))
|
||||
select(
|
||||
day_expr,
|
||||
InventoryTransaction.transaction_type,
|
||||
InventoryItem.specs,
|
||||
Brand.name,
|
||||
Warehouse.name,
|
||||
func.sum(InventoryTransaction.quantity),
|
||||
)
|
||||
.where(
|
||||
InventoryTransaction.transaction_date >= date_from,
|
||||
InventoryTransaction.transaction_date < date_to,
|
||||
*scope_conditions,
|
||||
*what_conditions,
|
||||
)
|
||||
.group_by(day_expr, InventoryTransaction.transaction_type)
|
||||
.group_by(
|
||||
day_expr,
|
||||
InventoryTransaction.transaction_type,
|
||||
InventoryItem.specs,
|
||||
Brand.name,
|
||||
Warehouse.name,
|
||||
),
|
||||
include_brand=True,
|
||||
)
|
||||
)
|
||||
).all()
|
||||
@@ -465,10 +495,47 @@ async def daily_flow(
|
||||
)
|
||||
)
|
||||
|
||||
def category_of(tx_type: str) -> str:
|
||||
if tx_type == "PURCHASE":
|
||||
return "inflow"
|
||||
if tx_type == "RETURN":
|
||||
return "returns"
|
||||
if tx_type == "WITHDRAWAL":
|
||||
return "siteOut"
|
||||
if tx_type in WASTE_TYPES:
|
||||
return "waste"
|
||||
# انتقالها/شمارش/یافتشده — خارج از چهار سبد نمایشی، علامتدار
|
||||
return "other"
|
||||
|
||||
signed_by_day: dict[date_cls, dict[str, float]] = {}
|
||||
for day, tx_type, signed in bucket_rows:
|
||||
breakdown_by_day: dict[date_cls, list[dict]] = {}
|
||||
for day, tx_type, specs, brand_name, warehouse_name, signed in detail_rows:
|
||||
day_key = day.date() if isinstance(day, datetime) else day
|
||||
signed_by_day.setdefault(day_key, {})[str(tx_type)] = _qty(signed)
|
||||
tx_type = str(tx_type)
|
||||
qty = _qty(signed)
|
||||
day_types = signed_by_day.setdefault(day_key, {})
|
||||
day_types[tx_type] = round(day_types.get(tx_type, 0.0) + qty, 4)
|
||||
breakdown_by_day.setdefault(day_key, []).append({
|
||||
"category": category_of(tx_type),
|
||||
"transaction_type": tx_type,
|
||||
"specs": specs or None,
|
||||
"brand": brand_name or None,
|
||||
"warehouse": warehouse_name,
|
||||
"quantity": qty,
|
||||
})
|
||||
|
||||
unit = None
|
||||
if material_id is not None:
|
||||
unit_row = (
|
||||
await db.execute(
|
||||
select(Unit.code, Unit.name_fa)
|
||||
.select_from(Material)
|
||||
.join(Unit, Unit.id == Material.canonical_unit_id)
|
||||
.where(Material.id == material_id)
|
||||
)
|
||||
).first()
|
||||
if unit_row is not None:
|
||||
unit = {"code": unit_row[0], "name_fa": unit_row[1]}
|
||||
|
||||
days = []
|
||||
running = opening
|
||||
@@ -485,10 +552,11 @@ async def daily_flow(
|
||||
"waste": waste,
|
||||
"returns": round(types.get("RETURN", 0.0), 4) or 0.0,
|
||||
"balance": running,
|
||||
"breakdown": breakdown_by_day.get(current, []),
|
||||
})
|
||||
current += timedelta(days=1)
|
||||
|
||||
return {"data": {"opening_balance": opening, "days": days}}
|
||||
return {"data": {"opening_balance": opening, "unit": unit, "days": days}}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user