Перейти до змісту

Система нарахування зарплати водіям (Fleet)

Бізнес-контекст

Зарплата водіїв у модулі Fleet — це внутрішня відрядно-преміальна система, яка служить базою для офіційного нарахування зарплати в модулі Essentials (коли він буде реалізований).

Типові схеми нарахування

# Схема Приклад Частота
1 Фіксований оклад 8 000 грн/міс гарантовано Завжди
2 За ходку 500 грн за кожну ходку Найчастіше
3 За довгу ходку +500 грн якщо км > 400 Часто
4 За додаткову ходку +500 грн за кожну ходку після першої Часто
5 Погодинна Тариф × години роботи Рідко
6 За км X грн за кожен км Іноді
7 За тонно-км X грн за кожен т·км Рідко
8 Премія Ручне нарахування Іноді
9 Утримання/штраф Від'ємна сума Іноді

Логіка роботи

Подорожній лист (Waybill)
  ├── При розрахунку/проведенні:
  │   └── Табличня частина "Нарахування водію" (WaybillDriverAccrual)
  │       ├── Рядок 1: За ходку — 500 грн × 3 ходки = 1500 грн
  │       ├── Рядок 2: За довгу ходку — 500 грн × 1 = 500 грн
  │       └── Рядок 3: За додат. ходку — 500 грн × 2 = 1000 грн
  ├── При проведенні (state=posted):
  │   └── Записи в регістр "Відрядна зарплата водіїв" (PieceworkSalaryJournal)
  └── Кнопка "Розрахувати нарахування" → автоматичний підбір тарифів
Документ "Нарахування зарплати водіям" (DriverSalaryAccrual) — щомісячний агрегатор
  ├── Заповнення:
  │   ├── Кнопка "Заповнити" → збирає дані з PieceworkSalaryJournal за місяць
  │   ├── + додає фіксований оклад з тарифів
  │   └── Табличня частина: водій | вид нарахування | показник | тариф | сума
  ├── При проведенні (state=posted):
  │   └── Записи в регістр "Нарахування зарплати водіїв" (DriverSalaryLedger)
  │       водій | місяць | вид нарахування | сума
  └── Зв'язок з Essentials:
      Essentials "Нарахування зарплати" (майбутнє) бере дані з DriverSalaryLedger

Архітектура моделей

1. DriverAccrualType (MasterDataModel) — Довідник видів нарахувань

Файл: backend/fleet/models/driver_accrual_type.py

Визначає що нараховується водію та як рахується.

