Игорь Надеин abef43136b
All checks were successful
Deploy production / deploy (push) Successful in 11m16s
Apply automatic payroll input transfer when closing periods
2026-10-19 10:47:57 +03:00

113 lines
3.2 KiB
Python

"""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