"""Read models used by the Finance Admin API.""" from decimal import Decimal from django.contrib.contenttypes.models import ContentType from django.db.models import Count, Max, OuterRef, Subquery from communications.models import Message from finance.balance_adjustments.services import locked_periods from finance.enums import ( ApprovalStatus, InputSource, PayrollPeriodStatus, ) from finance.models import ( PayrollBalanceAdjustment, PayrollCellCommentThread, PayrollInputLine, PayrollPeriod, PayrollStatement, ) from finance.payroll.components import get_payroll_components from payroll_core.balances import carryover_contribution from ..admin_serializers import ( PayrollCellCommentSerializer, PayrollComponentSerializer, PayrollEmployeeMiniSerializer, PayrollUserRefSerializer, ) def _cell_comment_summaries(period): threads = list( PayrollCellCommentThread.objects.filter( period=period, subject_employee__is_hidden_in_ui=False, ).values( "id", "subject_employee_id", "column_id", ) ) if not threads: return [] thread_by_id = {thread["id"]: thread for thread in threads} content_type = ContentType.objects.get_for_model(PayrollCellCommentThread) totals = ( Message.objects.filter( chat__type="comments", chat__context_content_type=content_type, chat__context_object_id__in=thread_by_id, is_deleted=False, ) .values("chat__context_object_id") .annotate( count=Count("id"), latest_at=Max("created_at"), latest_text=Subquery( Message.objects.filter( chat__type="comments", chat__context_content_type=content_type, chat__context_object_id=OuterRef("chat__context_object_id"), is_deleted=False, ) .order_by("-created_at", "-id") .values("content")[:1] ), ) ) result = [] for total in totals: thread = thread_by_id[int(total["chat__context_object_id"])] result.append( { "employee_id": thread["subject_employee_id"], "column_id": thread["column_id"], "count": total["count"], "latest_at": total["latest_at"], "latest_text": total["latest_text"], } ) return result def _serialize_cell_comment(message, request): return PayrollCellCommentSerializer( { "id": message.id, "author": message.author, "text": message.content, "created_at": message.created_at, }, context={"request": request}, ).data def _carryover_summary(period): statements = ( PayrollStatement.objects.filter( run=period.current_run, employee__is_hidden_in_ui=False, ) if period.current_run_id else PayrollStatement.objects.none() ) return { "outgoing": statements.exclude(closing_balance=0).count(), "incoming": statements.exclude(opening_balance=0).count(), "pending": 0, } def _automatic_balance_summaries(rows): summaries = [] for row in rows: balance = Decimal(row["closing_balance"]) if balance == 0: continue summaries.append( { "employee_id": row["employee"]["id"], "count": 1, "company_owes": str(max(balance, Decimal("0.00"))), "employee_owes": str(max(-balance, Decimal("0.00"))), "pending": 0, } ) return summaries def _carryover_ledger(period): lines = list( PayrollInputLine.objects.filter( period__currency=period.currency, period__date_to__lte=period.date_to, employee__is_hidden_in_ui=False, ) .exclude( source=InputSource.API, source_ref__startswith="payroll-carryover:", ) .select_related( "period", "employee", "employee__position", "created_by", ) .prefetch_related("employee__departments_links__department") .order_by("-period__date_to", "-created_at", "-id") ) components_by_period = { source_period.pk: { component.code: component for component in get_payroll_components(source_period) if component.include_in_carryover } for source_period in {line.period for line in lines} } entries = [] balances_by_employee = {} included_count = 0 excluded_count = 0 for line in lines: component = components_by_period.get(line.period_id, {}).get( line.component_code ) if component is None: continue current_period_line = line.period_id == period.pk active_status = line.status == ApprovalStatus.APPROVED or ( current_period_line and period.status != PayrollPeriodStatus.CLOSED and line.status == ApprovalStatus.DRAFT ) counts_in_balance = bool(line.include_in_carryover and active_status) raw_contribution = carryover_contribution( component.kind, line.amount, version=line.period.balance_rules_version ) contribution = raw_contribution if counts_in_balance else Decimal("0.00") balances_by_employee[line.employee_id] = ( balances_by_employee.get(line.employee_id, Decimal("0.00")) + contribution ) if line.include_in_carryover: included_count += 1 else: excluded_count += 1 entries.append( { "id": line.pk, "period": { "id": line.period_id, "code": line.period.code, "name": line.period.name, "date_from": line.period.date_from, "date_to": line.period.date_to, "currency": line.period.currency, }, "employee": PayrollEmployeeMiniSerializer(line.employee).data, "component": PayrollComponentSerializer(component).data, "amount": str(line.amount), "raw_contribution": str(raw_contribution), "contribution": str(contribution), "reason": line.reason, "status": line.status, "status_label": line.get_status_display(), "include_in_carryover": line.include_in_carryover, "counts_in_balance": counts_in_balance, "created_by": PayrollUserRefSerializer(line.created_by).data, "created_at": line.created_at, } ) frozen_starts = list( locked_periods( PayrollPeriod.objects.filter(currency=period.currency) ).values_list("date_from", flat=True) ) adjustments = ( PayrollBalanceAdjustment.objects.filter( effective_period__currency=period.currency, effective_period__date_from__lte=period.date_from, employee__is_hidden_in_ui=False, ) .select_related( "effective_period", "source_period", "employee", "employee__position", "created_by", "voided_by", ) .prefetch_related("employee__departments_links__department") ) for adjustment in adjustments: active = adjustment.voided_at is None contribution = adjustment.contribution if active else Decimal("0.00") balances_by_employee[adjustment.employee_id] = ( balances_by_employee.get(adjustment.employee_id, Decimal("0.00")) + contribution ) included_count += int(active) excluded_count += int(not active) effective = adjustment.effective_period entries.append( { "id": adjustment.pk, "entry_type": "manual_adjustment", "period": { "id": effective.pk, "code": effective.code, "name": effective.name, "date_from": effective.date_from, "date_to": effective.date_to, "currency": effective.currency, }, "source_period": ( { "id": adjustment.source_period_id, "name": adjustment.source_period.name, "code": adjustment.source_period.code, } if adjustment.source_period_id else None ), "employee": PayrollEmployeeMiniSerializer(adjustment.employee).data, "component": { "code": "MANUAL_BALANCE_ADJUSTMENT", "name": "Ручная корректировка остатка", }, "amount": str(adjustment.amount), "raw_contribution": str(adjustment.contribution), "contribution": str(contribution), "reason": adjustment.reason, "status": "approved" if active else "voided", "status_label": "Действует" if active else "Аннулирована", "include_in_carryover": active, "counts_in_balance": active, "created_by": PayrollUserRefSerializer(adjustment.created_by).data, "created_at": adjustment.created_at, "can_void": active and not any(start >= effective.date_from for start in frozen_starts), "voided_by": PayrollUserRefSerializer(adjustment.voided_by).data if adjustment.voided_by_id else None, "voided_at": adjustment.voided_at, "void_reason": adjustment.void_reason, } ) entries.sort( key=lambda entry: ( entry["period"]["date_from"], entry["created_at"], entry["id"], ), reverse=True, ) company_owes = sum( (max(balance, Decimal("0.00")) for balance in balances_by_employee.values()), Decimal("0.00"), ) employee_owes = sum( (max(-balance, Decimal("0.00")) for balance in balances_by_employee.values()), Decimal("0.00"), ) return { "period_id": period.pk, "currency": period.currency, "summary": { "included_count": included_count, "excluded_count": excluded_count, "company_owes": str(company_owes), "employee_owes": str(employee_owes), "balance": str(company_owes - employee_owes), }, "entries": entries, }