Corp/backend/finance/payroll/components.py
2026-08-26 14:50:51 +03:00

155 lines
5.3 KiB
Python

"""Code-owned payroll input component definitions."""
from dataclasses import dataclass
from finance.enums import PayrollComponentKind
from finance.enums import PayrollComponentPeriodScope
@dataclass(frozen=True, slots=True)
class PayrollComponentDefinition:
code: str
name: str
kind: str
requires_reason: bool
display_order: int
@property
def id(self) -> str:
return self.code
@property
def pk(self) -> str:
return self.code
@property
def is_active(self) -> bool:
return True
@property
def period_scope(self) -> str:
return PayrollComponentPeriodScope.ALL
@property
def scope_period_id(self) -> None:
return None
def get_kind_display(self) -> str:
return PayrollComponentKind(self.kind).label
PAYROLL_COMPONENTS = (
PayrollComponentDefinition("VACATION_PAY", "Отпускные", PayrollComponentKind.EARNING, False, 20),
PayrollComponentDefinition("BONUS", "Премия", PayrollComponentKind.EARNING, False, 30),
PayrollComponentDefinition("CORRECTION_CREDIT", "Положительная корректировка", PayrollComponentKind.ADJUSTMENT_CREDIT, True, 40),
PayrollComponentDefinition("CORRECTION_DEBIT", "Отрицательная корректировка", PayrollComponentKind.ADJUSTMENT_DEBIT, True, 41),
PayrollComponentDefinition("ONE_TIME_PAYMENT", "Разовое начисление", PayrollComponentKind.EARNING, True, 50),
PayrollComponentDefinition("DEDUCTION", "Удержание", PayrollComponentKind.DEDUCTION, True, 60),
PayrollComponentDefinition("ADVANCE", "Аванс", PayrollComponentKind.PAYMENT, False, 70),
PayrollComponentDefinition("SALARY_PAYMENT", "Фактическая выплата", PayrollComponentKind.PAYMENT, True, 80),
)
PAYROLL_COMPONENTS_BY_CODE = {component.code: component for component in PAYROLL_COMPONENTS}
PAYROLL_COMPONENT_CODE_CHOICES = tuple(
(component.code, component.name) for component in PAYROLL_COMPONENTS
)
class _PayrollComponentCollection(tuple):
def get(self, **criteria):
matches = self.filter(**criteria)
if len(matches) != 1:
raise LookupError(f"Ожидался один вид операции, найдено: {len(matches)}")
return matches[0]
def filter(self, **criteria):
return _PayrollComponentCollection(
component
for component in self
if all(getattr(component, key) == value for key, value in criteria.items())
)
def exclude(self, **criteria):
normalized = {
key.removesuffix("__in"): ("in" if key.endswith("__in") else "exact", value)
for key, value in criteria.items()
}
return _PayrollComponentCollection(
component
for component in self
if not all(
getattr(component, key) in value if lookup == "in" else getattr(component, key) == value
for key, (lookup, value) in normalized.items()
)
)
def all(self):
return self
def order_by(self, *fields):
result = list(self)
for field in reversed(fields):
reverse = field.startswith("-")
name = field.removeprefix("-")
result.sort(key=lambda component: getattr(component, name), reverse=reverse)
return _PayrollComponentCollection(result)
def values_list(self, field, *, flat=False):
values = [getattr(component, field) for component in self]
return values if flat else [(value,) for value in values]
class PayrollComponent:
"""Compatibility facade over the code registry; this is not a database model."""
class _Manager:
def all(self):
return get_payroll_components()
def get(self, **criteria):
return self.all().get(**criteria)
def filter(self, **criteria):
return self.all().filter(**criteria)
def exclude(self, **criteria):
return self.all().exclude(**criteria)
def order_by(self, *fields):
return self.all().order_by(*fields)
objects = _Manager()
def get_payroll_components(period=None) -> _PayrollComponentCollection:
"""Return built-ins plus active user-created component columns."""
from finance.models import PayrollCustomComponent
custom = list(
PayrollCustomComponent.objects.filter(is_active=True)
.select_related("scope_period")
.order_by("display_order", "name", "id")
)
if period is not None:
custom = [component for component in custom if component.applies_to_period(period)]
return _PayrollComponentCollection((*PAYROLL_COMPONENTS, *custom))
def get_payroll_component(code: str, *, period=None) -> PayrollComponentDefinition:
component = PAYROLL_COMPONENTS_BY_CODE.get(code)
if component is not None:
return component
from finance.models import PayrollCustomComponent
component = (
PayrollCustomComponent.objects.filter(code=code, is_active=True)
.select_related("scope_period")
.first()
)
if component is not None and (
period is None or component.applies_to_period(period)
):
return component
raise ValueError(f"Неизвестный вид зарплатной операции: {code}")