Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/app/services/business_dashboard_service.py

410 lines
14 KiB
Python
Executable file

from __future__ import annotations
import json
from datetime import datetime, timedelta
from decimal import Decimal
from types import SimpleNamespace
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
from sqlalchemy import and_, cast
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Session
from adapters.db.models.business import Business
from adapters.db.models.currency import Currency
from adapters.db.models.document import Document
from adapters.db.models.fiscal_year import FiscalYear
from adapters.db.repositories.business_permission_repo import BusinessPermissionRepository
from adapters.db.repositories.business_repo import BusinessRepository
from adapters.db.repositories.user_repo import UserRepository
from app.core.auth_dependency import AuthContext
from app.services.currency_quant import get_currency_quant_and_round
from app.services.document_line_fx_service import quantize_money
from app.services.invoice_service import (
INVOICE_PURCHASE,
INVOICE_PURCHASE_RETURN,
INVOICE_SALES,
INVOICE_SALES_RETURN,
)
from app.services.person_service import amount_in_document_currency_to_base
_STAT_INVOICE_TYPES = (
INVOICE_SALES,
INVOICE_SALES_RETURN,
INVOICE_PURCHASE,
INVOICE_PURCHASE_RETURN,
)
def get_business_dashboard_data(db: Session, business_id: int, ctx: AuthContext) -> Dict[str, Any]:
"""دریافت داده‌های داشبورد کسب و کار"""
business_repo = BusinessRepository(db)
business = business_repo.get_by_id(business_id)
if not business:
raise ValueError("کسب و کار یافت نشد")
# بررسی دسترسی کاربر
if not ctx.can_access_business(business_id):
raise ValueError("دسترسی غیرمجاز")
# دریافت اطلاعات کسب و کار
business_info = _get_business_info(business, db)
# دریافت آمار
statistics = _get_business_statistics(business_id, db)
# دریافت فعالیت‌های اخیر
recent_activities = _get_recent_activities(business_id, db)
return {
"business_info": business_info,
"statistics": statistics,
"recent_activities": recent_activities
}
def get_business_members(db: Session, business_id: int, ctx: AuthContext) -> Dict[str, Any]:
"""دریافت لیست اعضای کسب و کار"""
if not ctx.can_access_business(business_id):
raise ValueError("دسترسی غیرمجاز")
permission_repo = BusinessPermissionRepository(db)
user_repo = UserRepository(db)
# دریافت دسترسی‌های کسب و کار
permissions = permission_repo.get_business_users(business_id)
members = []
for permission in permissions:
user = user_repo.get_by_id(permission.user_id)
if user:
members.append({
"id": permission.id,
"user_id": user.id,
"first_name": user.first_name,
"last_name": user.last_name,
"email": user.email,
"mobile": user.mobile,
"role": _get_user_role(permission.business_permissions),
"permissions": permission.business_permissions or {},
"joined_at": permission.created_at.isoformat()
})
return {
"items": members,
"pagination": {
"total": len(members),
"page": 1,
"per_page": len(members),
"total_pages": 1,
"has_next": False,
"has_prev": False
}
}
def get_business_statistics(db: Session, business_id: int, ctx: AuthContext) -> Dict[str, Any]:
"""دریافت آمار تفصیلی کسب و کار"""
if not ctx.can_access_business(business_id):
raise ValueError("دسترسی غیرمجاز")
# آمار فروش ماهانه (نمونه)
sales_by_month = [
{"month": "2024-01", "amount": 500000},
{"month": "2024-02", "amount": 750000},
{"month": "2024-03", "amount": 600000}
]
# پرفروش‌ترین محصولات (نمونه)
top_products = [
{"name": "محصول A", "sales_count": 100, "revenue": 500000},
{"name": "محصول B", "sales_count": 80, "revenue": 400000},
{"name": "محصول C", "sales_count": 60, "revenue": 300000}
]
# آمار فعالیت اعضا
permission_repo = BusinessPermissionRepository(db)
members = permission_repo.get_business_users(business_id)
member_activity = {
"active_today": len([m for m in members if m.created_at.date() == datetime.now().date()]),
"active_this_week": len([m for m in members if m.created_at >= datetime.now() - timedelta(days=7)]),
"total_members": len(members)
}
return {
"sales_by_month": sales_by_month,
"top_products": top_products,
"member_activity": member_activity
}
def _get_business_info(business: Business, db: Session) -> Dict[str, Any]:
"""دریافت اطلاعات کسب و کار"""
permission_repo = BusinessPermissionRepository(db)
member_count = len(permission_repo.get_business_users(business.id))
return {
"id": business.id,
"name": business.name,
"business_type": business.business_type.value,
"business_field": business.business_field.value,
"owner_id": business.owner_id,
"address": business.address,
"phone": business.phone,
"mobile": business.mobile,
"created_at": business.created_at.isoformat(),
"member_count": member_count
}
def _currency_payload(currency: Optional[Currency]) -> Optional[Dict[str, Any]]:
if currency is None:
return None
return {
"id": int(currency.id),
"code": currency.code,
"title": currency.title,
"symbol": currency.symbol,
"decimal_places": int(getattr(currency, "decimal_places", None) or 0),
}
def _empty_statistics(
*,
currency: Optional[Currency] = None,
active_members: int = 0,
fiscal_year_id: Optional[int] = None,
) -> Dict[str, Any]:
return {
"total_sales": 0.0,
"total_purchases": 0.0,
"active_members": int(active_members),
"recent_transactions": 0,
"currency": _currency_payload(currency),
"fiscal_year_id": fiscal_year_id,
}
def _net_from_document(document: Any) -> Decimal:
extra = getattr(document, "extra_info", None)
if not isinstance(extra, dict):
extra = {}
totals = extra.get("totals")
if not isinstance(totals, dict):
totals = {}
try:
return Decimal(str(totals.get("net", 0) or 0))
except Exception:
return Decimal(0)
def _signed_net_for_type(document_type: str, net: Decimal) -> Tuple[Decimal, Decimal, int]:
"""برمی‌گرداند (فروش، خرید، تعداد فاکتور فروش). برگشت‌ها با علامت منفی در همان ستون."""
if document_type == INVOICE_SALES:
return net, Decimal(0), 1
if document_type == INVOICE_SALES_RETURN:
return -net, Decimal(0), 0
if document_type == INVOICE_PURCHASE:
return Decimal(0), net, 0
if document_type == INVOICE_PURCHASE_RETURN:
return Decimal(0), -net, 0
return Decimal(0), Decimal(0), 0
def aggregate_invoice_totals_in_base(
db: Optional[Session],
documents: Sequence[Any],
*,
rate_cache: Optional[Dict[int, Decimal]] = None,
base_currency_by_business: Optional[Dict[int, Optional[int]]] = None,
) -> Tuple[Decimal, Decimal, int]:
"""جمع فروش و خرید به ارز پایه با نرخ تسعیر سند (extra_info.fx سپس نرخ تاریخ سند)."""
cache = rate_cache if rate_cache is not None else {}
base_by_biz = base_currency_by_business if base_currency_by_business is not None else {}
total_sales = Decimal(0)
total_purchases = Decimal(0)
sales_count = 0
for doc in documents:
if getattr(doc, "is_proforma", False):
continue
doc_type = str(getattr(doc, "document_type", "") or "")
net = _net_from_document(doc)
if net == 0:
_sales_delta, _purchase_delta, count_delta = _signed_net_for_type(
doc_type, Decimal(0)
)
sales_count += count_delta
continue
net_base = amount_in_document_currency_to_base(
db,
doc,
net,
rate_cache=cache,
base_currency_by_business=base_by_biz,
)
sales_delta, purchase_delta, count_delta = _signed_net_for_type(doc_type, net_base)
total_sales += sales_delta
total_purchases += purchase_delta
sales_count += count_delta
return total_sales, total_purchases, sales_count
def _as_dict(value: Any) -> Optional[Dict[str, Any]]:
if isinstance(value, dict):
return value
if isinstance(value, str) and value.strip().startswith("{"):
try:
parsed = json.loads(value)
except Exception:
return None
return parsed if isinstance(parsed, dict) else None
return None
def _stat_docs_from_rows(rows: Iterable[Any]) -> List[Any]:
docs: List[Any] = []
for row in rows:
totals = _as_dict(getattr(row, "totals", None))
fx = _as_dict(getattr(row, "fx", None))
extra: Dict[str, Any] = {}
if totals is not None:
extra["totals"] = totals
if fx is not None:
extra["fx"] = fx
docs.append(
SimpleNamespace(
id=int(row.id),
business_id=int(row.business_id),
currency_id=int(row.currency_id) if row.currency_id is not None else None,
document_type=row.document_type,
document_date=row.document_date,
registered_at=row.registered_at,
is_proforma=bool(getattr(row, "is_proforma", False)),
extra_info=extra,
)
)
return docs
def _load_stat_invoice_docs(db: Session, business_id: int, fiscal_year_id: int) -> List[Any]:
extra_jb = cast(Document.extra_info, JSONB)
rows = (
db.query(
Document.id,
Document.business_id,
Document.currency_id,
Document.document_type,
Document.document_date,
Document.registered_at,
Document.is_proforma,
extra_jb["totals"].label("totals"),
extra_jb["fx"].label("fx"),
)
.filter(
and_(
Document.business_id == business_id,
Document.fiscal_year_id == fiscal_year_id,
Document.is_proforma.is_(False),
Document.document_type.in_(_STAT_INVOICE_TYPES),
)
)
.all()
)
return _stat_docs_from_rows(rows)
def _get_business_statistics(business_id: int, db: Session) -> Dict[str, Any]:
"""آمار واقعی سال مالی جاری: فروش/خرید به ارز پایه با تسعیر اسناد چندارزی."""
permission_repo = BusinessPermissionRepository(db)
active_members = len(permission_repo.get_business_users(business_id))
business = db.get(Business, business_id)
currency: Optional[Currency] = None
if business is not None and business.default_currency_id:
currency = db.get(Currency, int(business.default_currency_id))
fy = (
db.query(FiscalYear)
.filter(
and_(
FiscalYear.business_id == business_id,
FiscalYear.is_last.is_(True),
)
)
.first()
)
if fy is None:
return _empty_statistics(currency=currency, active_members=active_members)
docs = _load_stat_invoice_docs(db, business_id, int(fy.id))
total_sales, total_purchases, sales_count = aggregate_invoice_totals_in_base(
db,
docs,
rate_cache={},
base_currency_by_business={
business_id: int(currency.id) if currency is not None else None
},
)
quant = Decimal("1")
round_on = True
if currency is not None:
quant, round_on = get_currency_quant_and_round(db, int(currency.id))
total_sales = quantize_money(total_sales, quant, round_on=round_on)
total_purchases = quantize_money(total_purchases, quant, round_on=round_on)
return {
"total_sales": float(total_sales),
"total_purchases": float(total_purchases),
"active_members": int(active_members),
"recent_transactions": int(sales_count),
"currency": _currency_payload(currency),
"fiscal_year_id": int(fy.id),
}
def _get_recent_activities(business_id: int, db: Session) -> List[Dict[str, Any]]:
"""دریافت فعالیت‌های اخیر"""
# در اینجا می‌توانید فعالیت‌های واقعی را از جداول مربوطه دریافت کنید
# فعلاً داده‌های نمونه برمی‌گردانیم
return [
{
"id": 1,
"title": "فروش جدید",
"description": "فروش محصول A به مبلغ 100,000 تومان",
"icon": "sell",
"time_ago": "2 ساعت پیش"
},
{
"id": 2,
"title": "عضو جدید",
"description": "احمد احمدی به تیم اضافه شد",
"icon": "person_add",
"time_ago": "5 ساعت پیش"
},
{
"id": 3,
"title": "گزارش ماهانه",
"description": "گزارش فروش ماه ژانویه تولید شد",
"icon": "assessment",
"time_ago": "1 روز پیش"
}
]
def _get_user_role(permissions: Optional[Dict[str, Any]]) -> str:
"""تعیین نقش کاربر بر اساس دسترسی‌ها"""
if not permissions:
return "عضو"
# بررسی دسترسی‌های مختلف برای تعیین نقش
if permissions.get("settings", {}).get("manage_users"):
return "مدیر"
elif permissions.get("sales", {}).get("write"):
return "مدیر فروش"
elif permissions.get("accounting", {}).get("write"):
return "حسابدار"
else:
return "عضو"