1543 lines
60 KiB
Python
1543 lines
60 KiB
Python
# Generated by Django 5.2.4 on 2026-07-17 12:13
|
||
|
||
import django.core.validators
|
||
import django.db.models.deletion
|
||
import uuid
|
||
from decimal import Decimal
|
||
from django.conf import settings
|
||
from django.db import migrations, models
|
||
|
||
|
||
class Migration(migrations.Migration):
|
||
initial = True
|
||
|
||
dependencies = [
|
||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||
]
|
||
|
||
operations = [
|
||
migrations.CreateModel(
|
||
name="PayrollComponent",
|
||
fields=[
|
||
(
|
||
"id",
|
||
models.BigAutoField(
|
||
auto_created=True,
|
||
primary_key=True,
|
||
serialize=False,
|
||
verbose_name="ID",
|
||
),
|
||
),
|
||
(
|
||
"code",
|
||
models.CharField(
|
||
max_length=64,
|
||
unique=True,
|
||
validators=[
|
||
django.core.validators.RegexValidator(
|
||
message="Код должен начинаться с A-Z и содержать только A-Z, 0-9, _.-",
|
||
regex="^[A-Z][A-Z0-9_.-]{0,63}$",
|
||
)
|
||
],
|
||
verbose_name="Код",
|
||
),
|
||
),
|
||
("name", models.CharField(max_length=160, verbose_name="Название")),
|
||
(
|
||
"kind",
|
||
models.CharField(
|
||
choices=[
|
||
("earning", "Начисление"),
|
||
("adjustment_credit", "Коррекция +"),
|
||
("adjustment_debit", "Коррекция −"),
|
||
("deduction", "Удержание"),
|
||
("payment", "Выплата"),
|
||
],
|
||
max_length=24,
|
||
verbose_name="Тип",
|
||
),
|
||
),
|
||
(
|
||
"requires_reason",
|
||
models.BooleanField(
|
||
default=False, verbose_name="Требует основание"
|
||
),
|
||
),
|
||
(
|
||
"is_active",
|
||
models.BooleanField(default=True, verbose_name="Активен"),
|
||
),
|
||
(
|
||
"display_order",
|
||
models.PositiveIntegerField(default=100, verbose_name="Порядок"),
|
||
),
|
||
],
|
||
options={
|
||
"verbose_name": "Компонент зарплаты",
|
||
"verbose_name_plural": "Компоненты зарплаты",
|
||
"ordering": ["display_order", "code"],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name="PayrollPeriod",
|
||
fields=[
|
||
(
|
||
"id",
|
||
models.BigAutoField(
|
||
auto_created=True,
|
||
primary_key=True,
|
||
serialize=False,
|
||
verbose_name="ID",
|
||
),
|
||
),
|
||
(
|
||
"code",
|
||
models.CharField(max_length=32, unique=True, verbose_name="Код"),
|
||
),
|
||
(
|
||
"name",
|
||
models.CharField(
|
||
blank=True, max_length=120, verbose_name="Название"
|
||
),
|
||
),
|
||
("date_from", models.DateField(verbose_name="Начало периода")),
|
||
("date_to", models.DateField(verbose_name="Конец периода")),
|
||
(
|
||
"pay_date",
|
||
models.DateField(
|
||
blank=True, null=True, verbose_name="Дата выплаты"
|
||
),
|
||
),
|
||
(
|
||
"currency",
|
||
models.CharField(
|
||
default="RUB",
|
||
max_length=3,
|
||
validators=[
|
||
django.core.validators.RegexValidator(
|
||
message="Валюта должна быть трёхбуквенным кодом в верхнем регистре.",
|
||
regex="^[A-Z]{3}$",
|
||
)
|
||
],
|
||
verbose_name="Валюта",
|
||
),
|
||
),
|
||
(
|
||
"status",
|
||
models.CharField(
|
||
choices=[
|
||
("open", "Открыт"),
|
||
("calculated", "Рассчитан"),
|
||
("review", "На проверке"),
|
||
("approved", "Утверждён"),
|
||
("published", "Опубликован"),
|
||
("closed", "Закрыт"),
|
||
],
|
||
default="open",
|
||
max_length=20,
|
||
verbose_name="Статус",
|
||
),
|
||
),
|
||
(
|
||
"lock_version",
|
||
models.PositiveIntegerField(
|
||
default=0, verbose_name="Версия блокировки"
|
||
),
|
||
),
|
||
(
|
||
"created_at",
|
||
models.DateTimeField(auto_now_add=True, verbose_name="Создан"),
|
||
),
|
||
(
|
||
"updated_at",
|
||
models.DateTimeField(auto_now=True, verbose_name="Обновлён"),
|
||
),
|
||
(
|
||
"created_by",
|
||
models.ForeignKey(
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="created_payroll_periods",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Создал",
|
||
),
|
||
),
|
||
],
|
||
options={
|
||
"verbose_name": "Расчётный период",
|
||
"verbose_name_plural": "Расчётные периоды",
|
||
"ordering": ["-date_from", "-id"],
|
||
"permissions": [
|
||
("manage_payroll_inputs", "Может управлять данными для расчёта"),
|
||
("approve_payroll_inputs", "Может утверждать данные для расчёта"),
|
||
("calculate_payroll", "Может запускать расчёт зарплаты"),
|
||
("approve_payroll", "Может утверждать расчёт зарплаты"),
|
||
("publish_payroll", "Может публиковать расчётные листки"),
|
||
("view_all_payroll", "Может просматривать все расчёты"),
|
||
("audit_payroll", "Может просматривать аудит расчётов"),
|
||
],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name="PayrollRun",
|
||
fields=[
|
||
(
|
||
"id",
|
||
models.BigAutoField(
|
||
auto_created=True,
|
||
primary_key=True,
|
||
serialize=False,
|
||
verbose_name="ID",
|
||
),
|
||
),
|
||
("revision", models.PositiveIntegerField(verbose_name="Ревизия")),
|
||
(
|
||
"status",
|
||
models.CharField(
|
||
choices=[
|
||
("calculated", "Рассчитан"),
|
||
("review", "На проверке"),
|
||
("approved", "Утверждён"),
|
||
("published", "Опубликован"),
|
||
("superseded", "Заменён новой ревизией"),
|
||
],
|
||
default="calculated",
|
||
max_length=20,
|
||
verbose_name="Статус",
|
||
),
|
||
),
|
||
(
|
||
"idempotency_key",
|
||
models.UUIDField(
|
||
default=uuid.uuid4,
|
||
editable=False,
|
||
unique=True,
|
||
verbose_name="Ключ идемпотентности",
|
||
),
|
||
),
|
||
(
|
||
"ruleset_id",
|
||
models.CharField(max_length=120, verbose_name="Набор правил"),
|
||
),
|
||
(
|
||
"ruleset_version",
|
||
models.CharField(max_length=64, verbose_name="Версия правил"),
|
||
),
|
||
(
|
||
"ruleset_hash",
|
||
models.CharField(max_length=64, verbose_name="Хэш правил"),
|
||
),
|
||
(
|
||
"calculator_version",
|
||
models.CharField(max_length=64, verbose_name="Версия калькулятора"),
|
||
),
|
||
(
|
||
"input_hash",
|
||
models.CharField(max_length=64, verbose_name="Хэш входных данных"),
|
||
),
|
||
(
|
||
"result_hash",
|
||
models.CharField(max_length=64, verbose_name="Хэш результата"),
|
||
),
|
||
(
|
||
"employee_count",
|
||
models.PositiveIntegerField(default=0, verbose_name="Сотрудников"),
|
||
),
|
||
(
|
||
"gross_total",
|
||
models.DecimalField(
|
||
decimal_places=2,
|
||
default=Decimal("0"),
|
||
max_digits=21,
|
||
verbose_name="Всего начислено",
|
||
),
|
||
),
|
||
(
|
||
"deduction_total",
|
||
models.DecimalField(
|
||
decimal_places=2,
|
||
default=Decimal("0"),
|
||
max_digits=21,
|
||
verbose_name="Всего удержано",
|
||
),
|
||
),
|
||
(
|
||
"payable_total",
|
||
models.DecimalField(
|
||
decimal_places=2,
|
||
default=Decimal("0"),
|
||
max_digits=21,
|
||
verbose_name="Всего к выплате",
|
||
),
|
||
),
|
||
(
|
||
"requested_at",
|
||
models.DateTimeField(auto_now_add=True, verbose_name="Рассчитан"),
|
||
),
|
||
(
|
||
"approved_at",
|
||
models.DateTimeField(
|
||
blank=True, null=True, verbose_name="Утверждён"
|
||
),
|
||
),
|
||
(
|
||
"published_at",
|
||
models.DateTimeField(
|
||
blank=True, null=True, verbose_name="Опубликован"
|
||
),
|
||
),
|
||
(
|
||
"approved_by",
|
||
models.ForeignKey(
|
||
blank=True,
|
||
null=True,
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="approved_payroll_runs",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Утвердил",
|
||
),
|
||
),
|
||
(
|
||
"period",
|
||
models.ForeignKey(
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="runs",
|
||
to="finance.payrollperiod",
|
||
verbose_name="Период",
|
||
),
|
||
),
|
||
(
|
||
"published_by",
|
||
models.ForeignKey(
|
||
blank=True,
|
||
null=True,
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="published_payroll_runs",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Опубликовал",
|
||
),
|
||
),
|
||
(
|
||
"requested_by",
|
||
models.ForeignKey(
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="requested_payroll_runs",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Запустил",
|
||
),
|
||
),
|
||
(
|
||
"supersedes",
|
||
models.ForeignKey(
|
||
blank=True,
|
||
null=True,
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="superseded_by_runs",
|
||
to="finance.payrollrun",
|
||
verbose_name="Пересчитывает ревизию",
|
||
),
|
||
),
|
||
],
|
||
options={
|
||
"verbose_name": "Запуск расчёта",
|
||
"verbose_name_plural": "Запуски расчёта",
|
||
"ordering": ["-period__date_from", "-revision"],
|
||
},
|
||
),
|
||
migrations.AddField(
|
||
model_name="payrollperiod",
|
||
name="current_run",
|
||
field=models.ForeignKey(
|
||
blank=True,
|
||
null=True,
|
||
on_delete=django.db.models.deletion.SET_NULL,
|
||
related_name="active_for_periods",
|
||
to="finance.payrollrun",
|
||
verbose_name="Текущая ревизия расчёта",
|
||
),
|
||
),
|
||
migrations.CreateModel(
|
||
name="PayrollStatement",
|
||
fields=[
|
||
(
|
||
"id",
|
||
models.BigAutoField(
|
||
auto_created=True,
|
||
primary_key=True,
|
||
serialize=False,
|
||
verbose_name="ID",
|
||
),
|
||
),
|
||
(
|
||
"employee_snapshot",
|
||
models.JSONField(default=dict, verbose_name="Снимок сотрудника"),
|
||
),
|
||
(
|
||
"currency",
|
||
models.CharField(
|
||
max_length=3,
|
||
validators=[
|
||
django.core.validators.RegexValidator(
|
||
message="Валюта должна быть трёхбуквенным кодом в верхнем регистре.",
|
||
regex="^[A-Z]{3}$",
|
||
)
|
||
],
|
||
verbose_name="Валюта",
|
||
),
|
||
),
|
||
(
|
||
"point_delta",
|
||
models.DecimalField(
|
||
blank=True,
|
||
decimal_places=4,
|
||
max_digits=19,
|
||
null=True,
|
||
verbose_name="Отклонение баллов",
|
||
),
|
||
),
|
||
(
|
||
"gross_before_adjustments",
|
||
models.DecimalField(
|
||
decimal_places=2,
|
||
max_digits=21,
|
||
verbose_name="Начислено до корректировок",
|
||
),
|
||
),
|
||
(
|
||
"adjustment_total",
|
||
models.DecimalField(
|
||
decimal_places=2, max_digits=21, verbose_name="Корректировки"
|
||
),
|
||
),
|
||
(
|
||
"gross_total",
|
||
models.DecimalField(
|
||
decimal_places=2, max_digits=21, verbose_name="Итого начислено"
|
||
),
|
||
),
|
||
(
|
||
"deduction_total",
|
||
models.DecimalField(
|
||
decimal_places=2, max_digits=21, verbose_name="Удержания"
|
||
),
|
||
),
|
||
(
|
||
"net_pay",
|
||
models.DecimalField(
|
||
decimal_places=2, max_digits=21, verbose_name="После удержаний"
|
||
),
|
||
),
|
||
(
|
||
"payment_total",
|
||
models.DecimalField(
|
||
decimal_places=2, max_digits=21, verbose_name="Уже выплачено"
|
||
),
|
||
),
|
||
(
|
||
"payable",
|
||
models.DecimalField(
|
||
decimal_places=2, max_digits=21, verbose_name="К выплате"
|
||
),
|
||
),
|
||
(
|
||
"input_hash",
|
||
models.CharField(max_length=64, verbose_name="Хэш входных данных"),
|
||
),
|
||
(
|
||
"result_hash",
|
||
models.CharField(max_length=64, verbose_name="Хэш результата"),
|
||
),
|
||
(
|
||
"input_snapshot",
|
||
models.JSONField(default=dict, verbose_name="Входной снимок"),
|
||
),
|
||
(
|
||
"result_snapshot",
|
||
models.JSONField(default=dict, verbose_name="Результат расчёта"),
|
||
),
|
||
(
|
||
"created_at",
|
||
models.DateTimeField(auto_now_add=True, verbose_name="Создан"),
|
||
),
|
||
(
|
||
"employee",
|
||
models.ForeignKey(
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="payroll_statements",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Сотрудник",
|
||
),
|
||
),
|
||
(
|
||
"run",
|
||
models.ForeignKey(
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="statements",
|
||
to="finance.payrollrun",
|
||
verbose_name="Запуск",
|
||
),
|
||
),
|
||
(
|
||
"supersedes",
|
||
models.OneToOneField(
|
||
blank=True,
|
||
null=True,
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="superseded_by_statement",
|
||
to="finance.payrollstatement",
|
||
verbose_name="Заменяет листок",
|
||
),
|
||
),
|
||
],
|
||
options={
|
||
"verbose_name": "Расчётный листок",
|
||
"verbose_name_plural": "Расчётные листки",
|
||
"ordering": ["-run__period__date_from", "employee_id"],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name="PayrollStatementAcknowledgement",
|
||
fields=[
|
||
(
|
||
"id",
|
||
models.BigAutoField(
|
||
auto_created=True,
|
||
primary_key=True,
|
||
serialize=False,
|
||
verbose_name="ID",
|
||
),
|
||
),
|
||
(
|
||
"content_hash",
|
||
models.CharField(
|
||
max_length=64, verbose_name="Хэш подтверждённого листка"
|
||
),
|
||
),
|
||
(
|
||
"idempotency_key",
|
||
models.UUIDField(
|
||
default=uuid.uuid4,
|
||
editable=False,
|
||
unique=True,
|
||
verbose_name="Ключ идемпотентности",
|
||
),
|
||
),
|
||
(
|
||
"viewed_at",
|
||
models.DateTimeField(
|
||
blank=True, null=True, verbose_name="Просмотрен"
|
||
),
|
||
),
|
||
(
|
||
"acknowledged_at",
|
||
models.DateTimeField(
|
||
blank=True, null=True, verbose_name="Получение подтверждено"
|
||
),
|
||
),
|
||
(
|
||
"disputed_at",
|
||
models.DateTimeField(blank=True, null=True, verbose_name="Оспорен"),
|
||
),
|
||
(
|
||
"dispute_reason",
|
||
models.TextField(blank=True, verbose_name="Причина оспаривания"),
|
||
),
|
||
(
|
||
"created_at",
|
||
models.DateTimeField(auto_now_add=True, verbose_name="Создано"),
|
||
),
|
||
(
|
||
"updated_at",
|
||
models.DateTimeField(auto_now=True, verbose_name="Обновлено"),
|
||
),
|
||
(
|
||
"employee",
|
||
models.ForeignKey(
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="payroll_acknowledgements",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Сотрудник",
|
||
),
|
||
),
|
||
(
|
||
"statement",
|
||
models.OneToOneField(
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="acknowledgement",
|
||
to="finance.payrollstatement",
|
||
verbose_name="Расчётный листок",
|
||
),
|
||
),
|
||
],
|
||
options={
|
||
"verbose_name": "Подтверждение расчётного листка",
|
||
"verbose_name_plural": "Подтверждения расчётных листков",
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name="PayrollStatementLine",
|
||
fields=[
|
||
(
|
||
"id",
|
||
models.BigAutoField(
|
||
auto_created=True,
|
||
primary_key=True,
|
||
serialize=False,
|
||
verbose_name="ID",
|
||
),
|
||
),
|
||
("position", models.PositiveIntegerField(verbose_name="Позиция")),
|
||
(
|
||
"line_id",
|
||
models.CharField(
|
||
max_length=160, verbose_name="Идентификатор строки"
|
||
),
|
||
),
|
||
("code", models.CharField(max_length=64, verbose_name="Код")),
|
||
("label", models.CharField(max_length=160, verbose_name="Название")),
|
||
(
|
||
"kind",
|
||
models.CharField(
|
||
choices=[
|
||
("earning", "Начисление"),
|
||
("adjustment_credit", "Коррекция +"),
|
||
("adjustment_debit", "Коррекция −"),
|
||
("deduction", "Удержание"),
|
||
("payment", "Выплата"),
|
||
],
|
||
max_length=24,
|
||
verbose_name="Тип",
|
||
),
|
||
),
|
||
(
|
||
"amount",
|
||
models.DecimalField(
|
||
decimal_places=2, max_digits=21, verbose_name="Сумма"
|
||
),
|
||
),
|
||
(
|
||
"source_ref",
|
||
models.CharField(max_length=255, verbose_name="Источник"),
|
||
),
|
||
("reason", models.TextField(blank=True, verbose_name="Основание")),
|
||
(
|
||
"source_period_from",
|
||
models.DateField(
|
||
blank=True, null=True, verbose_name="Период-источник: начало"
|
||
),
|
||
),
|
||
(
|
||
"source_period_to",
|
||
models.DateField(
|
||
blank=True, null=True, verbose_name="Период-источник: конец"
|
||
),
|
||
),
|
||
(
|
||
"calculated",
|
||
models.BooleanField(default=False, verbose_name="Рассчитана ядром"),
|
||
),
|
||
(
|
||
"statement",
|
||
models.ForeignKey(
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="lines",
|
||
to="finance.payrollstatement",
|
||
verbose_name="Расчётный листок",
|
||
),
|
||
),
|
||
],
|
||
options={
|
||
"verbose_name": "Строка расчётного листка",
|
||
"verbose_name_plural": "Строки расчётных листков",
|
||
"ordering": ["statement_id", "position"],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name="PayrollWorkRecord",
|
||
fields=[
|
||
(
|
||
"id",
|
||
models.BigAutoField(
|
||
auto_created=True,
|
||
primary_key=True,
|
||
serialize=False,
|
||
verbose_name="ID",
|
||
),
|
||
),
|
||
(
|
||
"target_points",
|
||
models.DecimalField(
|
||
decimal_places=4,
|
||
max_digits=19,
|
||
validators=[
|
||
django.core.validators.MinValueValidator(Decimal("0.0001"))
|
||
],
|
||
verbose_name="Норма баллов",
|
||
),
|
||
),
|
||
(
|
||
"actual_points",
|
||
models.DecimalField(
|
||
decimal_places=4,
|
||
max_digits=19,
|
||
validators=[
|
||
django.core.validators.MinValueValidator(Decimal("0"))
|
||
],
|
||
verbose_name="Фактические баллы",
|
||
),
|
||
),
|
||
(
|
||
"expected_point_amount",
|
||
models.DecimalField(
|
||
blank=True,
|
||
decimal_places=2,
|
||
max_digits=19,
|
||
null=True,
|
||
verbose_name="Контроль: выработка по баллам",
|
||
),
|
||
),
|
||
(
|
||
"expected_gross",
|
||
models.DecimalField(
|
||
blank=True,
|
||
decimal_places=2,
|
||
max_digits=19,
|
||
null=True,
|
||
verbose_name="Контроль: итого начислено",
|
||
),
|
||
),
|
||
(
|
||
"expected_recalculated_gross",
|
||
models.DecimalField(
|
||
blank=True,
|
||
decimal_places=2,
|
||
max_digits=19,
|
||
null=True,
|
||
verbose_name="Контроль: перерасчёт",
|
||
),
|
||
),
|
||
(
|
||
"expected_payable",
|
||
models.DecimalField(
|
||
blank=True,
|
||
decimal_places=2,
|
||
max_digits=19,
|
||
null=True,
|
||
verbose_name="Контроль: к выплате",
|
||
),
|
||
),
|
||
(
|
||
"revision",
|
||
models.PositiveIntegerField(default=1, verbose_name="Ревизия"),
|
||
),
|
||
(
|
||
"status",
|
||
models.CharField(
|
||
choices=[
|
||
("draft", "Черновик"),
|
||
("approved", "Утверждено"),
|
||
("voided", "Аннулировано"),
|
||
],
|
||
default="draft",
|
||
max_length=16,
|
||
verbose_name="Статус",
|
||
),
|
||
),
|
||
(
|
||
"source",
|
||
models.CharField(
|
||
choices=[
|
||
("manual", "Вручную"),
|
||
("excel", "Excel"),
|
||
("api", "API"),
|
||
],
|
||
default="manual",
|
||
max_length=16,
|
||
verbose_name="Источник",
|
||
),
|
||
),
|
||
(
|
||
"source_ref",
|
||
models.CharField(
|
||
blank=True,
|
||
max_length=255,
|
||
verbose_name="Идентификатор в источнике",
|
||
),
|
||
),
|
||
(
|
||
"created_at",
|
||
models.DateTimeField(auto_now_add=True, verbose_name="Создана"),
|
||
),
|
||
(
|
||
"updated_at",
|
||
models.DateTimeField(auto_now=True, verbose_name="Обновлена"),
|
||
),
|
||
(
|
||
"approved_at",
|
||
models.DateTimeField(
|
||
blank=True, null=True, verbose_name="Утверждена"
|
||
),
|
||
),
|
||
(
|
||
"voided_at",
|
||
models.DateTimeField(
|
||
blank=True, null=True, verbose_name="Аннулирована"
|
||
),
|
||
),
|
||
(
|
||
"approved_by",
|
||
models.ForeignKey(
|
||
blank=True,
|
||
null=True,
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="approved_payroll_work_records",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Утвердил",
|
||
),
|
||
),
|
||
(
|
||
"created_by",
|
||
models.ForeignKey(
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="created_payroll_work_records",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Создал",
|
||
),
|
||
),
|
||
(
|
||
"employee",
|
||
models.ForeignKey(
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="payroll_work_records",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Сотрудник",
|
||
),
|
||
),
|
||
(
|
||
"period",
|
||
models.ForeignKey(
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="work_records",
|
||
to="finance.payrollperiod",
|
||
verbose_name="Период",
|
||
),
|
||
),
|
||
(
|
||
"replaces",
|
||
models.OneToOneField(
|
||
blank=True,
|
||
null=True,
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="replaced_by",
|
||
to="finance.payrollworkrecord",
|
||
verbose_name="Заменяет запись",
|
||
),
|
||
),
|
||
(
|
||
"voided_by",
|
||
models.ForeignKey(
|
||
blank=True,
|
||
null=True,
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="voided_payroll_work_records",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Аннулировал",
|
||
),
|
||
),
|
||
],
|
||
options={
|
||
"verbose_name": "Выработка за период",
|
||
"verbose_name_plural": "Выработка за периоды",
|
||
"ordering": ["period_id", "employee_id", "-revision"],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name="EmployeePayRate",
|
||
fields=[
|
||
(
|
||
"id",
|
||
models.BigAutoField(
|
||
auto_created=True,
|
||
primary_key=True,
|
||
serialize=False,
|
||
verbose_name="ID",
|
||
),
|
||
),
|
||
(
|
||
"rate_code",
|
||
models.CharField(
|
||
default="BASE",
|
||
max_length=64,
|
||
validators=[
|
||
django.core.validators.RegexValidator(
|
||
message="Код должен начинаться с A-Z и содержать только A-Z, 0-9, _.-",
|
||
regex="^[A-Z][A-Z0-9_.-]{0,63}$",
|
||
)
|
||
],
|
||
verbose_name="Код ставки",
|
||
),
|
||
),
|
||
(
|
||
"amount",
|
||
models.DecimalField(
|
||
decimal_places=4,
|
||
max_digits=19,
|
||
validators=[
|
||
django.core.validators.MinValueValidator(Decimal("0.0001"))
|
||
],
|
||
verbose_name="Сумма ставки",
|
||
),
|
||
),
|
||
(
|
||
"point_rate",
|
||
models.DecimalField(
|
||
decimal_places=4,
|
||
default=Decimal("0"),
|
||
max_digits=19,
|
||
validators=[
|
||
django.core.validators.MinValueValidator(Decimal("0"))
|
||
],
|
||
verbose_name="Цена балла сверх нормы",
|
||
),
|
||
),
|
||
(
|
||
"currency",
|
||
models.CharField(
|
||
default="RUB",
|
||
max_length=3,
|
||
validators=[
|
||
django.core.validators.RegexValidator(
|
||
message="Валюта должна быть трёхбуквенным кодом в верхнем регистре.",
|
||
regex="^[A-Z]{3}$",
|
||
)
|
||
],
|
||
verbose_name="Валюта",
|
||
),
|
||
),
|
||
("effective_from", models.DateField(verbose_name="Действует с")),
|
||
(
|
||
"revision",
|
||
models.PositiveIntegerField(default=1, verbose_name="Ревизия"),
|
||
),
|
||
(
|
||
"status",
|
||
models.CharField(
|
||
choices=[
|
||
("draft", "Черновик"),
|
||
("approved", "Утверждено"),
|
||
("voided", "Аннулировано"),
|
||
],
|
||
default="draft",
|
||
max_length=16,
|
||
verbose_name="Статус",
|
||
),
|
||
),
|
||
("reason", models.TextField(blank=True, verbose_name="Основание")),
|
||
(
|
||
"source",
|
||
models.CharField(
|
||
choices=[
|
||
("manual", "Вручную"),
|
||
("excel", "Excel"),
|
||
("api", "API"),
|
||
],
|
||
default="manual",
|
||
max_length=16,
|
||
verbose_name="Источник",
|
||
),
|
||
),
|
||
(
|
||
"source_ref",
|
||
models.CharField(
|
||
blank=True,
|
||
max_length=255,
|
||
verbose_name="Идентификатор в источнике",
|
||
),
|
||
),
|
||
(
|
||
"created_at",
|
||
models.DateTimeField(auto_now_add=True, verbose_name="Создана"),
|
||
),
|
||
(
|
||
"approved_at",
|
||
models.DateTimeField(
|
||
blank=True, null=True, verbose_name="Утверждена"
|
||
),
|
||
),
|
||
(
|
||
"voided_at",
|
||
models.DateTimeField(
|
||
blank=True, null=True, verbose_name="Аннулирована"
|
||
),
|
||
),
|
||
(
|
||
"void_reason",
|
||
models.TextField(blank=True, verbose_name="Причина аннулирования"),
|
||
),
|
||
(
|
||
"approved_by",
|
||
models.ForeignKey(
|
||
blank=True,
|
||
null=True,
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="approved_pay_rates",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Утвердил",
|
||
),
|
||
),
|
||
(
|
||
"created_by",
|
||
models.ForeignKey(
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="created_pay_rates",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Создал",
|
||
),
|
||
),
|
||
(
|
||
"employee",
|
||
models.ForeignKey(
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="pay_rates",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Сотрудник",
|
||
),
|
||
),
|
||
(
|
||
"replaces",
|
||
models.OneToOneField(
|
||
blank=True,
|
||
null=True,
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="replaced_by",
|
||
to="finance.employeepayrate",
|
||
verbose_name="Заменяет ставку",
|
||
),
|
||
),
|
||
(
|
||
"voided_by",
|
||
models.ForeignKey(
|
||
blank=True,
|
||
null=True,
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="voided_pay_rates",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Аннулировал",
|
||
),
|
||
),
|
||
],
|
||
options={
|
||
"verbose_name": "Ставка сотрудника",
|
||
"verbose_name_plural": "Ставки сотрудников",
|
||
"ordering": ["employee_id", "-effective_from", "-revision"],
|
||
"indexes": [
|
||
models.Index(
|
||
fields=["employee", "status", "effective_from"],
|
||
name="pay_rate_employee_date_idx",
|
||
)
|
||
],
|
||
"constraints": [
|
||
models.CheckConstraint(
|
||
condition=models.Q(("amount__gt", 0)),
|
||
name="pay_rate_amount_positive",
|
||
),
|
||
models.CheckConstraint(
|
||
condition=models.Q(("point_rate__gte", 0)),
|
||
name="pay_rate_point_nonnegative",
|
||
),
|
||
models.CheckConstraint(
|
||
condition=models.Q(
|
||
("approved_by__isnull", True),
|
||
models.Q(
|
||
("created_by", models.F("approved_by")), _negated=True
|
||
),
|
||
_connector="OR",
|
||
),
|
||
name="pay_rate_maker_not_approver",
|
||
),
|
||
models.UniqueConstraint(
|
||
fields=("employee", "rate_code", "effective_from", "revision"),
|
||
name="pay_rate_revision_unique",
|
||
),
|
||
models.UniqueConstraint(
|
||
condition=models.Q(("status", "approved")),
|
||
fields=("employee", "rate_code", "effective_from"),
|
||
name="pay_rate_one_approved_event",
|
||
),
|
||
models.UniqueConstraint(
|
||
condition=models.Q(("source_ref", ""), _negated=True),
|
||
fields=("source", "source_ref"),
|
||
name="pay_rate_source_ref_unique",
|
||
),
|
||
],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name="PayrollInputLine",
|
||
fields=[
|
||
(
|
||
"id",
|
||
models.BigAutoField(
|
||
auto_created=True,
|
||
primary_key=True,
|
||
serialize=False,
|
||
verbose_name="ID",
|
||
),
|
||
),
|
||
(
|
||
"amount",
|
||
models.DecimalField(
|
||
decimal_places=2,
|
||
max_digits=19,
|
||
validators=[
|
||
django.core.validators.MinValueValidator(Decimal("0.01"))
|
||
],
|
||
verbose_name="Сумма",
|
||
),
|
||
),
|
||
("reason", models.TextField(blank=True, verbose_name="Основание")),
|
||
(
|
||
"status",
|
||
models.CharField(
|
||
choices=[
|
||
("draft", "Черновик"),
|
||
("approved", "Утверждено"),
|
||
("voided", "Аннулировано"),
|
||
],
|
||
default="draft",
|
||
max_length=16,
|
||
verbose_name="Статус",
|
||
),
|
||
),
|
||
(
|
||
"source",
|
||
models.CharField(
|
||
choices=[
|
||
("manual", "Вручную"),
|
||
("excel", "Excel"),
|
||
("api", "API"),
|
||
],
|
||
default="manual",
|
||
max_length=16,
|
||
verbose_name="Источник",
|
||
),
|
||
),
|
||
(
|
||
"source_ref",
|
||
models.CharField(
|
||
blank=True,
|
||
max_length=255,
|
||
verbose_name="Идентификатор в источнике",
|
||
),
|
||
),
|
||
(
|
||
"idempotency_key",
|
||
models.UUIDField(
|
||
default=uuid.uuid4,
|
||
editable=False,
|
||
unique=True,
|
||
verbose_name="Ключ идемпотентности",
|
||
),
|
||
),
|
||
(
|
||
"created_at",
|
||
models.DateTimeField(auto_now_add=True, verbose_name="Создана"),
|
||
),
|
||
(
|
||
"approved_at",
|
||
models.DateTimeField(
|
||
blank=True, null=True, verbose_name="Утверждена"
|
||
),
|
||
),
|
||
(
|
||
"voided_at",
|
||
models.DateTimeField(
|
||
blank=True, null=True, verbose_name="Аннулирована"
|
||
),
|
||
),
|
||
(
|
||
"void_reason",
|
||
models.TextField(blank=True, verbose_name="Причина аннулирования"),
|
||
),
|
||
(
|
||
"approved_by",
|
||
models.ForeignKey(
|
||
blank=True,
|
||
null=True,
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="approved_payroll_input_lines",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Утвердил",
|
||
),
|
||
),
|
||
(
|
||
"component",
|
||
models.ForeignKey(
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="input_lines",
|
||
to="finance.payrollcomponent",
|
||
verbose_name="Компонент",
|
||
),
|
||
),
|
||
(
|
||
"created_by",
|
||
models.ForeignKey(
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="created_payroll_input_lines",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Создал",
|
||
),
|
||
),
|
||
(
|
||
"employee",
|
||
models.ForeignKey(
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="payroll_input_lines",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Сотрудник",
|
||
),
|
||
),
|
||
(
|
||
"reversal_of",
|
||
models.OneToOneField(
|
||
blank=True,
|
||
null=True,
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="reversed_by",
|
||
to="finance.payrollinputline",
|
||
verbose_name="Сторнирует строку",
|
||
),
|
||
),
|
||
(
|
||
"voided_by",
|
||
models.ForeignKey(
|
||
blank=True,
|
||
null=True,
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="voided_payroll_input_lines",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Аннулировал",
|
||
),
|
||
),
|
||
(
|
||
"period",
|
||
models.ForeignKey(
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="input_lines",
|
||
to="finance.payrollperiod",
|
||
verbose_name="Период выплаты",
|
||
),
|
||
),
|
||
(
|
||
"relates_to_period",
|
||
models.ForeignKey(
|
||
blank=True,
|
||
help_text="Заполняется для перерасчёта прошлого периода.",
|
||
null=True,
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="retro_input_lines",
|
||
to="finance.payrollperiod",
|
||
verbose_name="Относится к периоду",
|
||
),
|
||
),
|
||
],
|
||
options={
|
||
"verbose_name": "Строка начисления/удержания",
|
||
"verbose_name_plural": "Строки начислений/удержаний",
|
||
"ordering": [
|
||
"period_id",
|
||
"employee_id",
|
||
"component__display_order",
|
||
"id",
|
||
],
|
||
"indexes": [
|
||
models.Index(
|
||
fields=["period", "employee", "status"],
|
||
name="pay_input_period_employee_idx",
|
||
)
|
||
],
|
||
"constraints": [
|
||
models.CheckConstraint(
|
||
condition=models.Q(("amount__gt", 0)),
|
||
name="pay_input_amount_positive",
|
||
),
|
||
models.CheckConstraint(
|
||
condition=models.Q(
|
||
("approved_by__isnull", True),
|
||
models.Q(
|
||
("created_by", models.F("approved_by")), _negated=True
|
||
),
|
||
_connector="OR",
|
||
),
|
||
name="pay_input_maker_not_approver",
|
||
),
|
||
models.UniqueConstraint(
|
||
condition=models.Q(("source_ref", ""), _negated=True),
|
||
fields=("source", "source_ref"),
|
||
name="pay_input_source_ref_unique",
|
||
),
|
||
],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name="PayrollAuditEvent",
|
||
fields=[
|
||
(
|
||
"id",
|
||
models.BigAutoField(
|
||
auto_created=True,
|
||
primary_key=True,
|
||
serialize=False,
|
||
verbose_name="ID",
|
||
),
|
||
),
|
||
("action", models.CharField(max_length=80, verbose_name="Действие")),
|
||
(
|
||
"object_type",
|
||
models.CharField(max_length=80, verbose_name="Тип объекта"),
|
||
),
|
||
(
|
||
"object_id",
|
||
models.CharField(
|
||
max_length=80, verbose_name="Идентификатор объекта"
|
||
),
|
||
),
|
||
(
|
||
"before_hash",
|
||
models.CharField(blank=True, max_length=64, verbose_name="Хэш до"),
|
||
),
|
||
(
|
||
"after_hash",
|
||
models.CharField(
|
||
blank=True, max_length=64, verbose_name="Хэш после"
|
||
),
|
||
),
|
||
(
|
||
"metadata",
|
||
models.JSONField(
|
||
blank=True, default=dict, verbose_name="Безопасные метаданные"
|
||
),
|
||
),
|
||
(
|
||
"created_at",
|
||
models.DateTimeField(auto_now_add=True, verbose_name="Создано"),
|
||
),
|
||
(
|
||
"actor",
|
||
models.ForeignKey(
|
||
blank=True,
|
||
null=True,
|
||
on_delete=django.db.models.deletion.SET_NULL,
|
||
related_name="payroll_audit_events",
|
||
to=settings.AUTH_USER_MODEL,
|
||
verbose_name="Инициатор",
|
||
),
|
||
),
|
||
(
|
||
"period",
|
||
models.ForeignKey(
|
||
blank=True,
|
||
null=True,
|
||
on_delete=django.db.models.deletion.PROTECT,
|
||
related_name="audit_events",
|
||
to="finance.payrollperiod",
|
||
verbose_name="Период",
|
||
),
|
||
),
|
||
],
|
||
options={
|
||
"verbose_name": "Событие аудита зарплаты",
|
||
"verbose_name_plural": "События аудита зарплаты",
|
||
"ordering": ["-created_at", "-id"],
|
||
"indexes": [
|
||
models.Index(
|
||
fields=["object_type", "object_id", "created_at"],
|
||
name="pay_audit_object_time_idx",
|
||
),
|
||
models.Index(
|
||
fields=["period", "created_at"],
|
||
name="pay_audit_period_time_idx",
|
||
),
|
||
],
|
||
},
|
||
),
|
||
migrations.AddIndex(
|
||
model_name="payrollrun",
|
||
index=models.Index(
|
||
fields=["period", "status", "revision"],
|
||
name="pay_run_period_status_idx",
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollrun",
|
||
constraint=models.UniqueConstraint(
|
||
fields=("period", "revision"), name="pay_run_revision_unique"
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollrun",
|
||
constraint=models.CheckConstraint(
|
||
condition=models.Q(
|
||
("approved_by__isnull", True),
|
||
models.Q(("requested_by", models.F("approved_by")), _negated=True),
|
||
_connector="OR",
|
||
),
|
||
name="pay_run_maker_not_approver",
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollrun",
|
||
constraint=models.CheckConstraint(
|
||
condition=models.Q(("gross_total__gte", 0)),
|
||
name="pay_run_gross_nonnegative",
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollrun",
|
||
constraint=models.CheckConstraint(
|
||
condition=models.Q(("deduction_total__gte", 0)),
|
||
name="pay_run_deduction_nonnegative",
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollrun",
|
||
constraint=models.CheckConstraint(
|
||
condition=models.Q(("payable_total__gte", 0)),
|
||
name="pay_run_payable_nonnegative",
|
||
),
|
||
),
|
||
migrations.AddIndex(
|
||
model_name="payrollperiod",
|
||
index=models.Index(
|
||
fields=["status", "date_from"], name="pay_period_status_date_idx"
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollperiod",
|
||
constraint=models.CheckConstraint(
|
||
condition=models.Q(("date_from__lte", models.F("date_to"))),
|
||
name="pay_period_dates_valid",
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollperiod",
|
||
constraint=models.UniqueConstraint(
|
||
fields=("date_from", "date_to"), name="pay_period_dates_unique"
|
||
),
|
||
),
|
||
migrations.AddIndex(
|
||
model_name="payrollstatement",
|
||
index=models.Index(
|
||
fields=["employee", "run"], name="pay_statement_employee_idx"
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollstatement",
|
||
constraint=models.UniqueConstraint(
|
||
fields=("run", "employee"), name="pay_statement_run_employee_unique"
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollstatement",
|
||
constraint=models.CheckConstraint(
|
||
condition=models.Q(("gross_before_adjustments__gte", 0)),
|
||
name="pay_statement_pre_gross_nonnegative",
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollstatement",
|
||
constraint=models.CheckConstraint(
|
||
condition=models.Q(("gross_total__gte", 0)),
|
||
name="pay_statement_gross_nonnegative",
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollstatement",
|
||
constraint=models.CheckConstraint(
|
||
condition=models.Q(("deduction_total__gte", 0)),
|
||
name="pay_statement_deduction_nonnegative",
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollstatement",
|
||
constraint=models.CheckConstraint(
|
||
condition=models.Q(("payment_total__gte", 0)),
|
||
name="pay_statement_payment_nonnegative",
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollstatement",
|
||
constraint=models.CheckConstraint(
|
||
condition=models.Q(("payable__gte", 0)),
|
||
name="pay_statement_payable_nonnegative",
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollstatementline",
|
||
constraint=models.UniqueConstraint(
|
||
fields=("statement", "position"),
|
||
name="pay_statement_line_position_unique",
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollstatementline",
|
||
constraint=models.UniqueConstraint(
|
||
fields=("statement", "line_id"), name="pay_statement_line_id_unique"
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollstatementline",
|
||
constraint=models.CheckConstraint(
|
||
condition=models.Q(("amount__gte", 0)),
|
||
name="pay_statement_line_amount_nonnegative",
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollworkrecord",
|
||
constraint=models.CheckConstraint(
|
||
condition=models.Q(("target_points__gt", 0)),
|
||
name="pay_work_target_positive",
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollworkrecord",
|
||
constraint=models.CheckConstraint(
|
||
condition=models.Q(("actual_points__gte", 0)),
|
||
name="pay_work_actual_nonnegative",
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollworkrecord",
|
||
constraint=models.CheckConstraint(
|
||
condition=models.Q(
|
||
("approved_by__isnull", True),
|
||
models.Q(("created_by", models.F("approved_by")), _negated=True),
|
||
_connector="OR",
|
||
),
|
||
name="pay_work_maker_not_approver",
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollworkrecord",
|
||
constraint=models.UniqueConstraint(
|
||
fields=("period", "employee", "revision"),
|
||
name="pay_work_revision_unique",
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollworkrecord",
|
||
constraint=models.UniqueConstraint(
|
||
condition=models.Q(("status", "approved")),
|
||
fields=("period", "employee"),
|
||
name="pay_work_one_approved",
|
||
),
|
||
),
|
||
migrations.AddConstraint(
|
||
model_name="payrollworkrecord",
|
||
constraint=models.UniqueConstraint(
|
||
condition=models.Q(("source_ref", ""), _negated=True),
|
||
fields=("source", "source_ref"),
|
||
name="pay_work_source_ref_unique",
|
||
),
|
||
),
|
||
]
|