from collections import defaultdict from django.utils import timezone from commerce.models import ExternalProject, ProjectPayment from finance.analytics.models import ReportingEntry from finance.analytics.sales import ZERO, bounds, bucket, finish, months def payment_queryset(start, end, currency, source=None, project=None): lower, upper = bounds(start, end) qs = ProjectPayment.objects.filter( currency=currency, occurred_at__gte=lower, occurred_at__lt=upper, applicable=True, missing_from_source=False, ).select_related("project", "source") if source: qs = qs.filter(source=source) if project is not None: qs = qs.filter(project_id=None if project == "unassigned" else project.pk) return qs def journal_queryset(start, end, currency, source, project): entries = ReportingEntry.objects.filter( date__range=(start, end), currency=currency, voided_at__isnull=True, kind__in=["cash_out", "overhead", "purchase"], ).select_related("project") if source: entries = entries.filter(project__source=source) if project is not None: entries = entries.filter( project_id=None if project == "unassigned" else project.pk ) return entries def projects_report(start, end, currency, sales, source=None, project=None): rows = {} def row_for(key, name): if key not in rows: rows[key] = { **finish(bucket()), "id": key, "name": name, "paid": ZERO, "payment_count": 0, "manual_paid": ZERO, "overhead": ZERO, "purchases": ZERO, "archived": False, } return rows[key] catalog = ExternalProject.objects.all() if source: catalog = catalog.filter(source=source) if project is not None: catalog = ( catalog.none() if project == "unassigned" else catalog.filter(pk=project.pk) ) for p in catalog: row_for(f"project:{p.pk}", p.name)["archived"] = p.archived for sale_row in sales["projects"]: row_for(sale_row["id"], sale_row["name"]).update(sale_row) monthly = { m: {"id": m, "name": m, "paid": ZERO, "manual_paid": ZERO} for m in months(start, end) } categories = defaultdict(lambda: {"paid": ZERO, "payment_count": 0}) for p in payment_queryset(start, end, currency, source, project).iterator( chunk_size=500 ): row = row_for( f"project:{p.project_id}" if p.project_id else "unassigned", p.project.name if p.project else "Без проекта", ) row["paid"] += p.amount row["payment_count"] += 1 monthly[timezone.localtime(p.occurred_at).strftime("%Y-%m")]["paid"] += p.amount category = categories[p.expense_category or "Статья не указана"] category["paid"] += p.amount category["payment_count"] += 1 for entry in journal_queryset(start, end, currency, source, project): row = row_for( f"project:{entry.project_id}" if entry.project_id else "unassigned", entry.project.name if entry.project else "Без проекта", ) field = { "cash_out": "manual_paid", "overhead": "overhead", "purchase": "purchases", }[entry.kind] row[field] += entry.amount if entry.kind == "cash_out": monthly[entry.date.strftime("%Y-%m")]["manual_paid"] += entry.amount summary = { field: sum((r[field] for r in rows.values()), ZERO) for field in ["paid", "manual_paid", "overhead", "purchases", "payment_count"] } summary["unassigned_paid"] = rows.get("unassigned", {}).get("paid", ZERO) summary["project_count"] = catalog.count() return { "summary": summary, "rows": sorted(rows.values(), key=lambda r: (-r["paid"], r["name"])), "monthly": list(monthly.values()), "categories": [ {"id": k, "name": k, **v} for k, v in sorted(categories.items()) ], }