Corp/backend/finance/tasks.py

99 lines
2.8 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
from .payroll.carryovers import materialize_pending_carryovers
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:
materialize_pending_carryovers(existing)
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:
logger.exception("Could not open payroll period %s", period.code)
raise
logger.info("Opened payroll period %s", period.code)
materialize_pending_carryovers(period)
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}