"""Periodic Finance tasks.""" from __future__ import annotations import logging from calendar import monthrange from datetime import date from celery import shared_task from django.core.exceptions import ValidationError from django.db import IntegrityError, transaction from django.utils import timezone from .models import PayrollAuditEvent, PayrollPeriod logger = logging.getLogger(__name__) MONTH_NAMES_RU = ( "", "Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь", ) def ensure_monthly_payroll_period(reference_date: date | None = None): """Create the open payroll period for a month unless it already exists.""" reference_date = reference_date or timezone.localdate() date_from = reference_date.replace(day=1) date_to = reference_date.replace( day=monthrange(reference_date.year, reference_date.month)[1] ) existing = PayrollPeriod.objects.filter( date_from=date_from, date_to=date_to, ).first() if existing is not None: return existing, False period = PayrollPeriod( code=date_from.strftime("%Y-%m"), name=f"{MONTH_NAMES_RU[date_from.month]} {date_from.year}", date_from=date_from, date_to=date_to, currency="RUB", created_by=None, ) try: with transaction.atomic(): period.full_clean() period.save(force_insert=True) PayrollAuditEvent.objects.create( actor=None, action="payroll.period_created", object_type=period._meta.label_lower, object_id=str(period.pk), period=period, metadata={"channel": "automation", "schedule": "monthly"}, ) except IntegrityError: # Celery retries or two workers can race. The database constraint makes # creation safe; return the winner when it covers the same month. existing = PayrollPeriod.objects.filter( date_from=date_from, date_to=date_to, ).first() if existing is not None: return existing, False raise except ValidationError as error: logger.exception( "Payroll period opening failed", extra={ "event": "finance.payroll_period.open_failed", "payroll_period_id": period.pk, "period_code": period.code, "error_type": type(error).__name__, }, ) raise logger.info( "Payroll period opened", extra={ "event": "finance.payroll_period.opened", "payroll_period_id": period.pk, "period_code": period.code, }, ) return period, True @shared_task def open_monthly_payroll_period(): """Open the current calendar month's payroll period.""" period, created = ensure_monthly_payroll_period() return {"period_id": period.pk, "code": period.code, "created": created}