111 lines
3.1 KiB
Python
111 lines
3.1 KiB
Python
"""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}
|