Corp/backend/finance/payroll/components.py
2026-09-07 19:43:48 +03:00

184 lines
5.5 KiB
Python

"""Code-owned payroll input component definitions."""
from dataclasses import dataclass
from finance.enums import PayrollComponentKind, 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}")