Система нарахування зарплати водіям (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: driverAccrualTypes → driver-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: driverSalaryRates → driver-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:
EntityRegistry: waybillDriverAccruals → waybill-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: driverSalaryAccruals → driver-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/ — регістр нарахувань
Розрахунок¶
Структура файлів¶
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¶
Довідник видів нарахувань (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 моделі (довідники)¶
- Створити
backend/fleet/models/driver_accrual_type.py— DriverAccrualType - Створити
backend/fleet/models/driver_salary_rate.py— DriverSalaryRate - Оновити
backend/fleet/models/__init__.py— імпорти - Зареєструвати в EntityRegistry (
fleet/apps.py) - Додати serializers, viewsets, urls
- Міграція
Етап 2: Підтаблиця подорожнього листа¶
- Створити
backend/fleet/models/waybill_driver_accrual.py— WaybillDriverAccrual - Оновити
waybill.py— додатиdriverAccrualsдо_subtables - Зареєструвати в EntityRegistry
- Додати serializer, viewset, url
- Міграція
Етап 3: Сервіс розрахунку¶
- Створити
backend/fleet/services/driver_salary_calculator.py - Інтегрувати в
WaybillViewSet.calculate()— додати крок розрахунку зарплати
Етап 4: Регістр відрядної зарплати¶
- Створити
backend/registers/models/piecework_salary_journal.py - Оновити
registers/__init__.py - Зареєструвати в EntityRegistry
- Додати serializer, viewset, url
- Міграція
- Інтегрувати в проведення Waybill (post/unpost)
Етап 5: Документ нарахування зарплати¶
- Створити
backend/fleet/models/driver_salary_accrual.py— документ + рядки - Зареєструвати в EntityRegistry
- Додати serializers, viewsets, urls
- Міграція
Етап 6: Сервіс документа¶
- Створити
backend/fleet/services/driver_salary_accrual_service.py - Додати endpoints: fill/, post/, unpost/
Етап 7: Регістр нарахувань¶
- Створити
backend/registers/models/driver_salary_ledger.py - Зареєструвати, serializer, viewset, url
- Міграція
Етап 8: Frontend¶
- Оновити
frontend/erp/src/config/fleet.ts— нові items/subtables - Оновити
en.jsonтаua.json— i18n ключі
Етап 9: Seed Data¶
- Оновити
seed.py— targetdriver_salary - Створити seed функцію — тестові дані
Етап 10: Тестування¶
- Перевірити CRUD для всіх нових сутностей
- Перевірити розрахунок в подорожньому листі
- Перевірити проведення подорожнього листа → записи в PieceworkSalaryJournal
- Перевірити заповнення документа нарахування
- Перевірити проведення документа → записи в DriverSalaryLedger
Зв'язок з модулем Essentials (майбутнє)¶
Модуль Essentials в майбутньому матиме повноцінний функціонал зарплати з: - Довідником працівників (Person + трудові відносини) - Штатним розписом - Нарахуваннями та утриманнями (ПДФО, ЄСВ, військовий збір) - Розрахунковою відомістю - Виплатою зарплати
Інтеграція: Essentials зарплата буде читати дані з DriverSalaryLedger (регістр нарахувань Fleet) як один з джерел нарахувань. Це дозволяє:
- Fleet відповідає за розрахунок відрядної зарплати водіїв
- Essentials відповідає за офіційне нарахування (з податками, відрахуваннями)
- Дані передаються через регістр, без жорсткого зв'язку між модулями
Стан 2026-05-17: HRM Payroll plugin (Phase F-1) уже існує (
Position,Employee,PayrollPeriod,PayrollSlipзpost_slip→PostingGroup). Але bridgeDriverSalaryLedger→PayrollSlip.base_amountще не реалізований — Fleet salary world і HRM Payroll world зараз живуть окремо. Див. § Deferred нижче.
🔮 Deferred / Ideas¶
Fleet ↔ HRM Payroll bridge¶
Мотивація: Fleet вже рахує відрядно-преміальну зарплату водіїв (DriverSalaryLedger.amount = «водій Іваненко заробив 18 500 грн у березні»). HRM Payroll plugin рахує офіційне нарахування з ПДФО/ЄСВ/військовим збором (PayrollSlip → PostingGroup). Між ними немає автоматичного містка — бухгалтер мусить вручну переносити суми з DriverSalaryLedger у PayrollSlip як base_amount нарахування.
Чому відкладено: для модуля собівартості перевезень Fleet (CPM-калькулятор) bridge не потрібен — собівартість читає DriverSalaryLedger напряму. Bridge потрібен лише для офіційного бухгалтерського payroll. Більшість UA-перевізників ведуть зарплату водіїв окремо (часто паралельно у 1С/Excel) і поки не просили DOP закрити цей контур.
Trigger: реальний клієнт хоче офіційно нараховувати зарплату водіям через DOP-бухгалтерію (НЕ паралельно у 1С/Excel) АБО при онбордингу Wave 1 ShipCore-pilot з'ясовується що йому критично потрібен єдиний контур Fleet+Payroll.