from datetime import date, datetime from decimal import Decimal import pytest from django.urls import reverse from django.utils import timezone from commerce.models import Cost, Product, Sale, SaleLine, SourceSystem from finance.analytics.models import ReportingEntry from finance.analytics.people import people_report from finance.analytics.sales import production_report, sales_report from finance.models import FinanceSettings, PayrollPeriod, PayrollRun, PayrollStatement pytestmark = pytest.mark.django_db def moment(day): return timezone.make_aware(datetime.fromisoformat(day + "T12:00:00")) @pytest.fixture def facts(): source = SourceSystem.objects.create(code="test-analytics", name="Test") product = Product.objects.create(sku="ANALYTICS", name="Test product") def line(ref, revenue="100", cost="40", day="2026-01-10", **kwargs): sale = Sale.objects.create( source=source, external_id=ref, occurred_at=moment(day), channel="direct", customer_external_id="client-1", customer_name="Client", **kwargs, ) return SaleLine.objects.create( sale=sale, product=product, external_id="1", quantity=1, gross_amount=Decimal(revenue), cost_amount=Decimal(cost), ) return source, product, line def test_sales_reconcile_refunds_expenses_currency_and_date_boundaries(facts): source, product, line = facts sale_line = line("sale") line("return", revenue="25", cost="10", event_type="return") line("cancelled", counts_as_sale=False) line("usd", currency="USD") line("outside", day="2026-02-01") Cost.objects.create( source=source, product=product, sale_line=sale_line, external_id="fee", amount=5, cost_type="commission", occurred_at=moment("2026-01-31"), ) Cost.objects.create( source=source, product=product, amount=2, cost_type="advertising", occurred_at=moment("2026-01-15"), ) report = sales_report(date(2026, 1, 1), date(2026, 1, 31), "RUB") assert report["summary"]["revenue"] == 75 assert report["summary"]["cost_of_goods"] == 30 assert report["summary"]["profit"] == 38 assert report["summary"]["expenses"] == 7 for dimension in [ "products", "channels", "customers", "product_channels", "monthly", ]: assert sum(row["profit"] for row in report[dimension]) == 38 assert next(r for r in report["customers"] if r["id"] == "unknown")["expenses"] == 2 assert report["quality"]["missing_customer_lines"] == 0 def test_unknown_cost_is_not_reported_as_profit_and_source_filter(facts): source, _, line = facts line("missing", cost="0") report = sales_report(date(2026, 1, 1), date(2026, 1, 31), "RUB", source) assert report["summary"]["profit"] is None assert report["summary"]["missing_cost_lines"] == 1 other = SourceSystem.objects.create(code="empty", name="Empty") assert ( sales_report(date(2026, 1, 1), date(2026, 1, 31), "RUB", other)["summary"][ "revenue" ] == 0 ) def test_report_api_auth_allowlist_validation_and_no_store(api_client, user_factory): url = reverse("api:v1:finance-payroll:analytics-report") assert api_client.get(url).status_code == 401 api_client.force_authenticate(user=user_factory()) assert api_client.get(url).status_code == 403 staff = user_factory(staff=True) api_client.force_authenticate(user=staff) params = {"date_from": "2026-01-01", "date_to": "2026-01-31", "currency": "RUB"} response = api_client.get(url, params) assert response.status_code == 200 assert response.data["sales"]["summary"]["revenue"] == "0.0000" assert "no-store" in response["Cache-Control"] for changes in [ {"date_from": "bad"}, {"date_to": "2025-01-01"}, {"date_to": "2028-01-01"}, {"currency": "rub"}, {"section": "bad"}, {"source": "nope"}, ]: assert api_client.get(url, {**params, **changes}).status_code == 400 FinanceSettings.get_solo().management_users.add(user_factory(staff=True)) assert api_client.get(url, params).status_code == 403 def test_journal_validates_duplicates_and_void_preserves_history( api_client, user_factory, facts ): _, product, _ = facts staff = user_factory(staff=True) api_client.force_authenticate(user=staff) url = reverse("api:v1:finance-payroll:analytics-entries") payload = { "kind": "production", "reference": "output-1", "date": "2026-01-10", "quantity": "3", "product": str(product.pk), } for change in [ {"product": None}, {"quantity": "-1"}, {"amount": "10"}, {"date": "2099-01-01"}, ]: assert ( api_client.post(url, {**payload, **change}, format="json").status_code == 400 ) response = api_client.post(url, payload, format="json") assert response.status_code == 201, response.data assert api_client.post(url, payload, format="json").status_code == 400 start, end = date(2026, 1, 1), date(2026, 1, 31) sales = sales_report(start, end, "RUB") assert production_report(start, end, "RUB", sales)["totals"]["production"] == 3 void_url = reverse( "api:v1:finance-payroll:analytics-entry-void", args=[response.data["id"]] ) assert api_client.post(void_url).status_code == 200 assert api_client.post(void_url).status_code == 200 assert ReportingEntry.objects.count() == 1 assert production_report(start, end, "RUB", sales)["totals"]["production"] == 0 def test_payroll_uses_published_revision_month_sums_and_actual_tax(user_factory): author, approver, employee = user_factory(), user_factory(), user_factory() for index, (start, end, gross) in enumerate( [ ("2026-01-01", "2026-01-15", 40), ("2026-01-16", "2026-01-31", 60), ("2026-02-01", "2026-02-28", 120), ] ): period = PayrollPeriod.objects.create( code=f"a-{index}", date_from=start, date_to=end, currency="RUB" ) run = PayrollRun.objects.create( period=period, revision=1, status="published", requested_by=author, approved_by=approver, approved_at=timezone.now(), published_by=approver, published_at=timezone.now(), gross_total=gross, deduction_total=0, payable_total=gross, ) PayrollStatement.objects.create( run=run, employee=employee, currency="RUB", gross_before_adjustments=gross, adjustment_total=0, gross_total=gross, deduction_total=0, net_pay=gross, payment_total=0, payable=gross, ) # A new draft must not hide or double-count the published run. PayrollRun.objects.create( period=period, revision=2, requested_by=author, gross_total=999, deduction_total=0, payable_total=999, ) ReportingEntry.objects.create( kind="tax", reference="tax-1", date=date(2026, 2, 10), employee=employee, amount=13, created_by=author, ) report = people_report(date(2026, 1, 1), date(2026, 2, 28), "RUB") row = next(r for r in report["employees"] if r["id"] == employee.pk) assert row["salary_total"] == 220 assert row["average_salary"] == 110 assert row["salary_growth_percent"] == 20 assert row["tax_paid"] == 13 assert row["attendance_days"] == 0 partial = people_report(date(2026, 1, 10), date(2026, 1, 31), "RUB") assert ( next(r for r in partial["employees"] if r["id"] == employee.pk)["salary_total"] == 60 ) def test_personnel_history_leave_overlap_missing_tax_and_future_hire(user_factory): from employees.models import EmployeeAction employee = user_factory() future = user_factory() EmployeeAction.objects.bulk_create( [ EmployeeAction( employee=employee, action="hired", date=moment("2025-01-01") ), EmployeeAction( employee=employee, action="on_leave", date=moment("2026-01-10"), date_to=date(2026, 1, 20), ), EmployeeAction( employee=employee, action="on_sick_leave", date=moment("2026-01-15"), date_to=date(2026, 1, 17), ), EmployeeAction(employee=future, action="hired", date=moment("2026-03-01")), ] ) report = people_report(date(2026, 1, 1), date(2026, 1, 31), "RUB") row = next(r for r in report["employees"] if r["id"] == employee.pk) assert row["vacation_days"] == 8 assert row["sick_days"] == 3 assert row["tenure_days"] == 395 assert all(r["id"] != future.pk for r in report["employees"]) assert report["summary"]["tax_paid"] is None assert report["summary"]["average_salary"] is None def test_customer_backfill_extracts_only_counterparty_and_is_idempotent(facts): from importlib import import_module from types import SimpleNamespace from django.apps import apps from django.db import connection from commerce.models import SourceRecord source, _, line = facts record = SourceRecord.objects.create( source=source, entity_type="demand", external_id="backfill", payload={ "agent": { "meta": {"href": "https://example.test/counterparty/client-id"}, "name": "Customer", } }, ) sale = line("backfill").sale sale.source_record = record sale.customer_external_id = "" sale.customer_name = "" sale.save() restore = import_module( "commerce.migrations.0013_restore_sale_customers" ).restore_customers restore(apps, SimpleNamespace(connection=connection)) restore(apps, SimpleNamespace(connection=connection)) sale.refresh_from_db() assert sale.customer_external_id == "client-id" assert sale.customer_name == "Customer" def test_production_api_excludes_resold_products(api_client, user_factory, facts): _, product, line = facts staff = user_factory(staff=True) api_client.force_authenticate(user=staff) line("manufactured") resold = Product.objects.create(sku="RESOLD", name="Resold") other = line("resold", revenue="999", cost="1") other.product = resold other.save() ReportingEntry.objects.create( kind="production", reference="previous-year", date=date(2025, 12, 1), product=product, quantity=10, created_by=staff, ) response = api_client.get( reverse("api:v1:finance-payroll:analytics-report"), {"section": "production", "date_from": "2026-01-01", "date_to": "2026-01-31"}, ) assert response.status_code == 200, response.data assert response.data["sales"]["summary"]["revenue"] == "100.0000" assert response.data["production"]["record_counts"]["production"] == 0