DriverAccrualType (MasterDataModel)
├── name (CharField, 255)             — "За ходку", "Оклад", "За довгу ходку"
├── name_ua (CharField, 255)          — укр. назва (inherited)
├── code (CharField, 50)              — "per_trip", "salary", "long_trip" (inherited)
├── calc_method (CharField)           — метод розрахунку:
│   ├── "fixed"        — фіксована сума (оклад)
│   ├── "per_trip"     — за кожну ходку (task/work_result)
│   ├── "per_extra_trip" — за кожну ходку після першої
│   ├── "per_km"       — за кілометр пробігу
│   ├── "per_ton_km"   — за тонно-кілометр
│   ├── "per_hour"     — за годину роботи
│   ├── "conditional"  — умовне (якщо км > threshold)
│   └── "manual"       — ручне введення суми
├── threshold_km (Decimal, 10.2, null) — поріг км для conditional (напр. 400)
├── is_deduction (BooleanField, default=False) — це утримання (від'ємне)
├── is_monthly (BooleanField, default=False)   — нараховується раз на місяць (оклад)
│                                                а не в кожному подорожньому листі
└── description (TextField)

EntityRegistry: driverAccrualTypesdriver-accrual-types

2. DriverSalaryRate (MasterDataModel) — Тарифи зарплати водіїв

Файл: backend/fleet/models/driver_salary_rate.py

Зв'язує вид нарахування з конкретним автомобілем (або типом) та визначає ставку.

DriverSalaryRate (MasterDataModel)
├── name (CharField, 255)                   — авто-генерація: "КамАЗ АА1234 — За ходку"
├── vehicle (FK Vehicle, SET_NULL, null)     — конкретний автомобіль
├── vehicle_type (CharField, blank)          — або тип ТЗ (truck/van/car) для загального тарифу
├── accrual_type (FK DriverAccrualType, PROTECT) — вид нарахування
├── rate (Decimal, 15.2)                     — ставка (грн за одиницю або фікс. сума)
├── currency (FK Currency, PROTECT, null)    — валюта (за замовч. UAH)
├── effective_from (DateField, null)         — діє з
├── effective_to (DateField, null)           — діє до
├── priority (PositiveIntegerField, default=0) — пріоритет (вищий = перевіряється першим)
└── is_active (BooleanField, default=True)

Логіка пошуку тарифу: 1. Шукаємо по конкретному vehicle + accrual_type + дата в діапазоні + is_active 2. Якщо не знайдено — шукаємо по vehicle_type + accrual_type 3. Якщо не знайдено — тариф не застосовується (попередження)

EntityRegistry: driverSalaryRatesdriver-salary-rates

3. WaybillDriverAccrual (TenantAwareModel) — Підтаблиця подорожнього листа

Файл: backend/fleet/models/waybill_driver_accrual.py

Рядок нарахування водію в подорожньому листі.

WaybillDriverAccrual (TenantAwareModel)
├── waybill (FK Waybill, CASCADE)            — батьківський подорожній лист
├── sequence (PositiveIntegerField)          — порядковий номер
├── driver (FK Driver, PROTECT)              — водій
├── accrual_type (FK DriverAccrualType, PROTECT) — вид нарахування
├── indicator_value (Decimal, 15.3, null)    — показник виробітку (к-сть ходок, км, год)
├── rate (Decimal, 15.2, null)               — тариф (ставка)
├── amount (Decimal, 15.2)                   — сума нарахування
├── salary_rate (FK DriverSalaryRate, SET_NULL, null) — посилання на тариф
├── calc_details (JSONField, null)           — деталі розрахунку (audit)
└── notes (TextField, blank)

Додавання до Waybill:

# waybill.py
_subtables = [
    'tasks',
    'workResults',
    'fuelRecords',
    'driverAccruals',    # NEW
]

EntityRegistry: waybillDriverAccrualswaybill-driver-accruals

4. PieceworkSalaryJournal (RegisterModel) — Регістр відрядної зарплати

Файл: backend/registers/models/piecework_salary_journal.py

Журнал оборотів — записується при проведенні подорожнього листа.

PieceworkSalaryJournal (RegisterModel)
├── date (DateField)                         — дата подорожнього листа (inherited)
├── document_type (CharField)                — "Waybill" (inherited)
├── document_id (PositiveBigIntegerField)    — ID подорожнього листа (inherited)
├── driver (FK Driver, PROTECT)              — водій
├── vehicle (FK Vehicle, PROTECT, null)      — автомобіль
├── accrual_type (FK DriverAccrualType, PROTECT) — вид нарахування
├── indicator_value (Decimal, 15.3, null)    — показник виробітку
├── rate (Decimal, 15.2, null)               — тариф
├── amount (Decimal, 15.2)                   — сума
├── organization (FK Organization, PROTECT, null) — організація
└── department (FK Department, SET_NULL, null)    — підрозділ

Проведення Waybill: при state → posted — створити записи з WaybillDriverAccrual. Зняття проведення: при state → draft — видалити записи з document_type="Waybill", document_id=waybill.id.

5. DriverSalaryAccrual (TransactionModel) — Документ нарахування зарплати

Файл: backend/fleet/models/driver_salary_accrual.py

Щомісячний агрегатор — збирає всі нарахування з подорожніх листів + додає оклад.

DriverSalaryAccrual (TransactionModel)
├── number (CharField)                       — номер документа (inherited)
├── date (DateField)                         — дата документа (inherited)
├── state (CharField: draft/posted)          — стан (inherited)
├── period_from (DateField)                  — початок періоду (1-ше число місяця)
├── period_to (DateField)                    — кінець періоду (останнє число місяця)
├── organization (FK Organization, PROTECT, null) — організація
├── department (FK Department, SET_NULL, null)    — підрозділ
├── total_amount (Decimal, 15.2, default=0)  — загальна сума нарахувань
├── _subtables = ['lines']                   — підтаблиця рядків
└── status: 'draft' | 'approved' | 'posted' | 'cancelled'

EntityRegistry: driverSalaryAccrualsdriver-salary-accruals

6. DriverSalaryAccrualLine (TenantAwareModel) — Рядки нарахування

Файл: backend/fleet/models/driver_salary_accrual.py (в тому ж файлі)

DriverSalaryAccrualLine (TenantAwareModel)
├── document (FK DriverSalaryAccrual, CASCADE) — батьківський документ
├── sequence (PositiveIntegerField)
├── driver (FK Driver, PROTECT)               — водій
├── accrual_type (FK DriverAccrualType, PROTECT) — вид нарахування
├── indicator_value (Decimal, 15.3, null)     — показник (к-сть ходок, км, год, null для окладу)
├── rate (Decimal, 15.2, null)                — тариф
├── amount (Decimal, 15.2)                    — сума
├── source_document_type (CharField, blank)   — "Waybill" або "manual"
├── source_document_id (PositiveBigIntegerField, null) — ID джерела
└── notes (TextField, blank)

7. DriverSalaryLedger (RegisterModel) — Регістр нарахувань зарплати водіїв

Файл: backend/registers/models/driver_salary_ledger.py

Підсумковий регістр — записується при проведенні DriverSalaryAccrual.

DriverSalaryLedger (RegisterModel)
├── date (DateField)                          — дата документа (inherited)
├── document_type (CharField)                 — "DriverSalaryAccrual" (inherited)
├── document_id (PositiveBigIntegerField)     — ID документа (inherited)
├── driver (FK Driver, PROTECT)               — водій
├── period_month (DateField)                  — перше число місяця (для агрегації)
├── accrual_type (FK DriverAccrualType, PROTECT) — вид нарахування
├── amount (Decimal, 15.2)                    — сума
├── organization (FK Organization, PROTECT, null) — організація
└── department (FK Department, SET_NULL, null) — підрозділ

Сервіси

DriverSalaryCalculator

Файл: backend/fleet/services/driver_salary_calculator.py

class DriverSalaryCalculator:
    """Розрахунок нарахувань водію в подорожньому листі."""

    def calculate(self, waybill: Waybill) -> list[dict]:
        """
        Розрахувати нарахування для подорожнього листа.
        Повертає список dict для створення WaybillDriverAccrual.
        """
        driver = waybill.driver
        vehicle = waybill.vehicle
        work_results = waybill.work_results.all()
        tasks = waybill.tasks.all()

        accruals = []
        trip_count = work_results.count() or tasks.count()
        total_distance = waybill.distance or Decimal('0')

        # Отримати всі активні тарифи для цього авто
        rates = self._find_rates(vehicle, waybill.date)

        for rate_obj in rates:
            accrual_type = rate_obj.accrual_type

            if accrual_type.is_monthly:
                continue  # Оклад додається тільки в DriverSalaryAccrual

            result = self._calc_by_method(
                accrual_type, rate_obj, waybill,
                trip_count, total_distance, work_results
            )

            if result and result['amount'] > 0:
                accruals.append(result)

        return accruals

    def _find_rates(self, vehicle, date) -> QuerySet:
        """Знайти тарифи: спочатку по конкретному авто, потім по типу."""
        # 1. По конкретному vehicle
        # 2. Fallback по vehicle_type
        # 3. Фільтр по даті та is_active
        ...

    def _calc_by_method(self, accrual_type, rate_obj, waybill,
                         trip_count, total_distance, work_results) -> dict:
        """Розрахувати суму за методом нарахування."""
        method = accrual_type.calc_method
        rate = rate_obj.rate

        if method == 'per_trip':
            indicator = trip_count
            amount = rate * indicator

        elif method == 'per_extra_trip':
            indicator = max(0, trip_count - 1)
            amount = rate * indicator

        elif method == 'per_km':
            indicator = total_distance
            amount = rate * indicator

        elif method == 'per_ton_km':
            ton_km = sum(
                (wr.distance_actual or 0) * (wr.quantity or 0)
                for wr in work_results
            )
            indicator = ton_km
            amount = rate * indicator

        elif method == 'per_hour':
            hours = self._calc_work_hours(waybill)
            indicator = hours
            amount = rate * indicator

        elif method == 'conditional':
            # Наприклад: бонус за довгу ходку
            long_trips = sum(
                1 for wr in work_results
                if (wr.distance_actual or 0) > (accrual_type.threshold_km or 0)
            )
            indicator = long_trips
            amount = rate * indicator

        elif method == 'manual':
            return None  # Ручне введення, не автоматичний розрахунок

        else:
            return None

        return {
            'driver': waybill.driver,
            'accrual_type': accrual_type,
            'indicator_value': indicator,
            'rate': rate,
            'amount': amount,
            'salary_rate': rate_obj,
            'calc_details': {
                'method': method,
                'rate_id': rate_obj.id,
                'rate_name': rate_obj.name,
                'indicator': str(indicator),
                'rate_value': str(rate),
                'calculation': f"{method}: {indicator} x {rate} = {amount}"
            }
        }

DriverSalaryAccrualService

Файл: backend/fleet/services/driver_salary_accrual_service.py

class DriverSalaryAccrualService:
    """Заповнення та проведення документа нарахування зарплати."""

    def fill_from_waybills(self, document: DriverSalaryAccrual) -> list:
        """
        Заповнити рядки документа з PieceworkSalaryJournal за період.
        + додати фіксовані оклади з тарифів.
        """
        lines = []

        # 1. Зібрати відрядні нарахування з регістру
        journal_entries = PieceworkSalaryJournal.objects.filter(
            tenant=document.tenant,
            date__gte=document.period_from,
            date__lte=document.period_to,
        )
        if document.organization:
            journal_entries = journal_entries.filter(organization=document.organization)
        if document.department:
            journal_entries = journal_entries.filter(department=document.department)

        # Агрегація: водій + вид нарахування → сума
        aggregated = journal_entries.values(
            'driver', 'accrual_type'
        ).annotate(
            total_indicator=Sum('indicator_value'),
            total_amount=Sum('amount'),
            avg_rate=Avg('rate'),
        )

        for row in aggregated:
            lines.append({
                'driver_id': row['driver'],
                'accrual_type_id': row['accrual_type'],
                'indicator_value': row['total_indicator'],
                'rate': row['avg_rate'],
                'amount': row['total_amount'],
                'source_document_type': 'Waybill',
            })

        # 2. Додати фіксовані оклади
        drivers = set(entry['driver'] for entry in aggregated)
        # Також знайти водіїв які мали подорожні листи в цей період
        waybill_drivers = Waybill.objects.filter(
            tenant=document.tenant,
            date__gte=document.period_from,
            date__lte=document.period_to,
            state='posted',
        ).values_list('driver_id', flat=True).distinct()
        drivers.update(waybill_drivers)

        monthly_types = DriverAccrualType.objects.filter(
            tenant=document.tenant,
            is_monthly=True,
            is_active=True,
        )

        for driver_id in drivers:
            for accrual_type in monthly_types:
                # Знайти тариф для конкретного водія/авто
                rate_obj = self._find_monthly_rate(
                    document.tenant, driver_id, accrual_type, document.period_from
                )
                if rate_obj:
                    lines.append({
                        'driver_id': driver_id,
                        'accrual_type_id': accrual_type.id,
                        'indicator_value': Decimal('1'),
                        'rate': rate_obj.rate,
                        'amount': rate_obj.rate,
                        'source_document_type': 'manual',
                    })

        return lines

    def post(self, document: DriverSalaryAccrual):
        """Провести документ — створити записи в DriverSalaryLedger."""
        # Видалити старі записи (якщо перепроведення)
        DriverSalaryLedger.objects.filter(
            document_type='DriverSalaryAccrual',
            document_id=document.id,
            tenant=document.tenant,
        ).delete()

        period_month = document.period_from.replace(day=1)

        for line in document.lines.all():
            DriverSalaryLedger.objects.create(
                tenant=document.tenant,
                date=document.date,
                document_type='DriverSalaryAccrual',
                document_id=document.id,
                driver=line.driver,
                period_month=period_month,
                accrual_type=line.accrual_type,
                amount=line.amount,
                organization=document.organization,
                department=document.department,
                created_by=document.updated_by,
            )

        document.total_amount = document.lines.aggregate(
            total=Sum('amount')
        )['total'] or Decimal('0')
        document.state = 'posted'
        document.save()

    def unpost(self, document: DriverSalaryAccrual):
        """Зняти проведення."""
        DriverSalaryLedger.objects.filter(
            document_type='DriverSalaryAccrual',
            document_id=document.id,
            tenant=document.tenant,
        ).delete()
        document.state = 'draft'
        document.save()

Зміни до існуючих моделей

Waybill

# backend/fleet/models/waybill.py

_subtables = [
    'tasks',
    'workResults',
    'fuelRecords',
    'driverAccruals',    # NEW — нарахування водію
]

Waybill.calculate() — додати розрахунок зарплати

# backend/fleet/views.py — WaybillViewSet.calculate()

@action(detail=True, methods=['post'])
def calculate(self, request, pk=None):
    waybill = self.get_object()

    # 1. Fuel calculation (існуючий)
    fuel_calc = FuelCalculator()
    fuel_results = fuel_calc.calculate(waybill)

    # 2. Tariff calculation (існуючий)
    tariff_calc = TariffCalculator()
    tariff_results = tariff_calc.calculate(waybill)

    # 3. Driver salary calculation (NEW)
    salary_calc = DriverSalaryCalculator()
    salary_results = salary_calc.calculate(waybill)

    # Зберегти нарахування
    waybill.driver_accruals.all().delete()
    for i, accrual in enumerate(salary_results):
        WaybillDriverAccrual.objects.create(
            tenant=waybill.tenant,
            waybill=waybill,
            sequence=i + 1,
            **accrual,
            created_by=request.user,
        )

    return Response(serializer.data)

Waybill posting — записати в PieceworkSalaryJournal

# При проведенні Waybill (state → posted):
def post_waybill(waybill, user):
    # ... існуюча логіка (FuelLedger, FuelJournal) ...

    # NEW: Записати нарахування в PieceworkSalaryJournal
    for accrual in waybill.driver_accruals.all():
        PieceworkSalaryJournal.objects.create(
            tenant=waybill.tenant,
            date=waybill.date,
            document_type='Waybill',
            document_id=waybill.id,
            driver=accrual.driver,
            vehicle=waybill.vehicle,
            accrual_type=accrual.accrual_type,
            indicator_value=accrual.indicator_value,
            rate=accrual.rate,
            amount=accrual.amount,
            organization=waybill.organization,
            department=waybill.department,
            created_by=user,
        )

API Endpoints

Довідники

GET/POST   /api/v1/fleet/driver-accrual-types/
GET/PUT/DEL /api/v1/fleet/driver-accrual-types/{id}/

GET/POST   /api/v1/fleet/driver-salary-rates/
GET/PUT/DEL /api/v1/fleet/driver-salary-rates/{id}/

Підтаблиця подорожнього листа

GET/POST   /api/v1/fleet/waybill-driver-accruals/?waybill={id}
GET/PUT/DEL /api/v1/fleet/waybill-driver-accruals/{id}/

Документ нарахування зарплати

GET/POST   /api/v1/fleet/driver-salary-accruals/
GET/PUT/DEL /api/v1/fleet/driver-salary-accruals/{id}/

POST       /api/v1/fleet/driver-salary-accruals/{id}/fill/      — заповнити з подорожніх листів
POST       /api/v1/fleet/driver-salary-accruals/{id}/post/      — провести
POST       /api/v1/fleet/driver-salary-accruals/{id}/unpost/    — зняти проведення

Рядки нарахування

GET/POST   /api/v1/fleet/driver-salary-accrual-lines/?document={id}
GET/PUT/DEL /api/v1/fleet/driver-salary-accrual-lines/{id}/

Регістри

GET        /api/v1/registers/piecework-salary-journals/         — журнал відрядної зарплати
GET        /api/v1/registers/driver-salary-ledgers/             — регістр нарахувань

Розрахунок

POST       /api/v1/fleet/waybills/{id}/calculate/               — існуючий, +salary calc

Структура файлів

Backend — нові файли

backend/
├── fleet/
│   ├── models/
│   │   ├── driver_accrual_type.py          NEW — довідник видів нарахувань
│   │   ├── driver_salary_rate.py           NEW — тарифи зарплати
│   │   ├── waybill_driver_accrual.py       NEW — підтаблиця подорожнього листа
│   │   ├── driver_salary_accrual.py        NEW — документ + рядки нарахування
│   │   ├── waybill.py                      MOD — +_subtables driverAccruals
│   │   └── __init__.py                     MOD — +imports
│   ├── services/
│   │   ├── driver_salary_calculator.py     NEW — розрахунок в подорожньому листі
│   │   └── driver_salary_accrual_service.py NEW — заповнення/проведення документа
│   ├── serializers.py                      MOD — +serializers для нових моделей
│   ├── views.py                            MOD — +ViewSets, calculate() extended
│   ├── urls.py                             MOD — +router routes
│   └── apps.py                             MOD — +EntityRegistry для нових сутностей
├── registers/
│   ├── models/
│   │   ├── piecework_salary_journal.py     NEW — журнал відрядної зарплати
│   │   ├── driver_salary_ledger.py         NEW — регістр нарахувань
│   │   └── __init__.py                     MOD — +imports
│   ├── serializers.py                      MOD — +serializers
│   ├── views.py                            MOD — +ViewSets
│   └── urls.py                             MOD — +router routes
├── core/
│   ├── management/commands/seed.py         MOD — +target driver_salary
│   └── seeders/fleet.py                    MOD — +seed_driver_salary_data()
└── migrations/
    ├── fleet/0028_driver_salary_system.py  NEW
    └── registers/00XX_salary_registers.py  NEW

Frontend — зміни

frontend/erp/src/
├── config/
│   └── fleet.ts                            MOD — +groups/items для нових сутностей
├── i18n/locales/
│   ├── en.json                             MOD — +30 ключів
│   └── ua.json                             MOD — +30 ключів

Frontend Config (fleet.ts)

Нові елементи в навігації

// В групі fleetmasterdata — нові довідники:
{
  code: 'driverAccrualTypes',
  label: 'driverAccrualTypes',
  entityCode: 'driverAccrualTypes',
  type: ItemType.MASTERDATA,
  icon: 'IconReceipt',
},
{
  code: 'driverSalaryRates',
  label: 'driverSalaryRates',
  entityCode: 'driverSalaryRates',
  type: ItemType.MASTERDATA,
  icon: 'IconCurrencyHryvnia',
},

// В транзакційній групі — новий документ:
{
  code: 'driverSalaryAccruals',
  label: 'driverSalaryAccruals',
  entityCode: 'driverSalaryAccruals',
  type: ItemType.TRANSACTIONDATA,
  icon: 'IconReportMoney',
  subtables: [
    {
      code: 'lines',
      entityCode: 'driverSalaryAccrualLines',
      type: 'inline',
      autoload: true,
    }
  ],
},

// В регістрах — нові журнали:
{
  code: 'pieceworkSalaryJournals',
  label: 'pieceworkSalaryJournals',
  entityCode: 'pieceworkSalaryJournals',
  type: ItemType.JOURNAL,
  icon: 'IconFileSpreadsheet',
},
{
  code: 'driverSalaryLedgers',
  label: 'driverSalaryLedgers',
  entityCode: 'driverSalaryLedgers',
  type: ItemType.LEDGER,
  icon: 'IconReportAnalytics',
},

Waybill — додати subtable

// В конфігурації waybills:
subtables: [
  { code: 'tasks', entityCode: 'waybillTasks', type: 'related', autoload: true },
  { code: 'workResults', entityCode: 'waybillWorkResults', type: 'related', autoload: true },
  { code: 'fuelRecords', entityCode: 'waybillFuelRecords', type: 'related', autoload: true },
  { code: 'driverAccruals', entityCode: 'waybillDriverAccruals', type: 'related', autoload: true }, // NEW
],

i18n ключі

en.json (додати ~30 ключів)

{
  "driverAccrualTypes": "Accrual Types",
  "driverSalaryRates": "Salary Rates",
  "driverSalaryAccruals": "Driver Salary Accrual",
  "driverSalaryAccrualLines": "Accrual Lines",
  "pieceworkSalaryJournals": "Piecework Salary Journal",
  "driverSalaryLedgers": "Driver Salary Ledger",
  "waybillDriverAccruals": "Driver Accruals",

  "calc_method": "Calculation Method",
  "threshold_km": "Threshold (km)",
  "is_deduction": "Is Deduction",
  "is_monthly": "Monthly Accrual",
  "rate": "Rate",
  "effective_from": "Effective From",
  "effective_to": "Effective To",
  "indicator_value": "Output Indicator",
  "accrual_type": "Accrual Type",
  "salary_rate": "Salary Rate",
  "period_from": "Period From",
  "period_to": "Period To",
  "total_amount": "Total Amount",
  "period_month": "Period Month",
  "source_document_type": "Source Document",

  "calc_method_fixed": "Fixed Salary",
  "calc_method_per_trip": "Per Trip",
  "calc_method_per_extra_trip": "Per Extra Trip",
  "calc_method_per_km": "Per Kilometer",
  "calc_method_per_ton_km": "Per Ton-Kilometer",
  "calc_method_per_hour": "Per Hour",
  "calc_method_conditional": "Conditional",
  "calc_method_manual": "Manual",

  "fill_from_waybills": "Fill from Waybills",
  "calculate_accruals": "Calculate Accruals"
}

ua.json

{
  "driverAccrualTypes": "Види нарахувань",
  "driverSalaryRates": "Тарифи зарплати",
  "driverSalaryAccruals": "Нарахування зарплати водіям",
  "driverSalaryAccrualLines": "Рядки нарахування",
  "pieceworkSalaryJournals": "Журнал відрядної зарплати",
  "driverSalaryLedgers": "Регістр нарахувань зарплати",
  "waybillDriverAccruals": "Нарахування водію",

  "calc_method": "Метод розрахунку",
  "threshold_km": "Поріг (км)",
  "is_deduction": "Утримання",
  "is_monthly": "Щомісячне нарахування",
  "rate": "Тариф",
  "effective_from": "Діє з",
  "effective_to": "Діє до",
  "indicator_value": "Показник виробітку",
  "accrual_type": "Вид нарахування",
  "salary_rate": "Тариф зарплати",
  "period_from": "Період з",
  "period_to": "Період по",
  "total_amount": "Загальна сума",
  "period_month": "Місяць",
  "source_document_type": "Документ-джерело",

  "calc_method_fixed": "Фіксований оклад",
  "calc_method_per_trip": "За ходку",
  "calc_method_per_extra_trip": "За додаткову ходку",
  "calc_method_per_km": "За кілометр",
  "calc_method_per_ton_km": "За тонно-кілометр",
  "calc_method_per_hour": "За годину",
  "calc_method_conditional": "Умовне",
  "calc_method_manual": "Ручне",

  "fill_from_waybills": "Заповнити з подорожніх листів",
  "calculate_accruals": "Розрахувати нарахування"
}

Seed Data

python manage.py seed driver_salary

Довідник видів нарахувань (DriverAccrualType)

code name calc_method threshold_km is_monthly
salary Оклад fixed True
per_trip За ходку per_trip False
long_trip За довгу ходку conditional 400 False
extra_trip За додаткову ходку per_extra_trip False
per_km За кілометраж per_km False
per_hour За годину роботи per_hour False
bonus Премія manual False
penalty Штраф manual False (is_deduction=True)

Тарифи (DriverSalaryRate) — приклади

vehicle accrual_type rate Опис
КамАЗ АА1234 salary 8000 Оклад для КамАЗа
КамАЗ АА1234 per_trip 500 500 грн за ходку
КамАЗ АА1234 long_trip 500 +500 за довгу ходку
КамАЗ АА1234 extra_trip 500 +500 за кожну після першої
МАЗ ВВ5678 salary 10000 Оклад для МАЗа
МАЗ ВВ5678 per_trip 600 600 грн за ходку
(truck) salary 7000 Загальний оклад для вантажівок
(truck) per_trip 400 Загальна ставка за ходку

Діаграма потоку даних

┌─────────────────────────────────────────────────────────┐
│                    ДОВІДНИКИ                             │
│  DriverAccrualType    DriverSalaryRate                  │
│  (види нарахувань)     (тарифи по авто)                  │
└──────────┬────────────────────┬──────────────────────────┘
           │                    │
           ▼                    ▼
┌─────────────────────────────────────────────────────────┐
│              ПОДОРОЖНІЙ ЛИСТ (Waybill)                   │
│                                                          │
│  [Розрахувати] → DriverSalaryCalculator                  │
│      │                                                   │
│      ▼                                                   │
│  WaybillDriverAccrual (підтаблиця)                       │
│  ┌──────────┬──────────────┬───────┬────────┬──────┐    │
│  │ Водій    │ Вид нарах.   │ Показ.│ Тариф  │ Сума │    │
│  ├──────────┼──────────────┼───────┼────────┼──────┤    │
│  │ Іванов   │ За ходку     │ 3     │ 500    │ 1500 │    │
│  │ Іванов   │ За довгу     │ 1     │ 500    │ 500  │    │
│  │ Іванов   │ За додаткову │ 2     │ 500    │ 1000 │    │
│  └──────────┴──────────────┴───────┴────────┴──────┘    │
│                                                          │
│  [Провести] → state=posted                               │
│      │                                                   │
│      ▼                                                   │
│  PieceworkSalaryJournal (регістр оборотів)               │
│  (1 запис на кожен рядок WaybillDriverAccrual)           │
└──────────────────────────┬──────────────────────────────┘
                           │ (дані за місяць)
┌─────────────────────────────────────────────────────────┐
│     НАРАХУВАННЯ ЗАРПЛАТИ (DriverSalaryAccrual)           │
│                                                          │
│  [Заповнити] → DriverSalaryAccrualService.fill()         │
│      │                                                   │
│      ├── Зібрати з PieceworkSalaryJournal за місяць       │
│      ├── Агрегувати: водій + вид нарахування → сума      │
│      └── + Додати фіксований оклад з DriverSalaryRate    │
│                                                          │
│  DriverSalaryAccrualLine (підтаблиця)                    │
│  ┌──────────┬──────────────┬───────┬────────┬──────┐    │
│  │ Водій    │ Вид нарах.   │ Показ.│ Тариф  │ Сума │    │
│  ├──────────┼──────────────┼───────┼────────┼──────┤    │
│  │ Іванов   │ Оклад        │ 1     │ 8000   │ 8000 │    │
│  │ Іванов   │ За ходку     │ 12    │ 500    │ 6000 │    │
│  │ Іванов   │ За довгу     │ 3     │ 500    │ 1500 │    │
│  │ Іванов   │ За додаткову │ 9     │ 500    │ 4500 │    │
│  │ Петров   │ Оклад        │ 1     │ 10000  │10000 │    │
│  │ Петров   │ За ходку     │ 8     │ 600    │ 4800 │    │
│  └──────────┴──────────────┴───────┴────────┴──────┘    │
│  Разом: 34800 грн                                        │
│                                                          │
│  [Провести] → state=posted                               │
│      │                                                   │
│      ▼                                                   │
│  DriverSalaryLedger (регістр нарахувань)                 │
│  водій | місяць | вид нарахування | сума                  │
└──────────────────────────┬──────────────────────────────┘
                           │ (в майбутньому)
┌─────────────────────────────────────────────────────────┐
│          ESSENTIALS — Зарплата (майбутнє)                │
│                                                          │
│  Документ "Нарахування зарплати" бере дані               │
│  з DriverSalaryLedger як базу для офіційної              │
│  зарплати працівників.                                   │
└─────────────────────────────────────────────────────────┘

План імплементації (покроково)

Етап 1: Backend моделі (довідники)

  1. Створити backend/fleet/models/driver_accrual_type.py — DriverAccrualType
  2. Створити backend/fleet/models/driver_salary_rate.py — DriverSalaryRate
  3. Оновити backend/fleet/models/__init__.py — імпорти
  4. Зареєструвати в EntityRegistry (fleet/apps.py)
  5. Додати serializers, viewsets, urls
  6. Міграція

Етап 2: Підтаблиця подорожнього листа

  1. Створити backend/fleet/models/waybill_driver_accrual.py — WaybillDriverAccrual
  2. Оновити waybill.py — додати driverAccruals до _subtables
  3. Зареєструвати в EntityRegistry
  4. Додати serializer, viewset, url
  5. Міграція

Етап 3: Сервіс розрахунку

  1. Створити backend/fleet/services/driver_salary_calculator.py
  2. Інтегрувати в WaybillViewSet.calculate() — додати крок розрахунку зарплати

Етап 4: Регістр відрядної зарплати

  1. Створити backend/registers/models/piecework_salary_journal.py
  2. Оновити registers/__init__.py
  3. Зареєструвати в EntityRegistry
  4. Додати serializer, viewset, url
  5. Міграція
  6. Інтегрувати в проведення Waybill (post/unpost)

Етап 5: Документ нарахування зарплати

  1. Створити backend/fleet/models/driver_salary_accrual.py — документ + рядки
  2. Зареєструвати в EntityRegistry
  3. Додати serializers, viewsets, urls
  4. Міграція

Етап 6: Сервіс документа

  1. Створити backend/fleet/services/driver_salary_accrual_service.py
  2. Додати endpoints: fill/, post/, unpost/

Етап 7: Регістр нарахувань

  1. Створити backend/registers/models/driver_salary_ledger.py
  2. Зареєструвати, serializer, viewset, url
  3. Міграція

Етап 8: Frontend

  1. Оновити frontend/erp/src/config/fleet.ts — нові items/subtables
  2. Оновити en.json та ua.json — i18n ключі

Етап 9: Seed Data

  1. Оновити seed.py — target driver_salary
  2. Створити seed функцію — тестові дані

Етап 10: Тестування

  1. Перевірити CRUD для всіх нових сутностей
  2. Перевірити розрахунок в подорожньому листі
  3. Перевірити проведення подорожнього листа → записи в PieceworkSalaryJournal
  4. Перевірити заповнення документа нарахування
  5. Перевірити проведення документа → записи в DriverSalaryLedger

Зв'язок з модулем Essentials (майбутнє)

Модуль Essentials в майбутньому матиме повноцінний функціонал зарплати з: - Довідником працівників (Person + трудові відносини) - Штатним розписом - Нарахуваннями та утриманнями (ПДФО, ЄСВ, військовий збір) - Розрахунковою відомістю - Виплатою зарплати

Інтеграція: Essentials зарплата буде читати дані з DriverSalaryLedger (регістр нарахувань Fleet) як один з джерел нарахувань. Це дозволяє: - Fleet відповідає за розрахунок відрядної зарплати водіїв - Essentials відповідає за офіційне нарахування (з податками, відрахуваннями) - Дані передаються через регістр, без жорсткого зв'язку між модулями

Стан 2026-05-17: HRM Payroll plugin (Phase F-1) уже існує (Position, Employee, PayrollPeriod, PayrollSlip з post_slipPostingGroup). Але bridge DriverSalaryLedgerPayrollSlip.base_amount ще не реалізований — Fleet salary world і HRM Payroll world зараз живуть окремо. Див. § Deferred нижче.


🔮 Deferred / Ideas

Fleet ↔ HRM Payroll bridge

Мотивація: Fleet вже рахує відрядно-преміальну зарплату водіїв (DriverSalaryLedger.amount = «водій Іваненко заробив 18 500 грн у березні»). HRM Payroll plugin рахує офіційне нарахування з ПДФО/ЄСВ/військовим збором (PayrollSlipPostingGroup). Між ними немає автоматичного містка — бухгалтер мусить вручну переносити суми з DriverSalaryLedger у PayrollSlip як base_amount нарахування. Чому відкладено: для модуля собівартості перевезень Fleet (CPM-калькулятор) bridge не потрібен — собівартість читає DriverSalaryLedger напряму. Bridge потрібен лише для офіційного бухгалтерського payroll. Більшість UA-перевізників ведуть зарплату водіїв окремо (часто паралельно у 1С/Excel) і поки не просили DOP закрити цей контур. Trigger: реальний клієнт хоче офіційно нараховувати зарплату водіям через DOP-бухгалтерію (НЕ паралельно у 1С/Excel) АБО при онбордингу Wave 1 ShipCore-pilot з'ясовується що йому критично потрібен єдиний контур Fleet+Payroll.