255 lines
8.4 KiB
Python
255 lines
8.4 KiB
Python
"""Code-owned payroll input component definitions."""
|
|
|
|
from dataclasses import dataclass, replace
|
|
|
|
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
|
|
include_in_point_base: bool = False
|
|
include_in_carryover: bool = False
|
|
visible_to_employee: bool = True
|
|
period_scope: str = PayrollComponentPeriodScope.ALL
|
|
scope_period_id: int | None = None
|
|
is_custom: bool = False
|
|
highlight_color: str = ""
|
|
scheduled_target_component_code: str = ""
|
|
|
|
@property
|
|
def id(self) -> str:
|
|
return self.code
|
|
|
|
@property
|
|
def pk(self) -> str:
|
|
return self.code
|
|
|
|
@property
|
|
def is_active(self) -> bool:
|
|
return True
|
|
|
|
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,
|
|
include_in_point_base=True,
|
|
),
|
|
PayrollComponentDefinition(
|
|
"CORRECTION_CREDIT",
|
|
"Положительная корректировка",
|
|
PayrollComponentKind.ADJUSTMENT_CREDIT,
|
|
True,
|
|
40,
|
|
include_in_carryover=True,
|
|
highlight_color="green",
|
|
),
|
|
PayrollComponentDefinition(
|
|
"CORRECTION_DEBIT",
|
|
"Отрицательная корректировка",
|
|
PayrollComponentKind.ADJUSTMENT_DEBIT,
|
|
True,
|
|
41,
|
|
include_in_carryover=True,
|
|
highlight_color="red",
|
|
),
|
|
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 FinanceSettings, PayrollCustomComponent, PayrollPeriod
|
|
|
|
finance_settings = FinanceSettings.objects.first()
|
|
overrides = finance_settings.payroll_component_overrides if finance_settings else {}
|
|
builtins = []
|
|
for component in PAYROLL_COMPONENTS:
|
|
override = overrides.get(component.code, {})
|
|
builtins.append(
|
|
replace(
|
|
component,
|
|
name=override.get("name", component.name),
|
|
kind=override.get("kind", component.kind),
|
|
requires_reason=override.get(
|
|
"requires_reason", component.requires_reason
|
|
),
|
|
include_in_point_base=override.get(
|
|
"include_in_point_base", component.include_in_point_base
|
|
),
|
|
include_in_carryover=override.get(
|
|
"include_in_carryover", component.include_in_carryover
|
|
),
|
|
visible_to_employee=override.get(
|
|
"visible_to_employee", component.visible_to_employee
|
|
),
|
|
highlight_color=override.get(
|
|
"highlight_color", component.highlight_color
|
|
),
|
|
period_scope=override.get("period_scope", component.period_scope),
|
|
scope_period_id=override.get(
|
|
"scope_period_id", component.scope_period_id
|
|
),
|
|
scheduled_target_component_code=override.get(
|
|
"scheduled_target_component_code",
|
|
component.scheduled_target_component_code,
|
|
),
|
|
)
|
|
)
|
|
|
|
custom = list(
|
|
PayrollCustomComponent.objects.filter(is_active=True)
|
|
.select_related(
|
|
"scope_period",
|
|
"hidden_from_period",
|
|
)
|
|
.prefetch_related("excluded_periods")
|
|
.order_by("display_order", "name", "id")
|
|
)
|
|
if period is not None:
|
|
referenced_period_ids = {
|
|
component.scope_period_id
|
|
for component in builtins
|
|
if component.scope_period_id is not None
|
|
}
|
|
period_dates = dict(
|
|
PayrollPeriod.objects.filter(pk__in=referenced_period_ids).values_list(
|
|
"pk", "date_from"
|
|
)
|
|
)
|
|
builtins = [
|
|
component
|
|
for component in builtins
|
|
if component.period_scope == PayrollComponentPeriodScope.ALL
|
|
or (
|
|
component.scope_period_id is not None
|
|
and (
|
|
(
|
|
component.period_scope
|
|
== PayrollComponentPeriodScope.CURRENT_ONLY
|
|
and component.scope_period_id == period.pk
|
|
)
|
|
or (
|
|
component.period_scope
|
|
== PayrollComponentPeriodScope.CURRENT_AND_FUTURE
|
|
and period_dates.get(
|
|
component.scope_period_id,
|
|
period.date_from,
|
|
)
|
|
<= period.date_from
|
|
)
|
|
)
|
|
)
|
|
]
|
|
custom = [
|
|
component for component in custom if component.applies_to_period(period)
|
|
]
|
|
return _PayrollComponentCollection((*builtins, *custom))
|
|
|
|
|
|
def get_payroll_component(code: str, *, period=None) -> PayrollComponentDefinition:
|
|
component = get_payroll_components(period).filter(code=code)
|
|
if component:
|
|
return component[0]
|
|
raise ValueError(f"Неизвестный вид зарплатной операции: {code}")
|