"""Calendar payroll period factory shared by scheduling and period closure.""" import logging from calendar import monthrange from datetime import date from django.core.exceptions import ValidationError from django.db import IntegrityError, transaction from finance.models import PayrollAuditEvent, PayrollPeriod logger = logging.getLogger(__name__) MONTH_NAMES_RU = ( "", "Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь", ) def ensure_calendar_payroll_period( reference_date: date, *, currency="RUB", actor=None, channel="automation", metadata=None, ): """Create the open payroll period for a month unless it already exists.""" 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, currency=currency, ).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=currency, created_by=actor, ) try: with transaction.atomic(): period.full_clean() period.save(force_insert=True) PayrollAuditEvent.objects.create( actor=actor, action="payroll.period_created", object_type=period._meta.label_lower, object_id=str(period.pk), period=period, metadata={"channel": channel, **(metadata or {})}, ) 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, currency=currency, ).first() if existing is not None: return existing, False raise except ValidationError as error: # A concurrent opening can win between the first lookup and full_clean. existing = PayrollPeriod.objects.filter( date_from=date_from, date_to=date_to, currency=currency ).first() if existing is not None: return existing, False 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