From ae5c1f1848276b1c4e0cb9bdd61ae1ccdb4eb021 Mon Sep 17 00:00:00 2001 From: lokses Date: Sun, 9 Aug 2026 14:58:17 +0300 Subject: [PATCH] Add RRO, portal section perms, dislocation presets, and hiring intern flow. Ship cash-register registry, section-based portal access, configurable dislocation columns, and trial-hire intern accounts with credentials PDF. --- acore/settings/base.py | 1 + acore/urls.py | 3 + apps/ad_connector/admin.py | 9 +- apps/ad_connector/views.py | 12 +- apps/employees/admin.py | 137 +- .../migrations/0012_employee_defaults.py | 38 + .../0013_dislocation_column_presets.py | 18 + apps/employees/models/__init__.py | 1 + apps/employees/models/base.py | 9 + apps/employees/models/defaults.py | 43 + apps/hiring/admin.py | 25 +- .../0004_intern_account_and_user_location.py | 46 + .../migrations/0005_seed_intern_accounts.py | 36 + ...06_alter_internaccount_options_and_more.py | 140 + apps/hiring/models.py | 113 +- apps/hiring/pdf.py | 83 + apps/hiring/services.py | 276 +- apps/hiring/tests.py | 347 ++- apps/it_manager/views.py | 17 +- apps/locations/admin.py | 12 +- apps/locations/dislocation_columns.py | 102 + .../0010_dislocation_column_presets.py | 35 + .../0011_seed_dislocation_column_presets.py | 52 + apps/locations/models/__init__.py | 8 + apps/locations/models/dislocation.py | 51 + apps/locations/tests.py | 124 + apps/rro/__init__.py | 0 apps/rro/admin.py | 39 + apps/rro/apps.py | 7 + apps/rro/migrations/0001_initial.py | 47 + .../migrations/0002_seed_rro_directories.py | 56 + apps/rro/migrations/__init__.py | 0 apps/rro/models.py | 79 + apps/rro/tests.py | 134 + apps/rro/urls.py | 7 + apps/rro/views.py | 75 + core/admin.py | 18 +- .../0008_access_location_types_help.py | 18 + .../0009_portal_access_permissions.py | 26 + .../0010_seed_portal_section_perms.py | 99 + .../0011_ensure_portal_section_perms.py | 76 + core/models/__init__.py | 10 + core/models/directories.py | 6 +- core/models/portal_access.py | 18 + core/portal_access.py | 51 + core/portal_sections.py | 69 + core/templatetags/common_tags.py | 14 +- core/tests.py | 1 + core/tests_portal_access.py | 125 + core/views.py | 201 +- locale/en/LC_MESSAGES/django.mo | Bin 37500 -> 75683 bytes locale/en/LC_MESSAGES/django.po | 2248 +++++++++------- locale/uk/LC_MESSAGES/django.mo | Bin 46103 -> 93525 bytes locale/uk/LC_MESSAGES/django.po | 2256 ++++++++++------- requirements.txt | 1 + static/assets/js/hr-portal.js | 146 +- templates/index.html | 29 +- templates/pages/dislocation.html | 167 +- templates/pages/hr-portal.html | 220 +- templates/pages/it-manager.html | 5 +- templates/pages/pharmacy-detail.html | 50 + templates/pages/rro.html | 103 + templates/partials/dislo_cell.html | 65 + 63 files changed, 5987 insertions(+), 2217 deletions(-) create mode 100644 apps/employees/migrations/0012_employee_defaults.py create mode 100644 apps/employees/migrations/0013_dislocation_column_presets.py create mode 100644 apps/employees/models/defaults.py create mode 100644 apps/hiring/migrations/0004_intern_account_and_user_location.py create mode 100644 apps/hiring/migrations/0005_seed_intern_accounts.py create mode 100644 apps/hiring/migrations/0006_alter_internaccount_options_and_more.py create mode 100644 apps/hiring/pdf.py create mode 100644 apps/locations/dislocation_columns.py create mode 100644 apps/locations/migrations/0010_dislocation_column_presets.py create mode 100644 apps/locations/migrations/0011_seed_dislocation_column_presets.py create mode 100644 apps/locations/models/dislocation.py create mode 100644 apps/rro/__init__.py create mode 100644 apps/rro/admin.py create mode 100644 apps/rro/apps.py create mode 100644 apps/rro/migrations/0001_initial.py create mode 100644 apps/rro/migrations/0002_seed_rro_directories.py create mode 100644 apps/rro/migrations/__init__.py create mode 100644 apps/rro/models.py create mode 100644 apps/rro/tests.py create mode 100644 apps/rro/urls.py create mode 100644 apps/rro/views.py create mode 100644 core/migrations/0008_access_location_types_help.py create mode 100644 core/migrations/0009_portal_access_permissions.py create mode 100644 core/migrations/0010_seed_portal_section_perms.py create mode 100644 core/migrations/0011_ensure_portal_section_perms.py create mode 100644 core/models/portal_access.py create mode 100644 core/portal_access.py create mode 100644 core/portal_sections.py create mode 100644 core/tests_portal_access.py create mode 100644 templates/pages/rro.html create mode 100644 templates/partials/dislo_cell.html diff --git a/acore/settings/base.py b/acore/settings/base.py index 8c33efa..1ce32b8 100644 --- a/acore/settings/base.py +++ b/acore/settings/base.py @@ -77,6 +77,7 @@ INSTALLED_APPS = [ 'apps.tasks', 'apps.hiring', 'apps.utilities', + 'apps.rro', 'apps.ad_connector', 'apps.mail_connector', 'apps.mailparser', diff --git a/acore/urls.py b/acore/urls.py index 8971b6c..aa37f78 100644 --- a/acore/urls.py +++ b/acore/urls.py @@ -34,6 +34,7 @@ urlpatterns = [ path('', include('apps.it_manager.urls')), path('', include('apps.mattermost_connector.urls')), path('', include('apps.utilities.urls')), + path('', include('apps.rro.urls')), path('', include('apps.nextcloud_connector.urls')), path('tinymce/', include('tinymce.urls')), path('', views.index_view, name='index'), @@ -48,6 +49,7 @@ urlpatterns = [ path('employees//', views.employee_detail_view, name='employee_detail'), path('pharmacies/', views.pharmacies_view, name='pharmacies'), path('dislocation/', views.dislocation_view, name='dislocation'), + path('dislocation/columns/', views.dislocation_columns_view, name='dislocation_columns'), path('calendar/', views.calendar_view, name='calendar'), path('timetrack/', views.timetrack_view, name='timetrack'), path('reports/', views.reports_view, name='reports'), @@ -65,6 +67,7 @@ urlpatterns = [ path('hiring/', views.hiring_create_view, name='hiring_create'), path('hiring//approve/', views.hiring_approve_view, name='hiring_approve'), path('hiring//reject/', views.hiring_reject_view, name='hiring_reject'), + path('hiring//credentials.pdf/', views.hiring_credentials_pdf_view, name='hiring_credentials_pdf'), path('help/', views.help_view, name='help'), path('it-support/', views.it_support_view, name='it_support'), path('directory/quick-add/', views.directory_quick_add_view, name='directory_quick_add'), diff --git a/apps/ad_connector/admin.py b/apps/ad_connector/admin.py index 4f56804..580c0ba 100644 --- a/apps/ad_connector/admin.py +++ b/apps/ad_connector/admin.py @@ -10,6 +10,8 @@ class ADSyncLogAdmin(ModelAdmin): list_filter = ('action', 'status') search_fields = ('employee__username', 'employee__last_name', 'employee__first_name', 'message') date_hierarchy = 'created_at' + # Журнал только для просмотра — без массового удаления из changelist. + actions = None def has_add_permission(self, request): return False @@ -18,4 +20,9 @@ class ADSyncLogAdmin(ModelAdmin): return False def has_delete_permission(self, request, obj=None): - return False + # Нельзя False для всех: Django get_deleted_objects() зовёт этот метод + # при удалении Employee с CASCADE на ADSyncLog и блокирует удаление + # сообщением «нет прав на Запись синхронизации AD». + # Разрешаем CASCADE тем, кто может удалять сотрудников; UI журнала + # без delete_selected (actions = None). + return request.user.has_perm('employees.delete_employee') diff --git a/apps/ad_connector/views.py b/apps/ad_connector/views.py index d8c5c60..9c42b7b 100644 --- a/apps/ad_connector/views.py +++ b/apps/ad_connector/views.py @@ -1,7 +1,7 @@ import logging -from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth import get_user_model +from django.contrib.auth.decorators import login_required from django.shortcuts import get_object_or_404 from django.utils import timezone @@ -9,6 +9,7 @@ from apps.ad_connector.ad_service import ADConfigError, ADService from apps.ad_connector.services.sync_service import relocate_ou_references from apps.locations.models import Location from core.models import Directory +from core.portal_access import section_required from core.utils.htmx import render_page from core.utils.pagination import paginate @@ -68,7 +69,8 @@ def _find_suggestion(employee, ad_users, linked_dns): return None, None -@staff_member_required +@login_required +@section_required('ad_mapping') def ad_mapping_view(request): """Страница сопоставления офисных сотрудников с учётными записями AD""" if request.method == 'POST': @@ -177,7 +179,8 @@ def _handle_ou_action(request): service.unbind() -@staff_member_required +@login_required +@section_required('ad_mapping') def ad_ou_manage_view(request): """Управление организационными юнитами (OU) в Active Directory и их привязкой к отделам""" error, notice = None, None @@ -257,7 +260,8 @@ def _import_context(request): } -@staff_member_required +@login_required +@section_required('ad_mapping') def ad_import_view(request): """Импорт учётных записей AD, отсутствующих среди сотрудников, как новых офисных сотрудников. Ничего не создаётся, пока администратор явно не отметит и не подтвердит конкретные записи.""" diff --git a/apps/employees/admin.py b/apps/employees/admin.py index 4cd1e6c..3c11921 100644 --- a/apps/employees/admin.py +++ b/apps/employees/admin.py @@ -1,14 +1,22 @@ from django import forms from django.contrib import admin +from django.contrib.auth.models import Permission +from django.contrib.contenttypes.models import ContentType from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from unfold.admin import ModelAdmin, TabularInline -from core.models import Directory -from .models import Employee, PermissionGroup +from core.models import Directory, PortalAccess +from .models import Employee, PermissionGroup, EmployeeDefaults, EmployeePermissionOverride from apps.visit_manager.models import WorkSchedule, AttendanceLog + +def _portal_permissions_qs(): + ct = ContentType.objects.get_for_model(PortalAccess, for_concrete_model=False) + return Permission.objects.filter(content_type=ct, codename__startswith='access_').order_by('codename') + + class HierarchicalDepartmentChoiceField(forms.ModelChoiceField): """Поле выбора отдела с отступами по иерархии в выпадающем списке.""" @@ -78,6 +86,20 @@ class WorkScheduleInline(TabularInline): return False +class EmployeePermissionOverrideInline(TabularInline): + model = EmployeePermissionOverride + extra = 0 + fields = ('permission', 'override_type', 'valid_from', 'valid_until', 'is_active', 'comment') + autocomplete_fields = [] + verbose_name = _('Доп. право на раздел портала') + verbose_name_plural = _('Доп. права на разделы портала') + + def formfield_for_foreignkey(self, db_field, request, **kwargs): + if db_field.name == 'permission': + kwargs['queryset'] = _portal_permissions_qs() + return super().formfield_for_foreignkey(db_field, request, **kwargs) + + class AttendanceLogInline(TabularInline): model = AttendanceLog fk_name = 'employee' @@ -98,7 +120,7 @@ class AttendanceLogInline(TabularInline): @admin.register(Employee) class EmployeeAdmin(ModelAdmin): form = EmployeeAdminForm - inlines = [WorkScheduleInline, AttendanceLogInline] + inlines = [EmployeePermissionOverrideInline, WorkScheduleInline, AttendanceLogInline] class Media: css = {'all': ('users/css/employee_admin.css',)} @@ -256,4 +278,111 @@ class EmployeeAdmin(ModelAdmin): ) except Exception: return obj.department.name or '—' - department_display.short_description = _('Отдел') \ No newline at end of file + department_display.short_description = _('Отдел') + +@admin.register(EmployeeDefaults) +class EmployeeDefaultsAdmin(ModelAdmin): + """Единственная запись настроек по умолчанию для сотрудников.""" + + fieldsets = ( + (_('Офисные сотрудники'), { + 'fields': ('office_email_domain',), + 'description': _( + 'При утверждении заявки на найм офисному сотруднику ' + 'назначается рабочий email: логин@домен.' + ), + }), + ) + + def has_add_permission(self, request): + return not EmployeeDefaults.objects.exists() + + def has_delete_permission(self, request, obj=None): + return False + + +class PermissionGroupAdminForm(forms.ModelForm): + portal_permissions = forms.ModelMultipleChoiceField( + label=_('Разделы портала'), + queryset=Permission.objects.none(), + required=False, + widget=admin.widgets.FilteredSelectMultiple(_('разделы'), is_stacked=False), + help_text=_('Базовый набор доступов к разделам для этой роли. Доп. права сотруднику — через overrides.'), + ) + + class Meta: + model = PermissionGroup + fields = ('group', 'description', 'is_active') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['portal_permissions'].queryset = _portal_permissions_qs() + if self.instance and self.instance.pk and self.instance.group_id: + portal_pks = set(_portal_permissions_qs().values_list('pk', flat=True)) + self.fields['portal_permissions'].initial = [ + p for p in self.instance.group.permissions.all() if p.pk in portal_pks + ] + + def save_portal_permissions(self, obj): + portal_qs = list(_portal_permissions_qs()) + selected = self.cleaned_data.get('portal_permissions') + if selected is None: + return + group = obj.group + group.permissions.remove(*portal_qs) + group.permissions.add(*selected) + + +@admin.register(PermissionGroup) +class PermissionGroupAdmin(ModelAdmin): + form = PermissionGroupAdminForm + list_display = ('group', 'is_active', 'description') + list_filter = ('is_active',) + search_fields = ('group__name', 'description') + + fieldsets = ( + (None, { + 'fields': ('group', 'description', 'is_active', 'portal_permissions'), + }), + ) + + def save_model(self, request, obj, form, change): + super().save_model(request, obj, form, change) + form.save_portal_permissions(obj) + +@admin.register(EmployeePermissionOverride) +class EmployeePermissionOverrideAdmin(ModelAdmin): + list_display = ( + 'employee', 'permission', 'override_type', + 'valid_from', 'valid_until', 'is_active', + ) + list_filter = ('override_type', 'is_active') + search_fields = ( + 'employee__username', 'employee__last_name', 'employee__first_name', + 'permission__codename', 'comment', + ) + list_select_related = ('employee', 'permission') + autocomplete_fields = ('employee',) + readonly_fields = ('created_at', 'updated_at', 'uuid') + + fieldsets = ( + (None, { + 'fields': ('employee', 'permission', 'override_type', 'is_active'), + 'description': _( + 'grant — выдать раздел сверх группы; ' + 'revoke — запретить раздел даже если право есть в группе.' + ), + }), + (_('Срок действия'), { + 'fields': ('valid_from', 'valid_until', 'comment'), + }), + (_('Служебное'), { + 'classes': ('collapse',), + 'fields': ('uuid', 'created_at', 'updated_at'), + }), + ) + + def formfield_for_foreignkey(self, db_field, request, **kwargs): + if db_field.name == 'permission': + kwargs['queryset'] = _portal_permissions_qs() + return super().formfield_for_foreignkey(db_field, request, **kwargs) diff --git a/apps/employees/migrations/0012_employee_defaults.py b/apps/employees/migrations/0012_employee_defaults.py new file mode 100644 index 0000000..0e321c5 --- /dev/null +++ b/apps/employees/migrations/0012_employee_defaults.py @@ -0,0 +1,38 @@ +# Generated by Django 6.0.3 on 2026-07-28 19:38 + +from django.db import migrations, models + + +def seed_defaults(apps, schema_editor): + EmployeeDefaults = apps.get_model('employees', 'EmployeeDefaults') + EmployeeDefaults.objects.get_or_create( + pk=1, + defaults={'office_email_domain': 'aks-apteka.com.ua'}, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('employees', '0011_employee_totp_backup_codes_employee_totp_enabled_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='EmployeeDefaults', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('office_email_domain', models.CharField( + default='aks-apteka.com.ua', + help_text='При утверждении заявки на найм офисному сотруднику назначается email вида логин@этот-домен (вместо @local.domain).', + max_length=255, + verbose_name='Домен email для офисных сотрудников', + )), + ], + options={ + 'verbose_name': 'Настройки сотрудников по умолчанию', + 'verbose_name_plural': 'Настройки сотрудников по умолчанию', + }, + ), + migrations.RunPython(seed_defaults, migrations.RunPython.noop), + ] diff --git a/apps/employees/migrations/0013_dislocation_column_presets.py b/apps/employees/migrations/0013_dislocation_column_presets.py new file mode 100644 index 0000000..777c96a --- /dev/null +++ b/apps/employees/migrations/0013_dislocation_column_presets.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.3 on 2026-07-28 21:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('employees', '0012_employee_defaults'), + ] + + operations = [ + migrations.AddField( + model_name='employee', + name='dislocation_columns', + field=models.JSONField(blank=True, help_text='Персональный набор колонок таблицы /dislocation/. Пусто — пресет отдела или системный «Стандарт».', null=True, verbose_name='Колонки дислокации'), + ), + ] diff --git a/apps/employees/models/__init__.py b/apps/employees/models/__init__.py index e088ed0..0443f65 100644 --- a/apps/employees/models/__init__.py +++ b/apps/employees/models/__init__.py @@ -1,2 +1,3 @@ from .base import Employee, EmployeeManager +from .defaults import EmployeeDefaults from .permissions import PermissionGroup, EmployeePermissionOverride diff --git a/apps/employees/models/base.py b/apps/employees/models/base.py index 5ffc9ec..e62abfb 100644 --- a/apps/employees/models/base.py +++ b/apps/employees/models/base.py @@ -173,6 +173,15 @@ class Employee(AbstractBaseUser, PermissionsMixin, BaseModel): help_text=_('Сколько строк показывать в таблицах портала по умолчанию'), ) + dislocation_columns = models.JSONField( + _('Колонки дислокации'), + null=True, blank=True, + help_text=_( + 'Персональный набор колонок таблицы /dislocation/. ' + 'Пусто — пресет отдела или системный «Стандарт».' + ), + ) + last_seen = models.DateTimeField( _('Был онлайн'), null=True, blank=True, help_text=_('Обновляется автоматически при активности в портале (не в AD/Mattermost)'), diff --git a/apps/employees/models/defaults.py b/apps/employees/models/defaults.py new file mode 100644 index 0000000..1c8aa6e --- /dev/null +++ b/apps/employees/models/defaults.py @@ -0,0 +1,43 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class EmployeeDefaults(models.Model): + """Синглтон: значения по умолчанию для сотрудников (настраивается в админке). + + Сейчас используется при утверждении заявки на найм офисного сотрудника — + рабочий email становится «логин@office_email_domain». + """ + + office_email_domain = models.CharField( + _('Домен email для офисных сотрудников'), + max_length=255, + default='aks-apteka.com.ua', + help_text=_( + 'При утверждении заявки на найм офисному сотруднику ' + 'назначается email вида логин@этот-домен (вместо @local.domain).' + ), + ) + + class Meta: + verbose_name = _('Настройки сотрудников по умолчанию') + verbose_name_plural = _('Настройки сотрудников по умолчанию') + + def __str__(self): + return str(_('Настройки сотрудников по умолчанию')) + + def save(self, *args, **kwargs): + self.pk = 1 + super().save(*args, **kwargs) + + def delete(self, *args, **kwargs): + pass # синглтон нельзя удалить + + @classmethod + def load(cls): + obj, _ = cls.objects.get_or_create(pk=1) + return obj + + @property + def normalized_office_domain(self) -> str: + return (self.office_email_domain or 'aks-apteka.com.ua').strip().lstrip('@').lower() diff --git a/apps/hiring/admin.py b/apps/hiring/admin.py index a01a92d..84ffcd3 100644 --- a/apps/hiring/admin.py +++ b/apps/hiring/admin.py @@ -1,14 +1,31 @@ from django.contrib import admin from unfold.admin import ModelAdmin -from apps.hiring.models import HiringRequest +from apps.hiring.models import HiringRequest, InternAccount + + +@admin.register(InternAccount) +class InternAccountAdmin(ModelAdmin): + list_display = ['label', 'username', 'is_active', 'assigned_to', 'updated_at'] + list_filter = ['is_active'] + search_fields = ['label', 'username'] + list_select_related = ['assigned_to'] + ordering = ['label'] + readonly_fields = ['assigned_to', 'created_at', 'updated_at', 'uuid'] @admin.register(HiringRequest) class HiringRequestAdmin(ModelAdmin): - list_display = ['last_name', 'first_name', 'status', 'hire_date', 'trial_days', 'requested_by', 'created_at'] - list_filter = ['status', 'user_location_type'] + list_display = [ + 'last_name', 'first_name', 'status', 'hire_date', 'trial_days', + 'requested_by', 'granted_ticket', 'created_at', + ] + list_filter = ['status', 'user_location_type'] search_fields = ['last_name', 'first_name', 'middle_name', 'candidate_email'] - list_select_related = ['requested_by', 'decision_by', 'employee', 'post', 'department'] + list_select_related = [ + 'requested_by', 'decision_by', 'employee', 'post', 'department', + 'user_pharmacy', 'user_location', 'granted_ticket', + ] filter_horizontal = ['requested_access'] ordering = ['-created_at'] + readonly_fields = ['issued_password', 'created_at', 'updated_at', 'uuid'] diff --git a/apps/hiring/migrations/0004_intern_account_and_user_location.py b/apps/hiring/migrations/0004_intern_account_and_user_location.py new file mode 100644 index 0000000..8b9ef09 --- /dev/null +++ b/apps/hiring/migrations/0004_intern_account_and_user_location.py @@ -0,0 +1,46 @@ +# Generated by Django 6.0.3 on 2026-07-28 20:16 + +import core.models.fields +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hiring', '0003_hiringrequest_access_task'), + ('locations', '0009_pharmacy_carbonio_password'), + ] + + operations = [ + migrations.AddField( + model_name='hiringrequest', + name='issued_password', + field=core.models.fields.EncryptedCharField(blank=True, max_length=512, verbose_name='Виданий пароль (PDF)'), + ), + migrations.AddField( + model_name='hiringrequest', + name='user_location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hiring_requests_location', to='locations.location', verbose_name='Локація (офіс/склад)'), + ), + migrations.CreateModel( + name='InternAccount', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('label', models.CharField(max_length=50, unique=True, verbose_name='Мітка')), + ('username', core.models.fields.EncryptedCharField(max_length=512, verbose_name='Логін')), + ('password', core.models.fields.EncryptedCharField(max_length=512, verbose_name='Пароль')), + ('is_active', models.BooleanField(default=True, verbose_name='Активний')), + ('assigned_to', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='intern_account', to='hiring.hiringrequest', verbose_name='Призначено заявці')), + ], + options={ + 'verbose_name': 'Інтерн-обліковий запис', + 'verbose_name_plural': 'Пул інтерн-облікових записів', + 'ordering': ['label'], + }, + ), + ] diff --git a/apps/hiring/migrations/0005_seed_intern_accounts.py b/apps/hiring/migrations/0005_seed_intern_accounts.py new file mode 100644 index 0000000..7cb6e7d --- /dev/null +++ b/apps/hiring/migrations/0005_seed_intern_accounts.py @@ -0,0 +1,36 @@ +"""Seed pool of 5 shared intern accounts for hiring trial period.""" +from django.db import migrations + + +def seed_intern_accounts(apps, schema_editor): + # Concrete model — EncryptedCharField must encrypt on save. + from apps.hiring.models import InternAccount + + for i in range(1, 6): + InternAccount.objects.get_or_create( + label=f'Intern-{i}', + defaults={ + 'username': f'intern{i}', + 'password': f'InternPass{i}!', + 'is_active': True, + }, + ) + + +def unseed_intern_accounts(apps, schema_editor): + InternAccount = apps.get_model('hiring', 'InternAccount') + InternAccount.objects.filter( + label__in=[f'Intern-{i}' for i in range(1, 6)], + assigned_to__isnull=True, + ).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('hiring', '0004_intern_account_and_user_location'), + ] + + operations = [ + migrations.RunPython(seed_intern_accounts, unseed_intern_accounts), + ] diff --git a/apps/hiring/migrations/0006_alter_internaccount_options_and_more.py b/apps/hiring/migrations/0006_alter_internaccount_options_and_more.py new file mode 100644 index 0000000..a5576c7 --- /dev/null +++ b/apps/hiring/migrations/0006_alter_internaccount_options_and_more.py @@ -0,0 +1,140 @@ +# Generated by Django 6.0.3 on 2026-07-29 08:35 + +import core.models.fields +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_access_location_types_help'), + ('hiring', '0005_seed_intern_accounts'), + ('locations', '0011_seed_dislocation_column_presets'), + ('tasks', '0005_alter_task_priority_alter_task_status'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterModelOptions( + name='internaccount', + options={'ordering': ['label'], 'verbose_name': 'Интерн-аккаунт', 'verbose_name_plural': 'Пул интерн-аккаунтов'}, + ), + migrations.AlterField( + model_name='hiringrequest', + name='access_note', + field=models.TextField(blank=True, verbose_name='Примечание по доступам'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='access_task', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='tasks.task', verbose_name='Задача на выдачу доступов'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='candidate_email', + field=models.EmailField(blank=True, max_length=254, verbose_name='Личный email кандидата'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='decision_at', + field=models.DateTimeField(blank=True, null=True, verbose_name='Дата решения'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='decision_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hiring_requests_decided', to=settings.AUTH_USER_MODEL, verbose_name='Решение принял'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='decision_note', + field=models.TextField(blank=True, verbose_name='Комментарий к решению'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='department', + field=models.ForeignKey(blank=True, limit_choices_to={'directory_type__code': 'department', 'is_active': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hiring_requests_department', to='core.directory', verbose_name='Отдел'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='employee', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hiring_request', to=settings.AUTH_USER_MODEL, verbose_name='Учётная запись'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='first_name', + field=models.CharField(max_length=150, verbose_name='Имя'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='issued_password', + field=core.models.fields.EncryptedCharField(blank=True, max_length=512, verbose_name='Выданный пароль (PDF)'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='last_name', + field=models.CharField(max_length=150, verbose_name='Фамилия'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='middle_name', + field=models.CharField(blank=True, max_length=150, verbose_name='Отчество'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='post', + field=models.ForeignKey(blank=True, limit_choices_to={'directory_type__code': 'post', 'is_active': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hiring_requests_post', to='core.directory', verbose_name='Должность'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='requested_access', + field=models.ManyToManyField(blank=True, limit_choices_to={'directory_type__code': 'access_system', 'is_active': True}, related_name='hiring_requests_access', to='core.directory', verbose_name='Требуемые доступы'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='requested_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='hiring_requests_submitted', to=settings.AUTH_USER_MODEL, verbose_name='Подал HR'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='status', + field=models.CharField(choices=[('trial', 'На испытании'), ('approved', 'Утверждено'), ('rejected', 'Отклонено')], default='trial', max_length=10, verbose_name='Статус'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='trial_days', + field=models.PositiveSmallIntegerField(choices=[(1, '1 дн.'), (2, '2 дн.'), (3, '3 дн.'), (4, '4 дн.'), (5, '5 дн.')], default=3, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)], verbose_name='Срок испытания, дней'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='user_location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hiring_requests_location', to='locations.location', verbose_name='Локация (офис/склад)'), + ), + migrations.AlterField( + model_name='hiringrequest', + name='user_location_type', + field=models.CharField(choices=[('pharmacy', 'Аптека'), ('warehouse', 'Склад'), ('office', 'Офис')], max_length=20, verbose_name='Тип локации'), + ), + migrations.AlterField( + model_name='internaccount', + name='assigned_to', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='intern_account', to='hiring.hiringrequest', verbose_name='Назначено заявке'), + ), + migrations.AlterField( + model_name='internaccount', + name='is_active', + field=models.BooleanField(default=True, verbose_name='Активный'), + ), + migrations.AlterField( + model_name='internaccount', + name='label', + field=models.CharField(max_length=50, unique=True, verbose_name='Метка'), + ), + migrations.AlterField( + model_name='internaccount', + name='username', + field=core.models.fields.EncryptedCharField(max_length=512, verbose_name='Логин'), + ), + ] diff --git a/apps/hiring/models.py b/apps/hiring/models.py index e053092..b353281 100644 --- a/apps/hiring/models.py +++ b/apps/hiring/models.py @@ -1,30 +1,82 @@ from datetime import timedelta +from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ from core.models import BaseModel +from core.models.fields import EncryptedCharField from core.utils.validators import validate_ukrainian_phone +class InternAccount(BaseModel): + """Спільний інтерн-обліковий запис з пулу (5 слотів). Призначається заявці + на час випробування; після approve/reject звільняється.""" + + label = models.CharField(_('Метка'), max_length=50, unique=True) + username = EncryptedCharField(_('Логин'), max_length=512) + password = EncryptedCharField(_('Пароль'), max_length=512) + is_active = models.BooleanField(_('Активный'), default=True) + assigned_to = models.OneToOneField( + 'hiring.HiringRequest', on_delete=models.SET_NULL, + null=True, blank=True, + related_name='intern_account', verbose_name=_('Назначено заявке'), + ) + + class Meta: + verbose_name = _('Интерн-аккаунт') + verbose_name_plural = _('Пул интерн-аккаунтов') + ordering = ['label'] + + def __str__(self): + status = _('занят') if self.assigned_to_id else _('свободен') + return f'{self.label} ({status})' + + @classmethod + def allocate(cls, hiring_request): + """Виділяє свободен слот під select_for_update (викликати всередині atomic).""" + account = ( + cls.objects.select_for_update() + .filter(is_active=True, assigned_to__isnull=True) + .order_by('label') + .first() + ) + if account is None: + raise ValidationError({ + '__all__': _( + 'Нет свободных интерн-аккаунтов. ' + 'Дождитесь завершения стажировки или добавьте слот в админке.' + ), + }) + account.assigned_to = hiring_request + account.save(update_fields=['assigned_to', 'updated_at']) + return account + + def release(self): + if self.assigned_to_id is None: + return + self.assigned_to = None + self.save(update_fields=['assigned_to', 'updated_at']) + + class HiringRequest(BaseModel): - """Заявка HR на найм кандидата: створення тимчасового (стажерського) - облікового запису та подальше затвердження/відхилення.""" + """Заявка HR на найм кандидата: інтерн-слот + IT Ticket на випробуванні, + персональний Employee — лише після затвердження.""" STATUS_CHOICES = [ - ('trial', _('На випробуванні')), - ('approved', _('Затверджено')), - ('rejected', _('Відхилено')), + ('trial', _('На испытании')), + ('approved', _('Утверждено')), + ('rejected', _('Отклонено')), ] TRIAL_DAYS_CHOICES = [(i, _('%(n)d дн.') % {'n': i}) for i in range(1, 6)] - last_name = models.CharField(_('Прізвище'), max_length=150) - first_name = models.CharField(_('Ім\'я'), max_length=150) - middle_name = models.CharField(_('По батькові'), max_length=150, blank=True) + last_name = models.CharField(_('Фамилия'), max_length=150) + first_name = models.CharField(_('Имя'), max_length=150) + middle_name = models.CharField(_('Отчество'), max_length=150, blank=True) - candidate_email = models.EmailField(_('Особистий email кандидата'), blank=True) + candidate_email = models.EmailField(_('Личный email кандидата'), blank=True) candidate_phone = models.CharField( _('Телефон кандидата'), max_length=20, blank=True, validators=[validate_ukrainian_phone], @@ -33,60 +85,65 @@ class HiringRequest(BaseModel): LOCATION_TYPES = [ ('pharmacy', _('Аптека')), ('warehouse', _('Склад')), - ('office', _('Офіс')), + ('office', _('Офис')), ] user_location_type = models.CharField( - _('Тип локації'), max_length=20, choices=LOCATION_TYPES, + _('Тип локации'), max_length=20, choices=LOCATION_TYPES, ) department = models.ForeignKey( 'core.Directory', on_delete=models.SET_NULL, null=True, blank=True, limit_choices_to={'directory_type__code': 'department', 'is_active': True}, - related_name='hiring_requests_department', verbose_name=_('Відділ'), + related_name='hiring_requests_department', verbose_name=_('Отдел'), ) user_pharmacy = models.ForeignKey( 'locations.Pharmacy', on_delete=models.SET_NULL, null=True, blank=True, related_name='hiring_requests', verbose_name=_('Аптека'), ) + user_location = models.ForeignKey( + 'locations.Location', on_delete=models.SET_NULL, + null=True, blank=True, + related_name='hiring_requests_location', verbose_name=_('Локация (офис/склад)'), + ) post = models.ForeignKey( 'core.Directory', on_delete=models.SET_NULL, null=True, blank=True, limit_choices_to={'directory_type__code': 'post', 'is_active': True}, - related_name='hiring_requests_post', verbose_name=_('Посада'), + related_name='hiring_requests_post', verbose_name=_('Должность'), ) hire_date = models.DateField(_('Дата виходу'), default=timezone.localdate) trial_days = models.PositiveSmallIntegerField( - _('Термін випробування, днів'), choices=TRIAL_DAYS_CHOICES, default=3, + _('Срок испытания, дней'), choices=TRIAL_DAYS_CHOICES, default=3, validators=[MinValueValidator(1), MaxValueValidator(5)], ) requested_access = models.ManyToManyField( 'core.Directory', blank=True, limit_choices_to={'directory_type__code': 'access_system', 'is_active': True}, - related_name='hiring_requests_access', verbose_name=_('Потрібні доступи'), + related_name='hiring_requests_access', verbose_name=_('Требуемые доступы'), ) - access_note = models.TextField(_('Примітка щодо доступів'), blank=True) + access_note = models.TextField(_('Примечание по доступам'), blank=True) status = models.CharField(_('Статус'), max_length=10, choices=STATUS_CHOICES, default='trial') requested_by = models.ForeignKey( 'employees.Employee', on_delete=models.PROTECT, - related_name='hiring_requests_submitted', verbose_name=_('Подав HR'), + related_name='hiring_requests_submitted', verbose_name=_('Подал HR'), ) employee = models.OneToOneField( 'employees.Employee', on_delete=models.SET_NULL, null=True, blank=True, - related_name='hiring_request', verbose_name=_('Обліковий запис'), + related_name='hiring_request', verbose_name=_('Учётная запись'), ) decision_by = models.ForeignKey( 'employees.Employee', on_delete=models.SET_NULL, null=True, blank=True, - related_name='hiring_requests_decided', verbose_name=_('Рішення прийняв'), + related_name='hiring_requests_decided', verbose_name=_('Решение принял'), ) - decision_at = models.DateTimeField(_('Дата рішення'), null=True, blank=True) - decision_note = models.TextField(_('Коментар до рішення'), blank=True) + decision_at = models.DateTimeField(_('Дата решения'), null=True, blank=True) + decision_note = models.TextField(_('Комментарий к решению'), blank=True) granted_ticket = models.ForeignKey( 'it_manager.Ticket', on_delete=models.SET_NULL, @@ -96,7 +153,12 @@ class HiringRequest(BaseModel): access_task = models.ForeignKey( 'tasks.Task', on_delete=models.SET_NULL, null=True, blank=True, - related_name='+', verbose_name=_('Задача на видачу доступів'), + related_name='+', verbose_name=_('Задача на выдачу доступов'), + ) + + # Пароль персонального акаунта після approve — для одноразового PDF. + issued_password = EncryptedCharField( + _('Выданный пароль (PDF)'), max_length=512, blank=True, ) class Meta: @@ -117,3 +179,10 @@ class HiringRequest(BaseModel): @property def is_overdue(self): return self.status == 'trial' and self.trial_end_date < timezone.localdate() + + def release_intern(self): + try: + account = self.intern_account + except Exception: + return + account.release() diff --git a/apps/hiring/pdf.py b/apps/hiring/pdf.py new file mode 100644 index 0000000..59962ed --- /dev/null +++ b/apps/hiring/pdf.py @@ -0,0 +1,83 @@ +"""PDF з обліковими даними після затвердження кандидата.""" +from pathlib import Path + +from django.http import HttpResponse +from django.utils.translation import gettext as _ + +_FONT_CANDIDATES = ( + Path('/usr/share/fonts/google-noto/NotoSans-Regular.ttf'), + Path('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf'), + Path('/usr/share/fonts/dejavu/DejaVuSans.ttf'), +) + + +def _resolve_font(): + for path in _FONT_CANDIDATES: + if path.is_file(): + return str(path) + return None + + +def build_credentials_pdf(hiring_request) -> bytes: + from fpdf import FPDF + from fpdf.enums import XPos, YPos + + employee = hiring_request.employee + if employee is None: + raise ValueError('HiringRequest has no employee') + + pdf = FPDF() + pdf.set_auto_page_break(auto=True, margin=15) + pdf.add_page() + pdf.set_margins(20, 20, 20) + + font_path = _resolve_font() + font_name = 'Portal' if font_path else 'Helvetica' + if font_path: + pdf.add_font('Portal', fname=font_path) + pdf.set_font(font_name, size=14) + + pdf.cell( + 0, 10, _('Учётные данные сотрудника'), + new_x=XPos.LMARGIN, new_y=YPos.NEXT, + ) + pdf.ln(4) + pdf.set_font(font_name, size=11) + + dash = '-' + lines = [ + f"{_('ФИО')}: {employee.last_name} {employee.first_name} {employee.middle_name}".strip(), + f"{_('Должность')}: {employee.post.name if employee.post_id else dash}", + f"{_('Тип локации')}: {hiring_request.get_user_location_type_display()}", + f"{_('Логин')}: {employee.username}", + f"{_('Пароль')}: {hiring_request.issued_password or dash}", + f"{_('Рабочий email')}: {employee.email or dash}", + '', + f"{_('Запрошенные доступы')}:", + ] + accesses = list(hiring_request.requested_access.all()) + if accesses: + for access in accesses: + lines.append(f' - {access.name}') + else: + lines.append(f" {_('(не указано)')}") + + for line in lines: + if not line: + pdf.ln(4) + continue + pdf.set_x(pdf.l_margin) + pdf.multi_cell( + pdf.epw, 7, line, + new_x=XPos.LMARGIN, new_y=YPos.NEXT, + ) + + return bytes(pdf.output()) + + +def credentials_pdf_response(hiring_request) -> HttpResponse: + content = build_credentials_pdf(hiring_request) + filename = f'credentials-{hiring_request.employee.username}.pdf' + response = HttpResponse(content, content_type='application/pdf') + response['Content-Disposition'] = f'attachment; filename="{filename}"' + return response diff --git a/apps/hiring/services.py b/apps/hiring/services.py index c5acab1..4930d17 100644 --- a/apps/hiring/services.py +++ b/apps/hiring/services.py @@ -1,13 +1,14 @@ -"""Бізнес-логіка заявок на найм: створення тимчасового акаунта стажера та -подальше затвердження/відхилення. Використовує вже робочу AD/Mattermost -синхронізацію через сигнали Employee.save() — жодного нового коду для -зовнішніх систем тут немає.""" +"""Бізнес-логіка найму: інтерн-слот + IT Ticket на випробуванні; +персональний Employee і PDF — лише після затвердження.""" +import secrets + from django.db import transaction from django.utils import timezone +from django.utils.translation import gettext as _ from pytils.translit import slugify from apps.employees.models.base import Employee -from apps.hiring.models import HiringRequest +from apps.hiring.models import HiringRequest, InternAccount def _generate_username(last_name, first_name): @@ -20,32 +21,155 @@ def _generate_username(last_name, first_name): return username -@transaction.atomic -def create_hiring_request(*, requester, last_name, first_name, middle_name='', - candidate_email='', candidate_phone='', - user_location_type, department_id=None, user_pharmacy_id=None, post_id=None, - hire_date, trial_days, requested_access_ids=None, access_note=''): - """Створює одразу особистий обмежений обліковий запис (Employee) і - заявку (HiringRequest) на випробувальний термін.""" - username = _generate_username(last_name, first_name) +def _generate_password(): + return secrets.token_urlsafe(12) - employee = Employee.objects.create( - username=username, - last_name=last_name, - first_name=first_name, - middle_name=middle_name, - personal_phone=candidate_phone, + +def _coerce_location_fields(user_location_type, department_id, user_pharmacy_id, user_location_id): + """Приводить поля локації до правил Employee.clean(): аптека XOR офіс/склад.""" + if user_location_type == 'pharmacy': + return None, user_pharmacy_id, None + if user_location_type in ('office', 'warehouse'): + return department_id, None, user_location_id + return department_id, user_pharmacy_id, user_location_id + + +def _filter_access_ids_for_location(user_location_type, requested_access_ids): + """Залишає лише системи доступу, дозволені для типу локації. + + Порожній location_types у системи = доступна скрізь (як у посад). + """ + if not requested_access_ids: + return [] + from core.models import Directory + allowed = [] + for access in Directory.objects.filter( + pk__in=requested_access_ids, + directory_type__code='access_system', is_active=True, + ).prefetch_related('location_types'): + types = [lt.code for lt in access.location_types.all()] + if not types or user_location_type in types: + allowed.append(access.pk) + return allowed + + +def _validate_hire_location(*, user_location_type, department_id, user_pharmacy_id, + user_location_id, post_id, hire_date): + """Перевіряє правила локації Employee.clean() без збереження в БД.""" + emp = Employee( + username='__hire_validate__', + last_name='_', + first_name='_', hire_date=hire_date, - probation_end_date=hire_date + timezone.timedelta(days=trial_days), user_location_type=user_location_type, department_id=department_id, user_pharmacy_id=user_pharmacy_id, + user_location_id=user_location_id, post_id=post_id, - # AD-синхронізація стосується лише офісних співробітників — це вже - # чинне правило в apps/ad_connector/signals.py (_is_office), тут лише - # вмикаємо прапорець, без якого сигнал взагалі не спрацює. - ad_sync_enabled=(user_location_type == 'office'), + ) + emp.clean() + + +def _directory(type_code, code): + from core.models import Directory + return Directory.objects.get(directory_type__code=type_code, code=code) + + +def _ticket_location(hiring_request): + if hiring_request.user_location_type == 'pharmacy' and hiring_request.user_pharmacy_id: + return hiring_request.user_pharmacy + return hiring_request.user_location + + +def _build_ticket_description(hiring_request, intern): + loc_label = hiring_request.get_user_location_type_display() + if hiring_request.user_pharmacy_id: + loc_detail = str(hiring_request.user_pharmacy) + elif hiring_request.user_location_id: + loc_detail = str(hiring_request.user_location) + elif hiring_request.department_id: + loc_detail = hiring_request.department.name + else: + loc_detail = '—' + + lines = [ + f"{_('Кандидат')}: {hiring_request.last_name} {hiring_request.first_name} {hiring_request.middle_name}".strip(), + f"{_('Должность')}: {hiring_request.post.name if hiring_request.post_id else '—'}", + f"{_('Тип локации')}: {loc_label}", + f"{_('Локация / отдел')}: {loc_detail}", + f"{_('Дата выхода')}: {hiring_request.hire_date:%d.%m.%Y}", + f"{_('Испытательный срок')}: {hiring_request.trial_days} {_('дн.')}", + f"{_('Интерн-аккаунт')}: {intern.label} / {_('логин')} «{intern.username}»", + '', + f"{_('Требуемые доступы')}:", + ] + accesses = list(hiring_request.requested_access.all()) + if accesses: + for access in accesses: + lines.append(f' • {access.name}') + else: + lines.append(f" {_('(не указано)')}") + if hiring_request.access_note: + lines.append('') + lines.append(f"{_('Примечание')}: {hiring_request.access_note}") + lines.append('') + lines.append(_( + 'Подготовьте рабочее место и выдайте доступы к интерн-аккаунту на время ' + 'испытания. После утверждения HR будет создан персональный аккаунт.' + )) + return '\n'.join(lines) + + +def _create_access_ticket(hiring_request, requester, intern): + from apps.it_manager.models import Ticket + + fio = f'{hiring_request.last_name} {hiring_request.first_name}'.strip() + ticket = Ticket.objects.create( + title=f"{_('Подготовка рабочего места')}: {fio}", + ticket_type=_directory('ticket_type', 'access'), + priority=_directory('ticket_priority', 'high'), + requester=requester, + location=_ticket_location(hiring_request), + description=_build_ticket_description(hiring_request, intern), + ) + hiring_request.granted_ticket = ticket + hiring_request.save(update_fields=['granted_ticket', 'updated_at']) + return ticket + + +def _close_ticket(ticket): + if ticket is None: + return + closed = _directory('ticket_status', 'closed') + if ticket.status_id == closed.pk: + return + ticket.status = closed + ticket.save(update_fields=['status', 'closed_at', 'updated_at']) + + +@transaction.atomic +def create_hiring_request(*, requester, last_name, first_name, middle_name='', + candidate_email='', candidate_phone='', + user_location_type, department_id=None, user_pharmacy_id=None, + user_location_id=None, post_id=None, + hire_date, trial_days, requested_access_ids=None, access_note=''): + """Створює заявку на випробування: виділяє інтерн-слот і IT Ticket. + Персональний Employee ще не створюється. + """ + department_id, user_pharmacy_id, user_location_id = _coerce_location_fields( + user_location_type, department_id, user_pharmacy_id, user_location_id, + ) + _validate_hire_location( + user_location_type=user_location_type, + department_id=department_id, + user_pharmacy_id=user_pharmacy_id, + user_location_id=user_location_id, + post_id=post_id, + hire_date=hire_date, + ) + requested_access_ids = _filter_access_ids_for_location( + user_location_type, requested_access_ids or [], ) hiring_request = HiringRequest.objects.create( @@ -57,72 +181,30 @@ def create_hiring_request(*, requester, last_name, first_name, middle_name='', user_location_type=user_location_type, department_id=department_id, user_pharmacy_id=user_pharmacy_id, + user_location_id=user_location_id, post_id=post_id, hire_date=hire_date, trial_days=trial_days, access_note=access_note, status='trial', requested_by=requester, - employee=employee, ) if requested_access_ids: hiring_request.requested_access.set(requested_access_ids) - _create_access_task(hiring_request, requester) + intern = InternAccount.allocate(hiring_request) + _create_access_ticket(hiring_request, requester, intern) return hiring_request -def _create_access_task(hiring_request, requester): - """Ставить задачу IT-відділу на видачу доступів новому співробітнику - (чек-лист по обраних базах), призначає її IT-групі, рахує строк за - робочим часом і повідомляє постановника.""" - from apps.tasks.models import Task, TaskChecklistItem - from apps.visit_manager.services import add_working_hours - - accesses = list(hiring_request.requested_access.all()) - if not accesses: - return None - - fio = f'{hiring_request.last_name} {hiring_request.first_name}'.strip() - due_dt = add_working_hours(timezone.now(), 24) - - parts = ['Надати новому співробітнику доступ до обраних систем/баз (відмітьте виконане у чек-листі).'] - if hiring_request.post_id: - parts.append(f'Посада: {hiring_request.post.name}.') - if hiring_request.access_note: - parts.append(f'Примітка: {hiring_request.access_note}') - - task = Task.objects.create( - title=f'Доступи для {fio}', - description=' '.join(parts), - creator=requester, - priority='high', - due_date=due_dt.date(), - ) - for i, access in enumerate(accesses): - TaskChecklistItem.objects.create(task=task, label=f'Додати в «{access.name}»', order=i) - - # Призначаємо IT-відділ (co_executors) — це тригерить сповіщення виконавцям (сигнали tasks). - it_staff = list(Employee.objects.filter(groups__name='IT', is_active=True)) - if it_staff: - task.co_executors.add(*it_staff) - - _notify_requester_taken(requester, fio, due_dt) - - hiring_request.access_task = task - hiring_request.save(update_fields=['access_task', 'updated_at']) - return task +def _should_assign_office_email(user_location_type) -> bool: + return user_location_type == 'office' -def _notify_requester_taken(requester, fio, due_dt): - """Повідомити постановника (HR), що заявку взято в роботу і строк — 24 год робочого часу.""" - from apps.mattermost_connector.services.bot_service import send_direct_message - local = timezone.localtime(due_dt) - message = ( - f'Заявку на доступи для {fio} прийнято в роботу IT-відділом. ' - f'Виконаємо протягом 24 годин робочого часу — орієнтовно до {local:%d.%m %H:%M}.' - ) - send_direct_message(requester, message) +def _office_email_for(username: str) -> str: + from apps.employees.models import EmployeeDefaults + domain = EmployeeDefaults.load().normalized_office_domain + return f'{username}@{domain}' @transaction.atomic @@ -130,20 +212,43 @@ def approve_hiring_request(hiring_request, decided_by, note=''): if hiring_request.status != 'trial': return hiring_request - # Доступи видаються задачею IT-відділу ще на етапі створення заявки - # (_create_access_task), тому затвердження лише завершує випробувальний термін. - employee = hiring_request.employee - if employee is not None: - employee.probation_end_date = timezone.localdate() - employee.save(update_fields=['probation_end_date']) + username = _generate_username(hiring_request.last_name, hiring_request.first_name) + password = _generate_password() + employee = Employee( + username=username, + last_name=hiring_request.last_name, + first_name=hiring_request.first_name, + middle_name=hiring_request.middle_name, + personal_phone=hiring_request.candidate_phone or '', + personal_email=hiring_request.candidate_email or '', + is_active=True, + hire_date=hiring_request.hire_date, + probation_end_date=timezone.localdate(), + user_location_type=hiring_request.user_location_type, + department_id=hiring_request.department_id, + user_pharmacy_id=hiring_request.user_pharmacy_id, + user_location_id=hiring_request.user_location_id, + post_id=hiring_request.post_id, + ad_sync_enabled=(hiring_request.user_location_type == 'office'), + ) + if _should_assign_office_email(hiring_request.user_location_type): + employee.email = _office_email_for(username) + employee.set_password(password) + employee.full_clean() + employee.save() + + hiring_request.employee = employee + hiring_request.issued_password = password hiring_request.status = 'approved' hiring_request.decision_by = decided_by hiring_request.decision_at = timezone.now() hiring_request.decision_note = note hiring_request.save(update_fields=[ - 'status', 'decision_by', 'decision_at', 'decision_note', 'updated_at', + 'employee', 'issued_password', 'status', + 'decision_by', 'decision_at', 'decision_note', 'updated_at', ]) + hiring_request.release_intern() return hiring_request @@ -152,11 +257,8 @@ def reject_hiring_request(hiring_request, decided_by, note=''): if hiring_request.status != 'trial': return hiring_request - employee = hiring_request.employee - if employee is not None: - employee.is_active = False - employee.termination_date = timezone.localdate() - employee.save(update_fields=['is_active', 'termination_date']) + hiring_request.release_intern() + _close_ticket(hiring_request.granted_ticket) hiring_request.status = 'rejected' hiring_request.decision_by = decided_by diff --git a/apps/hiring/tests.py b/apps/hiring/tests.py index 39e0b88..2210b96 100644 --- a/apps/hiring/tests.py +++ b/apps/hiring/tests.py @@ -1,21 +1,32 @@ -"""Тести бізнес-логіки заявок на найм: створення тимчасового акаунта, -задача IT на доступи (чек-лист), затвердження і відхилення.""" +"""Тести найму: пул інтернів, IT Ticket, Employee лише після approve, PDF.""" from datetime import date, timedelta from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group -from django.test import TestCase +from django.core.exceptions import ValidationError +from django.test import Client, TestCase +from django.urls import reverse -from core.models import Directory -from apps.hiring.models import HiringRequest +from core.models import Directory, DirectoryType +from apps.hiring.models import HiringRequest, InternAccount from apps.hiring.services import approve_hiring_request, create_hiring_request, reject_hiring_request -from apps.tasks.models import Task +from apps.it_manager.models import Ticket +from apps.locations.models import Location, Pharmacy Employee = get_user_model() _employee_seq = 0 +def login_2fa(client, user): + client.force_login(user) + if user.is_staff and not user.totp_enabled: + user.totp_enabled = True + user.save(update_fields=['totp_enabled']) + session = client.session + session['2fa_verified'] = True + session.save() + + def make_employee(**overrides): global _employee_seq _employee_seq += 1 @@ -34,133 +45,291 @@ def make_employee(**overrides): return Employee.objects.create(**defaults) +def make_intern(label='Intern-1', username=None, password='intern-pass'): + return InternAccount.objects.create( + label=label, + username=username or f'intern.{label.lower().replace("-", "")}', + password=password, + is_active=True, + ) + + class HiringServiceTests(TestCase): + @classmethod + def setUpTestData(cls): + org_type = DirectoryType.objects.create(code='organization', name='Организация') + region_type = DirectoryType.objects.create(code='region', name='Регион') + dept_type = DirectoryType.objects.create(code='department', name='Відділ') + DirectoryType.objects.get_or_create(code='access_system', defaults={'name': 'Системи доступу'}) + + cls.region = Directory.objects.create(directory_type=region_type, code='od', name='Одеса') + cls.org = Directory.objects.create(directory_type=org_type, code='akc', name='АКС') + cls.department = Directory.objects.create(directory_type=dept_type, code='hr', name='HR') + cls.pharmacy = Pharmacy.objects.create( + organization=cls.org, location_region=cls.region, + pharmacy_number=1, location_type='pharmacy', + ) + cls.office = Location.objects.create( + location_type='office', location_region=cls.region, + street='Дерибасівська', house_number='1', + ) + def setUp(self): self.hr_user = make_employee() + # Сид-миграция 0005 наполняет пул Intern-1..5 — очищаем, чтобы тесты сами + # управляли составом стажёров (иначе конфликт лейблов и «свободные» из сида). + InternAccount.objects.all().delete() + self.intern = make_intern('Intern-1') - def test_create_hiring_request_creates_trial_employee(self): - hire_date = date.today() + timedelta(days=1) - hr = create_hiring_request( + def _pharmacy_kwargs(self, **extra): + defaults = dict( requester=self.hr_user, last_name='Кандидат', first_name='Іван', user_location_type='pharmacy', - hire_date=hire_date, + user_pharmacy_id=self.pharmacy.pk, + hire_date=date.today() + timedelta(days=1), trial_days=3, ) + defaults.update(extra) + return defaults + + def _office_kwargs(self, **extra): + defaults = dict( + requester=self.hr_user, + last_name='Офісний', + first_name='Кандидат', + user_location_type='office', + department_id=self.department.pk, + user_location_id=self.office.pk, + hire_date=date.today() + timedelta(days=1), + trial_days=2, + ) + defaults.update(extra) + return defaults + + def test_create_assigns_intern_and_ticket_without_employee(self): + hire_date = date.today() + timedelta(days=1) + hr = create_hiring_request(**self._pharmacy_kwargs(hire_date=hire_date)) self.assertEqual(hr.status, 'trial') - self.assertIsNotNone(hr.employee) - self.assertTrue(hr.employee.is_active) - self.assertEqual(hr.employee.probation_end_date, hire_date + timedelta(days=3)) + self.assertIsNone(hr.employee) + self.assertIsNotNone(hr.granted_ticket) + self.assertEqual(hr.granted_ticket.ticket_type.code, 'access') + self.assertEqual(hr.granted_ticket.priority.code, 'high') + self.assertEqual(hr.intern_account.pk, self.intern.pk) self.assertEqual(hr.trial_end_date, hire_date + timedelta(days=3)) self.assertFalse(hr.is_overdue) + self.assertIsNone(hr.access_task) - def test_ad_sync_enabled_only_for_office_employees(self): - office_hr = create_hiring_request( - requester=self.hr_user, last_name='Офісний', first_name='Кандидат', - user_location_type='office', hire_date=date.today() + timedelta(days=1), trial_days=2, - ) - pharmacy_hr = create_hiring_request( - requester=self.hr_user, last_name='Аптечний', first_name='Кандидат', - user_location_type='pharmacy', hire_date=date.today() + timedelta(days=1), trial_days=2, - ) + def test_no_free_intern_raises(self): + create_hiring_request(**self._pharmacy_kwargs()) + with self.assertRaises(ValidationError) as cm: + create_hiring_request(**self._pharmacy_kwargs( + last_name='Другий', first_name='Кандидат', + )) + self.assertIn('__all__', cm.exception.message_dict) - self.assertTrue(office_hr.employee.ad_sync_enabled) - self.assertFalse(pharmacy_hr.employee.ad_sync_enabled) + def test_pharmacy_without_pharmacy_raises(self): + with self.assertRaises(ValidationError) as cm: + create_hiring_request(**self._pharmacy_kwargs(user_pharmacy_id=None)) + self.assertIn('user_pharmacy', cm.exception.message_dict) + + def test_office_without_department_raises(self): + with self.assertRaises(ValidationError) as cm: + create_hiring_request(**self._office_kwargs(department_id=None)) + self.assertIn('department', cm.exception.message_dict) + + def test_office_without_location_raises(self): + with self.assertRaises(ValidationError) as cm: + create_hiring_request(**self._office_kwargs(user_location_id=None)) + self.assertIn('user_location', cm.exception.message_dict) def test_is_overdue_when_trial_period_passed(self): - hr = create_hiring_request( - requester=self.hr_user, last_name='Прострочений', first_name='Тест', - user_location_type='pharmacy', hire_date=date(2026, 1, 1), trial_days=1, - ) + hr = create_hiring_request(**self._pharmacy_kwargs( + last_name='Прострочений', first_name='Тест', + hire_date=date(2026, 1, 1), trial_days=1, + )) self.assertTrue(hr.is_overdue) - def test_create_hiring_request_generates_unique_username(self): - hr1 = create_hiring_request( - requester=self.hr_user, last_name='Дубль', first_name='Петро', - user_location_type='pharmacy', hire_date=date(2026, 6, 1), trial_days=1, - ) - hr2 = create_hiring_request( - requester=self.hr_user, last_name='Дубль', first_name='Петро', - user_location_type='pharmacy', hire_date=date(2026, 6, 1), trial_days=1, - ) - - self.assertNotEqual(hr1.employee.username, hr2.employee.username) - - # --- задача IT на доступи --- - def _access_ids(self, n=2): ids = list(Directory.objects.filter(directory_type__code='access_system', is_active=True) .values_list('id', flat=True)[:n]) self.assertTrue(ids, 'у тестовій БД немає довідника access_system') return ids - def test_create_with_access_creates_it_task_with_checklist(self): - it_group, _ = Group.objects.get_or_create(name='IT') - it_user = make_employee(username='it.tech', email='it.tech@company.local', last_name='Айтішник') - it_user.groups.add(it_group) - + def test_create_with_access_creates_ticket_not_task(self): access_ids = self._access_ids(2) - hr = create_hiring_request( - requester=self.hr_user, last_name='Доступний', first_name='Кандидат', - user_location_type='pharmacy', hire_date=date.today() + timedelta(days=1), trial_days=3, + hr = create_hiring_request(**self._pharmacy_kwargs( + last_name='Доступний', first_name='Кандидат', requested_access_ids=access_ids, - ) + )) - self.assertIsNotNone(hr.access_task) - task = hr.access_task - self.assertEqual(task.creator, self.hr_user) - self.assertEqual(task.priority, 'high') - self.assertIsNotNone(task.due_date) - self.assertEqual(task.checklist.count(), len(access_ids)) - self.assertIn(it_user, task.co_executors.all()) - # виконавець може відмічати пункти - item = task.checklist.first() - item.set_done(True, user=it_user) - self.assertTrue(task.checklist.get(pk=item.pk).is_done) - self.assertEqual(task.checklist_done, 1) - - def test_create_without_access_creates_no_task(self): - hr = create_hiring_request( - requester=self.hr_user, last_name='Без', first_name='Доступів', - user_location_type='pharmacy', hire_date=date.today() + timedelta(days=1), trial_days=3, - ) self.assertIsNone(hr.access_task) + self.assertIsNotNone(hr.granted_ticket) + self.assertEqual(hr.requested_access.count(), len(access_ids)) + self.assertIn('Доступний', hr.granted_ticket.description) + self.assertEqual(Ticket.objects.filter(pk=hr.granted_ticket_id).count(), 1) - def test_approve_ends_probation_without_ticket(self): - hr = create_hiring_request( - requester=self.hr_user, last_name='Кандидат', first_name='Марія', - user_location_type='pharmacy', hire_date=date(2026, 6, 1), trial_days=5, - ) + def test_create_without_access_still_creates_ticket(self): + hr = create_hiring_request(**self._pharmacy_kwargs( + last_name='Без', first_name='Доступів', + )) + self.assertIsNone(hr.access_task) + self.assertIsNotNone(hr.granted_ticket) + + def test_access_for_office_only_skipped_for_pharmacy(self): + access = Directory.objects.filter( + directory_type__code='access_system', is_active=True, + ).first() + self.assertIsNotNone(access) + loc_type = DirectoryType.objects.get(code='location_type') + office_lt = Directory.objects.get_or_create( + directory_type=loc_type, code='office', + defaults={'name': 'Офіс', 'is_active': True}, + )[0] + access.location_types.set([office_lt]) + + hr = create_hiring_request(**self._pharmacy_kwargs( + last_name='Фільтр', first_name='Доступ', + requested_access_ids=[access.pk], + )) + self.assertEqual(hr.requested_access.count(), 0) + + make_intern('Intern-2') + hr_office = create_hiring_request(**self._office_kwargs( + last_name='Фільтр', first_name='Офіс', + requested_access_ids=[access.pk], + )) + self.assertEqual(list(hr_office.requested_access.values_list('pk', flat=True)), [access.pk]) + + def test_approve_creates_employee_and_frees_intern(self): + hr = create_hiring_request(**self._pharmacy_kwargs( + last_name='Кандидат', first_name='Марія', + hire_date=date(2026, 6, 1), trial_days=5, + )) approved = approve_hiring_request(hr, decided_by=self.hr_user, note='ok') self.assertEqual(approved.status, 'approved') self.assertEqual(approved.decision_by, self.hr_user) - self.assertIsNone(approved.granted_ticket) # доступи — задачею при створенні, не тікетом - approved.employee.refresh_from_db() + self.assertIsNotNone(approved.employee) + self.assertTrue(approved.issued_password) self.assertEqual(approved.employee.probation_end_date, date.today()) + self.assertTrue(approved.employee.check_password(approved.issued_password)) - def test_reject_deactivates_employee(self): - hr = create_hiring_request( - requester=self.hr_user, last_name='Кандидат', first_name='Олег', - user_location_type='pharmacy', hire_date=date(2026, 6, 1), trial_days=2, + self.intern.refresh_from_db() + self.assertIsNone(self.intern.assigned_to_id) + + def test_approve_office_assigns_corporate_email(self): + from apps.employees.models import EmployeeDefaults + EmployeeDefaults.load() + + hr = create_hiring_request(**self._office_kwargs( + last_name='Петренко', first_name='Іван', + )) + approve_hiring_request(hr, decided_by=self.hr_user) + hr.refresh_from_db() + self.assertEqual( + hr.employee.email, + f'{hr.employee.username}@aks-apteka.com.ua', ) + + def test_approve_office_uses_admin_domain_setting(self): + from apps.employees.models import EmployeeDefaults + defaults = EmployeeDefaults.load() + defaults.office_email_domain = 'office.example.com' + defaults.save() + + hr = create_hiring_request(**self._office_kwargs( + last_name='Сидоренко', first_name='Оля', + )) + approve_hiring_request(hr, decided_by=self.hr_user) + hr.refresh_from_db() + self.assertEqual( + hr.employee.email, + f'{hr.employee.username}@office.example.com', + ) + + def test_approve_pharmacy_uses_placeholder_email(self): + hr = create_hiring_request(**self._pharmacy_kwargs( + last_name='Аптечна', first_name='Марія', + )) + approve_hiring_request(hr, decided_by=self.hr_user) + hr.refresh_from_db() + self.assertTrue(hr.employee.email.endswith('@local.domain')) + + def test_approve_ad_sync_only_for_office(self): + office_hr = create_hiring_request(**self._office_kwargs()) + make_intern('Intern-2') + pharmacy_hr = create_hiring_request(**self._pharmacy_kwargs( + last_name='Аптечний', first_name='Кандидат', + )) + approve_hiring_request(office_hr, decided_by=self.hr_user) + approve_hiring_request(pharmacy_hr, decided_by=self.hr_user) + office_hr.refresh_from_db() + pharmacy_hr.refresh_from_db() + self.assertTrue(office_hr.employee.ad_sync_enabled) + self.assertFalse(pharmacy_hr.employee.ad_sync_enabled) + + def test_approve_generates_unique_username(self): + hr1 = create_hiring_request(**self._pharmacy_kwargs( + last_name='Дубль', first_name='Петро', hire_date=date(2026, 6, 1), trial_days=1, + )) + make_intern('Intern-2') + hr2 = create_hiring_request(**self._pharmacy_kwargs( + last_name='Дубль', first_name='Петро', hire_date=date(2026, 6, 1), trial_days=1, + )) + approve_hiring_request(hr1, decided_by=self.hr_user) + approve_hiring_request(hr2, decided_by=self.hr_user) + hr1.refresh_from_db() + hr2.refresh_from_db() + self.assertNotEqual(hr1.employee.username, hr2.employee.username) + + def test_reject_frees_intern_closes_ticket_no_employee(self): + hr = create_hiring_request(**self._pharmacy_kwargs( + last_name='Кандидат', first_name='Олег', + hire_date=date(2026, 6, 1), trial_days=2, + )) + ticket = hr.granted_ticket rejected = reject_hiring_request(hr, decided_by=self.hr_user, note='не пройшов') self.assertEqual(rejected.status, 'rejected') - rejected.employee.refresh_from_db() - self.assertFalse(rejected.employee.is_active) - self.assertEqual(rejected.employee.termination_date, date.today()) + self.assertIsNone(rejected.employee) + self.intern.refresh_from_db() + self.assertIsNone(self.intern.assigned_to_id) + ticket.refresh_from_db() + self.assertEqual(ticket.status.code, 'closed') def test_decision_is_a_noop_once_already_decided(self): - hr = create_hiring_request( - requester=self.hr_user, last_name='Кандидат', first_name='Ліда', - user_location_type='pharmacy', hire_date=date(2026, 6, 1), trial_days=2, - ) + hr = create_hiring_request(**self._pharmacy_kwargs( + last_name='Кандидат', first_name='Ліда', + hire_date=date(2026, 6, 1), trial_days=2, + )) approve_hiring_request(hr, decided_by=self.hr_user) reject_hiring_request(hr, decided_by=self.hr_user) hr.refresh_from_db() - self.assertEqual(hr.status, 'approved') + + def test_credentials_pdf_after_approve(self): + hr = create_hiring_request(**self._pharmacy_kwargs( + last_name='PDF', first_name='Тест', + )) + approve_hiring_request(hr, decided_by=self.hr_user) + + client = Client() + login_2fa(client, self.hr_user) + url = reverse('hiring_credentials_pdf', kwargs={'pk': hr.pk}) + response = client.get(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/pdf') + self.assertTrue(response.content.startswith(b'%PDF')) + + def test_credentials_pdf_forbidden_while_trial(self): + hr = create_hiring_request(**self._pharmacy_kwargs( + last_name='PDF', first_name='Trial', + )) + client = Client() + login_2fa(client, self.hr_user) + response = client.get(reverse('hiring_credentials_pdf', kwargs={'pk': hr.pk})) + self.assertEqual(response.status_code, 403) diff --git a/apps/it_manager/views.py b/apps/it_manager/views.py index 4623fc7..7256d6b 100644 --- a/apps/it_manager/views.py +++ b/apps/it_manager/views.py @@ -3,7 +3,7 @@ from datetime import datetime from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required from django.db import models -from django.db.models import Sum +from django.db.models import Q, Sum from django.http import HttpResponseForbidden from django.shortcuts import get_object_or_404 from django.utils import timezone @@ -22,9 +22,20 @@ Employee = get_user_model() def _is_it_staff(user): - return user.is_superuser or user.groups.filter(name='IT').exists() + from core.portal_access import user_can_access + return user_can_access(user, 'it_manager') +def _it_assignees(): + """Кандидаты в исполнители: группа IT, а также staff/superuser + (если в группу IT ещё никого не добавили — селект не пустой).""" + return ( + Employee.objects.filter(is_active=True) + .filter(Q(groups__name='IT') | Q(is_staff=True) | Q(is_superuser=True)) + .distinct() + .order_by('last_name', 'first_name') + ) + def _directory_choices(type_code): """(code, name)-кортежі активних записів довідника — та сама форма, що й колишні CharField(choices=...), для сумісності з шаблонами.""" @@ -136,7 +147,7 @@ def _it_manager_context(request): 'ticket_statuses': ticket_statuses, 'ticket_priorities': _directory_choices('ticket_priority'), 'ticket_types': _directory_choices('ticket_type'), - 'it_staff': Employee.objects.filter(groups__name='IT', is_active=True).order_by('last_name'), + 'it_staff': _it_assignees(), 'worklogs_page': worklogs_page, 'worklog_types': _directory_choices('worklog_type'), diff --git a/apps/locations/admin.py b/apps/locations/admin.py index cdb4ff0..93b7592 100644 --- a/apps/locations/admin.py +++ b/apps/locations/admin.py @@ -9,7 +9,7 @@ from unfold.admin import ModelAdmin, TabularInline from import_export.admin import ImportExportModelAdmin from unfold.contrib.import_export.forms import ExportForm, ImportForm from core.models import Directory -from .models import Location, Pharmacy, PharmacyPhone, PharmacyHistory +from .models import Location, Pharmacy, PharmacyPhone, PharmacyHistory, DislocationColumnPreset # --------------------------------------------------------------------------- @@ -388,3 +388,13 @@ class PharmacyAdmin(ModelAdmin, ImportExportModelAdmin): return '—' return mark_safe('
'.join(numbers)) phone_numbers_display.short_description = _('Телефоны') + + +@admin.register(DislocationColumnPreset) +class DislocationColumnPresetAdmin(ModelAdmin): + list_display = ['name', 'code', 'is_default', 'department', 'updated_at'] + list_filter = ['is_default'] + search_fields = ['name', 'code'] + list_select_related = ['department'] + ordering = ['name'] + readonly_fields = ['created_at', 'updated_at', 'uuid'] diff --git a/apps/locations/dislocation_columns.py b/apps/locations/dislocation_columns.py new file mode 100644 index 0000000..5e4c9cc --- /dev/null +++ b/apps/locations/dislocation_columns.py @@ -0,0 +1,102 @@ +"""Каталог колонок таблицы дислокации аптек (whitelist). + +Секреты (пароль Carbonio и т.п.) сюда не входят. +Колонка ``pharmacy`` всегда обязательна, но порядок среди колонок свободный. +""" +from __future__ import annotations + +from dataclasses import dataclass + +from django.utils.translation import gettext_lazy as _ + +LOCKED_KEY = 'pharmacy' + +STANDARD_COLUMNS = [ + 'pharmacy', 'organization', 'head', 'staff', 'phone', 'hours', 'status', +] + +HR_COLUMNS = [ + 'pharmacy', 'organization', 'head', 'staff', 'curator', 'manager', +] + +OPS_COLUMNS = [ + 'pharmacy', 'region', 'phone', 'hours', 'status', 'category', +] + + +@dataclass(frozen=True) +class ColumnDef: + key: str + label: str + sort_type: str # 'string' | 'number' + locked: bool = False + + +COLUMN_DEFS: dict[str, ColumnDef] = { + 'pharmacy': ColumnDef('pharmacy', _('Аптека'), 'number', locked=True), + 'organization': ColumnDef('organization', _('Организация'), 'string'), + 'head': ColumnDef('head', _('Заведующий'), 'string'), + 'staff': ColumnDef('staff', _('Персонал'), 'number'), + 'phone': ColumnDef('phone', _('Телефон'), 'string'), + 'hours': ColumnDef('hours', _('Режим работы'), 'string'), + 'status': ColumnDef('status', _('Статус'), 'number'), + 'region': ColumnDef('region', _('Регион'), 'string'), + 'category': ColumnDef('category', _('Категория'), 'string'), + 'curator': ColumnDef('curator', _('Куратор'), 'string'), + 'manager': ColumnDef('manager', _('Ответственный менеджер'), 'string'), + 'license': ColumnDef('license', _('Лицензия'), 'string'), + 'vat': ColumnDef('vat', _('ПДВ'), 'string'), + 'email': ColumnDef('email', _('Email'), 'string'), +} + +ALLOWED_KEYS = frozenset(COLUMN_DEFS) + + +def normalize_columns(keys) -> list[str]: + """Оставляет только известные ключи без дублей; ``pharmacy`` всегда в наборе.""" + seen = set() + result = [] + for key in keys or []: + if key not in ALLOWED_KEYS or key in seen: + continue + seen.add(key) + result.append(key) + if LOCKED_KEY not in result: + result.insert(0, LOCKED_KEY) + return result + + +def column_defs_for(keys: list[str]) -> list[ColumnDef]: + return [COLUMN_DEFS[k] for k in keys if k in COLUMN_DEFS] + + +def all_column_defs() -> list[ColumnDef]: + # pharmacy first in the registry list, then the rest in declaration order + rest = [d for k, d in COLUMN_DEFS.items() if k != LOCKED_KEY] + return [COLUMN_DEFS[LOCKED_KEY], *rest] + + +def resolve_columns_for_user(user) -> list[str]: + """Персональный override → пресет отдела → is_default → STANDARD.""" + custom = getattr(user, 'dislocation_columns', None) + if custom: + return normalize_columns(custom) + + from apps.locations.models import DislocationColumnPreset + + dept_id = getattr(user, 'department_id', None) + if dept_id: + preset = ( + DislocationColumnPreset.objects + .filter(department_id=dept_id) + .order_by('id') + .first() + ) + if preset and preset.columns: + return normalize_columns(preset.columns) + + preset = DislocationColumnPreset.objects.filter(is_default=True).first() + if preset and preset.columns: + return normalize_columns(preset.columns) + + return list(STANDARD_COLUMNS) diff --git a/apps/locations/migrations/0010_dislocation_column_presets.py b/apps/locations/migrations/0010_dislocation_column_presets.py new file mode 100644 index 0000000..0631ee7 --- /dev/null +++ b/apps/locations/migrations/0010_dislocation_column_presets.py @@ -0,0 +1,35 @@ +# Generated by Django 6.0.3 on 2026-07-28 21:08 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_access_location_types_help'), + ('locations', '0009_pharmacy_carbonio_password'), + ] + + operations = [ + migrations.CreateModel( + name='DislocationColumnPreset', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=100, verbose_name='Название')), + ('code', models.SlugField(unique=True, verbose_name='Код')), + ('columns', models.JSONField(default=list, help_text='Упорядоченный список ключей из реестра колонок дислокации.', verbose_name='Колонки')), + ('is_default', models.BooleanField(default=False, help_text='Используется, если у сотрудника нет своего набора и нет пресета отдела.', verbose_name='По умолчанию')), + ('department', models.ForeignKey(blank=True, help_text='Если задан — пресет по умолчанию для сотрудников этого отдела.', limit_choices_to={'directory_type__code': 'department', 'is_active': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dislocation_column_presets', to='core.directory', verbose_name='Отдел')), + ], + options={ + 'verbose_name': 'Пресет колонок дислокации', + 'verbose_name_plural': 'Пресеты колонок дислокации', + 'ordering': ['name'], + }, + ), + ] diff --git a/apps/locations/migrations/0011_seed_dislocation_column_presets.py b/apps/locations/migrations/0011_seed_dislocation_column_presets.py new file mode 100644 index 0000000..652bb1c --- /dev/null +++ b/apps/locations/migrations/0011_seed_dislocation_column_presets.py @@ -0,0 +1,52 @@ +from django.db import migrations + +from apps.locations.dislocation_columns import HR_COLUMNS, OPS_COLUMNS, STANDARD_COLUMNS + + +def seed_presets(apps, schema_editor): + DislocationColumnPreset = apps.get_model('locations', 'DislocationColumnPreset') + presets = [ + { + 'code': 'standard', + 'name': 'Стандарт', + 'columns': list(STANDARD_COLUMNS), + 'is_default': True, + }, + { + 'code': 'hr', + 'name': 'HR', + 'columns': list(HR_COLUMNS), + 'is_default': False, + }, + { + 'code': 'ops', + 'name': 'Операции', + 'columns': list(OPS_COLUMNS), + 'is_default': False, + }, + ] + for data in presets: + DislocationColumnPreset.objects.update_or_create( + code=data['code'], + defaults={ + 'name': data['name'], + 'columns': data['columns'], + 'is_default': data['is_default'], + }, + ) + + +def unseed_presets(apps, schema_editor): + DislocationColumnPreset = apps.get_model('locations', 'DislocationColumnPreset') + DislocationColumnPreset.objects.filter(code__in=['standard', 'hr', 'ops']).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('locations', '0010_dislocation_column_presets'), + ] + + operations = [ + migrations.RunPython(seed_presets, unseed_presets), + ] diff --git a/apps/locations/models/__init__.py b/apps/locations/models/__init__.py index 53cb011..d5e5d02 100644 --- a/apps/locations/models/__init__.py +++ b/apps/locations/models/__init__.py @@ -1,2 +1,10 @@ from .base import Location, LocationManager from .pharmacies import Pharmacy, PharmacyManager, PharmacyPhone, PharmacyHistory +from .dislocation import DislocationColumnPreset + +__all__ = [ + 'Location', 'LocationManager', + 'Pharmacy', 'PharmacyManager', 'PharmacyPhone', 'PharmacyHistory', + 'DislocationColumnPreset', +] + diff --git a/apps/locations/models/dislocation.py b/apps/locations/models/dislocation.py new file mode 100644 index 0000000..193d327 --- /dev/null +++ b/apps/locations/models/dislocation.py @@ -0,0 +1,51 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from core.models import BaseModel +from apps.locations.dislocation_columns import STANDARD_COLUMNS, normalize_columns + + +class DislocationColumnPreset(BaseModel): + """Заготовка набора колонок таблицы дислокации. + + ``department`` — задел под преднастройки по отделам (nullable). + """ + + name = models.CharField(_('Название'), max_length=100) + code = models.SlugField(_('Код'), max_length=50, unique=True) + columns = models.JSONField( + _('Колонки'), + default=list, + help_text=_('Упорядоченный список ключей из реестра колонок дислокации.'), + ) + department = models.ForeignKey( + 'core.Directory', on_delete=models.SET_NULL, + null=True, blank=True, + limit_choices_to={'directory_type__code': 'department', 'is_active': True}, + related_name='dislocation_column_presets', + verbose_name=_('Отдел'), + help_text=_('Если задан — пресет по умолчанию для сотрудников этого отдела.'), + ) + is_default = models.BooleanField( + _('По умолчанию'), default=False, + help_text=_('Используется, если у сотрудника нет своего набора и нет пресета отдела.'), + ) + + class Meta: + verbose_name = _('Пресет колонок дислокации') + verbose_name_plural = _('Пресеты колонок дислокации') + ordering = ['name'] + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + self.columns = normalize_columns(self.columns or STANDARD_COLUMNS) + super().save(*args, **kwargs) + if self.is_default: + ( + DislocationColumnPreset.objects + .exclude(pk=self.pk) + .filter(is_default=True) + .update(is_default=False) + ) diff --git a/apps/locations/tests.py b/apps/locations/tests.py index 932e60d..dfed2f4 100644 --- a/apps/locations/tests.py +++ b/apps/locations/tests.py @@ -135,3 +135,127 @@ class CarbonioPasswordSyncTests(TestCase): self.assertEqual(p.carbonio_password, '') # на update пароль не трогаем fake.create_account.assert_not_called() fake.set_account_status.assert_called_once_with('acc-existing', 'active') + + +class DislocationColumnsTests(TestCase): + """Whitelist, резолв пресета default и сохранение preference пользователя.""" + + @classmethod + def setUpTestData(cls): + from datetime import date + from django.contrib.auth import get_user_model + from apps.locations.dislocation_columns import HR_COLUMNS, OPS_COLUMNS, STANDARD_COLUMNS + from apps.locations.models import DislocationColumnPreset + + Employee = get_user_model() + org_type = DirectoryType.objects.create(code='organization', name='Организация') + region_type = DirectoryType.objects.create(code='region', name='Регион') + cls.region = Directory.objects.create(directory_type=region_type, code='kv', name='Киев') + cls.org = Directory.objects.create(directory_type=org_type, code='org', name='ТестОрг') + cls.user = Employee.objects.create( + username='dislo.cols', + email='dislo.cols@company.local', + first_name='Коля', + last_name='Колонки', + hire_date=date(2026, 1, 1), + user_location_type='office', + mattermost_sync_enabled=False, + ad_sync_enabled=False, + ) + DislocationColumnPreset.objects.update_or_create( + code='standard', + defaults={ + 'name': 'Стандарт', + 'columns': list(STANDARD_COLUMNS), + 'is_default': True, + }, + ) + DislocationColumnPreset.objects.update_or_create( + code='hr', + defaults={'name': 'HR', 'columns': list(HR_COLUMNS), 'is_default': False}, + ) + DislocationColumnPreset.objects.update_or_create( + code='ops', + defaults={'name': 'Ops', 'columns': list(OPS_COLUMNS), 'is_default': False}, + ) + Pharmacy.objects.create( + organization=cls.org, location_region=cls.region, + pharmacy_number=101, location_type='pharmacy', street='Тестовая', house_number='1', + ) + + def test_normalize_whitelist_and_locked_pharmacy(self): + from apps.locations.dislocation_columns import normalize_columns + + self.assertEqual( + normalize_columns(['email', 'pharmacy', 'unknown', 'email', 'region']), + ['email', 'pharmacy', 'region'], + ) + self.assertEqual( + normalize_columns(['organization', 'pharmacy', 'phone']), + ['organization', 'pharmacy', 'phone'], + ) + self.assertEqual(normalize_columns([]), ['pharmacy']) + self.assertEqual(normalize_columns(['secret', 'password']), ['pharmacy']) + # pharmacy обязательна, но если её нет в списке — добавляется первой + self.assertEqual( + normalize_columns(['region', 'phone']), + ['pharmacy', 'region', 'phone'], + ) + def test_resolve_uses_default_preset(self): + from apps.locations.dislocation_columns import STANDARD_COLUMNS, resolve_columns_for_user + + self.user.dislocation_columns = None + self.user.save(update_fields=['dislocation_columns']) + self.assertEqual(resolve_columns_for_user(self.user), list(STANDARD_COLUMNS)) + + def test_resolve_uses_custom_preference(self): + from apps.locations.dislocation_columns import resolve_columns_for_user + + self.user.dislocation_columns = ['region', 'phone', 'bogus'] + self.user.save(update_fields=['dislocation_columns']) + self.assertEqual( + resolve_columns_for_user(self.user), + ['pharmacy', 'region', 'phone'], + ) + + def test_post_saves_columns_preference(self): + self.client.force_login(self.user) + session = self.client.session + session['2fa_verified'] = True + session.save() + + resp = self.client.post( + '/dislocation/columns/', + {'columns': ['organization', 'pharmacy', 'staff', 'region', 'email', 'evil']}, + HTTP_HX_REQUEST='true', + ) + self.assertEqual(resp.status_code, 200) + self.user.refresh_from_db() + self.assertEqual( + self.user.dislocation_columns, + ['organization', 'pharmacy', 'staff', 'region', 'email'], + ) + self.assertContains(resp, "disloSortBy('organization'") + self.assertContains(resp, "disloSortBy('region'") + self.assertContains(resp, "disloSortBy('email'") + self.assertNotContains(resp, "disloSortBy('phone'") + + def test_post_preset_code_applies_hr(self): + from apps.locations.dislocation_columns import HR_COLUMNS + + self.client.force_login(self.user) + session = self.client.session + session['2fa_verified'] = True + session.save() + + resp = self.client.post( + '/dislocation/columns/', + {'preset_code': 'hr'}, + HTTP_HX_REQUEST='true', + ) + self.assertEqual(resp.status_code, 200) + self.user.refresh_from_db() + self.assertEqual(self.user.dislocation_columns, list(HR_COLUMNS)) + self.assertContains(resp, "disloSortBy('curator'") + self.assertContains(resp, "disloSortBy('manager'") + self.assertNotContains(resp, "disloSortBy('phone'") diff --git a/apps/rro/__init__.py b/apps/rro/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/rro/admin.py b/apps/rro/admin.py new file mode 100644 index 0000000..fd3a649 --- /dev/null +++ b/apps/rro/admin.py @@ -0,0 +1,39 @@ +from django.contrib import admin +from unfold.admin import ModelAdmin + +from .models import CashRegister + + +@admin.register(CashRegister) +class CashRegisterAdmin(ModelAdmin): + list_display = [ + 'fiscal_number', 'kind', 'pharmacy', 'model_name', + 'vendor', 'status', 'is_primary', 'cert_expires_at', + ] + list_filter = ['kind', 'status', 'is_primary', 'pharmacy__organization', 'vendor'] + search_fields = [ + 'fiscal_number', 'serial_number', 'model_name', + 'pharmacy__pharmacy_number', 'pharmacy__address', 'notes', 'provider', + ] + list_select_related = [ + 'pharmacy', 'pharmacy__organization', 'vendor', 'status', + ] + autocomplete_fields = ['pharmacy'] + readonly_fields = ['created_at', 'updated_at', 'uuid'] + ordering = ['pharmacy__organization__name', 'pharmacy__pharmacy_number'] + + fieldsets = ( + (None, { + 'fields': ( + 'pharmacy', 'kind', 'fiscal_number', 'serial_number', + 'model_name', 'vendor', 'status', 'is_primary', + ), + }), + ('Регистрация', { + 'fields': ('registered_at', 'cert_expires_at', 'provider', 'notes'), + }), + ('Служебное', { + 'classes': ('collapse',), + 'fields': ('uuid', 'created_at', 'updated_at'), + }), + ) diff --git a/apps/rro/apps.py b/apps/rro/apps.py new file mode 100644 index 0000000..ccd089a --- /dev/null +++ b/apps/rro/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class RroConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.rro' + verbose_name = 'РРО / ПРРО' diff --git a/apps/rro/migrations/0001_initial.py b/apps/rro/migrations/0001_initial.py new file mode 100644 index 0000000..cb2dcb9 --- /dev/null +++ b/apps/rro/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# Generated by Django 6.0.3 on 2026-08-03 16:12 + +import core.models.directories +import django.db.models.deletion +import functools +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('core', '0008_access_location_types_help'), + ('locations', '0011_seed_dislocation_column_presets'), + ] + + operations = [ + migrations.CreateModel( + name='CashRegister', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('kind', models.CharField(choices=[('rro', 'РРО'), ('prro', 'ПРРО')], default='prro', max_length=10, verbose_name='Тип')), + ('fiscal_number', models.CharField(max_length=64, unique=True, verbose_name='Фискальный номер')), + ('serial_number', models.CharField(blank=True, max_length=64, verbose_name='Заводской / серийный номер')), + ('model_name', models.CharField(blank=True, max_length=120, verbose_name='Модель')), + ('registered_at', models.DateField(blank=True, null=True, verbose_name='Дата регистрации в ГНС')), + ('cert_expires_at', models.DateField(blank=True, help_text='Для ПРРО — срок действия КЭП/сертификата (задел под напоминания).', null=True, verbose_name='Срок сертификата / ключа')), + ('is_primary', models.BooleanField(default=False, verbose_name='Основной на аптеке')), + ('notes', models.TextField(blank=True, verbose_name='Комментарий')), + ('provider', models.CharField(blank=True, help_text='Slug провайдера без интеграции: checkbox, vchasno, …', max_length=50, verbose_name='Провайдер ПРРО')), + ('pharmacy', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='cash_registers', to='locations.pharmacy', verbose_name='Аптека')), + ('status', models.ForeignKey(default=functools.partial(core.models.directories._resolve_directory_default, 'rro_status', 'active'), limit_choices_to={'directory_type__code': 'rro_status', 'is_active': True}, on_delete=django.db.models.deletion.PROTECT, related_name='rro_status_registers', to='core.directory', verbose_name='Статус')), + ('vendor', models.ForeignKey(blank=True, limit_choices_to={'directory_type__code': 'rro_vendor', 'is_active': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rro_vendor_registers', to='core.directory', verbose_name='Производитель / вендор')), + ], + options={ + 'verbose_name': 'Кассовый аппарат', + 'verbose_name_plural': 'Кассовые аппараты (РРО/ПРРО)', + 'ordering': ['pharmacy__organization__name', 'pharmacy__pharmacy_number', 'fiscal_number'], + 'indexes': [models.Index(fields=['pharmacy', 'status'], name='rro_cashreg_pharmac_66c4f3_idx')], + }, + ), + ] diff --git a/apps/rro/migrations/0002_seed_rro_directories.py b/apps/rro/migrations/0002_seed_rro_directories.py new file mode 100644 index 0000000..bd1d295 --- /dev/null +++ b/apps/rro/migrations/0002_seed_rro_directories.py @@ -0,0 +1,56 @@ +from django.db import migrations + + +RRO_STATUSES = [ + ('active', 'Активный'), + ('reserve', 'Резерв'), + ('repair', 'В ремонте'), + ('decommissioned', 'Списан'), +] + +RRO_VENDORS = [ + ('datecs', 'Datecs'), + ('exellio', 'Exellio'), + ('checkbox', 'Checkbox'), +] + + +def forwards(apps, schema_editor): + DirectoryType = apps.get_model('core', 'DirectoryType') + Directory = apps.get_model('core', 'Directory') + + status_type, _ = DirectoryType.objects.get_or_create( + code='rro_status', + defaults={'name': 'Статус РРО/ПРРО', 'hierarchy_type': 'strict'}, + ) + for code, name in RRO_STATUSES: + Directory.objects.get_or_create( + directory_type=status_type, code=code, + defaults={'name': name, 'is_active': True}, + ) + + vendor_type, _ = DirectoryType.objects.get_or_create( + code='rro_vendor', + defaults={'name': 'Вендор РРО/ПРРО', 'hierarchy_type': 'strict'}, + ) + for code, name in RRO_VENDORS: + Directory.objects.get_or_create( + directory_type=vendor_type, code=code, + defaults={'name': name, 'is_active': True}, + ) + + +def backwards(apps, schema_editor): + DirectoryType = apps.get_model('core', 'DirectoryType') + DirectoryType.objects.filter(code__in=['rro_status', 'rro_vendor']).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('rro', '0001_initial'), + ] + + operations = [ + migrations.RunPython(forwards, backwards), + ] diff --git a/apps/rro/migrations/__init__.py b/apps/rro/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/rro/models.py b/apps/rro/models.py new file mode 100644 index 0000000..cf1d30f --- /dev/null +++ b/apps/rro/models.py @@ -0,0 +1,79 @@ +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from core.models import BaseModel +from core.models.directories import directory_default + + +class CashRegister(BaseModel): + """Кассовый аппарат (РРО) или программный РРО (ПРРО) на аптеке.""" + + class Kind(models.TextChoices): + RRO = 'rro', _('РРО') + PRRO = 'prro', _('ПРРО') + + pharmacy = models.ForeignKey( + 'locations.Pharmacy', on_delete=models.PROTECT, + related_name='cash_registers', verbose_name=_('Аптека'), + ) + kind = models.CharField( + _('Тип'), max_length=10, choices=Kind.choices, default=Kind.PRRO, + ) + fiscal_number = models.CharField( + _('Фискальный номер'), max_length=64, unique=True, + ) + serial_number = models.CharField( + _('Заводской / серийный номер'), max_length=64, blank=True, + ) + model_name = models.CharField(_('Модель'), max_length=120, blank=True) + vendor = models.ForeignKey( + 'core.Directory', on_delete=models.SET_NULL, + null=True, blank=True, + limit_choices_to={'directory_type__code': 'rro_vendor', 'is_active': True}, + related_name='rro_vendor_registers', verbose_name=_('Производитель / вендор'), + ) + status = models.ForeignKey( + 'core.Directory', on_delete=models.PROTECT, + limit_choices_to={'directory_type__code': 'rro_status', 'is_active': True}, + related_name='rro_status_registers', verbose_name=_('Статус'), + default=directory_default('rro_status', 'active'), + ) + registered_at = models.DateField( + _('Дата регистрации в ГНС'), null=True, blank=True, + ) + cert_expires_at = models.DateField( + _('Срок сертификата / ключа'), null=True, blank=True, + help_text=_('Для ПРРО — срок действия КЭП/сертификата (задел под напоминания).'), + ) + is_primary = models.BooleanField( + _('Основной на аптеке'), default=False, + ) + notes = models.TextField(_('Комментарий'), blank=True) + provider = models.CharField( + _('Провайдер ПРРО'), max_length=50, blank=True, + help_text=_('Slug провайдера без интеграции: checkbox, vchasno, …'), + ) + + class Meta: + verbose_name = _('Кассовый аппарат') + verbose_name_plural = _('Кассовые аппараты (РРО/ПРРО)') + ordering = ['pharmacy__organization__name', 'pharmacy__pharmacy_number', 'fiscal_number'] + indexes = [ + models.Index(fields=['pharmacy', 'status']), + ] + + def __str__(self): + label = self.get_kind_display() + return f'{label} {self.fiscal_number} — {self.pharmacy}' + + def clean(self): + super().clean() + if self.is_primary and self.pharmacy_id: + qs = CashRegister.objects.filter(pharmacy_id=self.pharmacy_id, is_primary=True) + if self.pk: + qs = qs.exclude(pk=self.pk) + if qs.exists(): + raise ValidationError({ + 'is_primary': _('На этой аптеке уже есть основной кассовый аппарат.'), + }) diff --git a/apps/rro/tests.py b/apps/rro/tests.py new file mode 100644 index 0000000..178d176 --- /dev/null +++ b/apps/rro/tests.py @@ -0,0 +1,134 @@ +from datetime import date + +from django.contrib.auth import get_user_model +from django.core.exceptions import ValidationError +from django.db import IntegrityError, transaction +from django.test import TestCase + +from core.models import Directory, DirectoryType +from apps.locations.models import Pharmacy +from apps.rro.models import CashRegister + +Employee = get_user_model() + + +class CashRegisterModelTests(TestCase): + @classmethod + def setUpTestData(cls): + org_type = DirectoryType.objects.create(code='organization', name='Организация') + region_type = DirectoryType.objects.create(code='region', name='Регион') + status_type, _ = DirectoryType.objects.get_or_create( + code='rro_status', defaults={'name': 'Статус РРО/ПРРО', 'hierarchy_type': 'strict'}, + ) + cls.status_active, _ = Directory.objects.get_or_create( + directory_type=status_type, code='active', + defaults={'name': 'Активный', 'is_active': True}, + ) + cls.region = Directory.objects.create(directory_type=region_type, code='od', name='Одесса') + cls.org = Directory.objects.create(directory_type=org_type, code='akc', name='АКС') + cls.pharmacy = Pharmacy.objects.create( + organization=cls.org, location_region=cls.region, + pharmacy_number=42, location_type='pharmacy', + street='Тестовая', house_number='1', + ) + cls.pharmacy2 = Pharmacy.objects.create( + organization=cls.org, location_region=cls.region, + pharmacy_number=43, location_type='pharmacy', + street='Другая', house_number='2', + ) + + def _reg(self, **kw): + defaults = { + 'pharmacy': self.pharmacy, + 'kind': CashRegister.Kind.PRRO, + 'fiscal_number': kw.pop('fiscal_number', f'FN-{CashRegister.objects.count()+1}'), + 'status': self.status_active, + } + defaults.update(kw) + return CashRegister(**defaults) + + def test_seed_statuses_exist(self): + codes = set( + Directory.objects.filter(directory_type__code='rro_status') + .values_list('code', flat=True) + ) + self.assertTrue({'active', 'reserve', 'repair', 'decommissioned'} <= codes) + + def test_unique_fiscal_number(self): + self._reg(fiscal_number='FN-UNIQUE').save() + with self.assertRaises(IntegrityError): + with transaction.atomic(): + self._reg(fiscal_number='FN-UNIQUE', pharmacy=self.pharmacy2).save() + + def test_only_one_primary_per_pharmacy(self): + a = self._reg(fiscal_number='FN-P1', is_primary=True) + a.full_clean() + a.save() + b = self._reg(fiscal_number='FN-P2', is_primary=True) + with self.assertRaises(ValidationError): + b.full_clean() + + def test_primary_ok_on_different_pharmacies(self): + a = self._reg(fiscal_number='FN-A', is_primary=True) + a.full_clean() + a.save() + b = self._reg(fiscal_number='FN-B', pharmacy=self.pharmacy2, is_primary=True) + b.full_clean() + b.save() + + +class CashRegisterPortalTests(TestCase): + @classmethod + def setUpTestData(cls): + org_type = DirectoryType.objects.create(code='organization', name='Организация') + region_type = DirectoryType.objects.create(code='region', name='Регион') + status_type, _ = DirectoryType.objects.get_or_create( + code='rro_status', defaults={'name': 'Статус РРО/ПРРО', 'hierarchy_type': 'strict'}, + ) + cls.status_active, _ = Directory.objects.get_or_create( + directory_type=status_type, code='active', + defaults={'name': 'Активный', 'is_active': True}, + ) + region = Directory.objects.create(directory_type=region_type, code='kv', name='Киев') + org = Directory.objects.create(directory_type=org_type, code='org', name='ТестОрг') + cls.pharmacy = Pharmacy.objects.create( + organization=org, location_region=region, + pharmacy_number=7, location_type='pharmacy', + street='Главная', house_number='7', + ) + CashRegister.objects.create( + pharmacy=cls.pharmacy, kind=CashRegister.Kind.RRO, + fiscal_number='FN-PORTAL-1', status=cls.status_active, + model_name='Datecs FP-700', is_primary=True, + ) + cls.user = Employee.objects.create( + username='rro.staff', + email='rro.staff@company.local', + first_name='РРО', + last_name='Тест', + hire_date=date(2026, 1, 1), + user_location_type='office', + is_staff=True, + mattermost_sync_enabled=False, + ad_sync_enabled=False, + totp_enabled=True, + ) + + def _login(self): + self.client.force_login(self.user) + session = self.client.session + session['2fa_verified'] = True + session.save() + + def test_rro_list_page(self): + self._login() + resp = self.client.get('/rro/', HTTP_HX_REQUEST='true') + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, 'FN-PORTAL-1') + + def test_pharmacy_detail_rro_tab(self): + self._login() + resp = self.client.get(f'/pharmacies/{self.pharmacy.pk}/', HTTP_HX_REQUEST='true') + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, 'FN-PORTAL-1') + self.assertContains(resp, 'ph-panel-rro') diff --git a/apps/rro/urls.py b/apps/rro/urls.py new file mode 100644 index 0000000..865bda7 --- /dev/null +++ b/apps/rro/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path('rro/', views.rro_list_view, name='rro_list'), +] diff --git a/apps/rro/views.py b/apps/rro/views.py new file mode 100644 index 0000000..e325af7 --- /dev/null +++ b/apps/rro/views.py @@ -0,0 +1,75 @@ +from django.contrib.auth.decorators import login_required +from django.db.models import Q + +from apps.locations.models.pharmacies import Pharmacy +from core.models.directories import Directory +from core.utils.htmx import render_page + +from .models import CashRegister + + +@login_required +def rro_list_view(request): + """Реестр РРО/ПРРО по аптекам. Staff видит все; сотрудник аптеки — свою.""" + is_staff = request.user.is_staff + own_pharmacy = request.user.user_pharmacy + + registers = CashRegister.objects.select_related( + 'pharmacy', 'pharmacy__organization', 'vendor', 'status', + ) + + filter_pharmacy = request.GET.get('pharmacy', '') + filter_org = request.GET.get('organization', '') + filter_kind = request.GET.get('kind', '') + filter_status = request.GET.get('status', '') + q = (request.GET.get('q') or '').strip() + + if is_staff: + if filter_pharmacy: + registers = registers.filter(pharmacy_id=filter_pharmacy) + if filter_org: + registers = registers.filter(pharmacy__organization_id=filter_org) + else: + if own_pharmacy: + registers = registers.filter(pharmacy=own_pharmacy) + else: + registers = registers.none() + + if filter_kind in {CashRegister.Kind.RRO, CashRegister.Kind.PRRO}: + registers = registers.filter(kind=filter_kind) + if filter_status: + registers = registers.filter(status_id=filter_status) + if q: + registers = registers.filter( + Q(fiscal_number__icontains=q) + | Q(serial_number__icontains=q) + | Q(model_name__icontains=q) + | Q(provider__icontains=q) + ) + + statuses = Directory.objects.filter( + directory_type__code='rro_status', is_active=True, + ).order_by('name') + + context = { + 'is_staff': is_staff, + 'registers': list(registers[:500]), + 'statuses': statuses, + 'kind_choices': CashRegister.Kind.choices, + 'filter_pharmacy': filter_pharmacy, + 'filter_org': filter_org, + 'filter_kind': filter_kind, + 'filter_status': filter_status, + 'filter_q': q, + 'all_pharmacies': ( + Pharmacy.objects.filter(is_active=True) + .select_related('organization') + .order_by('organization__name', 'pharmacy_number') + if is_staff else [] + ), + 'organizations': ( + Directory.objects.filter(directory_type__code='organization', is_active=True).order_by('name') + if is_staff else [] + ), + } + return render_page(request, 'rro.html', context) diff --git a/core/admin.py b/core/admin.py index 104c81f..16159d2 100644 --- a/core/admin.py +++ b/core/admin.py @@ -227,22 +227,34 @@ class DirectoryAdmin(ModelAdmin, ImportExportModelAdmin): class AccessSystemAdmin(ModelAdmin): """Отдельный раздел для списка систем/баз доступа (1С, почта, CRM…), которые выбираются чекбоксами в заявке на найм.""" - list_display = ['name', 'code', 'is_active', 'updated_at'] + list_display = ['name', 'code', 'location_types_display', 'is_active', 'updated_at'] list_filter = [ActiveFilter] list_filter_sheet = False list_filter_submit = True search_fields = ['name', 'code'] ordering = ['name'] - fields = ['name', 'code', 'description', 'is_active'] + fields = ['name', 'code', 'description', 'location_types', 'is_active'] + filter_horizontal = ['location_types'] def get_queryset(self, request): - return super().get_queryset(request).filter(directory_type__code='access_system') + return ( + super().get_queryset(request) + .filter(directory_type__code='access_system') + .prefetch_related('location_types') + ) def save_model(self, request, obj, form, change): if not obj.directory_type_id: obj.directory_type = DirectoryType.objects.get(code='access_system') super().save_model(request, obj, form, change) + def location_types_display(self, obj): + types = list(obj.location_types.all()) + if not types: + return mark_safe('все') + return ', '.join(t.name for t in types) + location_types_display.short_description = _('Типы локаций') + @admin.register(ConnectorConfig) class ConnectorConfigAdmin(ModelAdmin): diff --git a/core/migrations/0008_access_location_types_help.py b/core/migrations/0008_access_location_types_help.py new file mode 100644 index 0000000..4b5fbbb --- /dev/null +++ b/core/migrations/0008_access_location_types_help.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.3 on 2026-07-28 19:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_alter_connectorconfig_connector_type'), + ] + + operations = [ + migrations.AlterField( + model_name='directory', + name='location_types', + field=models.ManyToManyField(blank=True, help_text='Пусто = доступно для всех типов локаций. Для должностей и систем доступа: в каких локациях показывать (офис / аптека / склад).', limit_choices_to={'directory_type__code': 'location_type', 'is_active': True}, related_name='available_posts', to='core.directory', verbose_name='Типы локаций'), + ), + ] diff --git a/core/migrations/0009_portal_access_permissions.py b/core/migrations/0009_portal_access_permissions.py new file mode 100644 index 0000000..681a98c --- /dev/null +++ b/core/migrations/0009_portal_access_permissions.py @@ -0,0 +1,26 @@ +# Generated by Django 6.0.3 on 2026-08-03 16:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_access_location_types_help'), + ] + + operations = [ + migrations.CreateModel( + name='PortalAccess', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + options={ + 'verbose_name': 'Доступ к разделу портала', + 'verbose_name_plural': 'Доступы к разделам портала', + 'permissions': [('access_hr_portal', 'Доступ к разделу: HR-портал'), ('access_recruiting', 'Доступ к разделу: Рекрутинг'), ('access_structure', 'Доступ к разделу: Структура'), ('access_timetrack', 'Доступ к разделу: Учёт рабочего времени'), ('access_databases', 'Доступ к разделу: Базы данных'), ('access_reports', 'Доступ к разделу: Отчёты'), ('access_purchases', 'Доступ к разделу: Закупки'), ('access_email', 'Доступ к разделу: Почта'), ('access_it_manager', 'Доступ к разделу: IT-отдел'), ('access_ad_mapping', 'Доступ к разделу: Active Directory')], + 'managed': False, + 'default_permissions': (), + }, + ), + ] diff --git a/core/migrations/0010_seed_portal_section_perms.py b/core/migrations/0010_seed_portal_section_perms.py new file mode 100644 index 0000000..28856b7 --- /dev/null +++ b/core/migrations/0010_seed_portal_section_perms.py @@ -0,0 +1,99 @@ +"""Создаёт ContentType/permissions разделов портала, группу HR и раздаёт права.""" +from django.db import migrations + +PORTAL_PERMS = [ + ('access_hr_portal', 'Доступ к разделу: HR-портал'), + ('access_recruiting', 'Доступ к разделу: Рекрутинг'), + ('access_structure', 'Доступ к разделу: Структура'), + ('access_timetrack', 'Доступ к разделу: Учёт рабочего времени'), + ('access_databases', 'Доступ к разделу: Базы данных'), + ('access_reports', 'Доступ к разделу: Отчёты'), + ('access_purchases', 'Доступ к разделу: Закупки'), + ('access_email', 'Доступ к разделу: Почта'), + ('access_it_manager', 'Доступ к разделу: IT-отдел'), + ('access_ad_mapping', 'Доступ к разделу: Active Directory'), +] + +GROUP_PORTAL_PERMS = { + 'Администратор сети': [c for c, _ in PORTAL_PERMS], + 'IT': [ + 'access_it_manager', 'access_ad_mapping', 'access_databases', + ], + 'Офисный сотрудник': [ + 'access_structure', 'access_email', 'access_timetrack', 'access_databases', + 'access_reports', 'access_purchases', + ], + 'HR': [ + 'access_hr_portal', 'access_recruiting', + ], + 'Заведующий аптекой': [ + 'access_timetrack', + ], +} + + +def forwards(apps, schema_editor): + Group = apps.get_model('auth', 'Group') + Permission = apps.get_model('auth', 'Permission') + ContentType = apps.get_model('contenttypes', 'ContentType') + PermissionGroup = apps.get_model('employees', 'PermissionGroup') + + # post_migrate ещё не создал CT/perms для unmanaged PortalAccess — создаём явно + ct, _ = ContentType.objects.get_or_create(app_label='core', model='portalaccess') + perms_by_code = {} + for codename, name in PORTAL_PERMS: + perm, _ = Permission.objects.get_or_create( + content_type=ct, codename=codename, + defaults={'name': name}, + ) + if perm.name != name: + perm.name = name + perm.save(update_fields=['name']) + perms_by_code[codename] = perm + + for group_name, codes in GROUP_PORTAL_PERMS.items(): + group, _ = Group.objects.get_or_create(name=group_name) + PermissionGroup.objects.get_or_create( + group=group, + defaults={'description': f'Группа для роли: {group_name}', 'is_active': True}, + ) + to_add = [perms_by_code[c] for c in codes if c in perms_by_code] + if to_add: + group.permissions.add(*to_add) + + +def backwards(apps, schema_editor): + Group = apps.get_model('auth', 'Group') + Permission = apps.get_model('auth', 'Permission') + ContentType = apps.get_model('contenttypes', 'ContentType') + PermissionGroup = apps.get_model('employees', 'PermissionGroup') + + ct = ContentType.objects.filter(app_label='core', model='portalaccess').first() + if ct is not None: + portal_perms = list( + Permission.objects.filter(content_type=ct, codename__startswith='access_') + ) + for group in Group.objects.filter(name__in=GROUP_PORTAL_PERMS.keys()): + group.permissions.remove(*portal_perms) + + hr = Group.objects.filter(name='HR').first() + if hr is not None: + PermissionGroup.objects.filter(group=hr).delete() + # не удаляем Group HR если уже использовалась — только если создавали мы; + # для backwards удаляем + hr.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_portal_access_permissions'), + ('employees', '0013_dislocation_column_presets'), + ('it_manager', '0007_create_it_group'), + ('auth', '0012_alter_user_first_name_max_length'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.RunPython(forwards, backwards), + ] diff --git a/core/migrations/0011_ensure_portal_section_perms.py b/core/migrations/0011_ensure_portal_section_perms.py new file mode 100644 index 0000000..a3a5e97 --- /dev/null +++ b/core/migrations/0011_ensure_portal_section_perms.py @@ -0,0 +1,76 @@ +"""Повторный идемпотентный сид: 0010 мог выполниться до появления ContentType.""" +from django.db import migrations + +PORTAL_PERMS = [ + ('access_hr_portal', 'Доступ к разделу: HR-портал'), + ('access_recruiting', 'Доступ к разделу: Рекрутинг'), + ('access_structure', 'Доступ к разделу: Структура'), + ('access_timetrack', 'Доступ к разделу: Учёт рабочего времени'), + ('access_databases', 'Доступ к разделу: Базы данных'), + ('access_reports', 'Доступ к разделу: Отчёты'), + ('access_purchases', 'Доступ к разделу: Закупки'), + ('access_email', 'Доступ к разделу: Почта'), + ('access_it_manager', 'Доступ к разделу: IT-отдел'), + ('access_ad_mapping', 'Доступ к разделу: Active Directory'), +] + +GROUP_PORTAL_PERMS = { + 'Администратор сети': [c for c, _ in PORTAL_PERMS], + 'IT': [ + 'access_it_manager', 'access_ad_mapping', 'access_databases', + ], + 'Офисный сотрудник': [ + 'access_structure', 'access_email', 'access_timetrack', 'access_databases', + 'access_reports', 'access_purchases', + ], + 'HR': [ + 'access_hr_portal', 'access_recruiting', + ], + 'Заведующий аптекой': [ + 'access_timetrack', + ], +} + + +def forwards(apps, schema_editor): + Group = apps.get_model('auth', 'Group') + Permission = apps.get_model('auth', 'Permission') + ContentType = apps.get_model('contenttypes', 'ContentType') + PermissionGroup = apps.get_model('employees', 'PermissionGroup') + + ct, _ = ContentType.objects.get_or_create(app_label='core', model='portalaccess') + perms_by_code = {} + for codename, name in PORTAL_PERMS: + perm, _ = Permission.objects.get_or_create( + content_type=ct, codename=codename, + defaults={'name': name}, + ) + if perm.name != name: + perm.name = name + perm.save(update_fields=['name']) + perms_by_code[codename] = perm + + for group_name, codes in GROUP_PORTAL_PERMS.items(): + group, _ = Group.objects.get_or_create(name=group_name) + PermissionGroup.objects.get_or_create( + group=group, + defaults={'description': f'Группа для роли: {group_name}', 'is_active': True}, + ) + to_add = [perms_by_code[c] for c in codes if c in perms_by_code] + if to_add: + group.permissions.add(*to_add) + + +def backwards(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_seed_portal_section_perms'), + ] + + operations = [ + migrations.RunPython(forwards, backwards), + ] diff --git a/core/models/__init__.py b/core/models/__init__.py index 20b3aaa..2a53d26 100644 --- a/core/models/__init__.py +++ b/core/models/__init__.py @@ -1,3 +1,13 @@ from .base import BaseModel from .directories import DirectoryType, Directory, AccessSystem from .connector_config import ConnectorConfig +from .portal_access import PortalAccess + +__all__ = [ + 'BaseModel', + 'DirectoryType', + 'Directory', + 'AccessSystem', + 'ConnectorConfig', + 'PortalAccess', +] diff --git a/core/models/directories.py b/core/models/directories.py index 035b9a6..a3827f5 100644 --- a/core/models/directories.py +++ b/core/models/directories.py @@ -129,7 +129,11 @@ class Directory(TreeNodeModel, BaseModel): limit_choices_to={'directory_type__code': 'location_type', 'is_active': True}, related_name='available_posts', verbose_name=_('Типы локаций'), - help_text=_('Для должностей: в каких типах локаций доступна эта должность') + help_text=_( + 'Пусто = доступно для всех типов локаций. ' + 'Для должностей и систем доступа: в каких локациях показывать ' + '(офис / аптека / склад).' + ), ) # Active Directory интеграция (для отделов) diff --git a/core/models/portal_access.py b/core/models/portal_access.py new file mode 100644 index 0000000..309ed07 --- /dev/null +++ b/core/models/portal_access.py @@ -0,0 +1,18 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from core.portal_sections import portal_permission_codenames + + +class PortalAccess(models.Model): + """Маркер для Django permissions разделов портала (таблица не создаётся).""" + + class Meta: + managed = False + default_permissions = () + permissions = portal_permission_codenames() + verbose_name = _('Доступ к разделу портала') + verbose_name_plural = _('Доступы к разделам портала') + + def __str__(self): + return 'PortalAccess' diff --git a/core/portal_access.py b/core/portal_access.py new file mode 100644 index 0000000..11cf0d6 --- /dev/null +++ b/core/portal_access.py @@ -0,0 +1,51 @@ +"""Проверка доступа к разделам портала + декоратор для views.""" +from __future__ import annotations + +from functools import wraps + +from django.contrib.auth.views import redirect_to_login +from django.http import HttpResponseForbidden + +from core.portal_sections import get_section + + +def user_can_access(user, key: str) -> bool: + """Может ли пользователь открыть раздел портала ``key``. + + - не аутентифицирован → False + - superuser → True + - раздел без perm → True (любой залогиненный) + - is_staff → True (временный мост для офисных админов) + - иначе user.has_perm(section.perm) + """ + if not user or not getattr(user, 'is_authenticated', False): + return False + if getattr(user, 'is_superuser', False): + return True + + section = get_section(key) + if section is None: + return False + if section.perm is None: + return True + if getattr(user, 'is_staff', False): + return True + return user.has_perm(section.perm) + + +def section_required(key: str): + """Декоратор: доступ к view только при user_can_access(key).""" + + def decorator(view_func): + @wraps(view_func) + def _wrapped(request, *args, **kwargs): + user = request.user + if not user.is_authenticated: + return redirect_to_login(request.get_full_path()) + if not user_can_access(user, key): + return HttpResponseForbidden('Нет доступа к этому разделу портала.') + return view_func(request, *args, **kwargs) + + return _wrapped + + return decorator diff --git a/core/portal_sections.py b/core/portal_sections.py new file mode 100644 index 0000000..f08e0c8 --- /dev/null +++ b/core/portal_sections.py @@ -0,0 +1,69 @@ +"""Реестр разделов портала — единый источник для меню и проверки на view.""" +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class PortalSection: + key: str + path: str + label: str + nav: str # main | manage | support | none + perm: str | None = None # 'core.access_' or None = any authenticated + + +# Ограниченные разделы — нужны Django permissions core.access_* +RESTRICTED_KEYS = ( + 'hr_portal', + 'recruiting', + 'structure', + 'timetrack', + 'databases', + 'reports', + 'purchases', + 'email', + 'it_manager', + 'ad_mapping', +) + +PORTAL_SECTIONS: dict[str, PortalSection] = { + 'dashboard': PortalSection('dashboard', '/dashboard/', 'Главная', 'main'), + 'news': PortalSection('news', '/news/', 'Новости', 'main'), + 'employees': PortalSection('employees', '/employees/', 'Сотрудники', 'main'), + 'tasks': PortalSection('tasks', '/tasks/', 'Задачи', 'main'), + 'dislocation': PortalSection('dislocation', '/dislocation/', 'Дислокация', 'main'), + 'documents': PortalSection('documents', '/documents/', 'Документы', 'main'), + 'email': PortalSection('email', '/email/', 'Почта', 'main', 'core.access_email'), + 'chat': PortalSection('chat', '/chat/', 'Чат', 'main'), + 'calendar': PortalSection('calendar', '/calendar/', 'Календарь', 'main'), + 'databases': PortalSection('databases', '/databases/', 'Базы данных', 'main', 'core.access_databases'), + 'reports': PortalSection('reports', '/reports/', 'Отчёты', 'main', 'core.access_reports'), + 'purchases': PortalSection('purchases', '/purchases/', 'Закупки', 'main', 'core.access_purchases'), + 'hr_portal': PortalSection('hr_portal', '/hr-portal/', 'HR-портал', 'main', 'core.access_hr_portal'), + 'settings': PortalSection('settings', '/settings/', 'Настройки', 'main'), + 'profile': PortalSection('profile', '/profile/', 'Профиль', 'none'), + 'structure': PortalSection('structure', '/structure/', 'Структура', 'manage', 'core.access_structure'), + 'pharmacies': PortalSection('pharmacies', '/pharmacies/', 'Аптеки', 'manage'), + 'pharmacy_detail': PortalSection('pharmacy_detail', '/pharmacies/', 'Карточка аптеки', 'none'), + 'rro': PortalSection('rro', '/rro/', 'РРО / ПРРО', 'manage'), + 'utilities': PortalSection('utilities', '/utilities/', 'Коммунальные услуги', 'manage'), + 'recruiting': PortalSection('recruiting', '/recruiting/', 'Рекрутинг', 'manage', 'core.access_recruiting'), + 'timetrack': PortalSection('timetrack', '/timetrack/', 'Учёт рабочего времени', 'manage', 'core.access_timetrack'), + 'help': PortalSection('help', '/help/', 'Помощь', 'support'), + 'it_support': PortalSection('it_support', '/it-support/', 'IT-поддержка', 'support'), + 'it_manager': PortalSection('it_manager', '/it-manager/', 'IT-отдел', 'support', 'core.access_it_manager'), + 'ad_mapping': PortalSection('ad_mapping', '/ad-mapping/', 'Active Directory', 'support', 'core.access_ad_mapping'), +} + + +def get_section(key: str) -> PortalSection | None: + return PORTAL_SECTIONS.get(key) + + +def portal_permission_codenames() -> list[tuple[str, str]]: + """Список (codename, name) для Meta.permissions модели PortalAccess.""" + return [ + (f'access_{key}', f'Доступ к разделу: {PORTAL_SECTIONS[key].label}') + for key in RESTRICTED_KEYS + ] diff --git a/core/templatetags/common_tags.py b/core/templatetags/common_tags.py index 2bd06e9..54d469b 100644 --- a/core/templatetags/common_tags.py +++ b/core/templatetags/common_tags.py @@ -37,7 +37,13 @@ def clean_excerpt(html_text, length=140): @register.filter def is_it_staff(user): - """Чи входить користувач до групи IT (або суперюзер) — доступ до /it-manager/.""" - if not user or not user.is_authenticated: - return False - return user.is_superuser or user.groups.filter(name='IT').exists() + """Доступ к /it-manager/ — через единый реестр разделов портала.""" + from core.portal_access import user_can_access + return user_can_access(user, 'it_manager') + + +@register.filter +def can_section(user, key): + """Видимость пункта меню / доступ к разделу: {% if user|can_section:'hr_portal' %}.""" + from core.portal_access import user_can_access + return user_can_access(user, key) \ No newline at end of file diff --git a/core/tests.py b/core/tests.py index 570e500..1c3cd1a 100644 --- a/core/tests.py +++ b/core/tests.py @@ -55,6 +55,7 @@ class PortalPagesSmokeTests(TestCase): PAGES = [ '/dashboard/', '/employees/', '/pharmacies/', '/structure/', '/dislocation/', + '/rro/', '/timetrack/', '/calendar/', '/reports/', '/documents/', '/recruiting/', '/settings/', '/news/', '/tasks/', '/email/', '/chat/', '/databases/', '/purchases/', '/hr-portal/', diff --git a/core/tests_portal_access.py b/core/tests_portal_access.py new file mode 100644 index 0000000..91fd66e --- /dev/null +++ b/core/tests_portal_access.py @@ -0,0 +1,125 @@ +from datetime import date + +from django.contrib.auth.models import Group, Permission +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from apps.employees.models import Employee, EmployeePermissionOverride +from core.models import PortalAccess +from core.portal_access import user_can_access + + +def make_employee(**overrides): + n = Employee.objects.count() + 1 + defaults = { + 'username': f'portal.user{n}', + 'email': f'portal.user{n}@company.local', + 'first_name': 'Тест', + 'last_name': 'Портал', + 'hire_date': date(2026, 1, 1), + 'user_location_type': 'pharmacy', + 'mattermost_sync_enabled': False, + 'ad_sync_enabled': False, + 'is_staff': False, + } + defaults.update(overrides) + return Employee.objects.create(**defaults) + + +def login_2fa(client, user): + client.force_login(user) + if user.is_staff and not user.totp_enabled: + user.totp_enabled = True + user.save(update_fields=['totp_enabled']) + session = client.session + session['2fa_verified'] = True + session.save() + + +def portal_perm(codename: str) -> Permission: + ct = ContentType.objects.get_for_model(PortalAccess, for_concrete_model=False) + return Permission.objects.get(content_type=ct, codename=codename) + + +class PortalAccessUnitTests(TestCase): + def test_open_section_any_authenticated(self): + user = make_employee() + self.assertTrue(user_can_access(user, 'dashboard')) + self.assertTrue(user_can_access(user, 'rro')) + + def test_restricted_denied_without_perm(self): + user = make_employee() + self.assertFalse(user_can_access(user, 'hr_portal')) + self.assertFalse(user_can_access(user, 'it_manager')) + + def test_staff_bridge(self): + user = make_employee(is_staff=True) + self.assertTrue(user_can_access(user, 'hr_portal')) + self.assertTrue(user_can_access(user, 'databases')) + + def test_hr_group_perm(self): + user = make_employee() + hr = Group.objects.get(name='HR') + user.groups.add(hr) + # clear cached perms + if hasattr(user, '_perm_cache'): + del user._perm_cache + if hasattr(user, '_user_perm_cache'): + del user._user_perm_cache + if hasattr(user, '_group_perm_cache'): + del user._group_perm_cache + self.assertTrue(user_can_access(user, 'hr_portal')) + self.assertTrue(user_can_access(user, 'recruiting')) + self.assertFalse(user_can_access(user, 'it_manager')) + + def test_override_grant(self): + user = make_employee() + EmployeePermissionOverride.objects.create( + employee=user, + permission=portal_perm('access_hr_portal'), + override_type='grant', + ) + self.assertTrue(user_can_access(user, 'hr_portal')) + + def test_override_revoke(self): + user = make_employee() + it = Group.objects.get(name='IT') + user.groups.add(it) + if hasattr(user, '_perm_cache'): + del user._perm_cache + self.assertTrue(user_can_access(user, 'it_manager')) + EmployeePermissionOverride.objects.create( + employee=user, + permission=portal_perm('access_it_manager'), + override_type='revoke', + ) + self.assertFalse(user_can_access(user, 'it_manager')) + + +class PortalAccessViewTests(TestCase): + def test_pharmacist_open_ok_restricted_forbidden(self): + user = make_employee() + login_2fa(self.client, user) + self.assertEqual(self.client.get('/dashboard/', HTTP_HX_REQUEST='true').status_code, 200) + self.assertEqual(self.client.get('/rro/', HTTP_HX_REQUEST='true').status_code, 200) + self.assertEqual(self.client.get('/hr-portal/', HTTP_HX_REQUEST='true').status_code, 403) + self.assertEqual(self.client.get('/it-manager/', HTTP_HX_REQUEST='true').status_code, 403) + self.assertEqual(self.client.get('/databases/', HTTP_HX_REQUEST='true').status_code, 403) + + def test_hr_group_can_open_hr_portal(self): + user = make_employee() + user.groups.add(Group.objects.get(name='HR')) + login_2fa(self.client, user) + self.assertEqual(self.client.get('/hr-portal/', HTTP_HX_REQUEST='true').status_code, 200) + + def test_it_group_can_open_it_manager(self): + user = make_employee() + user.groups.add(Group.objects.get(name='IT')) + login_2fa(self.client, user) + self.assertEqual(self.client.get('/it-manager/', HTTP_HX_REQUEST='true').status_code, 200) + + def test_staff_bridge_views(self): + user = make_employee(is_staff=True, totp_enabled=True) + login_2fa(self.client, user) + self.assertEqual(self.client.get('/databases/', HTTP_HX_REQUEST='true').status_code, 200) + self.assertEqual(self.client.get('/ad-mapping/', HTTP_HX_REQUEST='true').status_code, 200) diff --git a/core/views.py b/core/views.py index b43267a..284ace5 100644 --- a/core/views.py +++ b/core/views.py @@ -12,6 +12,7 @@ from apps.employees.models.base import Employee from apps.locations.models.pharmacies import Pharmacy from core.models.directories import Directory from core.models import ConnectorConfig +from core.portal_access import section_required from core.utils.htmx import render_page from core.utils.pagination import paginate from django.utils.translation import gettext_lazy as _ @@ -210,9 +211,17 @@ def pharmacies_view(request): @login_required def dislocation_view(request): """Дислокація: аптеки на карті/у таблиці разом з їхнім персоналом (замінює вкладку «Аптеки» на /structure/).""" + from apps.locations.dislocation_columns import ( + all_column_defs, column_defs_for, resolve_columns_for_user, + ) + from apps.locations.models import DislocationColumnPreset + pharmacies = ( Pharmacy.objects - .select_related('organization', 'location_region', 'head_pharmacist') + .select_related( + 'organization', 'location_region', 'head_pharmacist', + 'curator', 'responsible_manager', + ) .prefetch_related(Prefetch( 'pharmacy_employees', queryset=Employee.objects.filter(is_active=True).select_related('post'), @@ -257,6 +266,13 @@ def dislocation_view(request): office_departments = [d for d in office_departments if d.staff] office_no_department = by_dept.get(None, []) + visible_keys = resolve_columns_for_user(request.user) + visible_columns = column_defs_for(visible_keys) + visible_set = set(visible_keys) + all_defs = all_column_defs() + panel_column_defs = list(visible_columns) + [d for d in all_defs if d.key not in visible_set] + presets = list(DislocationColumnPreset.objects.order_by('name')) + context = { 'pharmacies': pharmacies, 'active_count': active_count, @@ -268,14 +284,44 @@ def dislocation_view(request): 'office_departments': office_departments, 'office_no_department': office_no_department, 'office_employees_count': len(office_employees), + 'visible_columns': visible_columns, + 'visible_column_keys': visible_keys, + 'all_column_defs': all_defs, + 'panel_column_defs': panel_column_defs, + 'dislocation_presets': presets, + 'dislocation_presets_payload': [ + {'code': p.code, 'columns': list(p.columns or []), 'is_default': p.is_default} + for p in presets + ], + 'dislo_colspan': len(visible_columns) + 1, } return render_page(request, 'dislocation.html', context) + +@login_required +@require_POST +def dislocation_columns_view(request): + """Сохраняет персональный набор колонок дислокации (или применяет пресет).""" + from apps.locations.dislocation_columns import normalize_columns + from apps.locations.models import DislocationColumnPreset + + preset_code = (request.POST.get('preset_code') or '').strip() + if preset_code: + preset = get_object_or_404(DislocationColumnPreset, code=preset_code) + columns = normalize_columns(preset.columns) + else: + columns = normalize_columns(request.POST.getlist('columns')) + + Employee.objects.filter(pk=request.user.pk).update(dislocation_columns=columns) + request.user.dislocation_columns = columns + return dislocation_view(request) + @login_required def calendar_view(request): return render_page(request, 'calendar.html') @login_required +@section_required('timetrack') def timetrack_view(request): """Облік робочого часу: агрегація WorkShift (фактичні зміни з терміналів) по співробітниках за обраний місяць. Дані реальні — приходи/уходи @@ -370,6 +416,7 @@ def timetrack_view(request): return render_page(request, 'timetrack.html', context) @login_required +@section_required('reports') def reports_view(request): return render_page(request, 'reports.html') @@ -381,10 +428,12 @@ def documents_view(request): }) @login_required +@section_required('recruiting') def recruiting_view(request): return render_page(request, 'recruiting.html') @login_required +@section_required('structure') def structure_view(request): """Структура офісу: відділи. Аптеки з їхнім персоналом — на /dislocation/.""" departments = list( @@ -545,24 +594,27 @@ def chat_sync_now_view(request): return render_page(request, 'chat.html', _build_chat_context(request)) -def _static_page_view(template_name): +def _static_page_view(template_name, section=None): """Фабрика простых страниц: HTMX-фрагмент или полный layout""" @login_required def view(request): return render_page(request, template_name) + if section: + return section_required(section)(view) return view -email_view = _static_page_view('email.html') -purchases_view = _static_page_view('purchases.html') +email_view = _static_page_view('email.html', 'email') +purchases_view = _static_page_view('purchases.html', 'purchases') help_view = _static_page_view('help.html') @login_required def pharmacy_detail_view(request, pk): """Аптека целиком: реквизиты, лицензия, адрес/карта, персонал, - IT-обладнання, інтернет-договори, показання лічильників.""" + IT-обладнання, інтернет-договори, РРО/ПРРО, показання лічильників.""" from apps.it_manager.models import Equipment, InternetContract + from apps.rro.models import CashRegister from apps.utilities.models import UtilityReading pharmacy = get_object_or_404( @@ -584,6 +636,11 @@ def pharmacy_detail_view(request, pk): .select_related('provider', 'service_type', 'connection_type', 'status') .order_by('-start_date') ) + cash_registers = list( + CashRegister.objects.filter(pharmacy=pharmacy) + .select_related('vendor', 'status') + .order_by('-is_primary', 'kind', 'fiscal_number') + ) utility_readings = list( UtilityReading.objects.filter(pharmacy=pharmacy) .select_related('utility_type', 'submitted_by') @@ -596,6 +653,7 @@ def pharmacy_detail_view(request, pk): 'phones': list(pharmacy.phones.filter(is_active=True)), 'equipment': equipment, 'internet_contracts': internet_contracts, + 'cash_registers': cash_registers, 'utility_readings': utility_readings, 'can_edit': request.user.is_staff, } @@ -649,11 +707,30 @@ def _hr_portal_context(request): p.id: [lt.code for lt in p.location_types.all()] for p in hr_posts } + # «На испытании»: после перехода на пул интернов кандидаты живут в + # HiringRequest(status=trial); у Employee probation_end_date часто пустой. + employees_on_probation = [ + e for e in employees if e.is_on_probation + ] + trial_hiring_count = HiringRequest.objects.filter(status='trial').count() + intern_total = 0 + intern_free = 0 + try: + from apps.hiring.models import InternAccount + intern_qs = InternAccount.objects.filter(is_active=True) + intern_total = intern_qs.count() + intern_free = intern_qs.filter(assigned_to__isnull=True).count() + except Exception: + pass + context = { 'employees': employees, 'employees_total': employees.count(), 'employees_new': employees.filter(hire_date__gte=month_ago).count(), - 'employees_probation': employees.filter(probation_end_date__gte=today).count(), + 'employees_on_probation': employees_on_probation, + 'employees_probation': trial_hiring_count + len(employees_on_probation), + 'intern_slots_total': intern_total, + 'intern_slots_free': intern_free, 'hr_department_tree': _hr_department_tree(), 'hr_posts': hr_posts, 'hr_post_location_types': hr_post_location_types, @@ -661,17 +738,31 @@ def _hr_portal_context(request): } if request.user.is_staff: + from apps.locations.models import Location + hiring_requests_qs = HiringRequest.objects.select_related( - 'employee', 'post', 'department', 'user_pharmacy', 'requested_by', 'decision_by', + 'employee', 'post', 'department', 'user_pharmacy', 'user_location', + 'requested_by', 'decision_by', 'granted_ticket', 'intern_account', ).order_by('-created_at') + trial_hiring_qs = hiring_requests_qs.filter(status='trial') + hr_access_systems = list( + Directory.objects.filter( + directory_type__code='access_system', is_active=True, + ).prefetch_related('location_types').order_by('name') + ) context.update({ 'hiring_requests': paginate( request, hiring_requests_qs, page_param='hiring_page', size_param='hiring_per_page', ), - 'hr_access_systems': Directory.objects.filter( - directory_type__code='access_system', is_active=True, - ).order_by('name'), + 'trial_hiring_requests': list(trial_hiring_qs), + 'hr_access_systems': hr_access_systems, + 'hr_access_location_types': { + a.id: [lt.code for lt in a.location_types.all()] for a in hr_access_systems + }, 'hr_pharmacies': Pharmacy.objects.filter(is_active=True).order_by('pharmacy_number'), + 'hr_locations': Location.objects.filter( + location_type__in=['office', 'warehouse'], is_active=True, + ).order_by('location_type', 'address'), 'hr_trial_days_choices': range(1, 6), 'hr_departments_flat': Directory.objects.filter( directory_type__code='department', is_active=True, @@ -682,15 +773,16 @@ def _hr_portal_context(request): @login_required +@section_required('hr_portal') def hr_portal_view(request): return render_page(request, 'hr-portal.html', _hr_portal_context(request)) @login_required +@section_required('hr_portal') def hiring_create_view(request): - if not request.user.is_staff: - return HttpResponseForbidden() - + hiring_form_errors = None + hiring_just_created = None if request.method == 'POST': from datetime import datetime from apps.hiring.services import create_hiring_request @@ -715,35 +807,54 @@ def hiring_create_view(request): department_id = request.POST.get('department_id') or None pharmacy_id = request.POST.get('pharmacy_id') or None + location_id = request.POST.get('location_id') or None post_id = request.POST.get('post_id') or None - create_hiring_request( - requester=request.user, - last_name=last_name, - first_name=first_name, - middle_name=middle_name, - candidate_email=request.POST.get('candidate_email', '').strip(), - candidate_phone=request.POST.get('candidate_phone', '').strip(), - user_location_type=location_type, - department_id=department_id, - user_pharmacy_id=pharmacy_id, - post_id=post_id, - hire_date=hire_date, - trial_days=trial_days, - requested_access_ids=request.POST.getlist('requested_access'), - access_note=request.POST.get('access_note', '').strip(), - ) + try: + hiring_just_created = create_hiring_request( + requester=request.user, + last_name=last_name, + first_name=first_name, + middle_name=middle_name, + candidate_email=request.POST.get('candidate_email', '').strip(), + candidate_phone=request.POST.get('candidate_phone', '').strip(), + user_location_type=location_type, + department_id=department_id, + user_pharmacy_id=pharmacy_id, + user_location_id=location_id, + post_id=post_id, + hire_date=hire_date, + trial_days=trial_days, + requested_access_ids=request.POST.getlist('requested_access'), + access_note=request.POST.get('access_note', '').strip(), + ) + except ValidationError as exc: + hiring_form_errors = exc.message_dict if hasattr(exc, 'message_dict') else { + '__all__': list(exc.messages), + } context = _hr_portal_context(request) - context['hr_active_tab'] = 'hiring' + if hiring_form_errors: + context['hr_active_tab'] = 'hiring' + context['hiring_form_errors'] = hiring_form_errors + elif hiring_just_created is not None: + # После успешного создания показываем вкладку «На испытании» + # со свежим списком и баннером с данными интерна. + from apps.hiring.models import HiringRequest + context['hiring_just_created'] = ( + HiringRequest.objects + .select_related('intern_account', 'granted_ticket') + .get(pk=hiring_just_created.pk) + ) + context['hr_active_tab'] = 'probation' + else: + context['hr_active_tab'] = 'hiring' return render_page(request, 'hr-portal.html', context) @login_required +@section_required('hr_portal') def hiring_approve_view(request, pk): - if not request.user.is_staff: - return HttpResponseForbidden() - from apps.hiring.models import HiringRequest from apps.hiring.services import approve_hiring_request @@ -752,15 +863,13 @@ def hiring_approve_view(request, pk): approve_hiring_request(hiring_request, decided_by=request.user, note=request.POST.get('note', '').strip()) context = _hr_portal_context(request) - context['hr_active_tab'] = 'hiring' + context['hr_active_tab'] = 'probation' return render_page(request, 'hr-portal.html', context) @login_required +@section_required('hr_portal') def hiring_reject_view(request, pk): - if not request.user.is_staff: - return HttpResponseForbidden() - from apps.hiring.models import HiringRequest from apps.hiring.services import reject_hiring_request @@ -769,11 +878,27 @@ def hiring_reject_view(request, pk): reject_hiring_request(hiring_request, decided_by=request.user, note=request.POST.get('note', '').strip()) context = _hr_portal_context(request) - context['hr_active_tab'] = 'hiring' + context['hr_active_tab'] = 'probation' return render_page(request, 'hr-portal.html', context) @login_required +@section_required('hr_portal') +def hiring_credentials_pdf_view(request, pk): + from apps.hiring.models import HiringRequest + from apps.hiring.pdf import credentials_pdf_response + + hiring_request = get_object_or_404( + HiringRequest.objects.select_related('employee', 'post'), + pk=pk, + ) + if hiring_request.status != 'approved' or hiring_request.employee_id is None: + return HttpResponseForbidden() + return credentials_pdf_response(hiring_request) + + +@login_required +@section_required('databases') def databases_view(request): """Довідники: типи справочников с количеством записей""" from django.db.models import Count, Max diff --git a/locale/en/LC_MESSAGES/django.mo b/locale/en/LC_MESSAGES/django.mo index d5f4736a699f71bf847772aa4d755a79a464ef7b..2d44db1fe2ab9d5b4209befc65c261cdcf9556db 100644 GIT binary patch literal 75683 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 Sw|=F;p%i{e@h<5AJO2Z+SwRB; diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index 89f0d61..99e1c79 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-28 21:52+0300\n" +"POT-Creation-Date: 2026-07-29 11:36+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,10 +17,10 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: apps/ad_connector/models.py:10 apps/employees/models/base.py:198 +#: apps/ad_connector/models.py:10 apps/employees/models/base.py:207 #: apps/employees/models/permissions.py:47 osc/apps/ad_connector/models.py:10 #: osc/apps/employees/models.py:130 osc/apps/mattermost_connector/models.py:10 -#: templates/pages/employees.html:103 templates/pages/hr-portal.html:215 +#: templates/pages/employees.html:103 templates/pages/hr-portal.html:246 #: templates/pages/timetrack.html:58 msgid "Сотрудник" msgstr "Employee" @@ -29,42 +29,43 @@ msgstr "Employee" #: apps/nextcloud_connector/models.py:34 osc/apps/ad_connector/models.py:13 #: osc/apps/mattermost_connector/models.py:13 msgid "Действие" -msgstr "" +msgstr "Action" #: apps/ad_connector/models.py:15 apps/mail_connector/models.py:17 #: osc/apps/ad_connector/models.py:14 #: osc/apps/mattermost_connector/models.py:14 msgid "Создание" -msgstr "" +msgstr "Creation" #: apps/ad_connector/models.py:16 apps/mail_connector/models.py:18 #: osc/apps/ad_connector/models.py:14 #: osc/apps/mattermost_connector/models.py:14 msgid "Обновление" -msgstr "" +msgstr "Update" #: apps/ad_connector/models.py:17 apps/mail_connector/models.py:19 msgid "Включение" -msgstr "" +msgstr "Enable" #: apps/ad_connector/models.py:18 apps/mail_connector/models.py:20 msgid "Отключение" -msgstr "" +msgstr "Disable" -#: apps/ad_connector/models.py:22 apps/hiring/models.py:72 +#: apps/ad_connector/models.py:22 apps/hiring/models.py:129 #: apps/it_manager/admin.py:168 apps/it_manager/admin.py:263 #: apps/it_manager/admin.py:352 apps/it_manager/admin.py:498 +#: apps/locations/dislocation_columns.py:42 #: apps/locations/models/pharmacies.py:128 apps/mail_connector/models.py:24 #: apps/nextcloud_connector/models.py:35 apps/tasks/models.py:40 #: apps/visit_manager/admin.py:199 core/admin.py:32 #: osc/apps/ad_connector/models.py:17 #: osc/apps/mattermost_connector/models.py:17 -#: templates/pages/dislocation.html:122 templates/pages/employees.html:107 -#: templates/pages/hr-portal.html:535 templates/pages/it-manager.html:237 -#: templates/pages/it-manager.html:273 templates/pages/it-manager.html:350 -#: templates/pages/it-manager.html:403 templates/pages/it-manager.html:696 -#: templates/pages/it-manager.html:724 templates/pages/it-manager.html:973 -#: templates/pages/it-manager.html:1002 templates/pages/pharmacies.html:119 +#: templates/pages/employees.html:107 templates/pages/hr-portal.html:689 +#: templates/pages/it-manager.html:237 templates/pages/it-manager.html:273 +#: templates/pages/it-manager.html:350 templates/pages/it-manager.html:403 +#: templates/pages/it-manager.html:699 templates/pages/it-manager.html:727 +#: templates/pages/it-manager.html:976 templates/pages/it-manager.html:1005 +#: templates/pages/pharmacies.html:119 #: templates/pages/pharmacy-detail.html:197 #: templates/pages/pharmacy-detail.html:247 templates/pages/structure.html:63 msgid "Статус" @@ -74,7 +75,7 @@ msgstr "Status" #: apps/nextcloud_connector/models.py:25 osc/apps/ad_connector/models.py:18 #: osc/apps/mattermost_connector/models.py:18 msgid "Успех" -msgstr "" +msgstr "Success" #: apps/ad_connector/models.py:23 apps/employees/models/base.py:145 #: apps/employees/models/base.py:160 apps/locations/models/pharmacies.py:152 @@ -90,58 +91,60 @@ msgstr "Error" #: apps/nextcloud_connector/models.py:36 osc/apps/ad_connector/models.py:20 #: osc/apps/mattermost_connector/models.py:20 msgid "Сообщение" -msgstr "" +msgstr "Message" #: apps/ad_connector/models.py:28 osc/apps/ad_connector/models.py:23 msgid "Запись синхронизации AD" -msgstr "" +msgstr "AD sync record" #: apps/ad_connector/models.py:29 osc/apps/ad_connector/models.py:24 msgid "Журнал синхронизации AD" -msgstr "" +msgstr "AD sync log" #: apps/employees/admin.py:37 apps/employees/admin.py:259 -#: apps/employees/models/base.py:113 apps/nextcloud_connector/models.py:16 -#: osc/apps/employees/models.py:72 templates/pages/hr-portal.html:446 +#: apps/employees/models/base.py:113 apps/hiring/models.py:97 +#: apps/locations/models/dislocation.py:26 +#: apps/nextcloud_connector/models.py:16 osc/apps/employees/models.py:72 #: templates/pages/structure.html:59 msgid "Отдел" msgstr "Department" #: apps/employees/admin.py:39 msgid "— Выберите отдел —" -msgstr "" +msgstr "— Select department —" #: apps/employees/admin.py:44 apps/locations/admin.py:67 msgid "Выберите из списка. Уровни вложенности отображаются отступами." -msgstr "" +msgstr "Select from the list. Nesting levels are shown with indentation." #: apps/employees/admin.py:49 msgid "— Выберите офис или склад —" -msgstr "" +msgstr "— Select office or warehouse —" #: apps/employees/admin.py:50 apps/employees/models/base.py:107 +#: apps/hiring/models.py:107 msgid "Локация (офис/склад)" -msgstr "" +msgstr "Location (office/warehouse)" #: apps/employees/admin.py:51 apps/employees/models/base.py:108 msgid "Заполняется для location_type=office или warehouse" -msgstr "" +msgstr "Filled for location_type=office or warehouse" -#: apps/employees/admin.py:133 core/admin.py:259 +#: apps/employees/admin.py:133 core/admin.py:271 msgid "Аутентификация" -msgstr "" +msgstr "Authentication" -#: apps/employees/admin.py:137 +#: apps/employees/admin.py:137 apps/hiring/pdf.py:49 msgid "ФИО" -msgstr "" +msgstr "Full name" #: apps/employees/admin.py:141 msgid "Рабочая информация" -msgstr "" +msgstr "Work information" #: apps/employees/admin.py:149 apps/it_manager/admin.py:307 msgid "Даты" -msgstr "" +msgstr "Dates" #: apps/employees/admin.py:153 templates/pages/employee-detail.html:126 msgid "Личные данные" @@ -155,81 +158,96 @@ msgstr "Contacts" #: apps/employees/admin.py:173 msgid "Графік та відвідуваність" -msgstr "" +msgstr "Schedule and attendance" #: apps/employees/admin.py:176 msgid "Графіки роботи та записи відвідуваності відображаються нижче." -msgstr "" +msgstr "Work schedules and attendance records are shown below." #: apps/employees/admin.py:184 msgid "Системный доступ" -msgstr "" +msgstr "System access" #: apps/employees/admin.py:188 msgid "Только для IT-администраторов. is_staff — вход в Django admin." -msgstr "" +msgstr "For IT administrators only. is_staff — access to Django admin." #: apps/employees/admin.py:232 msgid "Предпросмотр" -msgstr "" +msgstr "Preview" #: apps/employees/admin.py:239 msgid "Группы" -msgstr "" +msgstr "Groups" -#: apps/employees/models/base.py:34 core/models/connector_config.py:29 +#: apps/employees/admin.py:266 +#| msgid "Офисных сотрудников" +msgid "Офисные сотрудники" +msgstr "Office employees" + +#: apps/employees/admin.py:269 +msgid "" +"При утверждении заявки на найм офисному сотруднику назначается рабочий " +"email: логин@домен." +msgstr "" +"When a hiring request is approved, an office employee is assigned a work " +"email: login@domain." + +#: apps/employees/models/base.py:34 apps/hiring/models.py:19 +#: apps/hiring/pdf.py:52 core/models/connector_config.py:29 #: osc/apps/employees/models.py:33 msgid "Логин" -msgstr "" +msgstr "Login" #: apps/employees/models/base.py:35 msgid "Уникальный логин для входа в систему" -msgstr "" +msgstr "Unique login for signing in" #: apps/employees/models/base.py:38 osc/apps/employees/models.py:37 msgid "Email адрес" -msgstr "" +msgstr "Email address" #: apps/employees/models/base.py:39 osc/apps/employees/models.py:38 msgid "Рабочий email. Если не указан — генерируется из username@local.domain" -msgstr "" +msgstr "Work email. If not set, generated as username@local.domain" -#: apps/employees/models/base.py:42 osc/apps/employees/models.py:41 +#: apps/employees/models/base.py:42 apps/hiring/models.py:75 +#: osc/apps/employees/models.py:41 msgid "Фамилия" -msgstr "" +msgstr "Last name" -#: apps/employees/models/base.py:43 osc/apps/employees/models.py:42 -#: templates/pages/profile.html:76 +#: apps/employees/models/base.py:43 apps/hiring/models.py:76 +#: osc/apps/employees/models.py:42 templates/pages/profile.html:76 msgid "Имя" msgstr "First name" -#: apps/employees/models/base.py:44 osc/apps/employees/models.py:43 -#: templates/pages/hr-portal.html:430 +#: apps/employees/models/base.py:44 apps/hiring/models.py:77 +#: osc/apps/employees/models.py:43 templates/pages/hr-portal.html:571 msgid "Отчество" -msgstr "Patronymic" +msgstr "Middle name" #: apps/employees/models/base.py:46 osc/apps/employees/models.py:46 msgid "Доступ к панели администратора" -msgstr "" +msgstr "Access to the admin panel" #: apps/employees/models/base.py:47 osc/apps/employees/models.py:47 msgid "Разрешает вход в Django admin. Только для IT-администраторов системы." -msgstr "" +msgstr "Allows access to Django admin. For system IT administrators only." -#: apps/employees/models/base.py:48 core/models/directories.py:29 -#: core/models/directories.py:121 templates/pages/employee-detail.html:45 -#: templates/pages/employees.html:148 templates/pages/hr-portal.html:174 -#: templates/pages/structure.html:89 +#: apps/employees/models/base.py:48 apps/hiring/models.py:21 +#: core/models/directories.py:29 core/models/directories.py:121 +#: templates/pages/employee-detail.html:45 templates/pages/employees.html:148 +#: templates/pages/hr-portal.html:205 templates/pages/structure.html:89 msgid "Активный" msgstr "Active" #: apps/employees/models/base.py:49 osc/apps/employees/models.py:49 msgid "Работает ли сотрудник сейчас" -msgstr "" +msgstr "Whether the employee is currently active" #: apps/employees/models/base.py:50 osc/apps/employees/models.py:50 msgid "Дата создания аккаунта" -msgstr "" +msgstr "Account creation date" #: apps/employees/models/base.py:53 osc/apps/employees/models.py:52 #: templates/pages/employee-detail.html:129 @@ -243,7 +261,7 @@ msgstr "Tax ID" #: apps/employees/models/base.py:58 osc/apps/employees/models.py:55 msgid "Идентификационный номер налогоплательщика" -msgstr "" +msgstr "Taxpayer identification number" #: apps/employees/models/base.py:63 osc/apps/employees/models.py:59 #: templates/pages/employee-detail.html:74 templates/pages/profile.html:57 @@ -252,11 +270,11 @@ msgstr "Work phone" #: apps/employees/models/base.py:67 msgid "Внутрішній номер" -msgstr "" +msgstr "Internal number" #: apps/employees/models/base.py:68 msgid "Короткий внутрішній (додатковий) номер АТС" -msgstr "" +msgstr "Short internal (extension) PBX number" #: apps/employees/models/base.py:71 osc/apps/employees/models.py:60 #: templates/pages/employee-detail.html:83 templates/pages/profile.html:61 @@ -270,11 +288,11 @@ msgstr "Personal email" #: apps/employees/models/base.py:76 osc/apps/employees/models.py:63 msgid "Контакт в экстренных случаях" -msgstr "" +msgstr "Emergency contact" #: apps/employees/models/base.py:77 osc/apps/employees/models.py:65 msgid "Телефон экстренного контакта" -msgstr "" +msgstr "Emergency contact phone" #: apps/employees/models/base.py:79 osc/apps/employees/models.py:67 #: templates/pages/profile.html:84 @@ -282,59 +300,63 @@ msgid "Степень родства" msgstr "Relationship" #: apps/employees/models/base.py:82 apps/employees/models/base.py:101 -#: apps/hiring/models.py:34 apps/hiring/models.py:50 -#: apps/locations/admin.py:347 apps/locations/models/base.py:29 -#: apps/locations/models/pharmacies.py:210 +#: apps/hiring/models.py:86 apps/hiring/models.py:102 +#: apps/locations/admin.py:347 apps/locations/dislocation_columns.py:36 +#: apps/locations/models/base.py:29 apps/locations/models/pharmacies.py:210 #: apps/locations/models/pharmacies.py:349 apps/mail_connector/models.py:12 #: apps/nextcloud_connector/models.py:17 apps/utilities/models.py:17 -#: templates/pages/dislocation.html:116 templates/pages/employees.html:64 -#: templates/pages/employees.html:90 templates/pages/hr-portal.html:126 -#: templates/pages/hr-portal.html:441 templates/pages/hr-portal.html:455 -#: templates/pages/hr-portal.html:459 +#: templates/pages/employees.html:64 templates/pages/employees.html:90 +#: templates/pages/hr-portal.html:156 templates/pages/hr-portal.html:582 +#: templates/pages/hr-portal.html:600 msgid "Аптека" msgstr "Pharmacy" -#: apps/employees/models/base.py:83 apps/hiring/models.py:35 -#: apps/locations/models/base.py:30 templates/pages/hr-portal.html:128 -#: templates/pages/hr-portal.html:442 +#: apps/employees/models/base.py:83 apps/hiring/models.py:87 +#: apps/locations/models/base.py:30 templates/pages/hr-portal.html:158 +#: templates/pages/hr-portal.html:583 msgid "Склад" msgstr "Warehouse" -#: apps/employees/models/base.py:84 apps/locations/models/base.py:31 -#: templates/pages/employees.html:56 templates/pages/hr-portal.html:124 -#: templates/pages/hr-portal.html:440 templates/pages/timetrack.html:47 +#: apps/employees/models/base.py:84 apps/hiring/models.py:88 +#: apps/locations/models/base.py:31 templates/pages/employees.html:56 +#: templates/pages/hr-portal.html:154 templates/pages/hr-portal.html:581 +#: templates/pages/timetrack.html:47 msgid "Офис" msgstr "Office" -#: apps/employees/models/base.py:87 apps/locations/models/base.py:28 +#: apps/employees/models/base.py:87 apps/hiring/models.py:91 +#: apps/hiring/pdf.py:51 apps/hiring/services.py:99 +#: apps/locations/models/base.py:28 msgid "Тип локации" -msgstr "" +msgstr "Location type" #: apps/employees/models/base.py:88 msgid "Где работает: аптека / склад / офис" -msgstr "" +msgstr "Where they work: pharmacy / warehouse / office" -#: apps/employees/models/base.py:95 apps/locations/models/pharmacies.py:88 -#: templates/pages/dislocation.html:117 templates/pages/pharmacies.html:113 +#: apps/employees/models/base.py:95 apps/locations/dislocation_columns.py:37 +#: apps/locations/models/pharmacies.py:88 templates/pages/pharmacies.html:113 #: templates/pages/pharmacy-detail.html:87 msgid "Организация" msgstr "Organization" #: apps/employees/models/base.py:96 apps/locations/models/pharmacies.py:89 msgid "Юрлицо: АКС, АКС Одесса, Тира, Пентальфа..." -msgstr "" +msgstr "Legal entity: AKS, AKS Odesa, Tira, Pentalfa..." #: apps/employees/models/base.py:102 msgid "Заполняется только для location_type=pharmacy" -msgstr "" +msgstr "Filled only for location_type=pharmacy" #: apps/employees/models/base.py:115 msgid "Заполняется для office / warehouse" -msgstr "" +msgstr "Filled for office / warehouse" -#: apps/employees/models/base.py:120 osc/apps/employees/models.py:77 -#: templates/pages/dislocation.html:297 templates/pages/employees.html:104 -#: templates/pages/hr-portal.html:216 templates/pages/hr-portal.html:467 +#: apps/employees/models/base.py:120 apps/hiring/models.py:113 +#: apps/hiring/pdf.py:50 apps/hiring/services.py:98 +#: osc/apps/employees/models.py:77 templates/pages/dislocation.html:310 +#: templates/pages/employees.html:104 templates/pages/hr-portal.html:247 +#: templates/pages/hr-portal.html:617 msgid "Должность" msgstr "Position" @@ -345,7 +367,7 @@ msgstr "Manager" #: apps/employees/models/base.py:129 osc/apps/employees/models.py:84 msgid "Дата приёма на работу" -msgstr "" +msgstr "Hire date" #: apps/employees/models/base.py:130 osc/apps/employees/models.py:85 #: templates/pages/employee-detail.html:116 @@ -354,25 +376,25 @@ msgstr "Termination date" #: apps/employees/models/base.py:131 osc/apps/employees/models.py:86 msgid "Конец испытательного срока" -msgstr "" +msgstr "End of probation period" #: apps/employees/models/base.py:134 apps/visit_manager/admin.py:262 #: apps/visit_manager/admin.py:289 osc/apps/employees/models.py:88 -#: templates/pages/hr-portal.html:165 templates/pages/hr-portal.html:234 +#: templates/pages/hr-portal.html:196 templates/pages/hr-portal.html:266 msgid "Фото" msgstr "Photo" #: apps/employees/models/base.py:138 osc/apps/employees/models.py:90 msgid "ID Mattermost" -msgstr "" +msgstr "Mattermost ID" #: apps/employees/models/base.py:139 osc/apps/employees/models.py:91 msgid "Логин Mattermost" -msgstr "" +msgstr "Mattermost login" #: apps/employees/models/base.py:141 osc/apps/employees/models.py:93 msgid "Статус синхронизации Mattermost" -msgstr "" +msgstr "Mattermost sync status" #: apps/employees/models/base.py:143 apps/tasks/models.py:24 #: osc/apps/employees/models.py:95 templates/pages/employee-detail.html:155 @@ -385,112 +407,145 @@ msgstr "Pending" #: apps/locations/models/pharmacies.py:151 osc/apps/employees/models.py:96 #: osc/apps/employees/models.py:114 msgid "Синхронизирован" -msgstr "" +msgstr "Synced" #: apps/employees/models/base.py:146 apps/employees/models/base.py:161 #: osc/apps/employees/models.py:98 osc/apps/employees/models.py:116 msgid "Отключена" -msgstr "" +msgstr "Disabled" #: apps/employees/models/base.py:150 osc/apps/employees/models.py:102 msgid "Последняя синхронизация Mattermost" -msgstr "" +msgstr "Last Mattermost sync" #: apps/employees/models/base.py:151 osc/apps/employees/models.py:103 msgid "Синхронизация Mattermost" -msgstr "" +msgstr "Mattermost sync" #: apps/employees/models/base.py:156 osc/apps/employees/models.py:111 msgid "Статус синхронизации AD" -msgstr "" +msgstr "AD sync status" #: apps/employees/models/base.py:158 osc/apps/employees/models.py:113 msgid "Не синхронизирован" -msgstr "" +msgstr "Not synced" #: apps/employees/models/base.py:165 osc/apps/employees/models.py:120 msgid "Ошибка синхронизации AD" -msgstr "" +msgstr "AD sync error" #: apps/employees/models/base.py:166 osc/apps/employees/models.py:121 msgid "Последняя синхронизация AD" -msgstr "" +msgstr "Last AD sync" #: apps/employees/models/base.py:167 osc/apps/employees/models.py:122 msgid "Синхронизация AD" -msgstr "" +msgstr "AD sync" #: apps/employees/models/base.py:168 msgid "Только для офисных сотрудников" -msgstr "" +msgstr "For office employees only" #: apps/employees/models/base.py:171 msgid "Записей на странице" -msgstr "" +msgstr "Records per page" #: apps/employees/models/base.py:173 msgid "Сколько строк показывать в таблицах портала по умолчанию" -msgstr "" +msgstr "How many rows to show in portal tables by default" #: apps/employees/models/base.py:177 -msgid "Был онлайн" -msgstr "" +#| msgid "Должность / локация" +msgid "Колонки дислокации" +msgstr "Dislocation columns" -#: apps/employees/models/base.py:178 -msgid "Обновляется автоматически при активности в портале (не в AD/Mattermost)" -msgstr "" - -#: apps/employees/models/base.py:183 -msgid "TOTP-секрет" -msgstr "" - -#: apps/employees/models/base.py:184 -msgid "Секрет TOTP (шифруется). Заполняется при настройке 2FA." +#: apps/employees/models/base.py:180 +msgid "" +"Персональный набор колонок таблицы /dislocation/. Пусто — пресет отдела или " +"системный «Стандарт»." msgstr "" +"Personal set of columns for the /dislocation/ table. Empty — the department " +"preset or the system \"Standard\"." #: apps/employees/models/base.py:186 +msgid "Был онлайн" +msgstr "Last seen" + +#: apps/employees/models/base.py:187 +msgid "" +"Обновляется автоматически при активности в портале (не в AD/Mattermost)" +msgstr "Updated automatically on portal activity (not AD/Mattermost)" + +#: apps/employees/models/base.py:192 +msgid "TOTP-секрет" +msgstr "TOTP secret" + +#: apps/employees/models/base.py:193 +msgid "Секрет TOTP (шифруется). Заполняется при настройке 2FA." +msgstr "TOTP secret (encrypted). Set when configuring 2FA." + +#: apps/employees/models/base.py:195 msgid "2FA включена" -msgstr "" +msgstr "2FA enabled" -#: apps/employees/models/base.py:188 +#: apps/employees/models/base.py:197 msgid "Резервные коды 2FA (хеши)" -msgstr "" +msgstr "2FA backup codes (hashes)" -#: apps/employees/models/base.py:189 +#: apps/employees/models/base.py:198 msgid "Хеши одноразовых резервных кодов; использованный код удаляется." -msgstr "" +msgstr "Hashes of one-time backup codes; a used code is removed." -#: apps/employees/models/base.py:199 osc/apps/employees/models.py:131 +#: apps/employees/models/base.py:208 osc/apps/employees/models.py:131 #: templates/index.html:251 templates/pages/dashboard.html:174 #: templates/pages/employees.html:8 templates/pages/employees.html:24 -#: templates/pages/hr-portal.html:92 +#: templates/pages/hr-portal.html:119 msgid "Сотрудники" msgstr "Employees" +#: apps/employees/models/defaults.py:13 +#| msgid "Офисных сотрудников" +msgid "Домен email для офисных сотрудников" +msgstr "Email domain for office employees" + +#: apps/employees/models/defaults.py:17 +msgid "" +"При утверждении заявки на найм офисному сотруднику назначается email вида " +"логин@этот-домен (вместо @local.domain)." +msgstr "" +"When a hiring request is approved, an office employee is assigned an email " +"of the form login@this-domain (instead of @local.domain)." + +#: apps/employees/models/defaults.py:23 apps/employees/models/defaults.py:24 +#: apps/employees/models/defaults.py:27 +#| msgid "Активных сотрудников пока нет" +msgid "Настройки сотрудников по умолчанию" +msgstr "Default employee settings" + #: apps/employees/models/permissions.py:14 msgid "Группа" -msgstr "" +msgstr "Group" #: apps/employees/models/permissions.py:16 apps/it_manager/admin.py:611 #: core/models/directories.py:28 core/models/directories.py:120 -#: templates/pages/it-manager.html:558 templates/pages/it-manager.html:1102 +#: templates/pages/it-manager.html:561 templates/pages/it-manager.html:1105 #: templates/pages/task-detail.html:62 msgid "Описание" msgstr "Description" #: apps/employees/models/permissions.py:17 apps/locations/models/base.py:59 -#: templates/pages/dislocation.html:180 templates/pages/dislocation.html:559 -#: templates/pages/pharmacies.html:156 +#: templates/pages/dislocation.html:572 templates/pages/pharmacies.html:156 +#: templates/partials/dislo_cell.html:40 msgid "Активна" msgstr "Active" #: apps/employees/models/permissions.py:20 msgid "Группа разрешений" -msgstr "" +msgstr "Permission group" #: apps/employees/models/permissions.py:21 msgid "Группы разрешений" -msgstr "" +msgstr "Permission groups" #: apps/employees/models/permissions.py:41 templates/pages/it-manager.html:199 #: templates/pages/it-manager.html:220 @@ -499,22 +554,22 @@ msgstr "Issue" #: apps/employees/models/permissions.py:42 msgid "Отозвать" -msgstr "" +msgstr "Revoke" #: apps/employees/models/permissions.py:51 msgid "Разрешение" -msgstr "" +msgstr "Permission" #: apps/employees/models/permissions.py:54 apps/it_manager/admin.py:154 #: apps/it_manager/admin.py:241 apps/it_manager/admin.py:334 #: apps/locations/models/pharmacies.py:332 templates/pages/it-manager.html:144 -#: templates/pages/it-manager.html:620 +#: templates/pages/it-manager.html:623 msgid "Тип" msgstr "Type" #: apps/employees/models/permissions.py:56 msgid "Действует с" -msgstr "" +msgstr "Effective from" #: apps/employees/models/permissions.py:57 #: templates/pages/pharmacy-detail.html:248 @@ -525,27 +580,27 @@ msgstr "Valid until" #: apps/locations/models/pharmacies.py:334 osc/apps/employees/models.py:49 #: osc/core/models/directory.py:32 msgid "Активен" -msgstr "" +msgstr "Active" #: apps/employees/models/permissions.py:59 msgid "Причина / комментарий" -msgstr "" +msgstr "Reason / comment" #: apps/employees/models/permissions.py:62 msgid "Override разрешения" -msgstr "" +msgstr "Permission override" #: apps/employees/models/permissions.py:63 msgid "Override разрешений" -msgstr "" +msgstr "Permission overrides" #: apps/feed/models.py:22 msgid "Загальне" -msgstr "" +msgstr "General" #: apps/feed/models.py:25 msgid "Мережа" -msgstr "" +msgstr "Network" #: apps/feed/models.py:36 templates/pages/news-edit.html:26 #: templates/pages/news.html:29 @@ -554,47 +609,55 @@ msgstr "Title" #: apps/feed/models.py:37 msgid "Текст" -msgstr "" +msgstr "Text" #: apps/feed/models.py:39 msgid "Категорія" -msgstr "" +msgstr "Category" #: apps/feed/models.py:42 msgid "Важливо" -msgstr "" +msgstr "Important" #: apps/feed/models.py:43 msgid "Закріплено" -msgstr "" +msgstr "Pinned" #: apps/feed/models.py:44 msgid "Опубліковано" -msgstr "" +msgstr "Published" #: apps/feed/models.py:47 msgid "Опубліковано в Mattermost" -msgstr "" +msgstr "Published in Mattermost" #: apps/feed/models.py:48 msgid "Час останньої публікації в чат — не блокує повторну публікацію" -msgstr "" +msgstr "Time of last publication to chat — does not block re-publishing" #: apps/feed/models.py:51 msgid "ID повідомлень в Mattermost" -msgstr "" +msgstr "Message IDs in Mattermost" #: apps/feed/models.py:52 -msgid "{team_name: post_id} — за наявності id повторна публікація редагує існуюче повідомлення" +msgid "" +"{team_name: post_id} — за наявності id повторна публікація редагує існуюче " +"повідомлення" msgstr "" +"{team_name: post_id} — if id exists, re-publishing edits the existing " +"message" #: apps/feed/models.py:55 msgid "Куди публікувати в Mattermost" -msgstr "" +msgstr "Where to publish in Mattermost" #: apps/feed/models.py:56 -msgid "Список команд (akc/pharmacys/warehouse) — обирається чекбоксами при створенні/редагуванні" +msgid "" +"Список команд (akc/pharmacys/warehouse) — обирається чекбоксами при " +"створенні/редагуванні" msgstr "" +"List of teams (akc/pharmacys/warehouse) — selected via checkboxes when " +"creating/editing" #: apps/feed/models.py:61 apps/feed/models.py:100 apps/tasks/models.py:126 #: templates/pages/task-detail.html:200 @@ -603,165 +666,241 @@ msgstr "Author" #: apps/feed/models.py:65 apps/feed/models.py:96 msgid "Пост" -msgstr "" +msgstr "Post" #: apps/feed/models.py:66 msgid "Пости" -msgstr "" +msgstr "Posts" #: apps/feed/models.py:102 apps/feed/models.py:115 apps/tasks/models.py:128 #: apps/utilities/models.py:26 msgid "Коментар" -msgstr "" +msgstr "Comment" #: apps/feed/models.py:109 msgid "ID відповідей в Mattermost" -msgstr "" +msgstr "Reply IDs in Mattermost" #: apps/feed/models.py:111 msgid "ID вихідного повідомлення з Mattermost" -msgstr "" +msgstr "Source message ID from Mattermost" #: apps/feed/models.py:116 msgid "Коментарі" -msgstr "" - -#: apps/hiring/models.py:17 -msgid "На випробуванні" -msgstr "" +msgstr "Comments" #: apps/hiring/models.py:18 -msgid "Затверджено" -msgstr "" +msgid "Метка" +msgstr "Label" -#: apps/hiring/models.py:19 -msgid "Відхилено" -msgstr "" - -#: apps/hiring/models.py:21 -#, python-format -msgid "%(n)d дн." -msgstr "" - -#: apps/hiring/models.py:23 -msgid "Прізвище" -msgstr "" - -#: apps/hiring/models.py:24 -msgid "Ім'я" -msgstr "" +#: apps/hiring/models.py:20 apps/hiring/pdf.py:53 +#: core/models/connector_config.py:32 +msgid "Пароль" +msgstr "Password" #: apps/hiring/models.py:25 -msgid "По батькові" -msgstr "" +msgid "Назначено заявке" +msgstr "Assigned to request" -#: apps/hiring/models.py:27 -msgid "Особистий email кандидата" -msgstr "" +#: apps/hiring/models.py:29 apps/hiring/services.py:103 +msgid "Интерн-аккаунт" +msgstr "Intern account" -#: apps/hiring/models.py:29 templates/pages/hr-portal.html:495 +#: apps/hiring/models.py:30 +msgid "Пул интерн-аккаунтов" +msgstr "Intern account pool" + +#: apps/hiring/models.py:34 +msgid "занят" +msgstr "busy" + +#: apps/hiring/models.py:34 +msgid "свободен" +msgstr "free" + +#: apps/hiring/models.py:49 +msgid "" +"Нет свободных интерн-аккаунтов. Дождитесь завершения стажировки или добавьте" +" слот в админке." +msgstr "" +"No free intern accounts. Wait for an internship to finish or add a slot in " +"the admin." + +#: apps/hiring/models.py:69 templates/pages/hr-portal.html:121 +#: templates/pages/hr-portal.html:274 templates/pages/hr-portal.html:333 +#: templates/pages/hr-portal.html:727 +msgid "На испытании" +msgstr "On trial" + +#: apps/hiring/models.py:70 templates/pages/hr-portal.html:467 +#: templates/pages/hr-portal.html:730 +msgid "Утверждено" +msgstr "Approved" + +#: apps/hiring/models.py:71 templates/pages/hr-portal.html:732 +msgid "Отклонено" +msgstr "Rejected" + +#: apps/hiring/models.py:73 +#, python-format +msgid "%(n)d дн." +msgstr "%(n)d days" + +#: apps/hiring/models.py:79 +msgid "Личный email кандидата" +msgstr "Candidate personal email" + +#: apps/hiring/models.py:81 templates/pages/hr-portal.html:645 msgid "Телефон кандидата" msgstr "Candidate phone" -#: apps/hiring/models.py:36 -msgid "Офіс" -msgstr "" - -#: apps/hiring/models.py:39 apps/visit_manager/admin.py:284 -msgid "Тип локації" -msgstr "" - -#: apps/hiring/models.py:45 -msgid "Відділ" -msgstr "" - -#: apps/hiring/models.py:56 -msgid "Посада" -msgstr "" - -#: apps/hiring/models.py:59 +#: apps/hiring/models.py:116 msgid "Дата виходу" -msgstr "" +msgstr "Exit date" -#: apps/hiring/models.py:61 -msgid "Термін випробування, днів" -msgstr "" +#: apps/hiring/models.py:118 +msgid "Срок испытания, дней" +msgstr "Trial period, days" -#: apps/hiring/models.py:68 -msgid "Потрібні доступи" -msgstr "" +#: apps/hiring/models.py:125 apps/hiring/services.py:105 +msgid "Требуемые доступы" +msgstr "Required access" -#: apps/hiring/models.py:70 -msgid "Примітка щодо доступів" -msgstr "" +#: apps/hiring/models.py:127 templates/pages/hr-portal.html:666 +msgid "Примечание по доступам" +msgstr "Access note" -#: apps/hiring/models.py:76 -msgid "Подав HR" -msgstr "" +#: apps/hiring/models.py:133 +msgid "Подал HR" +msgstr "Submitted by HR" -#: apps/hiring/models.py:81 -msgid "Обліковий запис" -msgstr "" +#: apps/hiring/models.py:138 +msgid "Учётная запись" +msgstr "Account" -#: apps/hiring/models.py:86 -msgid "Рішення прийняв" -msgstr "" +#: apps/hiring/models.py:143 +msgid "Решение принял" +msgstr "Decided by" -#: apps/hiring/models.py:88 -msgid "Дата рішення" -msgstr "" +#: apps/hiring/models.py:145 +msgid "Дата решения" +msgstr "Decision date" -#: apps/hiring/models.py:89 -msgid "Коментар до рішення" -msgstr "" +#: apps/hiring/models.py:146 +msgid "Комментарий к решению" +msgstr "Decision note" -#: apps/hiring/models.py:94 +#: apps/hiring/models.py:151 msgid "Заявка в IT" -msgstr "" +msgstr "IT ticket" -#: apps/hiring/models.py:99 -msgid "Задача на видачу доступів" -msgstr "" +#: apps/hiring/models.py:156 +msgid "Задача на выдачу доступов" +msgstr "Access provisioning task" -#: apps/hiring/models.py:103 +#: apps/hiring/models.py:161 +msgid "Выданный пароль (PDF)" +msgstr "Issued password (PDF)" + +#: apps/hiring/models.py:165 msgid "Заявка на найм" -msgstr "" +msgstr "Hiring request" -#: apps/hiring/models.py:104 +#: apps/hiring/models.py:166 msgid "Заявки на найм" +msgstr "Hiring requests" + +#: apps/hiring/pdf.py:41 +msgid "Учётные данные сотрудника" +msgstr "Employee credentials" + +#: apps/hiring/pdf.py:54 templates/pages/employee-detail.html:70 +msgid "Рабочий email" +msgstr "Work email" + +#: apps/hiring/pdf.py:56 +msgid "Запрошенные доступы" +msgstr "Requested access" + +#: apps/hiring/pdf.py:63 apps/hiring/services.py:112 +msgid "(не указано)" +msgstr "(not specified)" + +#: apps/hiring/services.py:97 templates/pages/hr-portal.html:318 +#: templates/pages/hr-portal.html:686 +msgid "Кандидат" +msgstr "Candidate" + +#: apps/hiring/services.py:100 +msgid "Локация / отдел" +msgstr "Location / department" + +#: apps/hiring/services.py:101 templates/pages/hr-portal.html:626 +msgid "Дата выхода" +msgstr "Start date" + +#: apps/hiring/services.py:102 templates/pages/employee-detail.html:49 +msgid "Испытательный срок" +msgstr "Trial period" + +#: apps/hiring/services.py:102 templates/pages/hr-portal.html:482 +#: templates/pages/hr-portal.html:491 templates/pages/hr-portal.html:500 +#: templates/pages/hr-portal.html:633 +msgid "дн." +msgstr "days" + +#: apps/hiring/services.py:103 +msgid "логин" +msgstr "login" + +#: apps/hiring/services.py:115 +msgid "Примечание" +msgstr "Note" + +#: apps/hiring/services.py:118 +msgid "" +"Подготовьте рабочее место и выдайте доступы к интерн-аккаунту на время " +"испытания. После утверждения HR будет создан персональный аккаунт." msgstr "" +"Prepare the workplace and grant access to the intern account for the trial " +"period. After approval, HR will create a personal account." + +#: apps/hiring/services.py:129 +msgid "Подготовка рабочего места" +msgstr "Workplace preparation" #: apps/it_manager/admin.py:34 apps/it_manager/admin.py:464 #: apps/it_manager/admin.py:567 apps/it_manager/admin.py:581 #: apps/locations/models/base.py:65 apps/visit_manager/admin.py:227 -#: templates/pages/hr-portal.html:218 templates/pages/it-manager.html:235 -#: templates/pages/it-manager.html:499 templates/pages/it-manager.html:559 -#: templates/pages/it-manager.html:880 +#: templates/pages/hr-portal.html:249 templates/pages/it-manager.html:235 +#: templates/pages/it-manager.html:502 templates/pages/it-manager.html:562 +#: templates/pages/it-manager.html:883 msgid "Локация" msgstr "Location" #: apps/it_manager/admin.py:91 msgid "Экземпляров" -msgstr "" +msgstr "Copies" #: apps/it_manager/admin.py:110 templates/pages/it-manager.html:13 -#: templates/pages/it-manager.html:514 templates/pages/pharmacy-detail.html:195 +#: templates/pages/it-manager.html:517 +#: templates/pages/pharmacy-detail.html:195 msgid "Оборудование" msgstr "Equipment" #: apps/it_manager/admin.py:113 apps/it_manager/admin.py:206 msgid "Статус и размещение" -msgstr "" +msgstr "Status and placement" #: apps/it_manager/admin.py:116 msgid "Гарантия и закупка" -msgstr "" +msgstr "Warranty and purchase" #: apps/it_manager/admin.py:119 apps/it_manager/admin.py:212 #: apps/it_manager/admin.py:427 apps/it_manager/admin.py:476 #: apps/locations/admin.py:319 apps/locations/models/pharmacies.py:335 -#: core/models/connector_config.py:44 templates/pages/it-manager.html:674 -#: templates/pages/it-manager.html:845 templates/pages/it-manager.html:950 +#: core/models/connector_config.py:44 templates/pages/it-manager.html:677 +#: templates/pages/it-manager.html:848 templates/pages/it-manager.html:953 #: templates/pages/pharmacy-detail.html:98 msgid "Примечания" msgstr "Notes" @@ -770,7 +909,7 @@ msgstr "Notes" #: apps/it_manager/admin.py:480 apps/it_manager/admin.py:617 #: apps/it_manager/admin.py:673 apps/locations/admin.py:109 msgid "Служебное" -msgstr "" +msgstr "Service" #: apps/it_manager/admin.py:159 templates/pages/it-manager.html:163 msgid "Модель" @@ -782,18 +921,18 @@ msgstr "Warranty" #: apps/it_manager/admin.py:203 msgid "Расходник" -msgstr "" +msgstr "Consumable" #: apps/it_manager/admin.py:209 msgid "Эксплуатация" -msgstr "" +msgstr "Operation" -#: apps/it_manager/admin.py:254 templates/pages/it-manager.html:629 -#: templates/pages/it-manager.html:694 +#: apps/it_manager/admin.py:254 templates/pages/it-manager.html:632 +#: templates/pages/it-manager.html:697 msgid "Цвет" msgstr "Color" -#: apps/it_manager/admin.py:295 templates/pages/it-manager.html:523 +#: apps/it_manager/admin.py:295 templates/pages/it-manager.html:526 msgid "Заявка" msgstr "Ticket" @@ -804,11 +943,11 @@ msgstr "Participants" #: apps/it_manager/admin.py:301 apps/it_manager/admin.py:614 #: apps/it_manager/admin.py:670 msgid "Связи" -msgstr "" +msgstr "Relations" #: apps/it_manager/admin.py:304 msgid "Описание и решение" -msgstr "" +msgstr "Description and resolution" #: apps/it_manager/admin.py:343 templates/pages/it-manager.html:346 msgid "Приоритет" @@ -816,7 +955,7 @@ msgstr "Priority" #: apps/it_manager/admin.py:359 msgid "SLA" -msgstr "" +msgstr "SLA" #: apps/it_manager/admin.py:366 templates/pages/task-detail.html:143 msgid "Взять в работу" @@ -824,66 +963,66 @@ msgstr "Take in progress" #: apps/it_manager/admin.py:375 msgid "Отметить решёнными" -msgstr "" +msgstr "Mark as resolved" #: apps/it_manager/admin.py:383 templates/pages/chat.html:88 #: templates/pages/chat.html:112 templates/pages/it-manager.html:75 -#: templates/pages/it-manager.html:135 templates/pages/it-manager.html:480 -#: templates/pages/it-manager.html:614 templates/pages/it-manager.html:815 -#: templates/pages/it-manager.html:865 templates/pages/it-manager.html:1048 +#: templates/pages/it-manager.html:135 templates/pages/it-manager.html:483 +#: templates/pages/it-manager.html:617 templates/pages/it-manager.html:818 +#: templates/pages/it-manager.html:868 templates/pages/it-manager.html:1051 msgid "Закрыть" msgstr "Close" -#: apps/it_manager/admin.py:418 templates/pages/it-manager.html:871 +#: apps/it_manager/admin.py:418 templates/pages/it-manager.html:874 msgid "Провайдер" msgstr "Provider" #: apps/it_manager/admin.py:424 msgid "Техподдержка" -msgstr "" +msgstr "Support" #: apps/it_manager/admin.py:440 msgid "Активных договоров" -msgstr "" +msgstr "Active contracts" #: apps/it_manager/admin.py:460 msgid "Договор" -msgstr "" +msgstr "Contract" #: apps/it_manager/admin.py:467 msgid "Параметры" -msgstr "" +msgstr "Parameters" #: apps/it_manager/admin.py:470 msgid "Сроки" -msgstr "" +msgstr "Deadlines" -#: apps/it_manager/admin.py:473 templates/pages/it-manager.html:944 +#: apps/it_manager/admin.py:473 templates/pages/it-manager.html:947 msgid "IP-адреса" msgstr "IP addresses" #: apps/it_manager/admin.py:503 msgid "Услуга" -msgstr "" +msgstr "Service" #: apps/it_manager/admin.py:508 templates/pages/pharmacy-detail.html:245 msgid "Технология" msgstr "Technology" -#: apps/it_manager/admin.py:520 templates/pages/it-manager.html:972 +#: apps/it_manager/admin.py:520 templates/pages/it-manager.html:975 msgid "Окончание" msgstr "Expiry" #: apps/it_manager/admin.py:534 msgid "Статические IP" -msgstr "" +msgstr "Static IPs" #: apps/it_manager/admin.py:608 templates/pages/employee-detail.html:96 msgid "Работа" msgstr "Work" -#: apps/it_manager/admin.py:639 templates/pages/it-manager.html:490 -#: templates/pages/it-manager.html:557 +#: apps/it_manager/admin.py:639 templates/pages/it-manager.html:493 +#: templates/pages/it-manager.html:560 msgid "Вид работ" msgstr "Work type" @@ -893,41 +1032,41 @@ msgstr "Request" #: apps/it_manager/admin.py:667 msgid "Содержание" -msgstr "" +msgstr "Contents" -#: apps/it_manager/admin.py:690 templates/pages/it-manager.html:1063 -#: templates/pages/it-manager.html:1100 +#: apps/it_manager/admin.py:690 templates/pages/it-manager.html:1066 +#: templates/pages/it-manager.html:1103 msgid "Канал" msgstr "Channel" -#: apps/it_manager/admin.py:695 templates/pages/it-manager.html:1103 +#: apps/it_manager/admin.py:695 templates/pages/it-manager.html:1106 msgid "Длительность" msgstr "Duration" #: apps/locations/admin.py:40 apps/locations/models/base.py:54 msgid "Координаты на карте" -msgstr "" +msgstr "Map coordinates" #: apps/locations/admin.py:45 msgid "Без координат" -msgstr "" +msgstr "Without coordinates" #: apps/locations/admin.py:46 msgid "С координатами" -msgstr "" +msgstr "With coordinates" #: apps/locations/admin.py:64 apps/locations/admin.py:203 #: apps/locations/models/base.py:36 msgid "Город/район" -msgstr "" +msgstr "City/district" #: apps/locations/admin.py:65 apps/locations/admin.py:205 msgid "— Выберите регион —" -msgstr "" +msgstr "— Select region —" #: apps/locations/admin.py:96 msgid "Тип и регион" -msgstr "" +msgstr "Type and region" #: apps/locations/admin.py:99 apps/locations/admin.py:282 #: templates/pages/employees.html:106 templates/pages/pharmacies.html:114 @@ -942,86 +1081,140 @@ msgstr "Coordinates" #: apps/locations/admin.py:105 msgid "Прочее" -msgstr "" +msgstr "Other" #: apps/locations/admin.py:132 msgid "Регион / Адрес" -msgstr "" +msgstr "Region / Address" #: apps/locations/admin.py:148 msgid "Статус лицензии" -msgstr "" +msgstr "License status" #: apps/locations/admin.py:153 msgid "Действительна" -msgstr "" +msgstr "Valid" #: apps/locations/admin.py:154 msgid "Истекает (30 дн.)" -msgstr "" +msgstr "Expiring (30 days)" #: apps/locations/admin.py:155 msgid "Истекла" -msgstr "" +msgstr "Expired" #: apps/locations/admin.py:156 msgid "Нет лицензии" -msgstr "" +msgstr "No license" -#: apps/locations/admin.py:274 core/admin.py:253 +#: apps/locations/admin.py:274 core/admin.py:265 msgid "Основное" -msgstr "" +msgstr "Main" #: apps/locations/admin.py:295 apps/locations/admin.py:366 +#: apps/locations/dislocation_columns.py:47 #: templates/pages/pharmacy-detail.html:54 msgid "Лицензия" msgstr "License" -#: apps/locations/admin.py:302 templates/pages/dislocation.html:119 -#: templates/pages/dislocation.html:557 templates/pages/pharmacy-detail.html:44 +#: apps/locations/admin.py:302 apps/locations/dislocation_columns.py:39 +#: templates/pages/dislocation.html:570 +#: templates/pages/pharmacy-detail.html:44 msgid "Персонал" -msgstr "" +msgstr "Staff" #: apps/locations/admin.py:309 apps/locations/models/pharmacies.py:156 msgid "Почта Carbonio" -msgstr "" +msgstr "Carbonio mail" #: apps/locations/admin.py:316 -msgid "Пусто в «Почта Carbonio» — адрес сгенерируется как <префикс организации><номер>@домен. Можно задать вручную." +msgid "" +"Пусто в «Почта Carbonio» — адрес сгенерируется как <префикс " +"организации><номер>@домен. Можно задать вручную." msgstr "" +"Empty in \"Carbonio mail\" — the address is generated as @domain. Can be set manually." -#: apps/locations/admin.py:376 core/views.py:352 +#: apps/locations/admin.py:376 core/views.py:396 #: templates/pages/structure.html:61 msgid "Сотрудников" -msgstr "" +msgstr "Employees" #: apps/locations/admin.py:390 msgid "Телефоны" -msgstr "" +msgstr "Phones" + +#: apps/locations/dislocation_columns.py:38 +#: templates/pages/pharmacies.html:115 templates/pages/pharmacy-detail.html:49 +#: templates/pages/pharmacy-detail.html:141 +msgid "Заведующий" +msgstr "Manager" + +#: apps/locations/dislocation_columns.py:40 +#: apps/locations/models/pharmacies.py:143 +#: apps/locations/models/pharmacies.py:330 templates/pages/it-manager.html:828 +#: templates/pages/pharmacies.html:117 templates/pages/profile.html:80 +msgid "Телефон" +msgstr "Phone" + +#: apps/locations/dislocation_columns.py:41 +#: templates/pages/pharmacies.html:118 templates/pages/pharmacy-detail.html:59 +msgid "Режим работы" +msgstr "Working hours" + +#: apps/locations/dislocation_columns.py:43 +#: templates/pages/pharmacy-detail.html:88 +msgid "Регион" +msgstr "Region" + +#: apps/locations/dislocation_columns.py:44 +#: templates/pages/pharmacy-detail.html:90 +msgid "Категория" +msgstr "Category" + +#: apps/locations/dislocation_columns.py:45 +#: apps/locations/models/pharmacies.py:119 +#: templates/pages/pharmacy-detail.html:93 +msgid "Куратор" +msgstr "Curator" + +#: apps/locations/dislocation_columns.py:46 +#: apps/locations/models/pharmacies.py:113 +#: templates/pages/pharmacy-detail.html:92 +msgid "Ответственный менеджер" +msgstr "Responsible manager" + +#: apps/locations/dislocation_columns.py:48 +msgid "ПДВ" +msgstr "VAT" + +#: apps/locations/dislocation_columns.py:49 +msgid "Email" +msgstr "Email" #: apps/locations/models/base.py:39 msgid "Улица" -msgstr "" +msgstr "Street" #: apps/locations/models/base.py:40 msgid "Номер дома" -msgstr "" +msgstr "House number" #: apps/locations/models/base.py:41 msgid "Корпус/строение" -msgstr "" +msgstr "Building/block" #: apps/locations/models/base.py:42 msgid "Помещение/офис" -msgstr "" +msgstr "Room/office" #: apps/locations/models/base.py:43 msgid "Полный адрес" -msgstr "" +msgstr "Full address" #: apps/locations/models/base.py:44 msgid "Заполняется автоматически из полей выше" -msgstr "" +msgstr "Filled automatically from the fields above" #: apps/locations/models/base.py:60 templates/pages/news-detail.html:78 #: templates/pages/task-detail.html:98 @@ -1030,11 +1223,54 @@ msgstr "Comments" #: apps/locations/models/base.py:66 msgid "Локации" -msgstr "" +msgstr "Locations" + +#: apps/locations/models/dislocation.py:14 core/admin.py:195 +#: core/models/directories.py:119 osc/core/models/directory.py:9 +#: osc/core/models/directory.py:26 templates/pages/it-manager.html:824 +msgid "Название" +msgstr "Name" + +#: apps/locations/models/dislocation.py:15 core/models/directories.py:118 +#: osc/core/models/directory.py:8 osc/core/models/directory.py:25 +msgid "Код" +msgstr "Code" + +#: apps/locations/models/dislocation.py:17 templates/pages/dislocation.html:98 +msgid "Колонки" +msgstr "Columns" + +#: apps/locations/models/dislocation.py:19 +msgid "Упорядоченный список ключей из реестра колонок дислокации." +msgstr "Ordered list of keys from the dislocation columns registry." + +#: apps/locations/models/dislocation.py:27 +msgid "Если задан — пресет по умолчанию для сотрудников этого отдела." +msgstr "If set — the default preset for employees of this department." + +#: apps/locations/models/dislocation.py:30 +#| msgid "Примечания" +msgid "По умолчанию" +msgstr "Default" + +#: apps/locations/models/dislocation.py:31 +msgid "" +"Используется, если у сотрудника нет своего набора и нет пресета отдела." +msgstr "Used when the employee has no personal set and no department preset." + +#: apps/locations/models/dislocation.py:35 +#| msgid "персонал по каждой локации" +msgid "Пресет колонок дислокации" +msgstr "Dislocation columns preset" + +#: apps/locations/models/dislocation.py:36 +#| msgid "персонал по каждой локации" +msgid "Пресеты колонок дислокации" +msgstr "Dislocation columns presets" #: apps/locations/models/pharmacies.py:60 msgid "Открыта" -msgstr "" +msgstr "Open" #: apps/locations/models/pharmacies.py:61 templates/pages/it-manager.html:377 msgid "Закрыта" @@ -1042,140 +1278,127 @@ msgstr "Closed" #: apps/locations/models/pharmacies.py:62 msgid "Остановлена" -msgstr "" +msgstr "Stopped" #: apps/locations/models/pharmacies.py:63 msgid "Подготовка к запуску" -msgstr "" +msgstr "Preparing to launch" #: apps/locations/models/pharmacies.py:74 msgid "ПДВ 20%" -msgstr "" +msgstr "VAT 20%" #: apps/locations/models/pharmacies.py:75 msgid "ПДВ 7% (пільгова)" -msgstr "" +msgstr "VAT 7% (reduced)" #: apps/locations/models/pharmacies.py:76 msgid "ПДВ 0%" -msgstr "" +msgstr "VAT 0%" #: apps/locations/models/pharmacies.py:77 msgid "Без ПДВ" -msgstr "" +msgstr "Without VAT" #: apps/locations/models/pharmacies.py:82 msgid "Номер аптеки" -msgstr "" +msgstr "Pharmacy number" #: apps/locations/models/pharmacies.py:83 msgid "Числовой номер аптеки внутри организации" -msgstr "" +msgstr "Numeric pharmacy number within the organization" #: apps/locations/models/pharmacies.py:99 msgid "Номер лицензии" -msgstr "" +msgstr "License number" #: apps/locations/models/pharmacies.py:101 msgid "Дата окончания лицензии" -msgstr "" +msgstr "License expiry date" #: apps/locations/models/pharmacies.py:108 msgid "Заведующий аптекой" -msgstr "" - -#: apps/locations/models/pharmacies.py:113 -#: templates/pages/pharmacy-detail.html:92 -msgid "Ответственный менеджер" -msgstr "Responsible manager" +msgstr "Pharmacy manager" #: apps/locations/models/pharmacies.py:114 msgid "Сотрудник с должностью «Відповідальний менеджер»" -msgstr "" - -#: apps/locations/models/pharmacies.py:119 -#: templates/pages/pharmacy-detail.html:93 -msgid "Куратор" -msgstr "Curator" +msgstr "Employee with the position \"Responsible manager\"" #: apps/locations/models/pharmacies.py:120 msgid "Сотрудник с должностью «куратор»" -msgstr "" +msgstr "Employee with the position \"curator\"" #: apps/locations/models/pharmacies.py:123 msgid "Количество фармацевтов" -msgstr "" +msgstr "Number of pharmacists" #: apps/locations/models/pharmacies.py:124 msgid "Кешированное поле — обновляется при синхронизации" -msgstr "" +msgstr "Cached field — updated on sync" #: apps/locations/models/pharmacies.py:133 msgid "Категория аптеки" -msgstr "" +msgstr "Pharmacy category" #: apps/locations/models/pharmacies.py:135 msgid "Классификация аптеки: A/B/C/D" -msgstr "" +msgstr "Pharmacy classification: A/B/C/D" #: apps/locations/models/pharmacies.py:139 msgid "Категорія ПДВ" -msgstr "" - -#: apps/locations/models/pharmacies.py:143 -#: apps/locations/models/pharmacies.py:330 templates/pages/dislocation.html:120 -#: templates/pages/it-manager.html:825 templates/pages/pharmacies.html:117 -#: templates/pages/profile.html:80 -msgid "Телефон" -msgstr "Phone" +msgstr "VAT category" #: apps/locations/models/pharmacies.py:145 msgid "Часы работы" -msgstr "" +msgstr "Working hours" #: apps/locations/models/pharmacies.py:150 msgid "Ожидает синхронизации" -msgstr "" +msgstr "Pending sync" #: apps/locations/models/pharmacies.py:153 msgid "Отключён" -msgstr "" +msgstr "Disabled" #: apps/locations/models/pharmacies.py:157 -msgid "Ящик аптеки. Пусто — сгенерируется как <префикс организации><номер>@домен. Можно задать вручную." +msgid "" +"Ящик аптеки. Пусто — сгенерируется как <префикс организации><номер>@домен. " +"Можно задать вручную." msgstr "" +"Pharmacy mailbox. Empty — generated as @domain." +" Can be set manually." #: apps/locations/models/pharmacies.py:160 msgid "ID аккаунта Carbonio" -msgstr "" +msgstr "Carbonio account ID" #: apps/locations/models/pharmacies.py:164 msgid "Пароль ящика Carbonio" -msgstr "" +msgstr "Carbonio mailbox password" #: apps/locations/models/pharmacies.py:165 msgid "Хранится зашифрованным; показывается для входа заведующему." -msgstr "" +msgstr "Stored encrypted; shown to the manager for signing in." #: apps/locations/models/pharmacies.py:168 msgid "Синхронизация с Carbonio" -msgstr "" +msgstr "Carbonio sync" #: apps/locations/models/pharmacies.py:169 msgid "Управлять почтовым ящиком аптеки в Carbonio" -msgstr "" +msgstr "Manage the pharmacy mailbox in Carbonio" #: apps/locations/models/pharmacies.py:172 msgid "Статус синхронизации Carbonio" -msgstr "" +msgstr "Carbonio sync status" #: apps/locations/models/pharmacies.py:176 msgid "Последняя синхронизация Carbonio" -msgstr "" +msgstr "Last Carbonio sync" #: apps/locations/models/pharmacies.py:177 msgid "Ошибка синхронизации Carbonio" -msgstr "" +msgstr "Carbonio sync error" #: apps/locations/models/pharmacies.py:211 templates/index.html:382 #: templates/pages/dislocation.html:16 templates/pages/pharmacies.html:6 @@ -1185,7 +1408,7 @@ msgstr "Pharmacies" #: apps/locations/models/pharmacies.py:259 msgid "Такой адрес Carbonio уже используется другой аптекой" -msgstr "" +msgstr "This Carbonio address is already used by another pharmacy" #: apps/locations/models/pharmacies.py:325 #: templates/pages/pharmacy-detail.html:113 @@ -1194,95 +1417,95 @@ msgstr "Primary" #: apps/locations/models/pharmacies.py:326 msgid "Мобильный" -msgstr "" +msgstr "Mobile" #: apps/locations/models/pharmacies.py:333 msgid "Основной номер" -msgstr "" +msgstr "Primary number" #: apps/locations/models/pharmacies.py:338 msgid "Телефон аптеки" -msgstr "" +msgstr "Pharmacy phone" #: apps/locations/models/pharmacies.py:339 msgid "Телефоны аптек" -msgstr "" +msgstr "Pharmacy phones" #: apps/locations/models/pharmacies.py:350 msgid "Изменённое поле" -msgstr "" +msgstr "Changed field" #: apps/locations/models/pharmacies.py:351 msgid "Старое значение" -msgstr "" +msgstr "Old value" #: apps/locations/models/pharmacies.py:352 msgid "Новое значение" -msgstr "" +msgstr "New value" #: apps/locations/models/pharmacies.py:353 msgid "Дата изменения" -msgstr "" +msgstr "Change date" #: apps/locations/models/pharmacies.py:356 msgid "Кто изменил" -msgstr "" +msgstr "Changed by" #: apps/locations/models/pharmacies.py:361 msgid "История изменений аптеки" -msgstr "" +msgstr "Pharmacy change history" #: apps/locations/models/pharmacies.py:362 msgid "История изменений аптек" -msgstr "" +msgstr "Pharmacies change history" #: apps/mail_connector/models.py:30 msgid "Запись синхронизации Carbonio" -msgstr "" +msgstr "Carbonio sync record" #: apps/mail_connector/models.py:31 msgid "Журнал синхронизации Carbonio" -msgstr "" +msgstr "Carbonio sync log" #: apps/nextcloud_connector/models.py:20 msgid "Создание папки" -msgstr "" +msgstr "Folder creation" #: apps/nextcloud_connector/models.py:21 msgid "Обновление ACL" -msgstr "" +msgstr "ACL update" #: apps/nextcloud_connector/models.py:22 msgid "Пропуск" -msgstr "" +msgstr "Skip" #: apps/nextcloud_connector/models.py:27 msgid "Пробный прогон" -msgstr "" +msgstr "Dry run" #: apps/nextcloud_connector/models.py:30 msgid "Тип сущности" -msgstr "" +msgstr "Entity type" #: apps/nextcloud_connector/models.py:31 msgid "Ссылка на сущность" -msgstr "" +msgstr "Entity reference" #: apps/nextcloud_connector/models.py:32 msgid "Код отдела или номер аптеки" -msgstr "" +msgstr "Department code or pharmacy number" #: apps/nextcloud_connector/models.py:33 msgid "Путь Group Folder" -msgstr "" +msgstr "Group Folder path" #: apps/nextcloud_connector/models.py:39 msgid "Запись синхронизации Nextcloud" -msgstr "" +msgstr "Nextcloud sync record" #: apps/nextcloud_connector/models.py:40 msgid "Журнал синхронизации Nextcloud" -msgstr "" +msgstr "Nextcloud sync log" #: apps/tasks/models.py:25 templates/pages/it-manager.html:322 #: templates/pages/it-manager.html:374 templates/pages/task-detail.html:21 @@ -1315,79 +1538,79 @@ msgstr "High" #: apps/tasks/models.py:38 msgid "Назва" -msgstr "" +msgstr "Name" #: apps/tasks/models.py:39 apps/visit_manager/admin.py:88 msgid "Опис" -msgstr "" +msgstr "Description" #: apps/tasks/models.py:42 msgid "Пріоритет" -msgstr "" +msgstr "Priority" #: apps/tasks/models.py:44 msgid "Термін" -msgstr "" +msgstr "Deadline" #: apps/tasks/models.py:48 msgid "Постановник" -msgstr "" +msgstr "Reporter" #: apps/tasks/models.py:55 msgid "Відповідальний" -msgstr "" +msgstr "Assignee" #: apps/tasks/models.py:59 msgid "Співвиконавці" -msgstr "" +msgstr "Co-executors" #: apps/tasks/models.py:63 msgid "Спостерігачі" -msgstr "" +msgstr "Watchers" #: apps/tasks/models.py:66 apps/tasks/models.py:148 msgid "Виконано в" -msgstr "" +msgstr "Completed at" #: apps/tasks/models.py:71 apps/tasks/models.py:122 apps/tasks/models.py:144 msgid "Задача" -msgstr "" +msgstr "Task" #: apps/tasks/models.py:72 msgid "Задачі" -msgstr "" +msgstr "Tasks" #: apps/tasks/models.py:131 msgid "Коментар до задачі" -msgstr "" +msgstr "Task comment" #: apps/tasks/models.py:132 msgid "Коментарі до задач" -msgstr "" +msgstr "Task comments" #: apps/tasks/models.py:146 msgid "Пункт" -msgstr "" +msgstr "Item" #: apps/tasks/models.py:147 msgid "Виконано" -msgstr "" +msgstr "Done" #: apps/tasks/models.py:151 msgid "Відмітив" -msgstr "" +msgstr "Marked by" #: apps/tasks/models.py:153 osc/core/models/directory.py:33 msgid "Порядок" -msgstr "" +msgstr "Order" #: apps/tasks/models.py:156 msgid "Пункт чек-листа" -msgstr "" +msgstr "Checklist item" #: apps/tasks/models.py:157 msgid "Чек-лист задачі" -msgstr "" +msgstr "Task checklist" #: apps/tasks/views.py:211 templates/pages/task-edit.html:2 #: templates/pages/tasks.html:35 @@ -1406,140 +1629,144 @@ msgstr "Observers" #: apps/utilities/models.py:22 msgid "Тип лічильника" -msgstr "" +msgstr "Meter type" #: apps/utilities/models.py:24 msgid "Дата зняття показань" -msgstr "" +msgstr "Reading date" #: apps/utilities/models.py:25 msgid "Показання" -msgstr "" +msgstr "Reading" #: apps/utilities/models.py:31 msgid "Хто подав" -msgstr "" +msgstr "Submitted by" #: apps/utilities/models.py:35 msgid "Показання лічильника" -msgstr "" +msgstr "Meter reading" #: apps/utilities/models.py:36 msgid "Показання лічильників" -msgstr "" +msgstr "Meter readings" #: apps/utilities/models.py:54 msgid "Показання не може бути від'ємним" -msgstr "" +msgstr "The reading cannot be negative" #: apps/utilities/models.py:56 msgid "Дата не може бути в майбутньому" -msgstr "" +msgstr "The date cannot be in the future" #: apps/visit_manager/admin.py:30 apps/visit_manager/admin.py:78 #: apps/visit_manager/admin.py:137 apps/visit_manager/admin.py:259 #: apps/visit_manager/admin.py:311 msgid "Основне" -msgstr "" +msgstr "Main" #: apps/visit_manager/admin.py:33 msgid "Час роботи" -msgstr "" +msgstr "Working time" #: apps/visit_manager/admin.py:37 msgid "Обідня перерва" -msgstr "" +msgstr "Lunch break" #: apps/visit_manager/admin.py:40 msgid "Понаднормові" -msgstr "" +msgstr "Overtime" #: apps/visit_manager/admin.py:53 apps/visit_manager/admin.py:140 #: apps/visit_manager/admin.py:314 msgid "Час" -msgstr "" +msgstr "Time" #: apps/visit_manager/admin.py:61 msgid "Колір" -msgstr "" +msgstr "Color" #: apps/visit_manager/admin.py:81 msgid "Характеристики" -msgstr "" +msgstr "Characteristics" #: apps/visit_manager/admin.py:85 msgid "Відображення" -msgstr "" +msgstr "Display" #: apps/visit_manager/admin.py:92 msgid "Регіони" -msgstr "" +msgstr "Regions" #: apps/visit_manager/admin.py:107 msgid "Залишилось" -msgstr "" +msgstr "Remaining" #: apps/visit_manager/admin.py:144 msgid "Тиждень" -msgstr "" +msgstr "Week" #: apps/visit_manager/admin.py:147 msgid "Цикл (для змінного графіку)" -msgstr "" +msgstr "Cycle (for rotating schedule)" #: apps/visit_manager/admin.py:151 msgid "Налаштування" -msgstr "" +msgstr "Settings" #: apps/visit_manager/admin.py:154 msgid "Придатність" -msgstr "" +msgstr "Validity" #: apps/visit_manager/admin.py:178 msgid "Співробітник та шаблон" -msgstr "" +msgstr "Employee and template" #: apps/visit_manager/admin.py:181 msgid "Період" -msgstr "" +msgstr "Period" #: apps/visit_manager/admin.py:184 msgid "Налаштування відміток" -msgstr "" +msgstr "Check-in settings" #: apps/visit_manager/admin.py:188 msgid "Примітки" -msgstr "" +msgstr "Notes" #: apps/visit_manager/admin.py:265 msgid "Технічне" -msgstr "" +msgstr "Technical" #: apps/visit_manager/admin.py:277 msgid "Направлення" -msgstr "" +msgstr "Direction" + +#: apps/visit_manager/admin.py:284 +msgid "Тип локації" +msgstr "Location type" #: apps/visit_manager/admin.py:296 msgid "Превью" -msgstr "" +msgstr "Preview" #: apps/visit_manager/admin.py:317 msgid "Записи" -msgstr "" +msgstr "Records" #: apps/visit_manager/admin.py:329 msgid "Відпрацьовано" -msgstr "" +msgstr "Worked" #: apps/visit_manager/admin.py:334 msgid "Повна зміна" -msgstr "" +msgstr "Full shift" #: core/admin.py:20 core/models/directories.py:54 #: core/models/directories.py:116 osc/core/models/directory.py:12 #: osc/core/models/directory.py:23 msgid "Тип справочника" -msgstr "" +msgstr "Directory type" #: core/admin.py:37 templates/pages/dislocation.html:90 #: templates/pages/pharmacies.html:86 @@ -1553,19 +1780,19 @@ msgstr "Inactive" #: core/admin.py:49 msgid "Наличие потомков" -msgstr "" +msgstr "Has descendants" #: core/admin.py:54 msgid "Есть потомки" -msgstr "" +msgstr "Has descendants" #: core/admin.py:55 msgid "Нет потомков" -msgstr "" +msgstr "No descendants" #: core/admin.py:86 msgid "Записей" -msgstr "" +msgstr "Records" #: core/admin.py:144 templates/pages/pharmacy-detail.html:67 #: templates/pages/pharmacy-detail.html:85 @@ -1574,313 +1801,326 @@ msgstr "General information" #: core/admin.py:147 msgid "Должности и локации" -msgstr "" +msgstr "Positions and locations" #: core/admin.py:153 core/models/directories.py:123 msgid "Дополнительные данные" -msgstr "" +msgstr "Additional data" #: core/admin.py:167 msgid "🔄 Перестроить дерево" -msgstr "" +msgstr "🔄 Rebuild tree" #: core/admin.py:172 msgid "Дерево справочников перестроено" -msgstr "" +msgstr "The directory tree has been rebuilt" -#: core/admin.py:195 core/models/directories.py:119 -#: osc/core/models/directory.py:9 osc/core/models/directory.py:26 -#: templates/pages/it-manager.html:821 -msgid "Название" -msgstr "Name" - -#: core/admin.py:212 core/models/directories.py:131 +#: core/admin.py:212 core/admin.py:256 core/models/directories.py:131 msgid "Типы локаций" -msgstr "" +msgstr "Location types" -#: core/admin.py:256 +#: core/admin.py:268 msgid "Подключение" -msgstr "" +msgstr "Connection" -#: core/admin.py:261 +#: core/admin.py:273 msgid "Укажите токен или логин/пароль — в зависимости от типа коннектора" -msgstr "" +msgstr "Specify a token or login/password — depending on the connector type" -#: core/admin.py:263 core/models/connector_config.py:40 +#: core/admin.py:275 core/models/connector_config.py:40 msgid "Дополнительные параметры" -msgstr "" +msgstr "Additional parameters" -#: core/admin.py:274 +#: core/admin.py:286 msgid "Коннектор" -msgstr "" +msgstr "Connector" #: core/models/connector_config.py:18 msgid "Тип коннектора" -msgstr "" +msgstr "Connector type" #: core/models/connector_config.py:21 msgid "Включён" -msgstr "" +msgstr "Enabled" #: core/models/connector_config.py:24 msgid "Хост" -msgstr "" +msgstr "Host" #: core/models/connector_config.py:25 msgid "Порт" -msgstr "" +msgstr "Port" #: core/models/connector_config.py:26 msgid "Использовать SSL" -msgstr "" - -#: core/models/connector_config.py:32 -msgid "Пароль" -msgstr "" +msgstr "Use SSL" #: core/models/connector_config.py:34 msgid "Токен / API Key" -msgstr "" +msgstr "Token / API Key" #: core/models/connector_config.py:35 msgid "Bearer-токен или API-ключ, если используется вместо логина/пароля" -msgstr "" +msgstr "Bearer token or API key, if used instead of login/password" #: core/models/connector_config.py:41 msgid "Специфичные для коннектора настройки в формате JSON" -msgstr "" +msgstr "Connector-specific settings in JSON format" #: core/models/connector_config.py:47 msgid "Настройки коннектора" -msgstr "" +msgstr "Connector settings" #: core/models/connector_config.py:48 osc/core/models/connector_settings.py:33 #: osc/core/models/connector_settings.py:34 #: osc/core/models/connector_settings.py:37 msgid "Настройки коннекторов" -msgstr "" +msgstr "Connectors settings" #: core/models/connector_config.py:58 msgid "Хост обязателен для включённого коннектора" -msgstr "" +msgstr "Host is required for an enabled connector" #: core/models/connector_config.py:61 msgid "Для включённого коннектора необходимо указать токен или логин" -msgstr "" +msgstr "An enabled connector requires a token or login" #: core/models/directories.py:26 msgid "Код типа" -msgstr "" +msgstr "Type code" #: core/models/directories.py:27 msgid "Название типа" -msgstr "" +msgstr "Type name" #: core/models/directories.py:33 msgid "Строгая иерархия (одинаковые типы)" -msgstr "" +msgstr "Strict hierarchy (same types)" #: core/models/directories.py:34 msgid "Гибкая иерархия (разные типы)" -msgstr "" +msgstr "Flexible hierarchy (different types)" #: core/models/directories.py:35 msgid "Кастомная иерархия" -msgstr "" +msgstr "Custom hierarchy" #: core/models/directories.py:42 msgid "Тип иерархии" -msgstr "" +msgstr "Hierarchy type" #: core/models/directories.py:49 msgid "Разрешенные типы потомков" -msgstr "" +msgstr "Allowed descendant types" #: core/models/directories.py:50 msgid "Только для гибкой и кастомной иерархии" -msgstr "" +msgstr "For flexible and custom hierarchy only" #: core/models/directories.py:55 osc/core/models/directory.py:13 msgid "Типы справочников" -msgstr "" +msgstr "Directory types" -#: core/models/directories.py:118 osc/core/models/directory.py:8 -#: osc/core/models/directory.py:25 -msgid "Код" +#: core/models/directories.py:133 +msgid "" +"Пусто = доступно для всех типов локаций. Для должностей и систем доступа: в " +"каких локациях показывать (офис / аптека / склад)." msgstr "" +"Empty = available for all location types. For positions and access systems: " +"in which locations to show (office / pharmacy / warehouse)." -#: core/models/directories.py:132 -msgid "Для должностей: в каких типах локаций доступна эта должность" -msgstr "" - -#: core/models/directories.py:154 +#: core/models/directories.py:158 msgid "Элемент справочника" -msgstr "" +msgstr "Directory item" -#: core/models/directories.py:155 +#: core/models/directories.py:159 msgid "Элементы справочников" -msgstr "" +msgstr "Directory items" #: core/utils/validators.py:13 msgid "Номер телефона должен быть в формате +380XXXXXXXXX" -msgstr "" +msgstr "The phone number must be in the format +380XXXXXXXXX" #: core/utils/validators.py:22 msgid "Номер телефона должен быть в международном формате +XXXXXXXXXXX" -msgstr "" +msgstr "The phone number must be in international format +XXXXXXXXXXX" #: core/utils/validators.py:27 msgid "Номер телефона должен содержать от 8 до 16 цифр включая код страны" msgstr "" +"The phone number must contain 8 to 16 digits including the country code" #: core/utils/validators.py:46 msgid "Дата рождения не может быть в будущем" -msgstr "" +msgstr "The date of birth cannot be in the future" #: core/utils/validators.py:54 msgid "Возраст должен быть не менее 18 лет" -msgstr "" +msgstr "Age must be at least 18 years" #: core/utils/validators.py:83 msgid "Использование временных email запрещено" -msgstr "" +msgstr "Using temporary emails is not allowed" #: core/utils/validators.py:94 #, python-format msgid "Недопустимый IP адрес: %(value)s" -msgstr "" +msgstr "Invalid IP address: %(value)s" #: core/utils/validators.py:103 -msgid "MAC адрес должен быть в формате XX:XX:XX:XX:XX:XX или XX-XX-XX-XX-XX-XX" +msgid "" +"MAC адрес должен быть в формате XX:XX:XX:XX:XX:XX или XX-XX-XX-XX-XX-XX" msgstr "" +"The MAC address must be in the format XX:XX:XX:XX:XX:XX or XX-XX-XX-XX-XX-XX" #: core/utils/validators.py:114 msgid "Украинский налоговый номер должен содержать 10 цифр" -msgstr "" +msgstr "The Ukrainian tax number must contain 10 digits" #: core/utils/validators.py:132 msgid "Неверная контрольная сумма украинского налогового номера" -msgstr "" +msgstr "Invalid checksum of the Ukrainian tax number" #: core/utils/validators.py:142 msgid "ИНН должен содержать 10 или 12 цифр" -msgstr "" +msgstr "The tax ID must contain 10 or 12 digits" #: core/utils/validators.py:159 core/utils/validators.py:176 msgid "Неверная контрольная сумма ИНН" -msgstr "" +msgstr "Invalid tax ID checksum" -#: core/views.py:351 +#: core/views.py:395 msgid "Рабочих дней" msgstr "Working days" -#: core/views.py:351 +#: core/views.py:395 msgid "с отметками" msgstr "with check-ins" -#: core/views.py:352 +#: core/views.py:396 msgid "отмечались" msgstr "checked in" -#: core/views.py:353 +#: core/views.py:397 msgid "Смен" msgstr "Shifts" -#: core/views.py:353 +#: core/views.py:397 msgid "за месяц" msgstr "this month" -#: core/views.py:354 templates/pages/timetrack.html:62 +#: core/views.py:398 templates/pages/timetrack.html:62 msgid "Незакрытых" msgstr "Open" -#: core/views.py:354 +#: core/views.py:398 msgid "без ухода" msgstr "without checkout" -#: core/views.py:355 +#: core/views.py:399 msgid "Всего часов" msgstr "Total hours" -#: core/views.py:355 +#: core/views.py:399 msgid "отработано" msgstr "worked" #: osc/apps/employees/models.py:34 msgid "Логин для входа, совпадает с учётной записью AD" -msgstr "" +msgstr "Login for sign-in, matches the AD account" #: osc/apps/employees/models.py:105 msgid "Логин AD (sAMAccountName)" -msgstr "" +msgstr "AD login (sAMAccountName)" #: osc/apps/employees/models.py:107 msgid "AD User DN" -msgstr "" +msgstr "AD User DN" #: osc/apps/employees/models.py:151 msgid "Не может быть раньше даты приёма" -msgstr "" +msgstr "Cannot be earlier than the hire date" #: osc/apps/employees/models.py:153 msgid "Сотрудник не может быть своим руководителем" -msgstr "" +msgstr "An employee cannot be their own manager" #: osc/apps/mattermost_connector/models.py:23 msgid "Запись синхронизации Mattermost" -msgstr "" +msgstr "Mattermost sync record" #: osc/apps/mattermost_connector/models.py:24 msgid "Журнал синхронизации Mattermost" -msgstr "" +msgstr "Mattermost sync log" #: osc/core/models/connector_settings.py:10 msgid "Требовать смену пароля при создании" -msgstr "" +msgstr "Require password change on creation" #: osc/core/models/connector_settings.py:12 -msgid "Новым учётным записям в AD выставляется флаг «сменить пароль при следующем входе». Если выключено — выданный пароль действует сразу, без принудительной смены." +msgid "" +"Новым учётным записям в AD выставляется флаг «сменить пароль при следующем " +"входе». Если выключено — выданный пароль действует сразу, без принудительной" +" смены." msgstr "" +"New AD accounts get the \"change password at next logon\" flag. If disabled " +"— the issued password works immediately, without a forced change." #: osc/core/models/connector_settings.py:17 msgid "Пароль не имеет срока действия" -msgstr "" +msgstr "Password never expires" #: osc/core/models/connector_settings.py:19 -msgid "Новым и существующим учётным записям в AD выставляется флаг «Password never expires». Если выключено — действует обычная парольная политика домена." +msgid "" +"Новым и существующим учётным записям в AD выставляется флаг «Password never " +"expires». Если выключено — действует обычная парольная политика домена." msgstr "" +"New and existing AD accounts get the \"Password never expires\" flag. If " +"disabled — the normal domain password policy applies." #: osc/core/models/connector_settings.py:24 msgid "Команда Mattermost по умолчанию" -msgstr "" +msgstr "Default Mattermost team" #: osc/core/models/connector_settings.py:25 -msgid "Сотрудники автоматически добавляются в эту команду при синхронизации с Mattermost." +msgid "" +"Сотрудники автоматически добавляются в эту команду при синхронизации с " +"Mattermost." msgstr "" +"Employees are automatically added to this team during Mattermost sync." #: osc/core/models/connector_settings.py:28 msgid "Каналы по умолчанию" -msgstr "" +msgstr "Default channels" #: osc/core/models/connector_settings.py:29 -msgid "Сотрудники автоматически добавляются в эти каналы при синхронизации с Mattermost." +msgid "" +"Сотрудники автоматически добавляются в эти каналы при синхронизации с " +"Mattermost." msgstr "" +"Employees are automatically added to these channels during Mattermost sync." #: osc/core/models/directory.py:29 msgid "Родительский элемент" -msgstr "" +msgstr "Parent item" #: osc/core/models/directory.py:30 msgid "Должен принадлежать тому же типу справочника" -msgstr "" +msgstr "Must belong to the same directory type" #: osc/core/models/directory.py:35 msgid "OU в Active Directory" -msgstr "" +msgstr "OU in Active Directory" #: osc/core/models/directory.py:37 -msgid "Используется только для справочника отделов (department): сотрудники этого отдела создаются/переносятся в указанную OU. Например: OU=IT,OU=OL_Users,DC=ol,DC=local. Если не задано — используется AD_USERS_OU." +msgid "" +"Используется только для справочника отделов (department): сотрудники этого " +"отдела создаются/переносятся в указанную OU. Например: " +"OU=IT,OU=OL_Users,DC=ol,DC=local. Если не задано — используется AD_USERS_OU." msgstr "" +"Used only for the departments directory (department): employees of this " +"department are created/moved to the specified OU. For example: " +"OU=IT,OU=OL_Users,DC=ol,DC=local. If not set — AD_USERS_OU is used." #: osc/core/models/directory.py:44 templates/pages/dashboard.html:163 msgid "Справочник" @@ -1888,65 +2128,69 @@ msgstr "Directory" #: osc/core/models/directory.py:45 msgid "Справочники" -msgstr "" +msgstr "Directories" #: osc/core/models/directory.py:56 msgid "Элемент не может быть родителем самому себе" -msgstr "" +msgstr "An item cannot be its own parent" #: osc/core/models/directory.py:58 msgid "Родитель должен принадлежать тому же типу справочника" -msgstr "" +msgstr "The parent must belong to the same directory type" #: osc/core/models/directory.py:62 msgid "Циклическая вложенность недопустима" -msgstr "" +msgstr "Cyclic nesting is not allowed" #: osc/core/models/site_settings.py:9 msgid "Название портала" -msgstr "" +msgstr "Portal name" #: osc/core/models/site_settings.py:10 msgid "Логотип" -msgstr "" +msgstr "Logo" #: osc/core/models/site_settings.py:13 osc/core/models/site_settings.py:14 #: osc/core/models/site_settings.py:17 msgid "Настройки сайта" -msgstr "" +msgstr "Site settings" #: osc/core/utils/validators.py:16 msgid "Введите корректный украинский номер телефона, например +380501234567" -msgstr "" +msgstr "Enter a valid Ukrainian phone number, e.g. +380501234567" #: osc/core/utils/validators.py:24 msgid "Дата рождения должна быть в прошлом" -msgstr "" +msgstr "The date of birth must be in the past" #: osc/core/utils/validators.py:27 msgid "Сотруднику должно быть не менее 16 лет" -msgstr "" +msgstr "The employee must be at least 16 years old" #: osc/core/utils/validators.py:29 msgid "Проверьте дату рождения — возраст превышает 100 лет" -msgstr "" +msgstr "Check the date of birth — the age exceeds 100 years" #: osc/core/utils/validators.py:35 msgid "ИНН должен состоять из 10 цифр" -msgstr "" +msgstr "The tax ID must consist of 10 digits" #: osc/core/utils/validators.py:38 msgid "Некорректный ИНН: неверная контрольная сумма" -msgstr "" +msgstr "Invalid tax ID: incorrect checksum" #: osc/templates/admin/employees/employee/delete_confirmation.html:10 #, python-format -msgid "Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"? All of the following related items will be deleted:" +msgid "" +"Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"? " +"All of the following related items will be deleted:" msgstr "" +"Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"? " +"All of the following related items will be deleted:" #: osc/templates/admin/employees/employee/delete_confirmation.html:17 msgid "Objects" -msgstr "" +msgstr "Objects" #: templates/index.html:211 msgid "Сеть аптек" @@ -2032,7 +2276,7 @@ msgstr "Utilities" msgid "Рекрутинг" msgstr "Recruiting" -#: templates/index.html:411 templates/pages/hr-portal.html:292 +#: templates/index.html:411 templates/pages/hr-portal.html:428 #: templates/pages/timetrack.html:7 msgid "Учёт рабочего времени" msgstr "Time tracking" @@ -2041,7 +2285,7 @@ msgstr "Time tracking" msgid "Поддержка" msgstr "Support" -#: templates/index.html:429 templates/pages/hr-portal.html:284 +#: templates/index.html:429 templates/pages/hr-portal.html:420 msgid "Справка" msgstr "Reference" @@ -2065,7 +2309,8 @@ msgstr "Search..." msgid "Язык" msgstr "Language" -#: templates/index.html:608 templates/pages/hr-portal.html:199 +#: templates/index.html:608 templates/pages/hr-portal.html:230 +#: templates/pages/hr-portal.html:392 msgid "Профиль" msgstr "Profile" @@ -2083,11 +2328,11 @@ msgstr "Loading..." #: templates/index.html:675 msgid "Люди" -msgstr "" +msgstr "People" #: templates/index.html:691 msgid "Меню" -msgstr "" +msgstr "Menu" #: templates/pages/chat.html:6 msgid "Внутренний корпоративный мессенджер (Mattermost)" @@ -2155,7 +2400,9 @@ msgid "Ваша учётная запись ещё не синхронизиро msgstr "Your account is not synced with Mattermost yet" #: templates/pages/chat.html:132 -msgid "Нажмите кнопку ниже, чтобы создать или привязать вашу учётную запись в Mattermost." +msgid "" +"Нажмите кнопку ниже, чтобы создать или привязать вашу учётную запись в " +"Mattermost." msgstr "Click the button below to create or link your Mattermost account." #: templates/pages/chat.html:135 @@ -2167,16 +2414,26 @@ msgid "Не удалось получить доступ к Mattermost" msgstr "Failed to access Mattermost" #: templates/pages/chat.html:146 -msgid "Обратитесь в IT-отдел — возможно, отключена выдача Personal Access Token на сервере Mattermost." -msgstr "Contact the IT department — Personal Access Token issuance may be disabled on the Mattermost server." +msgid "" +"Обратитесь в IT-отдел — возможно, отключена выдача Personal Access Token на " +"сервере Mattermost." +msgstr "" +"Contact the IT department — Personal Access Token issuance may be disabled " +"on the Mattermost server." #: templates/pages/chat.html:155 msgid "Чат ещё не подключён" msgstr "Chat is not connected yet" #: templates/pages/chat.html:157 -msgid "Укажите публичный URL Mattermost (поле public_url в «Дополнительные параметры» коннектора Mattermost в админке) — и он появится здесь." -msgstr "Specify the public Mattermost URL (the public_url field under «Additional parameters» of the Mattermost connector in the admin) and it will appear here." +msgid "" +"Укажите публичный URL Mattermost (поле public_url в " +"«Дополнительные параметры» коннектора Mattermost в админке) — и он появится " +"здесь." +msgstr "" +"Specify the public Mattermost URL (the public_url field under " +"«Additional parameters» of the Mattermost connector in the admin) and it " +"will appear here." #: templates/pages/dashboard.html:7 msgid "Добрый день" @@ -2206,7 +2463,7 @@ msgstr "Messages" msgid "Сотрудников онлайн" msgstr "Employees online" -#: templates/pages/dashboard.html:22 templates/pages/dislocation.html:720 +#: templates/pages/dashboard.html:22 templates/pages/dislocation.html:791 msgid "из" msgstr "of" @@ -2337,8 +2594,8 @@ msgid "Все предприятия" msgstr "All organizations" #: templates/pages/dislocation.html:76 templates/pages/dislocation.html:81 -#: templates/pages/dislocation.html:82 templates/pages/dislocation.html:437 -#: templates/pages/dislocation.html:454 templates/pages/pharmacies.html:72 +#: templates/pages/dislocation.html:82 templates/pages/dislocation.html:450 +#: templates/pages/dislocation.html:467 templates/pages/pharmacies.html:72 msgid "Все города/регионы" msgstr "All cities/regions" @@ -2346,126 +2603,136 @@ msgstr "All cities/regions" msgid "Все статусы" msgstr "All statuses" -#: templates/pages/dislocation.html:98 templates/pages/hr-portal.html:110 +#: templates/pages/dislocation.html:103 templates/pages/hr-portal.html:139 msgid "Таблица" msgstr "Table" -#: templates/pages/dislocation.html:103 +#: templates/pages/dislocation.html:108 msgid "Карта" msgstr "Map" -#: templates/pages/dislocation.html:118 templates/pages/pharmacies.html:115 -#: templates/pages/pharmacy-detail.html:49 -#: templates/pages/pharmacy-detail.html:141 -msgid "Заведующий" -msgstr "Manager" +#: templates/pages/dislocation.html:115 +msgid "Колонки таблицы" +msgstr "Table columns" -#: templates/pages/dislocation.html:121 templates/pages/pharmacies.html:118 -#: templates/pages/pharmacy-detail.html:59 -msgid "Режим работы" -msgstr "Working hours" +#: templates/pages/dislocation.html:117 +msgid "Пресет" +msgstr "Preset" -#: templates/pages/dislocation.html:159 -msgid "не назначен" -msgstr "not assigned" +#: templates/pages/dislocation.html:119 +msgid "— свой набор —" +msgstr "— custom set —" -#: templates/pages/dislocation.html:166 -msgid "Нехватка" -msgstr "Shortage" +#: templates/pages/dislocation.html:137 +msgid "обяз." +msgstr "req." -#: templates/pages/dislocation.html:168 -msgid "Внимание" -msgstr "Warning" +#: templates/pages/dislocation.html:138 +msgid "Выше" +msgstr "Up" -#: templates/pages/dislocation.html:170 -msgid "Норма" -msgstr "Normal" +#: templates/pages/dislocation.html:139 +msgid "Ниже" +msgstr "Down" -#: templates/pages/dislocation.html:182 templates/pages/dislocation.html:559 -#: templates/pages/pharmacies.html:158 templates/pages/pharmacy-detail.html:36 -msgid "Неактивна" -msgstr "Inactive" +#: templates/pages/dislocation.html:144 templates/pages/it-manager.html:290 +#: templates/pages/it-manager.html:423 templates/pages/it-manager.html:735 +#: templates/pages/it-manager.html:1013 templates/pages/settings.html:43 +#: templates/pages/settings.html:60 +msgid "Сохранить" +msgstr "Save" -#: templates/pages/dislocation.html:208 +#: templates/pages/dislocation.html:145 +#| msgid "Отменено" +msgid "Отмена" +msgstr "Cancel" + +#: templates/pages/dislocation.html:221 #: templates/pages/pharmacy-detail.html:149 msgid "В этой аптеке пока нет активных сотрудников" msgstr "No active employees in this pharmacy yet" -#: templates/pages/dislocation.html:214 templates/pages/pharmacies.html:164 +#: templates/pages/dislocation.html:227 templates/pages/pharmacies.html:164 msgid "Аптеки не найдены" msgstr "No pharmacies found" -#: templates/pages/dislocation.html:221 templates/pages/pharmacies.html:171 +#: templates/pages/dislocation.html:234 templates/pages/pharmacies.html:171 msgid "Показано 0–0 из 0" msgstr "Showing 0–0 of 0" -#: templates/pages/dislocation.html:224 templates/pages/pharmacies.html:174 +#: templates/pages/dislocation.html:237 templates/pages/pharmacies.html:174 msgid "На странице:" msgstr "Per page:" -#: templates/pages/dislocation.html:233 +#: templates/pages/dislocation.html:246 msgid "Предыдущая" msgstr "Previous" -#: templates/pages/dislocation.html:234 templates/pages/pharmacies.html:184 +#: templates/pages/dislocation.html:247 templates/pages/pharmacies.html:184 msgid "Стр. 1 из 1" msgstr "Page 1 of 1" -#: templates/pages/dislocation.html:235 templates/pages/pharmacies.html:185 +#: templates/pages/dislocation.html:248 templates/pages/pharmacies.html:185 msgid "Следующая" msgstr "Next" -#: templates/pages/dislocation.html:246 +#: templates/pages/dislocation.html:259 msgid "апт. без указанного адреса не показаны на карте (нет координат)" msgstr "pharm. without address not shown on map (no coordinates)" -#: templates/pages/dislocation.html:255 +#: templates/pages/dislocation.html:268 msgid "Список аптек" msgstr "Pharmacy list" -#: templates/pages/dislocation.html:288 +#: templates/pages/dislocation.html:301 msgid "сотрудников офиса и склада, сгруппировано по отделам" msgstr "office and warehouse employees, grouped by department" -#: templates/pages/dislocation.html:296 +#: templates/pages/dislocation.html:309 msgid "Внутр. номер" msgstr "Ext. number" -#: templates/pages/dislocation.html:298 +#: templates/pages/dislocation.html:311 msgid "Ф.И.О." msgstr "Full name" -#: templates/pages/dislocation.html:299 +#: templates/pages/dislocation.html:312 msgid "Корпоративный телефон" msgstr "Corporate phone" -#: templates/pages/dislocation.html:320 templates/pages/structure.html:35 +#: templates/pages/dislocation.html:333 templates/pages/structure.html:35 msgid "Без отдела" msgstr "No department" -#: templates/pages/dislocation.html:335 +#: templates/pages/dislocation.html:348 msgid "Сотрудников офиса/склада не найдено" msgstr "No office/warehouse employees found" -#: templates/pages/dislocation.html:561 +#: templates/pages/dislocation.html:572 templates/pages/pharmacies.html:158 +#: templates/pages/pharmacy-detail.html:36 +#: templates/partials/dislo_cell.html:42 +msgid "Неактивна" +msgstr "Inactive" + +#: templates/pages/dislocation.html:574 msgid "Подробнее" msgstr "Details" -#: templates/pages/dislocation.html:720 +#: templates/pages/dislocation.html:791 msgid "Показано" msgstr "Showing" -#: templates/pages/dislocation.html:720 +#: templates/pages/dislocation.html:791 msgid "Нет записей" msgstr "No records" #: templates/pages/documents.html:6 msgid "Файловое хранилище: общие документы, обучение, отделы" -msgstr "" +msgstr "File storage: shared documents, training, departments" #: templates/pages/documents.html:29 msgid "Нет доступных разделов документов." -msgstr "" +msgstr "No available document sections." #: templates/pages/employee-detail.html:28 templates/pages/news-detail.html:17 #: templates/pages/task-detail.html:41 templates/pages/tasks.html:127 @@ -2476,14 +2743,6 @@ msgstr "Edit" msgid "Не активный" msgstr "Inactive" -#: templates/pages/employee-detail.html:49 -msgid "Испытательный срок" -msgstr "Probation period" - -#: templates/pages/employee-detail.html:70 -msgid "Рабочий email" -msgstr "Work email" - #: templates/pages/employee-detail.html:78 msgid "Внутренний номер" msgstr "Internal number" @@ -2492,7 +2751,7 @@ msgstr "Internal number" msgid "Подчинённых" msgstr "Subordinates" -#: templates/pages/employee-detail.html:112 templates/pages/hr-portal.html:219 +#: templates/pages/employee-detail.html:112 templates/pages/hr-portal.html:250 msgid "Дата приёма" msgstr "Hire date" @@ -2546,7 +2805,7 @@ msgstr "Add employee" msgid "сотрудников, из них" msgstr "employees, of them" -#: templates/pages/employees.html:48 templates/pages/hr-portal.html:122 +#: templates/pages/employees.html:48 templates/pages/hr-portal.html:152 #: templates/pages/news.html:111 templates/pages/tasks.html:60 #: templates/pages/timetrack.html:45 msgid "Все" @@ -2557,11 +2816,11 @@ msgstr "All" msgid "Все аптеки" msgstr "All pharmacies" -#: templates/pages/employees.html:105 templates/pages/hr-portal.html:217 +#: templates/pages/employees.html:105 templates/pages/hr-portal.html:248 msgid "Отдел / Аптека" msgstr "Department / Pharmacy" -#: templates/pages/employees.html:108 templates/pages/it-manager.html:914 +#: templates/pages/employees.html:108 templates/pages/it-manager.html:917 msgid "Дата начала" msgstr "Start date" @@ -2577,324 +2836,406 @@ msgstr "HR Portal" msgid "Управление персоналом и кадровые процессы" msgstr "HR management and personnel processes" +#: templates/pages/hr-portal.html:13 +#| msgid "Заявок пока нет" +msgid "Заявку створено" +msgstr "Request created" + #: templates/pages/hr-portal.html:16 +#| msgid "Интерфейс" +msgid "Інтерн-слот" +msgstr "Intern slot" + +#: templates/pages/hr-portal.html:16 +msgid "логін" +msgstr "login" + +#: templates/pages/hr-portal.html:16 +msgid "пароль" +msgstr "password" + +#: templates/pages/hr-portal.html:20 +#| msgid "Заявка IT:" +msgid "Заявка IT" +msgstr "IT request" + +#: templates/pages/hr-portal.html:27 +#| msgid "Подать заявку" +msgid "Не удалось создать заявку" +msgstr "Failed to create the request" + +#: templates/pages/hr-portal.html:43 msgid "Всего сотрудников" msgstr "Total employees" -#: templates/pages/hr-portal.html:18 +#: templates/pages/hr-portal.html:45 msgid "за последний месяц" msgstr "in the last month" -#: templates/pages/hr-portal.html:31 +#: templates/pages/hr-portal.html:56 +#| msgid "Срок испытания" +msgid "Открыть список на испытании" +msgstr "Open the probation list" + +#: templates/pages/hr-portal.html:58 msgid "На испытательном сроке" msgstr "On probation" -#: templates/pages/hr-portal.html:33 +#: templates/pages/hr-portal.html:60 msgid "требуют оценки" msgstr "require evaluation" -#: templates/pages/hr-portal.html:44 +#: templates/pages/hr-portal.html:71 msgid "Запланированные тренинги" msgstr "Scheduled trainings" -#: templates/pages/hr-portal.html:46 +#: templates/pages/hr-portal.html:73 msgid "раздел в разработке" msgstr "section under development" -#: templates/pages/hr-portal.html:57 +#: templates/pages/hr-portal.html:84 msgid "Заявки на отпуск" msgstr "Vacation requests" -#: templates/pages/hr-portal.html:59 +#: templates/pages/hr-portal.html:86 msgid "ожидает решения" msgstr "awaiting decision" -#: templates/pages/hr-portal.html:76 +#: templates/pages/hr-portal.html:103 msgid "Поиск работников, вакансий, тренингов..." msgstr "Search employees, vacancies, trainings..." -#: templates/pages/hr-portal.html:83 +#: templates/pages/hr-portal.html:110 msgid "Создать" msgstr "Create" -#: templates/pages/hr-portal.html:94 +#: templates/pages/hr-portal.html:123 msgid "Заявки и отпуска" msgstr "Requests and vacations" -#: templates/pages/hr-portal.html:96 +#: templates/pages/hr-portal.html:125 msgid "Тренинги" msgstr "Trainings" -#: templates/pages/hr-portal.html:98 +#: templates/pages/hr-portal.html:127 msgid "Вакансии" msgstr "Vacancies" -#: templates/pages/hr-portal.html:105 +#: templates/pages/hr-portal.html:134 msgid "Карточки" msgstr "Cards" -#: templates/pages/hr-portal.html:136 +#: templates/pages/hr-portal.html:166 msgid "Все отделы" msgstr "All departments" -#: templates/pages/hr-portal.html:143 +#: templates/pages/hr-portal.html:173 msgid "Все должности" msgstr "All positions" -#: templates/pages/hr-portal.html:176 +#: templates/pages/hr-portal.html:207 templates/pages/hr-portal.html:390 msgid "На испытании до" msgstr "On probation until" -#: templates/pages/hr-portal.html:182 +#: templates/pages/hr-portal.html:213 msgid "Отдел:" msgstr "Department:" -#: templates/pages/hr-portal.html:186 +#: templates/pages/hr-portal.html:217 msgid "Дата приёма:" msgstr "Hire date:" -#: templates/pages/hr-portal.html:194 +#: templates/pages/hr-portal.html:225 msgid "Телефон:" msgstr "Phone:" -#: templates/pages/hr-portal.html:202 templates/pages/hr-portal.html:255 +#: templates/pages/hr-portal.html:233 templates/pages/hr-portal.html:287 msgid "Активных сотрудников пока нет" msgstr "No active employees yet" -#: templates/pages/hr-portal.html:206 templates/pages/hr-portal.html:261 +#: templates/pages/hr-portal.html:237 templates/pages/hr-portal.html:293 msgid "Ничего не найдено по заданным фильтрам" msgstr "Nothing found for the given filters" -#: templates/pages/hr-portal.html:242 templates/pages/hr-portal.html:559 -msgid "На испытании" -msgstr "On probation" - -#: templates/pages/hr-portal.html:265 +#: templates/pages/hr-portal.html:297 msgid "Полный список сотрудников →" msgstr "Full employee list →" -#: templates/pages/hr-portal.html:277 -msgid "Отпуск" -msgstr "Vacation" - -#: templates/pages/hr-portal.html:278 -msgid "Подать заявку" -msgstr "Submit request" - -#: templates/pages/hr-portal.html:285 -msgid "Заказать документ" -msgstr "Order document" - -#: templates/pages/hr-portal.html:291 -msgid "Табель" -msgstr "Timesheet" - -#: templates/pages/hr-portal.html:298 -msgid "Начисления" -msgstr "Accruals" - -#: templates/pages/hr-portal.html:299 -msgid "Зарплата и премии" -msgstr "Salary and bonuses" - #: templates/pages/hr-portal.html:308 -msgid "Мои заявки" -msgstr "My requests" +#| msgid "На испытании" +msgid "Кандидаты на испытании" +msgstr "Candidates on probation" -#: templates/pages/hr-portal.html:309 templates/pages/it-manager.html:373 -msgid "Новая" -msgstr "New" +#: templates/pages/hr-portal.html:309 +msgid "Заявки на найм до утверждения персонального аккаунта" +msgstr "Hiring requests before approval of a personal account" -#: templates/pages/hr-portal.html:314 -msgid "Отпуск 01–14 мая" -msgstr "Vacation May 01–14" +#: templates/pages/hr-portal.html:311 +#| msgid "Новая задача" +msgid "Новая заявка" +msgstr "New request" -#: templates/pages/hr-portal.html:315 -msgid "Подано 10.04.2026" -msgstr "Submitted 10.04.2026" - -#: templates/pages/hr-portal.html:317 templates/pages/hr-portal.html:374 -msgid "На рассмотрении" -msgstr "Under review" - -#: templates/pages/hr-portal.html:321 -msgid "Справка о зарплате" -msgstr "Salary certificate" - -#: templates/pages/hr-portal.html:322 -msgid "Подано 05.04.2026" -msgstr "Submitted 05.04.2026" - -#: templates/pages/hr-portal.html:324 -msgid "Готово" -msgstr "Done" - -#: templates/pages/hr-portal.html:328 -msgid "Больничный 28–29 марта" -msgstr "Sick leave March 28–29" - -#: templates/pages/hr-portal.html:329 -msgid "Подано 01.04.2026" -msgstr "Submitted 01.04.2026" - -#: templates/pages/hr-portal.html:331 templates/pages/hr-portal.html:562 -msgid "Утверждено" -msgstr "Approved" - -#: templates/pages/hr-portal.html:339 -msgid "Баланс отпусков — 2026" -msgstr "Vacation balance — 2026" - -#: templates/pages/hr-portal.html:345 -msgid "Основной отпуск" -msgstr "Main vacation" - -#: templates/pages/hr-portal.html:346 templates/pages/hr-portal.html:355 -#: templates/pages/hr-portal.html:364 templates/pages/hr-portal.html:483 -msgid "дн." -msgstr "days" - -#: templates/pages/hr-portal.html:349 -msgid "Остаток: 10 дней" -msgstr "Balance: 10 days" - -#: templates/pages/hr-portal.html:354 -msgid "Дополнительный отпуск" -msgstr "Additional vacation" - -#: templates/pages/hr-portal.html:358 -msgid "Остаток: 7 дней" -msgstr "Balance: 7 days" - -#: templates/pages/hr-portal.html:363 -msgid "Больничные" -msgstr "Sick leaves" - -#: templates/pages/hr-portal.html:367 -msgid "Остаток: 28 дней" -msgstr "Balance: 28 days" - -#: templates/pages/hr-portal.html:371 -msgid "Следующий отпуск" -msgstr "Next vacation" - -#: templates/pages/hr-portal.html:389 templates/pages/hr-portal.html:400 -msgid "Раздел в разработке" -msgstr "Section under development" - -#: templates/pages/hr-portal.html:390 -msgid "Планирование тренингов и обучения появится в ближайшее время" -msgstr "Training and learning planning coming soon" - -#: templates/pages/hr-portal.html:401 -msgid "Управление открытыми вакансиями появится в ближайшее время" -msgstr "Open vacancies management coming soon" - -#: templates/pages/hr-portal.html:415 -msgid "Новая заявка на найм" -msgstr "New hiring request" - -#: templates/pages/hr-portal.html:416 -msgid "Сразу создаёт личный учётный запис кандидата на срок испытания." -msgstr "Immediately creates a personal candidate account for the probation period." - -#: templates/pages/hr-portal.html:422 -msgid "Фамилия " -msgstr "Last name " - -#: templates/pages/hr-portal.html:426 -msgid "Имя " -msgstr "First name " - -#: templates/pages/hr-portal.html:437 -msgid "Тип локации " -msgstr "Location type " - -#: templates/pages/hr-portal.html:439 -msgid "Выберите..." -msgstr "Select..." - -#: templates/pages/hr-portal.html:476 -msgid "Дата выхода" -msgstr "Start date" - -#: templates/pages/hr-portal.html:480 -msgid "Срок испытания" -msgstr "Probation period" - -#: templates/pages/hr-portal.html:491 -msgid "Email кандидата" -msgstr "Candidate email" - -#: templates/pages/hr-portal.html:501 -msgid "Требуются доступы (после утверждения — заявка в IT)" -msgstr "Required access (after approval — IT request)" - -#: templates/pages/hr-portal.html:513 -msgid "Примечание по доступам" -msgstr "Note on access" - -#: templates/pages/hr-portal.html:514 -msgid "Например: 1С Бухгалтерия — только просмотр" -msgstr "e.g. 1C Accounting — read only" - -#: templates/pages/hr-portal.html:520 -msgid "Создать заявку и временный аккаунт" -msgstr "Create request and temporary account" - -#: templates/pages/hr-portal.html:521 -msgid "Создание учётной записи…" -msgstr "Creating account…" - -#: templates/pages/hr-portal.html:533 -msgid "Кандидат" -msgstr "Candidate" - -#: templates/pages/hr-portal.html:534 +#: templates/pages/hr-portal.html:319 templates/pages/hr-portal.html:687 msgid "Должность / локация" msgstr "Position / location" -#: templates/pages/hr-portal.html:536 +#: templates/pages/hr-portal.html:320 templates/pages/hr-portal.html:688 +msgid "Інтерн / IT" +msgstr "Intern / IT" + +#: templates/pages/hr-portal.html:321 templates/pages/hr-portal.html:690 msgid "Испытание до" msgstr "Probation until" -#: templates/pages/hr-portal.html:537 templates/pages/pharmacy-detail.html:302 -msgid "Подал" -msgstr "Submitted by" - -#: templates/pages/hr-portal.html:538 templates/pages/task-detail.html:138 +#: templates/pages/hr-portal.html:322 templates/pages/hr-portal.html:692 +#: templates/pages/task-detail.html:138 msgid "Действия" msgstr "Actions" -#: templates/pages/hr-portal.html:557 +#: templates/pages/hr-portal.html:331 templates/pages/hr-portal.html:725 msgid "Просрочено" msgstr "Overdue" -#: templates/pages/hr-portal.html:564 -msgid "Отклонено" -msgstr "Rejected" - -#: templates/pages/hr-portal.html:572 +#: templates/pages/hr-portal.html:351 templates/pages/hr-portal.html:740 msgid "Утвердить кандидата" msgstr "Approve candidate" -#: templates/pages/hr-portal.html:575 +#: templates/pages/hr-portal.html:354 templates/pages/hr-portal.html:743 msgid "Утвердить" msgstr "Approve" -#: templates/pages/hr-portal.html:581 +#: templates/pages/hr-portal.html:360 templates/pages/hr-portal.html:749 msgid "Причина отказа" msgstr "Reason for rejection" -#: templates/pages/hr-portal.html:583 +#: templates/pages/hr-portal.html:362 templates/pages/hr-portal.html:751 msgid "Отклонить" msgstr "Reject" -#: templates/pages/hr-portal.html:591 -msgid "Заявка IT:" -msgstr "IT request:" +#: templates/pages/hr-portal.html:371 +#| msgid "На испытании" +msgid "Нет кандидатов на испытании" +msgstr "No candidates on probation" -#: templates/pages/hr-portal.html:598 +#: templates/pages/hr-portal.html:383 +#| msgid "На испытательном сроке" +msgid "Сотрудники с испытательным сроком" +msgstr "Employees on probation" + +#: templates/pages/hr-portal.html:384 +msgid "Учётные записи с заполненной датой окончания испытания" +msgstr "Accounts with a filled probation end date" + +#: templates/pages/hr-portal.html:399 +#| msgid "На испытательном сроке" +msgid "Нет сотрудников на испытательном сроке" +msgstr "No employees on probation" + +#: templates/pages/hr-portal.html:413 +msgid "Отпуск" +msgstr "Vacation" + +#: templates/pages/hr-portal.html:414 +msgid "Подать заявку" +msgstr "Submit request" + +#: templates/pages/hr-portal.html:421 +msgid "Заказать документ" +msgstr "Order document" + +#: templates/pages/hr-portal.html:427 +msgid "Табель" +msgstr "Timesheet" + +#: templates/pages/hr-portal.html:434 +msgid "Начисления" +msgstr "Accruals" + +#: templates/pages/hr-portal.html:435 +msgid "Зарплата и премии" +msgstr "Salary and bonuses" + +#: templates/pages/hr-portal.html:444 +msgid "Мои заявки" +msgstr "My requests" + +#: templates/pages/hr-portal.html:445 templates/pages/it-manager.html:373 +msgid "Новая" +msgstr "New" + +#: templates/pages/hr-portal.html:450 +msgid "Отпуск 01–14 мая" +msgstr "Vacation May 01–14" + +#: templates/pages/hr-portal.html:451 +msgid "Подано 10.04.2026" +msgstr "Submitted 10.04.2026" + +#: templates/pages/hr-portal.html:453 templates/pages/hr-portal.html:510 +msgid "На рассмотрении" +msgstr "Under review" + +#: templates/pages/hr-portal.html:457 +msgid "Справка о зарплате" +msgstr "Salary certificate" + +#: templates/pages/hr-portal.html:458 +msgid "Подано 05.04.2026" +msgstr "Submitted 05.04.2026" + +#: templates/pages/hr-portal.html:460 +msgid "Готово" +msgstr "Done" + +#: templates/pages/hr-portal.html:464 +msgid "Больничный 28–29 марта" +msgstr "Sick leave March 28–29" + +#: templates/pages/hr-portal.html:465 +msgid "Подано 01.04.2026" +msgstr "Submitted 01.04.2026" + +#: templates/pages/hr-portal.html:475 +msgid "Баланс отпусков — 2026" +msgstr "Vacation balance — 2026" + +#: templates/pages/hr-portal.html:481 +msgid "Основной отпуск" +msgstr "Main vacation" + +#: templates/pages/hr-portal.html:485 +msgid "Остаток: 10 дней" +msgstr "Balance: 10 days" + +#: templates/pages/hr-portal.html:490 +msgid "Дополнительный отпуск" +msgstr "Additional vacation" + +#: templates/pages/hr-portal.html:494 +msgid "Остаток: 7 дней" +msgstr "Balance: 7 days" + +#: templates/pages/hr-portal.html:499 +msgid "Больничные" +msgstr "Sick leaves" + +#: templates/pages/hr-portal.html:503 +msgid "Остаток: 28 дней" +msgstr "Balance: 28 days" + +#: templates/pages/hr-portal.html:507 +msgid "Следующий отпуск" +msgstr "Next vacation" + +#: templates/pages/hr-portal.html:525 templates/pages/hr-portal.html:536 +msgid "Раздел в разработке" +msgstr "Section under development" + +#: templates/pages/hr-portal.html:526 +msgid "Планирование тренингов и обучения появится в ближайшее время" +msgstr "Training and learning planning coming soon" + +#: templates/pages/hr-portal.html:537 +msgid "Управление открытыми вакансиями появится в ближайшее время" +msgstr "Open vacancies management coming soon" + +#: templates/pages/hr-portal.html:550 +msgid "Новая заявка на найм" +msgstr "New hiring request" + +#: templates/pages/hr-portal.html:551 +msgid "" +"Создаёт IT-заявку и выдаёт интерн-учётку на срок испытания. Персональный " +"аккаунт — после утверждения." +msgstr "" +"Creates an IT request and issues an intern account for the probation period." +" A personal account — after approval." + +#: templates/pages/hr-portal.html:553 +#, python-format +msgid "Свободных интерн-слотов: %(free)s из %(total)s" +msgstr "Free intern slots: %(free)s of %(total)s" + +#: templates/pages/hr-portal.html:555 +msgid "" +"добавьте слоты в админке (Пул інтерн-облікових записів) или дождитесь " +"освобождения" +msgstr "" +"add slots in the admin (Intern accounts pool) or wait for one to free up" + +#: templates/pages/hr-portal.html:563 +msgid "Фамилия " +msgstr "Last name " + +#: templates/pages/hr-portal.html:567 +msgid "Имя " +msgstr "First name " + +#: templates/pages/hr-portal.html:578 +msgid "Тип локации " +msgstr "Location type " + +#: templates/pages/hr-portal.html:580 +msgid "Выберите..." +msgstr "Select..." + +#: templates/pages/hr-portal.html:587 +#| msgid "Отдел" +msgid "Отдел " +msgstr "Department " + +#: templates/pages/hr-portal.html:596 +#| msgid "Аптека" +msgid "Аптека " +msgstr "Pharmacy " + +#: templates/pages/hr-portal.html:605 +#| msgid "Офис / Склад" +msgid "Офис / склад " +msgstr "Office / warehouse " + +#: templates/pages/hr-portal.html:630 +msgid "Срок испытания" +msgstr "Probation period" + +#: templates/pages/hr-portal.html:641 +msgid "Email кандидата" +msgstr "Candidate email" + +#: templates/pages/hr-portal.html:651 +#| msgid "Быстрый доступ" +msgid "Требуются доступы" +msgstr "Access required" + +#: templates/pages/hr-portal.html:652 +msgid "" +"Створює заявку IT на підготовку робочого місця. Список залежить від типу " +"локації." +msgstr "" +"Creates an IT request to prepare the workplace. The list depends on the " +"location type." + +#: templates/pages/hr-portal.html:667 +msgid "Например: 1С Бухгалтерия — только просмотр" +msgstr "e.g. 1C Accounting — read only" + +#: templates/pages/hr-portal.html:673 +msgid "Створити заявку (інтерн + IT)" +msgstr "Create request (intern + IT)" + +#: templates/pages/hr-portal.html:674 +msgid "Створення…" +msgstr "Creating…" + +#: templates/pages/hr-portal.html:691 templates/pages/pharmacy-detail.html:302 +msgid "Подал" +msgstr "Submitted by" + +#: templates/pages/hr-portal.html:762 +msgid "PDF з даними" +msgstr "PDF with data" + +#: templates/pages/hr-portal.html:772 msgid "Заявок на найм пока нет" msgstr "No hiring requests yet" @@ -2914,7 +3255,7 @@ msgstr "Completed work" msgid "Расходные материалы" msgstr "Consumables" -#: templates/pages/it-manager.html:21 templates/pages/it-manager.html:791 +#: templates/pages/it-manager.html:21 templates/pages/it-manager.html:794 msgid "Провайдеры" msgstr "Providers" @@ -2928,7 +3269,8 @@ msgid "На складе" msgstr "In stock" #: templates/pages/it-manager.html:39 templates/pages/it-manager.html:236 -#: templates/pages/it-manager.html:260 templates/pages/pharmacy-detail.html:198 +#: templates/pages/it-manager.html:260 +#: templates/pages/pharmacy-detail.html:198 #: templates/pages/pharmacy-detail.html:214 msgid "Выдано" msgstr "Issued" @@ -2970,10 +3312,14 @@ msgid "По одному в строку — вставьте список се msgstr "One per line — paste serial numbers, e.g." #: templates/pages/it-manager.html:96 -msgid "Сколько серийников — столько единиц будет создано. Дубликаты и уже имеющиеся пропускаются." +msgid "" +"Сколько серийников — столько единиц будет создано. Дубликаты и уже имеющиеся" +" пропускаются." msgstr "" +"As many serial numbers — as many units will be created. Duplicates and " +"existing ones are skipped." -#: templates/pages/it-manager.html:100 templates/pages/it-manager.html:670 +#: templates/pages/it-manager.html:100 templates/pages/it-manager.html:673 msgid "Дата покупки" msgstr "Purchase date" @@ -2990,8 +3336,8 @@ msgid "(необязательно)" msgstr "(optional)" #: templates/pages/it-manager.html:110 templates/pages/it-manager.html:202 -#: templates/pages/it-manager.html:500 templates/pages/it-manager.html:656 -#: templates/pages/it-manager.html:881 +#: templates/pages/it-manager.html:503 templates/pages/it-manager.html:659 +#: templates/pages/it-manager.html:884 msgid "Поиск по адресу или номеру аптеки..." msgstr "Search by address or pharmacy number..." @@ -3004,8 +3350,8 @@ msgid "Создать экземпляры" msgstr "Create units" #: templates/pages/it-manager.html:122 templates/pages/it-manager.html:172 -#: templates/pages/it-manager.html:542 templates/pages/it-manager.html:680 -#: templates/pages/it-manager.html:1086 +#: templates/pages/it-manager.html:545 templates/pages/it-manager.html:683 +#: templates/pages/it-manager.html:1089 msgid "Сохранение…" msgstr "Saving…" @@ -3017,8 +3363,8 @@ msgstr "Cancel" msgid "Новая модель" msgstr "New model" -#: templates/pages/it-manager.html:149 templates/pages/it-manager.html:625 -#: templates/pages/it-manager.html:900 +#: templates/pages/it-manager.html:149 templates/pages/it-manager.html:628 +#: templates/pages/it-manager.html:903 msgid "+ Добавить новый тип..." msgstr "+ Add new type..." @@ -3056,7 +3402,7 @@ msgstr "Catalog is empty — add the first model." #: templates/pages/it-manager.html:199 msgid "ед.:" -msgstr "" +msgstr "pcs:" #: templates/pages/it-manager.html:201 msgid "На локацию" @@ -3082,7 +3428,8 @@ msgstr "Type / Brand / Model" msgid "С/н · Инв. №" msgstr "S/n · Inv. no." -#: templates/pages/it-manager.html:261 templates/pages/pharmacy-detail.html:215 +#: templates/pages/it-manager.html:261 +#: templates/pages/pharmacy-detail.html:215 msgid "В ремонте" msgstr "Under repair" @@ -3098,13 +3445,7 @@ msgstr "Issued to" msgid "— (на складе)" msgstr "— (in stock)" -#: templates/pages/it-manager.html:290 templates/pages/it-manager.html:420 -#: templates/pages/it-manager.html:732 templates/pages/it-manager.html:1010 -#: templates/pages/settings.html:43 templates/pages/settings.html:60 -msgid "Сохранить" -msgstr "Save" - -#: templates/pages/it-manager.html:294 templates/pages/it-manager.html:739 +#: templates/pages/it-manager.html:294 templates/pages/it-manager.html:742 msgid "Примечания:" msgstr "Notes" @@ -3141,12 +3482,12 @@ msgid "Заявитель" msgstr "Requester" #: templates/pages/it-manager.html:348 templates/pages/it-manager.html:411 -#: templates/pages/it-manager.html:556 +#: templates/pages/it-manager.html:559 msgid "Исполнитель" msgstr "Assignee" -#: templates/pages/it-manager.html:349 templates/pages/it-manager.html:486 -#: templates/pages/it-manager.html:555 templates/pages/it-manager.html:1098 +#: templates/pages/it-manager.html:349 templates/pages/it-manager.html:489 +#: templates/pages/it-manager.html:558 templates/pages/it-manager.html:1101 #: templates/pages/pharmacy-detail.html:299 msgid "Дата" msgstr "Date" @@ -3163,268 +3504,273 @@ msgstr "Critical" msgid "Решена" msgstr "Resolved" -#: templates/pages/it-manager.html:391 templates/pages/it-manager.html:430 +#: templates/pages/it-manager.html:391 templates/pages/it-manager.html:433 msgid "внутренний" msgstr "internal" -#: templates/pages/it-manager.html:413 +#: templates/pages/it-manager.html:414 msgid "— не назначено" msgstr "— not assigned" -#: templates/pages/it-manager.html:428 +#: templates/pages/it-manager.html:418 +msgid "Нет сотрудников IT / staff — добавьте в группу IT" +msgstr "No IT / staff employees — add them to the IT group" + +#: templates/pages/it-manager.html:431 msgid "Комментарий..." msgstr "Comment..." -#: templates/pages/it-manager.html:433 templates/pages/it-manager.html:679 +#: templates/pages/it-manager.html:436 templates/pages/it-manager.html:682 msgid "Добавить" msgstr "Add" -#: templates/pages/it-manager.html:440 +#: templates/pages/it-manager.html:443 msgid "Заявок пока нет" msgstr "No tickets yet" -#: templates/pages/it-manager.html:455 +#: templates/pages/it-manager.html:458 msgid "Записей в этом месяце" msgstr "Records this month" -#: templates/pages/it-manager.html:459 +#: templates/pages/it-manager.html:462 msgid "Часов в этом месяце" msgstr "Hours this month" -#: templates/pages/it-manager.html:469 templates/pages/it-manager.html:541 +#: templates/pages/it-manager.html:472 templates/pages/it-manager.html:544 msgid "Добавить запись" msgstr "Add record" -#: templates/pages/it-manager.html:479 +#: templates/pages/it-manager.html:482 msgid "Добавить запись о выполненной работе" msgstr "Add completed work record" -#: templates/pages/it-manager.html:495 +#: templates/pages/it-manager.html:498 msgid "+ Добавить новый вид работ..." msgstr "+ Add new work type..." -#: templates/pages/it-manager.html:510 +#: templates/pages/it-manager.html:513 msgid "Затрачено часов" msgstr "Hours spent" -#: templates/pages/it-manager.html:532 +#: templates/pages/it-manager.html:535 msgid "Описание работ" msgstr "Work description" -#: templates/pages/it-manager.html:536 +#: templates/pages/it-manager.html:539 msgid "Результат" msgstr "Result" -#: templates/pages/it-manager.html:560 templates/pages/timetrack.html:59 +#: templates/pages/it-manager.html:563 templates/pages/timetrack.html:59 msgid "Часов" msgstr "Hours" -#: templates/pages/it-manager.html:574 +#: templates/pages/it-manager.html:577 msgid "Записей ещё нет" msgstr "No records yet" -#: templates/pages/it-manager.html:589 +#: templates/pages/it-manager.html:592 msgid "Всего позиций" msgstr "Total items" -#: templates/pages/it-manager.html:593 +#: templates/pages/it-manager.html:596 msgid "Заканчивается / на замену" msgstr "Expiring / to replace" -#: templates/pages/it-manager.html:603 +#: templates/pages/it-manager.html:606 msgid "Добавить материал" msgstr "Add material" -#: templates/pages/it-manager.html:613 +#: templates/pages/it-manager.html:616 msgid "Добавить расходный материал" msgstr "Add consumable" -#: templates/pages/it-manager.html:634 +#: templates/pages/it-manager.html:637 msgid "+ Добавить новый цвет..." msgstr "+ Add new color..." -#: templates/pages/it-manager.html:638 +#: templates/pages/it-manager.html:641 msgid "Производитель" msgstr "Manufacturer" -#: templates/pages/it-manager.html:642 +#: templates/pages/it-manager.html:645 msgid "Модель / артикул" msgstr "Model / article" -#: templates/pages/it-manager.html:646 templates/pages/it-manager.html:695 +#: templates/pages/it-manager.html:649 templates/pages/it-manager.html:698 msgid "Принтер" msgstr "Printer" -#: templates/pages/it-manager.html:655 +#: templates/pages/it-manager.html:658 msgid "Локация хранения" msgstr "Storage location" -#: templates/pages/it-manager.html:666 +#: templates/pages/it-manager.html:669 msgid "Ресурс (страниц)" msgstr "Capacity (pages)" -#: templates/pages/it-manager.html:693 +#: templates/pages/it-manager.html:696 msgid "Тип / Модель" msgstr "Type / Model" -#: templates/pages/it-manager.html:736 +#: templates/pages/it-manager.html:739 msgid "Ресурс:" msgstr "Capacity:" -#: templates/pages/it-manager.html:736 +#: templates/pages/it-manager.html:739 msgid "стр." msgstr "pages" -#: templates/pages/it-manager.html:745 +#: templates/pages/it-manager.html:748 msgid "Расходных материалов ещё не добавлено" msgstr "No consumables added yet" -#: templates/pages/it-manager.html:760 +#: templates/pages/it-manager.html:763 msgid "Провайдеров" msgstr "Providers" -#: templates/pages/it-manager.html:764 +#: templates/pages/it-manager.html:767 msgid "Договоров" msgstr "Contracts" -#: templates/pages/it-manager.html:768 +#: templates/pages/it-manager.html:771 msgid "Заканчиваются через 30 дней" msgstr "Expiring in 30 days" -#: templates/pages/it-manager.html:778 templates/pages/it-manager.html:850 +#: templates/pages/it-manager.html:781 templates/pages/it-manager.html:853 msgid "Добавить провайдера" msgstr "Add provider" -#: templates/pages/it-manager.html:784 templates/pages/it-manager.html:955 +#: templates/pages/it-manager.html:787 templates/pages/it-manager.html:958 msgid "Добавить договор" msgstr "Add contract" -#: templates/pages/it-manager.html:803 +#: templates/pages/it-manager.html:806 msgid "Провайдеров ещё нет" msgstr "No providers yet" -#: templates/pages/it-manager.html:814 +#: templates/pages/it-manager.html:817 msgid "Новый провайдер" msgstr "New provider" -#: templates/pages/it-manager.html:833 +#: templates/pages/it-manager.html:836 msgid "Сайт" msgstr "Website" -#: templates/pages/it-manager.html:837 +#: templates/pages/it-manager.html:840 msgid "Телефон техподдержки" msgstr "Support phone" -#: templates/pages/it-manager.html:841 +#: templates/pages/it-manager.html:844 msgid "Email техподдержки" msgstr "Support email" -#: templates/pages/it-manager.html:864 +#: templates/pages/it-manager.html:867 msgid "Новый договор" msgstr "New contract" -#: templates/pages/it-manager.html:891 +#: templates/pages/it-manager.html:894 msgid "Номер договора" msgstr "Contract number" -#: templates/pages/it-manager.html:895 templates/pages/it-manager.html:970 +#: templates/pages/it-manager.html:898 templates/pages/it-manager.html:973 #: templates/pages/pharmacy-detail.html:244 msgid "Тип услуги" msgstr "Service type" -#: templates/pages/it-manager.html:904 +#: templates/pages/it-manager.html:907 msgid "Технология подключения" msgstr "Connection technology" -#: templates/pages/it-manager.html:910 +#: templates/pages/it-manager.html:913 msgid "+ Добавить новую технологию..." msgstr "+ Add new technology..." -#: templates/pages/it-manager.html:918 +#: templates/pages/it-manager.html:921 msgid "Дата окончания" msgstr "End date" -#: templates/pages/it-manager.html:922 +#: templates/pages/it-manager.html:925 msgid "Стоимость/мес." msgstr "Cost/month" -#: templates/pages/it-manager.html:926 +#: templates/pages/it-manager.html:929 msgid "Скорость ↓ Мбит/с" msgstr "Speed ↓ Mbps" -#: templates/pages/it-manager.html:930 +#: templates/pages/it-manager.html:933 msgid "Скорость ↑ Мбит/с" msgstr "Speed ↑ Mbps" -#: templates/pages/it-manager.html:934 +#: templates/pages/it-manager.html:937 msgid "Лицевой счёт" msgstr "Account number" -#: templates/pages/it-manager.html:941 +#: templates/pages/it-manager.html:944 msgid "Выделенный IP" msgstr "Dedicated IP" -#: templates/pages/it-manager.html:945 +#: templates/pages/it-manager.html:948 msgid "По одному IP-адресу на строку" msgstr "One IP address per line" -#: templates/pages/it-manager.html:969 +#: templates/pages/it-manager.html:972 msgid "Провайдер / Локация" msgstr "Provider / Location" -#: templates/pages/it-manager.html:971 templates/pages/pharmacy-detail.html:246 +#: templates/pages/it-manager.html:974 +#: templates/pages/pharmacy-detail.html:246 msgid "Скорость" msgstr "Speed" -#: templates/pages/it-manager.html:1013 +#: templates/pages/it-manager.html:1016 msgid "Счёт:" msgstr "Account:" -#: templates/pages/it-manager.html:1013 +#: templates/pages/it-manager.html:1016 msgid "грн/мес" msgstr "UAH/mo" -#: templates/pages/it-manager.html:1018 +#: templates/pages/it-manager.html:1021 msgid "Договоров ещё нет" msgstr "No contracts yet" -#: templates/pages/it-manager.html:1032 +#: templates/pages/it-manager.html:1035 msgid "обращений" msgstr "requests" -#: templates/pages/it-manager.html:1037 templates/pages/it-manager.html:1047 +#: templates/pages/it-manager.html:1040 templates/pages/it-manager.html:1050 msgid "Новое обращение" msgstr "New request" -#: templates/pages/it-manager.html:1054 templates/pages/it-manager.html:1099 +#: templates/pages/it-manager.html:1057 templates/pages/it-manager.html:1102 msgid "Кто обратился" msgstr "Who requested" -#: templates/pages/it-manager.html:1068 +#: templates/pages/it-manager.html:1071 msgid "+ Добавить новый канал..." msgstr "+ Add new channel..." -#: templates/pages/it-manager.html:1072 +#: templates/pages/it-manager.html:1075 msgid "Затрачено минут" msgstr "Minutes spent" -#: templates/pages/it-manager.html:1076 +#: templates/pages/it-manager.html:1079 msgid "Описание обращения" msgstr "Request description" -#: templates/pages/it-manager.html:1080 +#: templates/pages/it-manager.html:1083 msgid "Решение" msgstr "Resolution" -#: templates/pages/it-manager.html:1085 +#: templates/pages/it-manager.html:1088 msgid "Добавить обращение" msgstr "Add request" -#: templates/pages/it-manager.html:1101 +#: templates/pages/it-manager.html:1104 msgid "Принял" msgstr "Accepted by" -#: templates/pages/it-manager.html:1117 +#: templates/pages/it-manager.html:1120 msgid "Обращений ещё нет" msgstr "No requests yet" @@ -3441,7 +3787,8 @@ msgid "Это необратимо." msgstr "This is irreversible." #: templates/pages/news-detail.html:24 templates/pages/task-detail.html:48 -#: templates/pages/tasks.html:135 templates/partials/pharmacy-documents.html:51 +#: templates/pages/tasks.html:135 +#: templates/partials/pharmacy-documents.html:51 msgid "Удалить" msgstr "Delete" @@ -3572,14 +3919,6 @@ msgstr "Internet provider" msgid "Коммунальный отдел" msgstr "Utilities department" -#: templates/pages/pharmacy-detail.html:88 -msgid "Регион" -msgstr "Region" - -#: templates/pages/pharmacy-detail.html:90 -msgid "Категория" -msgstr "Category" - #: templates/pages/pharmacy-detail.html:91 msgid "Категория НДС" msgstr "VAT category" @@ -3681,8 +4020,12 @@ msgid "Количество записей в таблицах по умолча msgstr "Default number of rows in tables" #: templates/pages/profile.html:100 -msgid "Применяется ко всем таблицам портала, если для конкретной таблицы размер не выбран вручную." -msgstr "Applies to all portal tables unless a size is chosen manually for a specific table." +msgid "" +"Применяется ко всем таблицам портала, если для конкретной таблицы размер не " +"выбран вручную." +msgstr "" +"Applies to all portal tables unless a size is chosen manually for a specific" +" table." #: templates/pages/settings.html:6 msgid "Конфигурация портала" @@ -3860,33 +4203,62 @@ msgstr "No time records for" msgid "сотрудников" msgstr "employees" +#: templates/partials/dislo_cell.html:15 +msgid "не назначен" +msgstr "not assigned" + +#: templates/partials/dislo_cell.html:23 +msgid "Нехватка" +msgstr "Shortage" + +#: templates/partials/dislo_cell.html:25 +msgid "Внимание" +msgstr "Warning" + +#: templates/partials/dislo_cell.html:27 +msgid "Норма" +msgstr "Normal" + #: templates/partials/documents-browser.html:28 #: templates/partials/pharmacy-documents.html:18 msgid "Файловое хранилище пока не настроено." -msgstr "" +msgstr "File storage is not configured yet." #: templates/partials/documents-browser.html:32 #: templates/partials/pharmacy-documents.html:22 msgid "Не удалось получить список документов." -msgstr "" +msgstr "Failed to fetch the document list." #: templates/partials/documents-browser.html:61 msgid "В этой папке пока нет файлов." -msgstr "" +msgstr "There are no files in this folder yet." #: templates/partials/documents-browser.html:74 #: templates/partials/pharmacy-documents.html:76 msgid "Загрузить" -msgstr "" +msgstr "Upload" #: templates/partials/pharmacy-documents.html:4 msgid "Документы аптеки" -msgstr "" +msgstr "Pharmacy documents" #: templates/partials/pharmacy-documents.html:48 msgid "Удалить файл?" -msgstr "" +msgstr "Delete the file?" #: templates/partials/pharmacy-documents.html:63 msgid "В папке аптеки пока нет файлов." -msgstr "" +msgstr "There are no files in the pharmacy folder yet." + +#~ msgid "Сразу создаёт личный учётный запис кандидата на срок испытания." +#~ msgstr "" +#~ "Immediately creates a personal candidate account for the probation period." + +#~ msgid "Требуются доступы (после утверждения — заявка в IT)" +#~ msgstr "Required access (after approval — IT request)" + +#~ msgid "Создать заявку и временный аккаунт" +#~ msgstr "Create request and temporary account" + +#~ msgid "Создание учётной записи…" +#~ msgstr "Creating account…" diff --git a/locale/uk/LC_MESSAGES/django.mo b/locale/uk/LC_MESSAGES/django.mo index a2bd89b01b306ddad805115cbde8128085fc14bc..e8a6665f5a4eb06c6f39bee3d370912542182c38 100644 GIT binary patch literal 93525 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 d9D%(aI$A%-FQ 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\n" "Language-Team: LANGUAGE \n" @@ -17,10 +17,10 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n" -#: apps/ad_connector/models.py:10 apps/employees/models/base.py:198 +#: apps/ad_connector/models.py:10 apps/employees/models/base.py:207 #: apps/employees/models/permissions.py:47 osc/apps/ad_connector/models.py:10 #: osc/apps/employees/models.py:130 osc/apps/mattermost_connector/models.py:10 -#: templates/pages/employees.html:103 templates/pages/hr-portal.html:215 +#: templates/pages/employees.html:103 templates/pages/hr-portal.html:246 #: templates/pages/timetrack.html:58 msgid "Сотрудник" msgstr "Співробітник" @@ -29,42 +29,43 @@ msgstr "Співробітник" #: apps/nextcloud_connector/models.py:34 osc/apps/ad_connector/models.py:13 #: osc/apps/mattermost_connector/models.py:13 msgid "Действие" -msgstr "" +msgstr "Дія" #: apps/ad_connector/models.py:15 apps/mail_connector/models.py:17 #: osc/apps/ad_connector/models.py:14 #: osc/apps/mattermost_connector/models.py:14 msgid "Создание" -msgstr "" +msgstr "Створення" #: apps/ad_connector/models.py:16 apps/mail_connector/models.py:18 #: osc/apps/ad_connector/models.py:14 #: osc/apps/mattermost_connector/models.py:14 msgid "Обновление" -msgstr "" +msgstr "Оновлення" #: apps/ad_connector/models.py:17 apps/mail_connector/models.py:19 msgid "Включение" -msgstr "" +msgstr "Увімкнення" #: apps/ad_connector/models.py:18 apps/mail_connector/models.py:20 msgid "Отключение" -msgstr "" +msgstr "Вимкнення" -#: apps/ad_connector/models.py:22 apps/hiring/models.py:72 +#: apps/ad_connector/models.py:22 apps/hiring/models.py:129 #: apps/it_manager/admin.py:168 apps/it_manager/admin.py:263 #: apps/it_manager/admin.py:352 apps/it_manager/admin.py:498 +#: apps/locations/dislocation_columns.py:42 #: apps/locations/models/pharmacies.py:128 apps/mail_connector/models.py:24 #: apps/nextcloud_connector/models.py:35 apps/tasks/models.py:40 #: apps/visit_manager/admin.py:199 core/admin.py:32 #: osc/apps/ad_connector/models.py:17 #: osc/apps/mattermost_connector/models.py:17 -#: templates/pages/dislocation.html:122 templates/pages/employees.html:107 -#: templates/pages/hr-portal.html:535 templates/pages/it-manager.html:237 -#: templates/pages/it-manager.html:273 templates/pages/it-manager.html:350 -#: templates/pages/it-manager.html:403 templates/pages/it-manager.html:696 -#: templates/pages/it-manager.html:724 templates/pages/it-manager.html:973 -#: templates/pages/it-manager.html:1002 templates/pages/pharmacies.html:119 +#: templates/pages/employees.html:107 templates/pages/hr-portal.html:689 +#: templates/pages/it-manager.html:237 templates/pages/it-manager.html:273 +#: templates/pages/it-manager.html:350 templates/pages/it-manager.html:403 +#: templates/pages/it-manager.html:699 templates/pages/it-manager.html:727 +#: templates/pages/it-manager.html:976 templates/pages/it-manager.html:1005 +#: templates/pages/pharmacies.html:119 #: templates/pages/pharmacy-detail.html:197 #: templates/pages/pharmacy-detail.html:247 templates/pages/structure.html:63 msgid "Статус" @@ -74,7 +75,7 @@ msgstr "Статус" #: apps/nextcloud_connector/models.py:25 osc/apps/ad_connector/models.py:18 #: osc/apps/mattermost_connector/models.py:18 msgid "Успех" -msgstr "" +msgstr "Успіх" #: apps/ad_connector/models.py:23 apps/employees/models/base.py:145 #: apps/employees/models/base.py:160 apps/locations/models/pharmacies.py:152 @@ -90,58 +91,60 @@ msgstr "Помилка" #: apps/nextcloud_connector/models.py:36 osc/apps/ad_connector/models.py:20 #: osc/apps/mattermost_connector/models.py:20 msgid "Сообщение" -msgstr "" +msgstr "Повідомлення" #: apps/ad_connector/models.py:28 osc/apps/ad_connector/models.py:23 msgid "Запись синхронизации AD" -msgstr "" +msgstr "Запис синхронізації AD" #: apps/ad_connector/models.py:29 osc/apps/ad_connector/models.py:24 msgid "Журнал синхронизации AD" -msgstr "" +msgstr "Журнал синхронізації AD" #: apps/employees/admin.py:37 apps/employees/admin.py:259 -#: apps/employees/models/base.py:113 apps/nextcloud_connector/models.py:16 -#: osc/apps/employees/models.py:72 templates/pages/hr-portal.html:446 +#: apps/employees/models/base.py:113 apps/hiring/models.py:97 +#: apps/locations/models/dislocation.py:26 +#: apps/nextcloud_connector/models.py:16 osc/apps/employees/models.py:72 #: templates/pages/structure.html:59 msgid "Отдел" msgstr "Відділ" #: apps/employees/admin.py:39 msgid "— Выберите отдел —" -msgstr "" +msgstr "— Оберіть відділ —" #: apps/employees/admin.py:44 apps/locations/admin.py:67 msgid "Выберите из списка. Уровни вложенности отображаются отступами." -msgstr "" +msgstr "Оберіть зі списку. Рівні вкладеності відображаються відступами." #: apps/employees/admin.py:49 msgid "— Выберите офис или склад —" -msgstr "" +msgstr "— Оберіть офіс або склад —" #: apps/employees/admin.py:50 apps/employees/models/base.py:107 +#: apps/hiring/models.py:107 msgid "Локация (офис/склад)" -msgstr "" +msgstr "Локація (офіс/склад)" #: apps/employees/admin.py:51 apps/employees/models/base.py:108 msgid "Заполняется для location_type=office или warehouse" -msgstr "" +msgstr "Заповнюється для location_type=office або warehouse" -#: apps/employees/admin.py:133 core/admin.py:259 +#: apps/employees/admin.py:133 core/admin.py:271 msgid "Аутентификация" -msgstr "" +msgstr "Автентифікація" -#: apps/employees/admin.py:137 +#: apps/employees/admin.py:137 apps/hiring/pdf.py:49 msgid "ФИО" -msgstr "" +msgstr "ПІБ" #: apps/employees/admin.py:141 msgid "Рабочая информация" -msgstr "" +msgstr "Робоча інформація" #: apps/employees/admin.py:149 apps/it_manager/admin.py:307 msgid "Даты" -msgstr "" +msgstr "Дати" #: apps/employees/admin.py:153 templates/pages/employee-detail.html:126 msgid "Личные данные" @@ -155,81 +158,96 @@ msgstr "Контакти" #: apps/employees/admin.py:173 msgid "Графік та відвідуваність" -msgstr "" +msgstr "Графік та відвідуваність" #: apps/employees/admin.py:176 msgid "Графіки роботи та записи відвідуваності відображаються нижче." -msgstr "" +msgstr "Графіки роботи та записи відвідуваності відображаються нижче." #: apps/employees/admin.py:184 msgid "Системный доступ" -msgstr "" +msgstr "Системний доступ" #: apps/employees/admin.py:188 msgid "Только для IT-администраторов. is_staff — вход в Django admin." -msgstr "" +msgstr "Тільки для IT-адміністраторів. is_staff — вхід у Django admin." #: apps/employees/admin.py:232 msgid "Предпросмотр" -msgstr "" +msgstr "Попередній перегляд" #: apps/employees/admin.py:239 msgid "Группы" -msgstr "" +msgstr "Групи" -#: apps/employees/models/base.py:34 core/models/connector_config.py:29 +#: apps/employees/admin.py:266 +#| msgid "Офисных сотрудников" +msgid "Офисные сотрудники" +msgstr "Офісні співробітники" + +#: apps/employees/admin.py:269 +msgid "" +"При утверждении заявки на найм офисному сотруднику назначается рабочий " +"email: логин@домен." +msgstr "" +"При затвердженні заявки на найм офісному співробітнику призначається робочий" +" email: логін@домен." + +#: apps/employees/models/base.py:34 apps/hiring/models.py:19 +#: apps/hiring/pdf.py:52 core/models/connector_config.py:29 #: osc/apps/employees/models.py:33 msgid "Логин" -msgstr "" +msgstr "Логін" #: apps/employees/models/base.py:35 msgid "Уникальный логин для входа в систему" -msgstr "" +msgstr "Унікальний логін для входу в систему" #: apps/employees/models/base.py:38 osc/apps/employees/models.py:37 msgid "Email адрес" -msgstr "" +msgstr "Email адреса" #: apps/employees/models/base.py:39 osc/apps/employees/models.py:38 msgid "Рабочий email. Если не указан — генерируется из username@local.domain" -msgstr "" +msgstr "Робочий email. Якщо не вказано — генерується з username@local.domain" -#: apps/employees/models/base.py:42 osc/apps/employees/models.py:41 +#: apps/employees/models/base.py:42 apps/hiring/models.py:75 +#: osc/apps/employees/models.py:41 msgid "Фамилия" -msgstr "" +msgstr "Прізвище" -#: apps/employees/models/base.py:43 osc/apps/employees/models.py:42 -#: templates/pages/profile.html:76 +#: apps/employees/models/base.py:43 apps/hiring/models.py:76 +#: osc/apps/employees/models.py:42 templates/pages/profile.html:76 msgid "Имя" msgstr "Ім'я" -#: apps/employees/models/base.py:44 osc/apps/employees/models.py:43 -#: templates/pages/hr-portal.html:430 +#: apps/employees/models/base.py:44 apps/hiring/models.py:77 +#: osc/apps/employees/models.py:43 templates/pages/hr-portal.html:571 msgid "Отчество" msgstr "По батькові" #: apps/employees/models/base.py:46 osc/apps/employees/models.py:46 msgid "Доступ к панели администратора" -msgstr "" +msgstr "Доступ до панелі адміністратора" #: apps/employees/models/base.py:47 osc/apps/employees/models.py:47 msgid "Разрешает вход в Django admin. Только для IT-администраторов системы." -msgstr "" +msgstr "Дозволяє вхід у Django admin. Тільки для IT-адміністраторів системи." -#: apps/employees/models/base.py:48 core/models/directories.py:29 -#: core/models/directories.py:121 templates/pages/employee-detail.html:45 -#: templates/pages/employees.html:148 templates/pages/hr-portal.html:174 -#: templates/pages/structure.html:89 +#: apps/employees/models/base.py:48 apps/hiring/models.py:21 +#: core/models/directories.py:29 core/models/directories.py:121 +#: templates/pages/employee-detail.html:45 templates/pages/employees.html:148 +#: templates/pages/hr-portal.html:205 templates/pages/structure.html:89 msgid "Активный" msgstr "Активний" #: apps/employees/models/base.py:49 osc/apps/employees/models.py:49 msgid "Работает ли сотрудник сейчас" -msgstr "" +msgstr "Чи працює співробітник зараз" #: apps/employees/models/base.py:50 osc/apps/employees/models.py:50 msgid "Дата создания аккаунта" -msgstr "" +msgstr "Дата створення облікового запису" #: apps/employees/models/base.py:53 osc/apps/employees/models.py:52 #: templates/pages/employee-detail.html:129 @@ -243,7 +261,7 @@ msgstr "ІПН" #: apps/employees/models/base.py:58 osc/apps/employees/models.py:55 msgid "Идентификационный номер налогоплательщика" -msgstr "" +msgstr "Ідентифікаційний номер платника податків" #: apps/employees/models/base.py:63 osc/apps/employees/models.py:59 #: templates/pages/employee-detail.html:74 templates/pages/profile.html:57 @@ -252,11 +270,11 @@ msgstr "Робочий телефон" #: apps/employees/models/base.py:67 msgid "Внутрішній номер" -msgstr "" +msgstr "Внутрішній номер" #: apps/employees/models/base.py:68 msgid "Короткий внутрішній (додатковий) номер АТС" -msgstr "" +msgstr "Короткий внутрішній (додатковий) номер АТС" #: apps/employees/models/base.py:71 osc/apps/employees/models.py:60 #: templates/pages/employee-detail.html:83 templates/pages/profile.html:61 @@ -270,11 +288,11 @@ msgstr "Особистий email" #: apps/employees/models/base.py:76 osc/apps/employees/models.py:63 msgid "Контакт в экстренных случаях" -msgstr "" +msgstr "Контакт в екстрених випадках" #: apps/employees/models/base.py:77 osc/apps/employees/models.py:65 msgid "Телефон экстренного контакта" -msgstr "" +msgstr "Телефон екстреного контакту" #: apps/employees/models/base.py:79 osc/apps/employees/models.py:67 #: templates/pages/profile.html:84 @@ -282,59 +300,63 @@ msgid "Степень родства" msgstr "Ступінь спорідненості" #: apps/employees/models/base.py:82 apps/employees/models/base.py:101 -#: apps/hiring/models.py:34 apps/hiring/models.py:50 -#: apps/locations/admin.py:347 apps/locations/models/base.py:29 -#: apps/locations/models/pharmacies.py:210 +#: apps/hiring/models.py:86 apps/hiring/models.py:102 +#: apps/locations/admin.py:347 apps/locations/dislocation_columns.py:36 +#: apps/locations/models/base.py:29 apps/locations/models/pharmacies.py:210 #: apps/locations/models/pharmacies.py:349 apps/mail_connector/models.py:12 #: apps/nextcloud_connector/models.py:17 apps/utilities/models.py:17 -#: templates/pages/dislocation.html:116 templates/pages/employees.html:64 -#: templates/pages/employees.html:90 templates/pages/hr-portal.html:126 -#: templates/pages/hr-portal.html:441 templates/pages/hr-portal.html:455 -#: templates/pages/hr-portal.html:459 +#: templates/pages/employees.html:64 templates/pages/employees.html:90 +#: templates/pages/hr-portal.html:156 templates/pages/hr-portal.html:582 +#: templates/pages/hr-portal.html:600 msgid "Аптека" msgstr "Аптека" -#: apps/employees/models/base.py:83 apps/hiring/models.py:35 -#: apps/locations/models/base.py:30 templates/pages/hr-portal.html:128 -#: templates/pages/hr-portal.html:442 +#: apps/employees/models/base.py:83 apps/hiring/models.py:87 +#: apps/locations/models/base.py:30 templates/pages/hr-portal.html:158 +#: templates/pages/hr-portal.html:583 msgid "Склад" msgstr "Склад" -#: apps/employees/models/base.py:84 apps/locations/models/base.py:31 -#: templates/pages/employees.html:56 templates/pages/hr-portal.html:124 -#: templates/pages/hr-portal.html:440 templates/pages/timetrack.html:47 +#: apps/employees/models/base.py:84 apps/hiring/models.py:88 +#: apps/locations/models/base.py:31 templates/pages/employees.html:56 +#: templates/pages/hr-portal.html:154 templates/pages/hr-portal.html:581 +#: templates/pages/timetrack.html:47 msgid "Офис" msgstr "Офіс" -#: apps/employees/models/base.py:87 apps/locations/models/base.py:28 +#: apps/employees/models/base.py:87 apps/hiring/models.py:91 +#: apps/hiring/pdf.py:51 apps/hiring/services.py:99 +#: apps/locations/models/base.py:28 msgid "Тип локации" -msgstr "" +msgstr "Тип локації" #: apps/employees/models/base.py:88 msgid "Где работает: аптека / склад / офис" -msgstr "" +msgstr "Де працює: аптека / склад / офіс" -#: apps/employees/models/base.py:95 apps/locations/models/pharmacies.py:88 -#: templates/pages/dislocation.html:117 templates/pages/pharmacies.html:113 +#: apps/employees/models/base.py:95 apps/locations/dislocation_columns.py:37 +#: apps/locations/models/pharmacies.py:88 templates/pages/pharmacies.html:113 #: templates/pages/pharmacy-detail.html:87 msgid "Организация" msgstr "Організація" #: apps/employees/models/base.py:96 apps/locations/models/pharmacies.py:89 msgid "Юрлицо: АКС, АКС Одесса, Тира, Пентальфа..." -msgstr "" +msgstr "Юрособа: АКС, АКС Одеса, Тира, Пентальфа..." #: apps/employees/models/base.py:102 msgid "Заполняется только для location_type=pharmacy" -msgstr "" +msgstr "Заповнюється тільки для location_type=pharmacy" #: apps/employees/models/base.py:115 msgid "Заполняется для office / warehouse" -msgstr "" +msgstr "Заповнюється для office / warehouse" -#: apps/employees/models/base.py:120 osc/apps/employees/models.py:77 -#: templates/pages/dislocation.html:297 templates/pages/employees.html:104 -#: templates/pages/hr-portal.html:216 templates/pages/hr-portal.html:467 +#: apps/employees/models/base.py:120 apps/hiring/models.py:113 +#: apps/hiring/pdf.py:50 apps/hiring/services.py:98 +#: osc/apps/employees/models.py:77 templates/pages/dislocation.html:310 +#: templates/pages/employees.html:104 templates/pages/hr-portal.html:247 +#: templates/pages/hr-portal.html:617 msgid "Должность" msgstr "Посада" @@ -345,7 +367,7 @@ msgstr "Керівник" #: apps/employees/models/base.py:129 osc/apps/employees/models.py:84 msgid "Дата приёма на работу" -msgstr "" +msgstr "Дата прийому на роботу" #: apps/employees/models/base.py:130 osc/apps/employees/models.py:85 #: templates/pages/employee-detail.html:116 @@ -354,25 +376,25 @@ msgstr "Дата звільнення" #: apps/employees/models/base.py:131 osc/apps/employees/models.py:86 msgid "Конец испытательного срока" -msgstr "" +msgstr "Кінець випробувального терміну" #: apps/employees/models/base.py:134 apps/visit_manager/admin.py:262 #: apps/visit_manager/admin.py:289 osc/apps/employees/models.py:88 -#: templates/pages/hr-portal.html:165 templates/pages/hr-portal.html:234 +#: templates/pages/hr-portal.html:196 templates/pages/hr-portal.html:266 msgid "Фото" msgstr "Фото" #: apps/employees/models/base.py:138 osc/apps/employees/models.py:90 msgid "ID Mattermost" -msgstr "" +msgstr "ID Mattermost" #: apps/employees/models/base.py:139 osc/apps/employees/models.py:91 msgid "Логин Mattermost" -msgstr "" +msgstr "Логін Mattermost" #: apps/employees/models/base.py:141 osc/apps/employees/models.py:93 msgid "Статус синхронизации Mattermost" -msgstr "" +msgstr "Статус синхронізації Mattermost" #: apps/employees/models/base.py:143 apps/tasks/models.py:24 #: osc/apps/employees/models.py:95 templates/pages/employee-detail.html:155 @@ -385,112 +407,145 @@ msgstr "Очікує" #: apps/locations/models/pharmacies.py:151 osc/apps/employees/models.py:96 #: osc/apps/employees/models.py:114 msgid "Синхронизирован" -msgstr "" +msgstr "Синхронізовано" #: apps/employees/models/base.py:146 apps/employees/models/base.py:161 #: osc/apps/employees/models.py:98 osc/apps/employees/models.py:116 msgid "Отключена" -msgstr "" +msgstr "Вимкнено" #: apps/employees/models/base.py:150 osc/apps/employees/models.py:102 msgid "Последняя синхронизация Mattermost" -msgstr "" +msgstr "Остання синхронізація Mattermost" #: apps/employees/models/base.py:151 osc/apps/employees/models.py:103 msgid "Синхронизация Mattermost" -msgstr "" +msgstr "Синхронізація Mattermost" #: apps/employees/models/base.py:156 osc/apps/employees/models.py:111 msgid "Статус синхронизации AD" -msgstr "" +msgstr "Статус синхронізації AD" #: apps/employees/models/base.py:158 osc/apps/employees/models.py:113 msgid "Не синхронизирован" -msgstr "" +msgstr "Не синхронізовано" #: apps/employees/models/base.py:165 osc/apps/employees/models.py:120 msgid "Ошибка синхронизации AD" -msgstr "" +msgstr "Помилка синхронізації AD" #: apps/employees/models/base.py:166 osc/apps/employees/models.py:121 msgid "Последняя синхронизация AD" -msgstr "" +msgstr "Остання синхронізація AD" #: apps/employees/models/base.py:167 osc/apps/employees/models.py:122 msgid "Синхронизация AD" -msgstr "" +msgstr "Синхронізація AD" #: apps/employees/models/base.py:168 msgid "Только для офисных сотрудников" -msgstr "" +msgstr "Тільки для офісних співробітників" #: apps/employees/models/base.py:171 msgid "Записей на странице" -msgstr "" +msgstr "Записів на сторінці" #: apps/employees/models/base.py:173 msgid "Сколько строк показывать в таблицах портала по умолчанию" -msgstr "" +msgstr "Скільки рядків показувати в таблицях порталу за замовчуванням" #: apps/employees/models/base.py:177 -msgid "Был онлайн" -msgstr "" +#| msgid "Должность / локация" +msgid "Колонки дислокации" +msgstr "Колонки дислокації" -#: apps/employees/models/base.py:178 -msgid "Обновляется автоматически при активности в портале (не в AD/Mattermost)" -msgstr "" - -#: apps/employees/models/base.py:183 -msgid "TOTP-секрет" -msgstr "" - -#: apps/employees/models/base.py:184 -msgid "Секрет TOTP (шифруется). Заполняется при настройке 2FA." +#: apps/employees/models/base.py:180 +msgid "" +"Персональный набор колонок таблицы /dislocation/. Пусто — пресет отдела или " +"системный «Стандарт»." msgstr "" +"Персональний набір колонок таблиці /dislocation/. Порожньо — пресет відділу " +"або системний «Стандарт»." #: apps/employees/models/base.py:186 +msgid "Был онлайн" +msgstr "Був онлайн" + +#: apps/employees/models/base.py:187 +msgid "" +"Обновляется автоматически при активности в портале (не в AD/Mattermost)" +msgstr "Оновлюється автоматично при активності в порталі (не в AD/Mattermost)" + +#: apps/employees/models/base.py:192 +msgid "TOTP-секрет" +msgstr "TOTP-секрет" + +#: apps/employees/models/base.py:193 +msgid "Секрет TOTP (шифруется). Заполняется при настройке 2FA." +msgstr "Секрет TOTP (шифрується). Заповнюється при налаштуванні 2FA." + +#: apps/employees/models/base.py:195 msgid "2FA включена" -msgstr "" +msgstr "2FA увімкнено" -#: apps/employees/models/base.py:188 +#: apps/employees/models/base.py:197 msgid "Резервные коды 2FA (хеши)" -msgstr "" +msgstr "Резервні коди 2FA (хеші)" -#: apps/employees/models/base.py:189 +#: apps/employees/models/base.py:198 msgid "Хеши одноразовых резервных кодов; использованный код удаляется." -msgstr "" +msgstr "Хеші одноразових резервних кодів; використаний код видаляється." -#: apps/employees/models/base.py:199 osc/apps/employees/models.py:131 +#: apps/employees/models/base.py:208 osc/apps/employees/models.py:131 #: templates/index.html:251 templates/pages/dashboard.html:174 #: templates/pages/employees.html:8 templates/pages/employees.html:24 -#: templates/pages/hr-portal.html:92 +#: templates/pages/hr-portal.html:119 msgid "Сотрудники" msgstr "Працівники" +#: apps/employees/models/defaults.py:13 +#| msgid "Офисных сотрудников" +msgid "Домен email для офисных сотрудников" +msgstr "Домен email для офісних співробітників" + +#: apps/employees/models/defaults.py:17 +msgid "" +"При утверждении заявки на найм офисному сотруднику назначается email вида " +"логин@этот-домен (вместо @local.domain)." +msgstr "" +"При затвердженні заявки на найм офісному співробітнику призначається email " +"виду логін@цей-домен (замість @local.domain)." + +#: apps/employees/models/defaults.py:23 apps/employees/models/defaults.py:24 +#: apps/employees/models/defaults.py:27 +#| msgid "Активных сотрудников пока нет" +msgid "Настройки сотрудников по умолчанию" +msgstr "Налаштування співробітників за замовчуванням" + #: apps/employees/models/permissions.py:14 msgid "Группа" -msgstr "" +msgstr "Група" #: apps/employees/models/permissions.py:16 apps/it_manager/admin.py:611 #: core/models/directories.py:28 core/models/directories.py:120 -#: templates/pages/it-manager.html:558 templates/pages/it-manager.html:1102 +#: templates/pages/it-manager.html:561 templates/pages/it-manager.html:1105 #: templates/pages/task-detail.html:62 msgid "Описание" msgstr "Опис" #: apps/employees/models/permissions.py:17 apps/locations/models/base.py:59 -#: templates/pages/dislocation.html:180 templates/pages/dislocation.html:559 -#: templates/pages/pharmacies.html:156 +#: templates/pages/dislocation.html:572 templates/pages/pharmacies.html:156 +#: templates/partials/dislo_cell.html:40 msgid "Активна" msgstr "Активна" #: apps/employees/models/permissions.py:20 msgid "Группа разрешений" -msgstr "" +msgstr "Група дозволів" #: apps/employees/models/permissions.py:21 msgid "Группы разрешений" -msgstr "" +msgstr "Групи дозволів" #: apps/employees/models/permissions.py:41 templates/pages/it-manager.html:199 #: templates/pages/it-manager.html:220 @@ -499,22 +554,22 @@ msgstr "Видати" #: apps/employees/models/permissions.py:42 msgid "Отозвать" -msgstr "" +msgstr "Відкликати" #: apps/employees/models/permissions.py:51 msgid "Разрешение" -msgstr "" +msgstr "Дозвіл" #: apps/employees/models/permissions.py:54 apps/it_manager/admin.py:154 #: apps/it_manager/admin.py:241 apps/it_manager/admin.py:334 #: apps/locations/models/pharmacies.py:332 templates/pages/it-manager.html:144 -#: templates/pages/it-manager.html:620 +#: templates/pages/it-manager.html:623 msgid "Тип" msgstr "Тип" #: apps/employees/models/permissions.py:56 msgid "Действует с" -msgstr "" +msgstr "Діє з" #: apps/employees/models/permissions.py:57 #: templates/pages/pharmacy-detail.html:248 @@ -525,27 +580,27 @@ msgstr "Діє до" #: apps/locations/models/pharmacies.py:334 osc/apps/employees/models.py:49 #: osc/core/models/directory.py:32 msgid "Активен" -msgstr "" +msgstr "Активний" #: apps/employees/models/permissions.py:59 msgid "Причина / комментарий" -msgstr "" +msgstr "Причина / коментар" #: apps/employees/models/permissions.py:62 msgid "Override разрешения" -msgstr "" +msgstr "Override дозволу" #: apps/employees/models/permissions.py:63 msgid "Override разрешений" -msgstr "" +msgstr "Override дозволів" #: apps/feed/models.py:22 msgid "Загальне" -msgstr "" +msgstr "Загальне" #: apps/feed/models.py:25 msgid "Мережа" -msgstr "" +msgstr "Мережа" #: apps/feed/models.py:36 templates/pages/news-edit.html:26 #: templates/pages/news.html:29 @@ -554,47 +609,55 @@ msgstr "Заголовок" #: apps/feed/models.py:37 msgid "Текст" -msgstr "" +msgstr "Текст" #: apps/feed/models.py:39 msgid "Категорія" -msgstr "" +msgstr "Категорія" #: apps/feed/models.py:42 msgid "Важливо" -msgstr "" +msgstr "Важливо" #: apps/feed/models.py:43 msgid "Закріплено" -msgstr "" +msgstr "Закріплено" #: apps/feed/models.py:44 msgid "Опубліковано" -msgstr "" +msgstr "Опубліковано" #: apps/feed/models.py:47 msgid "Опубліковано в Mattermost" -msgstr "" +msgstr "Опубліковано в Mattermost" #: apps/feed/models.py:48 msgid "Час останньої публікації в чат — не блокує повторну публікацію" -msgstr "" +msgstr "Час останньої публікації в чат — не блокує повторну публікацію" #: apps/feed/models.py:51 msgid "ID повідомлень в Mattermost" -msgstr "" +msgstr "ID повідомлень в Mattermost" #: apps/feed/models.py:52 -msgid "{team_name: post_id} — за наявності id повторна публікація редагує існуюче повідомлення" +msgid "" +"{team_name: post_id} — за наявності id повторна публікація редагує існуюче " +"повідомлення" msgstr "" +"{team_name: post_id} — за наявності id повторна публікація редагує існуюче " +"повідомлення" #: apps/feed/models.py:55 msgid "Куди публікувати в Mattermost" -msgstr "" +msgstr "Куди публікувати в Mattermost" #: apps/feed/models.py:56 -msgid "Список команд (akc/pharmacys/warehouse) — обирається чекбоксами при створенні/редагуванні" +msgid "" +"Список команд (akc/pharmacys/warehouse) — обирається чекбоксами при " +"створенні/редагуванні" msgstr "" +"Список команд (akc/pharmacys/warehouse) — обирається чекбоксами при " +"створенні/редагуванні" #: apps/feed/models.py:61 apps/feed/models.py:100 apps/tasks/models.py:126 #: templates/pages/task-detail.html:200 @@ -603,165 +666,241 @@ msgstr "Автор" #: apps/feed/models.py:65 apps/feed/models.py:96 msgid "Пост" -msgstr "" +msgstr "Пост" #: apps/feed/models.py:66 msgid "Пости" -msgstr "" +msgstr "Пости" #: apps/feed/models.py:102 apps/feed/models.py:115 apps/tasks/models.py:128 #: apps/utilities/models.py:26 msgid "Коментар" -msgstr "" +msgstr "Коментар" #: apps/feed/models.py:109 msgid "ID відповідей в Mattermost" -msgstr "" +msgstr "ID відповідей в Mattermost" #: apps/feed/models.py:111 msgid "ID вихідного повідомлення з Mattermost" -msgstr "" +msgstr "ID вихідного повідомлення з Mattermost" #: apps/feed/models.py:116 msgid "Коментарі" -msgstr "" - -#: apps/hiring/models.py:17 -msgid "На випробуванні" -msgstr "" +msgstr "Коментарі" #: apps/hiring/models.py:18 -msgid "Затверджено" -msgstr "" +msgid "Метка" +msgstr "Мітка" -#: apps/hiring/models.py:19 -msgid "Відхилено" -msgstr "" - -#: apps/hiring/models.py:21 -#, python-format -msgid "%(n)d дн." -msgstr "" - -#: apps/hiring/models.py:23 -msgid "Прізвище" -msgstr "" - -#: apps/hiring/models.py:24 -msgid "Ім'я" -msgstr "" +#: apps/hiring/models.py:20 apps/hiring/pdf.py:53 +#: core/models/connector_config.py:32 +msgid "Пароль" +msgstr "Пароль" #: apps/hiring/models.py:25 -msgid "По батькові" -msgstr "" +msgid "Назначено заявке" +msgstr "Призначено заявці" -#: apps/hiring/models.py:27 -msgid "Особистий email кандидата" -msgstr "" +#: apps/hiring/models.py:29 apps/hiring/services.py:103 +msgid "Интерн-аккаунт" +msgstr "Інтерн-обліковий запис" -#: apps/hiring/models.py:29 templates/pages/hr-portal.html:495 +#: apps/hiring/models.py:30 +msgid "Пул интерн-аккаунтов" +msgstr "Пул інтерн-облікових записів" + +#: apps/hiring/models.py:34 +msgid "занят" +msgstr "зайнятий" + +#: apps/hiring/models.py:34 +msgid "свободен" +msgstr "вільний" + +#: apps/hiring/models.py:49 +msgid "" +"Нет свободных интерн-аккаунтов. Дождитесь завершения стажировки или добавьте" +" слот в админке." +msgstr "" +"Немає вільних інтерн-облікових записів. Дочекайтеся завершення стажування " +"або додайте слот в адмінці." + +#: apps/hiring/models.py:69 templates/pages/hr-portal.html:121 +#: templates/pages/hr-portal.html:274 templates/pages/hr-portal.html:333 +#: templates/pages/hr-portal.html:727 +msgid "На испытании" +msgstr "На випробуванні" + +#: apps/hiring/models.py:70 templates/pages/hr-portal.html:467 +#: templates/pages/hr-portal.html:730 +msgid "Утверждено" +msgstr "Затверджено" + +#: apps/hiring/models.py:71 templates/pages/hr-portal.html:732 +msgid "Отклонено" +msgstr "Відхилено" + +#: apps/hiring/models.py:73 +#, python-format +msgid "%(n)d дн." +msgstr "%(n)d дн." + +#: apps/hiring/models.py:79 +msgid "Личный email кандидата" +msgstr "Особистий email кандидата" + +#: apps/hiring/models.py:81 templates/pages/hr-portal.html:645 msgid "Телефон кандидата" msgstr "Телефон кандидата" -#: apps/hiring/models.py:36 -msgid "Офіс" -msgstr "" - -#: apps/hiring/models.py:39 apps/visit_manager/admin.py:284 -msgid "Тип локації" -msgstr "" - -#: apps/hiring/models.py:45 -msgid "Відділ" -msgstr "" - -#: apps/hiring/models.py:56 -msgid "Посада" -msgstr "" - -#: apps/hiring/models.py:59 +#: apps/hiring/models.py:116 msgid "Дата виходу" -msgstr "" +msgstr "Дата виходу" -#: apps/hiring/models.py:61 -msgid "Термін випробування, днів" -msgstr "" +#: apps/hiring/models.py:118 +msgid "Срок испытания, дней" +msgstr "Термін випробування, днів" -#: apps/hiring/models.py:68 -msgid "Потрібні доступи" -msgstr "" +#: apps/hiring/models.py:125 apps/hiring/services.py:105 +msgid "Требуемые доступы" +msgstr "Потрібні доступи" -#: apps/hiring/models.py:70 -msgid "Примітка щодо доступів" -msgstr "" +#: apps/hiring/models.py:127 templates/pages/hr-portal.html:666 +msgid "Примечание по доступам" +msgstr "Примітка щодо доступів" -#: apps/hiring/models.py:76 -msgid "Подав HR" -msgstr "" +#: apps/hiring/models.py:133 +msgid "Подал HR" +msgstr "Подав HR" -#: apps/hiring/models.py:81 -msgid "Обліковий запис" -msgstr "" +#: apps/hiring/models.py:138 +msgid "Учётная запись" +msgstr "Обліковий запис" -#: apps/hiring/models.py:86 -msgid "Рішення прийняв" -msgstr "" +#: apps/hiring/models.py:143 +msgid "Решение принял" +msgstr "Рішення прийняв" -#: apps/hiring/models.py:88 -msgid "Дата рішення" -msgstr "" +#: apps/hiring/models.py:145 +msgid "Дата решения" +msgstr "Дата рішення" -#: apps/hiring/models.py:89 -msgid "Коментар до рішення" -msgstr "" +#: apps/hiring/models.py:146 +msgid "Комментарий к решению" +msgstr "Коментар до рішення" -#: apps/hiring/models.py:94 +#: apps/hiring/models.py:151 msgid "Заявка в IT" -msgstr "" +msgstr "Заявка в IT" -#: apps/hiring/models.py:99 -msgid "Задача на видачу доступів" -msgstr "" +#: apps/hiring/models.py:156 +msgid "Задача на выдачу доступов" +msgstr "Задача на видачу доступів" -#: apps/hiring/models.py:103 +#: apps/hiring/models.py:161 +msgid "Выданный пароль (PDF)" +msgstr "Виданий пароль (PDF)" + +#: apps/hiring/models.py:165 msgid "Заявка на найм" -msgstr "" +msgstr "Заявка на найм" -#: apps/hiring/models.py:104 +#: apps/hiring/models.py:166 msgid "Заявки на найм" +msgstr "Заявки на найм" + +#: apps/hiring/pdf.py:41 +msgid "Учётные данные сотрудника" +msgstr "Облікові дані співробітника" + +#: apps/hiring/pdf.py:54 templates/pages/employee-detail.html:70 +msgid "Рабочий email" +msgstr "Робочий email" + +#: apps/hiring/pdf.py:56 +msgid "Запрошенные доступы" +msgstr "Запитані доступи" + +#: apps/hiring/pdf.py:63 apps/hiring/services.py:112 +msgid "(не указано)" +msgstr "(не вказано)" + +#: apps/hiring/services.py:97 templates/pages/hr-portal.html:318 +#: templates/pages/hr-portal.html:686 +msgid "Кандидат" +msgstr "Кандидат" + +#: apps/hiring/services.py:100 +msgid "Локация / отдел" +msgstr "Локація / відділ" + +#: apps/hiring/services.py:101 templates/pages/hr-portal.html:626 +msgid "Дата выхода" +msgstr "Дата виходу" + +#: apps/hiring/services.py:102 templates/pages/employee-detail.html:49 +msgid "Испытательный срок" +msgstr "Випробувальний термін" + +#: apps/hiring/services.py:102 templates/pages/hr-portal.html:482 +#: templates/pages/hr-portal.html:491 templates/pages/hr-portal.html:500 +#: templates/pages/hr-portal.html:633 +msgid "дн." +msgstr "дн." + +#: apps/hiring/services.py:103 +msgid "логин" +msgstr "логін" + +#: apps/hiring/services.py:115 +msgid "Примечание" +msgstr "Примітка" + +#: apps/hiring/services.py:118 +msgid "" +"Подготовьте рабочее место и выдайте доступы к интерн-аккаунту на время " +"испытания. После утверждения HR будет создан персональный аккаунт." msgstr "" +"Підготуйте робоче місце та видайте доступи до інтерн-облікового запису на " +"час випробування. Після затвердження HR буде створено персональний акаунт." + +#: apps/hiring/services.py:129 +msgid "Подготовка рабочего места" +msgstr "Підготовка робочого місця" #: apps/it_manager/admin.py:34 apps/it_manager/admin.py:464 #: apps/it_manager/admin.py:567 apps/it_manager/admin.py:581 #: apps/locations/models/base.py:65 apps/visit_manager/admin.py:227 -#: templates/pages/hr-portal.html:218 templates/pages/it-manager.html:235 -#: templates/pages/it-manager.html:499 templates/pages/it-manager.html:559 -#: templates/pages/it-manager.html:880 +#: templates/pages/hr-portal.html:249 templates/pages/it-manager.html:235 +#: templates/pages/it-manager.html:502 templates/pages/it-manager.html:562 +#: templates/pages/it-manager.html:883 msgid "Локация" msgstr "Локація" #: apps/it_manager/admin.py:91 msgid "Экземпляров" -msgstr "" +msgstr "Примірників" #: apps/it_manager/admin.py:110 templates/pages/it-manager.html:13 -#: templates/pages/it-manager.html:514 templates/pages/pharmacy-detail.html:195 +#: templates/pages/it-manager.html:517 +#: templates/pages/pharmacy-detail.html:195 msgid "Оборудование" msgstr "Обладнання" #: apps/it_manager/admin.py:113 apps/it_manager/admin.py:206 msgid "Статус и размещение" -msgstr "" +msgstr "Статус і розміщення" #: apps/it_manager/admin.py:116 msgid "Гарантия и закупка" -msgstr "" +msgstr "Гарантія та закупівля" #: apps/it_manager/admin.py:119 apps/it_manager/admin.py:212 #: apps/it_manager/admin.py:427 apps/it_manager/admin.py:476 #: apps/locations/admin.py:319 apps/locations/models/pharmacies.py:335 -#: core/models/connector_config.py:44 templates/pages/it-manager.html:674 -#: templates/pages/it-manager.html:845 templates/pages/it-manager.html:950 +#: core/models/connector_config.py:44 templates/pages/it-manager.html:677 +#: templates/pages/it-manager.html:848 templates/pages/it-manager.html:953 #: templates/pages/pharmacy-detail.html:98 msgid "Примечания" msgstr "Примітки" @@ -770,7 +909,7 @@ msgstr "Примітки" #: apps/it_manager/admin.py:480 apps/it_manager/admin.py:617 #: apps/it_manager/admin.py:673 apps/locations/admin.py:109 msgid "Служебное" -msgstr "" +msgstr "Службове" #: apps/it_manager/admin.py:159 templates/pages/it-manager.html:163 msgid "Модель" @@ -782,18 +921,18 @@ msgstr "Гарантія" #: apps/it_manager/admin.py:203 msgid "Расходник" -msgstr "" +msgstr "Витратний матеріал" #: apps/it_manager/admin.py:209 msgid "Эксплуатация" -msgstr "" +msgstr "Експлуатація" -#: apps/it_manager/admin.py:254 templates/pages/it-manager.html:629 -#: templates/pages/it-manager.html:694 +#: apps/it_manager/admin.py:254 templates/pages/it-manager.html:632 +#: templates/pages/it-manager.html:697 msgid "Цвет" msgstr "Колір" -#: apps/it_manager/admin.py:295 templates/pages/it-manager.html:523 +#: apps/it_manager/admin.py:295 templates/pages/it-manager.html:526 msgid "Заявка" msgstr "Заявка" @@ -804,11 +943,11 @@ msgstr "Учасники" #: apps/it_manager/admin.py:301 apps/it_manager/admin.py:614 #: apps/it_manager/admin.py:670 msgid "Связи" -msgstr "" +msgstr "Зв’язки" #: apps/it_manager/admin.py:304 msgid "Описание и решение" -msgstr "" +msgstr "Опис і рішення" #: apps/it_manager/admin.py:343 templates/pages/it-manager.html:346 msgid "Приоритет" @@ -816,7 +955,7 @@ msgstr "Пріоритет" #: apps/it_manager/admin.py:359 msgid "SLA" -msgstr "" +msgstr "SLA" #: apps/it_manager/admin.py:366 templates/pages/task-detail.html:143 msgid "Взять в работу" @@ -824,66 +963,66 @@ msgstr "Взяти в роботу" #: apps/it_manager/admin.py:375 msgid "Отметить решёнными" -msgstr "" +msgstr "Позначити вирішеними" #: apps/it_manager/admin.py:383 templates/pages/chat.html:88 #: templates/pages/chat.html:112 templates/pages/it-manager.html:75 -#: templates/pages/it-manager.html:135 templates/pages/it-manager.html:480 -#: templates/pages/it-manager.html:614 templates/pages/it-manager.html:815 -#: templates/pages/it-manager.html:865 templates/pages/it-manager.html:1048 +#: templates/pages/it-manager.html:135 templates/pages/it-manager.html:483 +#: templates/pages/it-manager.html:617 templates/pages/it-manager.html:818 +#: templates/pages/it-manager.html:868 templates/pages/it-manager.html:1051 msgid "Закрыть" msgstr "Закрити" -#: apps/it_manager/admin.py:418 templates/pages/it-manager.html:871 +#: apps/it_manager/admin.py:418 templates/pages/it-manager.html:874 msgid "Провайдер" msgstr "Провайдер" #: apps/it_manager/admin.py:424 msgid "Техподдержка" -msgstr "" +msgstr "Техпідтримка" #: apps/it_manager/admin.py:440 msgid "Активных договоров" -msgstr "" +msgstr "Активних договорів" #: apps/it_manager/admin.py:460 msgid "Договор" -msgstr "" +msgstr "Договір" #: apps/it_manager/admin.py:467 msgid "Параметры" -msgstr "" +msgstr "Параметри" #: apps/it_manager/admin.py:470 msgid "Сроки" -msgstr "" +msgstr "Терміни" -#: apps/it_manager/admin.py:473 templates/pages/it-manager.html:944 +#: apps/it_manager/admin.py:473 templates/pages/it-manager.html:947 msgid "IP-адреса" msgstr "IP-адреси" #: apps/it_manager/admin.py:503 msgid "Услуга" -msgstr "" +msgstr "Послуга" #: apps/it_manager/admin.py:508 templates/pages/pharmacy-detail.html:245 msgid "Технология" msgstr "Технологія" -#: apps/it_manager/admin.py:520 templates/pages/it-manager.html:972 +#: apps/it_manager/admin.py:520 templates/pages/it-manager.html:975 msgid "Окончание" msgstr "Закінчення" #: apps/it_manager/admin.py:534 msgid "Статические IP" -msgstr "" +msgstr "Статичні IP" #: apps/it_manager/admin.py:608 templates/pages/employee-detail.html:96 msgid "Работа" msgstr "Робота" -#: apps/it_manager/admin.py:639 templates/pages/it-manager.html:490 -#: templates/pages/it-manager.html:557 +#: apps/it_manager/admin.py:639 templates/pages/it-manager.html:493 +#: templates/pages/it-manager.html:560 msgid "Вид работ" msgstr "Вид робіт" @@ -893,41 +1032,41 @@ msgstr "Обращення" #: apps/it_manager/admin.py:667 msgid "Содержание" -msgstr "" +msgstr "Зміст" -#: apps/it_manager/admin.py:690 templates/pages/it-manager.html:1063 -#: templates/pages/it-manager.html:1100 +#: apps/it_manager/admin.py:690 templates/pages/it-manager.html:1066 +#: templates/pages/it-manager.html:1103 msgid "Канал" msgstr "Канал" -#: apps/it_manager/admin.py:695 templates/pages/it-manager.html:1103 +#: apps/it_manager/admin.py:695 templates/pages/it-manager.html:1106 msgid "Длительность" msgstr "Тривалість" #: apps/locations/admin.py:40 apps/locations/models/base.py:54 msgid "Координаты на карте" -msgstr "" +msgstr "Координати на карті" #: apps/locations/admin.py:45 msgid "Без координат" -msgstr "" +msgstr "Без координат" #: apps/locations/admin.py:46 msgid "С координатами" -msgstr "" +msgstr "З координатами" #: apps/locations/admin.py:64 apps/locations/admin.py:203 #: apps/locations/models/base.py:36 msgid "Город/район" -msgstr "" +msgstr "Місто/район" #: apps/locations/admin.py:65 apps/locations/admin.py:205 msgid "— Выберите регион —" -msgstr "" +msgstr "— Оберіть регіон —" #: apps/locations/admin.py:96 msgid "Тип и регион" -msgstr "" +msgstr "Тип і регіон" #: apps/locations/admin.py:99 apps/locations/admin.py:282 #: templates/pages/employees.html:106 templates/pages/pharmacies.html:114 @@ -942,86 +1081,140 @@ msgstr "Координати" #: apps/locations/admin.py:105 msgid "Прочее" -msgstr "" +msgstr "Інше" #: apps/locations/admin.py:132 msgid "Регион / Адрес" -msgstr "" +msgstr "Регіон / Адреса" #: apps/locations/admin.py:148 msgid "Статус лицензии" -msgstr "" +msgstr "Статус ліцензії" #: apps/locations/admin.py:153 msgid "Действительна" -msgstr "" +msgstr "Дійсна" #: apps/locations/admin.py:154 msgid "Истекает (30 дн.)" -msgstr "" +msgstr "Спливає (30 дн.)" #: apps/locations/admin.py:155 msgid "Истекла" -msgstr "" +msgstr "Спливла" #: apps/locations/admin.py:156 msgid "Нет лицензии" -msgstr "" +msgstr "Немає ліцензії" -#: apps/locations/admin.py:274 core/admin.py:253 +#: apps/locations/admin.py:274 core/admin.py:265 msgid "Основное" -msgstr "" +msgstr "Основне" #: apps/locations/admin.py:295 apps/locations/admin.py:366 +#: apps/locations/dislocation_columns.py:47 #: templates/pages/pharmacy-detail.html:54 msgid "Лицензия" msgstr "Ліцензія" -#: apps/locations/admin.py:302 templates/pages/dislocation.html:119 -#: templates/pages/dislocation.html:557 templates/pages/pharmacy-detail.html:44 +#: apps/locations/admin.py:302 apps/locations/dislocation_columns.py:39 +#: templates/pages/dislocation.html:570 +#: templates/pages/pharmacy-detail.html:44 msgid "Персонал" -msgstr "" +msgstr "Персонал" #: apps/locations/admin.py:309 apps/locations/models/pharmacies.py:156 msgid "Почта Carbonio" -msgstr "" +msgstr "Пошта Carbonio" #: apps/locations/admin.py:316 -msgid "Пусто в «Почта Carbonio» — адрес сгенерируется как <префикс организации><номер>@домен. Можно задать вручную." +msgid "" +"Пусто в «Почта Carbonio» — адрес сгенерируется как <префикс " +"организации><номер>@домен. Можно задать вручную." msgstr "" +"Порожньо в «Пошта Carbonio» — адреса згенерується як <префікс " +"організації><номер>@домен. Можна задати вручну." -#: apps/locations/admin.py:376 core/views.py:352 +#: apps/locations/admin.py:376 core/views.py:396 #: templates/pages/structure.html:61 msgid "Сотрудников" -msgstr "" +msgstr "Співробітників" #: apps/locations/admin.py:390 msgid "Телефоны" -msgstr "" +msgstr "Телефони" + +#: apps/locations/dislocation_columns.py:38 +#: templates/pages/pharmacies.html:115 templates/pages/pharmacy-detail.html:49 +#: templates/pages/pharmacy-detail.html:141 +msgid "Заведующий" +msgstr "Завідуючий" + +#: apps/locations/dislocation_columns.py:40 +#: apps/locations/models/pharmacies.py:143 +#: apps/locations/models/pharmacies.py:330 templates/pages/it-manager.html:828 +#: templates/pages/pharmacies.html:117 templates/pages/profile.html:80 +msgid "Телефон" +msgstr "Телефон" + +#: apps/locations/dislocation_columns.py:41 +#: templates/pages/pharmacies.html:118 templates/pages/pharmacy-detail.html:59 +msgid "Режим работы" +msgstr "Режим роботи" + +#: apps/locations/dislocation_columns.py:43 +#: templates/pages/pharmacy-detail.html:88 +msgid "Регион" +msgstr "Регіон" + +#: apps/locations/dislocation_columns.py:44 +#: templates/pages/pharmacy-detail.html:90 +msgid "Категория" +msgstr "Категорія" + +#: apps/locations/dislocation_columns.py:45 +#: apps/locations/models/pharmacies.py:119 +#: templates/pages/pharmacy-detail.html:93 +msgid "Куратор" +msgstr "Куратор" + +#: apps/locations/dislocation_columns.py:46 +#: apps/locations/models/pharmacies.py:113 +#: templates/pages/pharmacy-detail.html:92 +msgid "Ответственный менеджер" +msgstr "Відповідальний менеджер" + +#: apps/locations/dislocation_columns.py:48 +msgid "ПДВ" +msgstr "ПДВ" + +#: apps/locations/dislocation_columns.py:49 +msgid "Email" +msgstr "Email" #: apps/locations/models/base.py:39 msgid "Улица" -msgstr "" +msgstr "Вулиця" #: apps/locations/models/base.py:40 msgid "Номер дома" -msgstr "" +msgstr "Номер будинку" #: apps/locations/models/base.py:41 msgid "Корпус/строение" -msgstr "" +msgstr "Корпус/будівля" #: apps/locations/models/base.py:42 msgid "Помещение/офис" -msgstr "" +msgstr "Приміщення/офіс" #: apps/locations/models/base.py:43 msgid "Полный адрес" -msgstr "" +msgstr "Повна адреса" #: apps/locations/models/base.py:44 msgid "Заполняется автоматически из полей выше" -msgstr "" +msgstr "Заповнюється автоматично з полів вище" #: apps/locations/models/base.py:60 templates/pages/news-detail.html:78 #: templates/pages/task-detail.html:98 @@ -1030,11 +1223,57 @@ msgstr "Коментарі" #: apps/locations/models/base.py:66 msgid "Локации" +msgstr "Локації" + +#: apps/locations/models/dislocation.py:14 core/admin.py:195 +#: core/models/directories.py:119 osc/core/models/directory.py:9 +#: osc/core/models/directory.py:26 templates/pages/it-manager.html:824 +msgid "Название" +msgstr "Назва" + +#: apps/locations/models/dislocation.py:15 core/models/directories.py:118 +#: osc/core/models/directory.py:8 osc/core/models/directory.py:25 +msgid "Код" +msgstr "Код" + +#: apps/locations/models/dislocation.py:17 templates/pages/dislocation.html:98 +msgid "Колонки" +msgstr "Колонки" + +#: apps/locations/models/dislocation.py:19 +msgid "Упорядоченный список ключей из реестра колонок дислокации." +msgstr "Упорядкований список ключів з реєстру колонок дислокації." + +#: apps/locations/models/dislocation.py:27 +msgid "Если задан — пресет по умолчанию для сотрудников этого отдела." msgstr "" +"Якщо задано — пресет за замовчуванням для співробітників цього відділу." + +#: apps/locations/models/dislocation.py:30 +#| msgid "Примечания" +msgid "По умолчанию" +msgstr "За замовчуванням" + +#: apps/locations/models/dislocation.py:31 +msgid "" +"Используется, если у сотрудника нет своего набора и нет пресета отдела." +msgstr "" +"Використовується, якщо у співробітника немає власного набору і немає пресету" +" відділу." + +#: apps/locations/models/dislocation.py:35 +#| msgid "персонал по каждой локации" +msgid "Пресет колонок дислокации" +msgstr "Пресет колонок дислокації" + +#: apps/locations/models/dislocation.py:36 +#| msgid "персонал по каждой локации" +msgid "Пресеты колонок дислокации" +msgstr "Пресети колонок дислокації" #: apps/locations/models/pharmacies.py:60 msgid "Открыта" -msgstr "" +msgstr "Відкрита" #: apps/locations/models/pharmacies.py:61 templates/pages/it-manager.html:377 msgid "Закрыта" @@ -1042,140 +1281,127 @@ msgstr "Закрита" #: apps/locations/models/pharmacies.py:62 msgid "Остановлена" -msgstr "" +msgstr "Зупинена" #: apps/locations/models/pharmacies.py:63 msgid "Подготовка к запуску" -msgstr "" +msgstr "Підготовка до запуску" #: apps/locations/models/pharmacies.py:74 msgid "ПДВ 20%" -msgstr "" +msgstr "ПДВ 20%" #: apps/locations/models/pharmacies.py:75 msgid "ПДВ 7% (пільгова)" -msgstr "" +msgstr "ПДВ 7% (пільгова)" #: apps/locations/models/pharmacies.py:76 msgid "ПДВ 0%" -msgstr "" +msgstr "ПДВ 0%" #: apps/locations/models/pharmacies.py:77 msgid "Без ПДВ" -msgstr "" +msgstr "Без ПДВ" #: apps/locations/models/pharmacies.py:82 msgid "Номер аптеки" -msgstr "" +msgstr "Номер аптеки" #: apps/locations/models/pharmacies.py:83 msgid "Числовой номер аптеки внутри организации" -msgstr "" +msgstr "Числовий номер аптеки всередині організації" #: apps/locations/models/pharmacies.py:99 msgid "Номер лицензии" -msgstr "" +msgstr "Номер ліцензії" #: apps/locations/models/pharmacies.py:101 msgid "Дата окончания лицензии" -msgstr "" +msgstr "Дата закінчення ліцензії" #: apps/locations/models/pharmacies.py:108 msgid "Заведующий аптекой" -msgstr "" - -#: apps/locations/models/pharmacies.py:113 -#: templates/pages/pharmacy-detail.html:92 -msgid "Ответственный менеджер" -msgstr "Відповідальний менеджер" +msgstr "Завідувач аптеки" #: apps/locations/models/pharmacies.py:114 msgid "Сотрудник с должностью «Відповідальний менеджер»" -msgstr "" - -#: apps/locations/models/pharmacies.py:119 -#: templates/pages/pharmacy-detail.html:93 -msgid "Куратор" -msgstr "Куратор" +msgstr "Співробітник з посадою «Відповідальний менеджер»" #: apps/locations/models/pharmacies.py:120 msgid "Сотрудник с должностью «куратор»" -msgstr "" +msgstr "Співробітник з посадою «куратор»" #: apps/locations/models/pharmacies.py:123 msgid "Количество фармацевтов" -msgstr "" +msgstr "Кількість фармацевтів" #: apps/locations/models/pharmacies.py:124 msgid "Кешированное поле — обновляется при синхронизации" -msgstr "" +msgstr "Кешоване поле — оновлюється при синхронізації" #: apps/locations/models/pharmacies.py:133 msgid "Категория аптеки" -msgstr "" +msgstr "Категорія аптеки" #: apps/locations/models/pharmacies.py:135 msgid "Классификация аптеки: A/B/C/D" -msgstr "" +msgstr "Класифікація аптеки: A/B/C/D" #: apps/locations/models/pharmacies.py:139 msgid "Категорія ПДВ" -msgstr "" - -#: apps/locations/models/pharmacies.py:143 -#: apps/locations/models/pharmacies.py:330 templates/pages/dislocation.html:120 -#: templates/pages/it-manager.html:825 templates/pages/pharmacies.html:117 -#: templates/pages/profile.html:80 -msgid "Телефон" -msgstr "Телефон" +msgstr "Категорія ПДВ" #: apps/locations/models/pharmacies.py:145 msgid "Часы работы" -msgstr "" +msgstr "Години роботи" #: apps/locations/models/pharmacies.py:150 msgid "Ожидает синхронизации" -msgstr "" +msgstr "Очікує синхронізації" #: apps/locations/models/pharmacies.py:153 msgid "Отключён" -msgstr "" +msgstr "Вимкнено" #: apps/locations/models/pharmacies.py:157 -msgid "Ящик аптеки. Пусто — сгенерируется как <префикс организации><номер>@домен. Можно задать вручную." +msgid "" +"Ящик аптеки. Пусто — сгенерируется как <префикс организации><номер>@домен. " +"Можно задать вручную." msgstr "" +"Скринька аптеки. Порожньо — згенерується як <префікс " +"організації><номер>@домен. Можна задати вручну." #: apps/locations/models/pharmacies.py:160 msgid "ID аккаунта Carbonio" -msgstr "" +msgstr "ID облікового запису Carbonio" #: apps/locations/models/pharmacies.py:164 msgid "Пароль ящика Carbonio" -msgstr "" +msgstr "Пароль скриньки Carbonio" #: apps/locations/models/pharmacies.py:165 msgid "Хранится зашифрованным; показывается для входа заведующему." -msgstr "" +msgstr "Зберігається зашифрованим; показується для входу завідувачу." #: apps/locations/models/pharmacies.py:168 msgid "Синхронизация с Carbonio" -msgstr "" +msgstr "Синхронізація з Carbonio" #: apps/locations/models/pharmacies.py:169 msgid "Управлять почтовым ящиком аптеки в Carbonio" -msgstr "" +msgstr "Керувати поштовою скринькою аптеки в Carbonio" #: apps/locations/models/pharmacies.py:172 msgid "Статус синхронизации Carbonio" -msgstr "" +msgstr "Статус синхронізації Carbonio" #: apps/locations/models/pharmacies.py:176 msgid "Последняя синхронизация Carbonio" -msgstr "" +msgstr "Остання синхронізація Carbonio" #: apps/locations/models/pharmacies.py:177 msgid "Ошибка синхронизации Carbonio" -msgstr "" +msgstr "Помилка синхронізації Carbonio" #: apps/locations/models/pharmacies.py:211 templates/index.html:382 #: templates/pages/dislocation.html:16 templates/pages/pharmacies.html:6 @@ -1185,7 +1411,7 @@ msgstr "Аптеки" #: apps/locations/models/pharmacies.py:259 msgid "Такой адрес Carbonio уже используется другой аптекой" -msgstr "" +msgstr "Така адреса Carbonio вже використовується іншою аптекою" #: apps/locations/models/pharmacies.py:325 #: templates/pages/pharmacy-detail.html:113 @@ -1194,95 +1420,95 @@ msgstr "Основний" #: apps/locations/models/pharmacies.py:326 msgid "Мобильный" -msgstr "" +msgstr "Мобільний" #: apps/locations/models/pharmacies.py:333 msgid "Основной номер" -msgstr "" +msgstr "Основний номер" #: apps/locations/models/pharmacies.py:338 msgid "Телефон аптеки" -msgstr "" +msgstr "Телефон аптеки" #: apps/locations/models/pharmacies.py:339 msgid "Телефоны аптек" -msgstr "" +msgstr "Телефони аптек" #: apps/locations/models/pharmacies.py:350 msgid "Изменённое поле" -msgstr "" +msgstr "Змінене поле" #: apps/locations/models/pharmacies.py:351 msgid "Старое значение" -msgstr "" +msgstr "Старе значення" #: apps/locations/models/pharmacies.py:352 msgid "Новое значение" -msgstr "" +msgstr "Нове значення" #: apps/locations/models/pharmacies.py:353 msgid "Дата изменения" -msgstr "" +msgstr "Дата зміни" #: apps/locations/models/pharmacies.py:356 msgid "Кто изменил" -msgstr "" +msgstr "Хто змінив" #: apps/locations/models/pharmacies.py:361 msgid "История изменений аптеки" -msgstr "" +msgstr "Історія змін аптеки" #: apps/locations/models/pharmacies.py:362 msgid "История изменений аптек" -msgstr "" +msgstr "Історія змін аптек" #: apps/mail_connector/models.py:30 msgid "Запись синхронизации Carbonio" -msgstr "" +msgstr "Запис синхронізації Carbonio" #: apps/mail_connector/models.py:31 msgid "Журнал синхронизации Carbonio" -msgstr "" +msgstr "Журнал синхронізації Carbonio" #: apps/nextcloud_connector/models.py:20 msgid "Создание папки" -msgstr "" +msgstr "Створення папки" #: apps/nextcloud_connector/models.py:21 msgid "Обновление ACL" -msgstr "" +msgstr "Оновлення ACL" #: apps/nextcloud_connector/models.py:22 msgid "Пропуск" -msgstr "" +msgstr "Пропуск" #: apps/nextcloud_connector/models.py:27 msgid "Пробный прогон" -msgstr "" +msgstr "Пробний прогін" #: apps/nextcloud_connector/models.py:30 msgid "Тип сущности" -msgstr "" +msgstr "Тип сутності" #: apps/nextcloud_connector/models.py:31 msgid "Ссылка на сущность" -msgstr "" +msgstr "Посилання на сутність" #: apps/nextcloud_connector/models.py:32 msgid "Код отдела или номер аптеки" -msgstr "" +msgstr "Код відділу або номер аптеки" #: apps/nextcloud_connector/models.py:33 msgid "Путь Group Folder" -msgstr "" +msgstr "Шлях Group Folder" #: apps/nextcloud_connector/models.py:39 msgid "Запись синхронизации Nextcloud" -msgstr "" +msgstr "Запис синхронізації Nextcloud" #: apps/nextcloud_connector/models.py:40 msgid "Журнал синхронизации Nextcloud" -msgstr "" +msgstr "Журнал синхронізації Nextcloud" #: apps/tasks/models.py:25 templates/pages/it-manager.html:322 #: templates/pages/it-manager.html:374 templates/pages/task-detail.html:21 @@ -1315,79 +1541,79 @@ msgstr "Високий" #: apps/tasks/models.py:38 msgid "Назва" -msgstr "" +msgstr "Назва" #: apps/tasks/models.py:39 apps/visit_manager/admin.py:88 msgid "Опис" -msgstr "" +msgstr "Опис" #: apps/tasks/models.py:42 msgid "Пріоритет" -msgstr "" +msgstr "Пріоритет" #: apps/tasks/models.py:44 msgid "Термін" -msgstr "" +msgstr "Термін" #: apps/tasks/models.py:48 msgid "Постановник" -msgstr "" +msgstr "Постановник" #: apps/tasks/models.py:55 msgid "Відповідальний" -msgstr "" +msgstr "Відповідальний" #: apps/tasks/models.py:59 msgid "Співвиконавці" -msgstr "" +msgstr "Співвиконавці" #: apps/tasks/models.py:63 msgid "Спостерігачі" -msgstr "" +msgstr "Спостерігачі" #: apps/tasks/models.py:66 apps/tasks/models.py:148 msgid "Виконано в" -msgstr "" +msgstr "Виконано в" #: apps/tasks/models.py:71 apps/tasks/models.py:122 apps/tasks/models.py:144 msgid "Задача" -msgstr "" +msgstr "Задача" #: apps/tasks/models.py:72 msgid "Задачі" -msgstr "" +msgstr "Задачі" #: apps/tasks/models.py:131 msgid "Коментар до задачі" -msgstr "" +msgstr "Коментар до задачі" #: apps/tasks/models.py:132 msgid "Коментарі до задач" -msgstr "" +msgstr "Коментарі до задач" #: apps/tasks/models.py:146 msgid "Пункт" -msgstr "" +msgstr "Пункт" #: apps/tasks/models.py:147 msgid "Виконано" -msgstr "" +msgstr "Виконано" #: apps/tasks/models.py:151 msgid "Відмітив" -msgstr "" +msgstr "Відмітив" #: apps/tasks/models.py:153 osc/core/models/directory.py:33 msgid "Порядок" -msgstr "" +msgstr "Порядок" #: apps/tasks/models.py:156 msgid "Пункт чек-листа" -msgstr "" +msgstr "Пункт чек-листа" #: apps/tasks/models.py:157 msgid "Чек-лист задачі" -msgstr "" +msgstr "Чек-лист задачі" #: apps/tasks/views.py:211 templates/pages/task-edit.html:2 #: templates/pages/tasks.html:35 @@ -1406,140 +1632,144 @@ msgstr "Спостерігачі" #: apps/utilities/models.py:22 msgid "Тип лічильника" -msgstr "" +msgstr "Тип лічильника" #: apps/utilities/models.py:24 msgid "Дата зняття показань" -msgstr "" +msgstr "Дата зняття показань" #: apps/utilities/models.py:25 msgid "Показання" -msgstr "" +msgstr "Показання" #: apps/utilities/models.py:31 msgid "Хто подав" -msgstr "" +msgstr "Хто подав" #: apps/utilities/models.py:35 msgid "Показання лічильника" -msgstr "" +msgstr "Показання лічильника" #: apps/utilities/models.py:36 msgid "Показання лічильників" -msgstr "" +msgstr "Показання лічильників" #: apps/utilities/models.py:54 msgid "Показання не може бути від'ємним" -msgstr "" +msgstr "Показання не може бути від'ємним" #: apps/utilities/models.py:56 msgid "Дата не може бути в майбутньому" -msgstr "" +msgstr "Дата не може бути в майбутньому" #: apps/visit_manager/admin.py:30 apps/visit_manager/admin.py:78 #: apps/visit_manager/admin.py:137 apps/visit_manager/admin.py:259 #: apps/visit_manager/admin.py:311 msgid "Основне" -msgstr "" +msgstr "Основне" #: apps/visit_manager/admin.py:33 msgid "Час роботи" -msgstr "" +msgstr "Час роботи" #: apps/visit_manager/admin.py:37 msgid "Обідня перерва" -msgstr "" +msgstr "Обідня перерва" #: apps/visit_manager/admin.py:40 msgid "Понаднормові" -msgstr "" +msgstr "Понаднормові" #: apps/visit_manager/admin.py:53 apps/visit_manager/admin.py:140 #: apps/visit_manager/admin.py:314 msgid "Час" -msgstr "" +msgstr "Час" #: apps/visit_manager/admin.py:61 msgid "Колір" -msgstr "" +msgstr "Колір" #: apps/visit_manager/admin.py:81 msgid "Характеристики" -msgstr "" +msgstr "Характеристики" #: apps/visit_manager/admin.py:85 msgid "Відображення" -msgstr "" +msgstr "Відображення" #: apps/visit_manager/admin.py:92 msgid "Регіони" -msgstr "" +msgstr "Регіони" #: apps/visit_manager/admin.py:107 msgid "Залишилось" -msgstr "" +msgstr "Залишилось" #: apps/visit_manager/admin.py:144 msgid "Тиждень" -msgstr "" +msgstr "Тиждень" #: apps/visit_manager/admin.py:147 msgid "Цикл (для змінного графіку)" -msgstr "" +msgstr "Цикл (для змінного графіку)" #: apps/visit_manager/admin.py:151 msgid "Налаштування" -msgstr "" +msgstr "Налаштування" #: apps/visit_manager/admin.py:154 msgid "Придатність" -msgstr "" +msgstr "Придатність" #: apps/visit_manager/admin.py:178 msgid "Співробітник та шаблон" -msgstr "" +msgstr "Співробітник та шаблон" #: apps/visit_manager/admin.py:181 msgid "Період" -msgstr "" +msgstr "Період" #: apps/visit_manager/admin.py:184 msgid "Налаштування відміток" -msgstr "" +msgstr "Налаштування відміток" #: apps/visit_manager/admin.py:188 msgid "Примітки" -msgstr "" +msgstr "Примітки" #: apps/visit_manager/admin.py:265 msgid "Технічне" -msgstr "" +msgstr "Технічне" #: apps/visit_manager/admin.py:277 msgid "Направлення" -msgstr "" +msgstr "Направлення" + +#: apps/visit_manager/admin.py:284 +msgid "Тип локації" +msgstr "Тип локації" #: apps/visit_manager/admin.py:296 msgid "Превью" -msgstr "" +msgstr "Прев’ю" #: apps/visit_manager/admin.py:317 msgid "Записи" -msgstr "" +msgstr "Записи" #: apps/visit_manager/admin.py:329 msgid "Відпрацьовано" -msgstr "" +msgstr "Відпрацьовано" #: apps/visit_manager/admin.py:334 msgid "Повна зміна" -msgstr "" +msgstr "Повна зміна" #: core/admin.py:20 core/models/directories.py:54 #: core/models/directories.py:116 osc/core/models/directory.py:12 #: osc/core/models/directory.py:23 msgid "Тип справочника" -msgstr "" +msgstr "Тип довідника" #: core/admin.py:37 templates/pages/dislocation.html:90 #: templates/pages/pharmacies.html:86 @@ -1553,19 +1783,19 @@ msgstr "Неактивні" #: core/admin.py:49 msgid "Наличие потомков" -msgstr "" +msgstr "Наявність нащадків" #: core/admin.py:54 msgid "Есть потомки" -msgstr "" +msgstr "Є нащадки" #: core/admin.py:55 msgid "Нет потомков" -msgstr "" +msgstr "Немає нащадків" #: core/admin.py:86 msgid "Записей" -msgstr "" +msgstr "Записів" #: core/admin.py:144 templates/pages/pharmacy-detail.html:67 #: templates/pages/pharmacy-detail.html:85 @@ -1574,313 +1804,329 @@ msgstr "Основна інформація" #: core/admin.py:147 msgid "Должности и локации" -msgstr "" +msgstr "Посади та локації" #: core/admin.py:153 core/models/directories.py:123 msgid "Дополнительные данные" -msgstr "" +msgstr "Додаткові дані" #: core/admin.py:167 msgid "🔄 Перестроить дерево" -msgstr "" +msgstr "🔄 Перебудувати дерево" #: core/admin.py:172 msgid "Дерево справочников перестроено" -msgstr "" +msgstr "Дерево довідників перебудовано" -#: core/admin.py:195 core/models/directories.py:119 -#: osc/core/models/directory.py:9 osc/core/models/directory.py:26 -#: templates/pages/it-manager.html:821 -msgid "Название" -msgstr "Назва" - -#: core/admin.py:212 core/models/directories.py:131 +#: core/admin.py:212 core/admin.py:256 core/models/directories.py:131 msgid "Типы локаций" -msgstr "" +msgstr "Типи локацій" -#: core/admin.py:256 +#: core/admin.py:268 msgid "Подключение" -msgstr "" +msgstr "Підключення" -#: core/admin.py:261 +#: core/admin.py:273 msgid "Укажите токен или логин/пароль — в зависимости от типа коннектора" -msgstr "" +msgstr "Вкажіть токен або логін/пароль — залежно від типу конектора" -#: core/admin.py:263 core/models/connector_config.py:40 +#: core/admin.py:275 core/models/connector_config.py:40 msgid "Дополнительные параметры" -msgstr "" +msgstr "Додаткові параметри" -#: core/admin.py:274 +#: core/admin.py:286 msgid "Коннектор" -msgstr "" +msgstr "Конектор" #: core/models/connector_config.py:18 msgid "Тип коннектора" -msgstr "" +msgstr "Тип конектора" #: core/models/connector_config.py:21 msgid "Включён" -msgstr "" +msgstr "Увімкнено" #: core/models/connector_config.py:24 msgid "Хост" -msgstr "" +msgstr "Хост" #: core/models/connector_config.py:25 msgid "Порт" -msgstr "" +msgstr "Порт" #: core/models/connector_config.py:26 msgid "Использовать SSL" -msgstr "" - -#: core/models/connector_config.py:32 -msgid "Пароль" -msgstr "" +msgstr "Використовувати SSL" #: core/models/connector_config.py:34 msgid "Токен / API Key" -msgstr "" +msgstr "Токен / API Key" #: core/models/connector_config.py:35 msgid "Bearer-токен или API-ключ, если используется вместо логина/пароля" msgstr "" +"Bearer-токен або API-ключ, якщо використовується замість логіна/пароля" #: core/models/connector_config.py:41 msgid "Специфичные для коннектора настройки в формате JSON" -msgstr "" +msgstr "Специфічні для конектора налаштування у форматі JSON" #: core/models/connector_config.py:47 msgid "Настройки коннектора" -msgstr "" +msgstr "Налаштування конектора" #: core/models/connector_config.py:48 osc/core/models/connector_settings.py:33 #: osc/core/models/connector_settings.py:34 #: osc/core/models/connector_settings.py:37 msgid "Настройки коннекторов" -msgstr "" +msgstr "Налаштування конекторів" #: core/models/connector_config.py:58 msgid "Хост обязателен для включённого коннектора" -msgstr "" +msgstr "Хост обов’язковий для увімкненого конектора" #: core/models/connector_config.py:61 msgid "Для включённого коннектора необходимо указать токен или логин" -msgstr "" +msgstr "Для увімкненого конектора необхідно вказати токен або логін" #: core/models/directories.py:26 msgid "Код типа" -msgstr "" +msgstr "Код типу" #: core/models/directories.py:27 msgid "Название типа" -msgstr "" +msgstr "Назва типу" #: core/models/directories.py:33 msgid "Строгая иерархия (одинаковые типы)" -msgstr "" +msgstr "Сувора ієрархія (однакові типи)" #: core/models/directories.py:34 msgid "Гибкая иерархия (разные типы)" -msgstr "" +msgstr "Гнучка ієрархія (різні типи)" #: core/models/directories.py:35 msgid "Кастомная иерархия" -msgstr "" +msgstr "Кастомна ієрархія" #: core/models/directories.py:42 msgid "Тип иерархии" -msgstr "" +msgstr "Тип ієрархії" #: core/models/directories.py:49 msgid "Разрешенные типы потомков" -msgstr "" +msgstr "Дозволені типи нащадків" #: core/models/directories.py:50 msgid "Только для гибкой и кастомной иерархии" -msgstr "" +msgstr "Тільки для гнучкої та кастомної ієрархії" #: core/models/directories.py:55 osc/core/models/directory.py:13 msgid "Типы справочников" -msgstr "" +msgstr "Типи довідників" -#: core/models/directories.py:118 osc/core/models/directory.py:8 -#: osc/core/models/directory.py:25 -msgid "Код" +#: core/models/directories.py:133 +msgid "" +"Пусто = доступно для всех типов локаций. Для должностей и систем доступа: в " +"каких локациях показывать (офис / аптека / склад)." msgstr "" +"Порожньо = доступно для всіх типів локацій. Для посад і систем доступу: в " +"яких локаціях показувати (офіс / аптека / склад)." -#: core/models/directories.py:132 -msgid "Для должностей: в каких типах локаций доступна эта должность" -msgstr "" - -#: core/models/directories.py:154 +#: core/models/directories.py:158 msgid "Элемент справочника" -msgstr "" +msgstr "Елемент довідника" -#: core/models/directories.py:155 +#: core/models/directories.py:159 msgid "Элементы справочников" -msgstr "" +msgstr "Елементи довідників" #: core/utils/validators.py:13 msgid "Номер телефона должен быть в формате +380XXXXXXXXX" -msgstr "" +msgstr "Номер телефону має бути у форматі +380XXXXXXXXX" #: core/utils/validators.py:22 msgid "Номер телефона должен быть в международном формате +XXXXXXXXXXX" -msgstr "" +msgstr "Номер телефону має бути в міжнародному форматі +XXXXXXXXXXX" #: core/utils/validators.py:27 msgid "Номер телефона должен содержать от 8 до 16 цифр включая код страны" -msgstr "" +msgstr "Номер телефону має містити від 8 до 16 цифр разом із кодом країни" #: core/utils/validators.py:46 msgid "Дата рождения не может быть в будущем" -msgstr "" +msgstr "Дата народження не може бути в майбутньому" #: core/utils/validators.py:54 msgid "Возраст должен быть не менее 18 лет" -msgstr "" +msgstr "Вік має бути не менше 18 років" #: core/utils/validators.py:83 msgid "Использование временных email запрещено" -msgstr "" +msgstr "Використання тимчасових email заборонено" #: core/utils/validators.py:94 #, python-format msgid "Недопустимый IP адрес: %(value)s" -msgstr "" +msgstr "Неприпустима IP адреса: %(value)s" #: core/utils/validators.py:103 -msgid "MAC адрес должен быть в формате XX:XX:XX:XX:XX:XX или XX-XX-XX-XX-XX-XX" -msgstr "" +msgid "" +"MAC адрес должен быть в формате XX:XX:XX:XX:XX:XX или XX-XX-XX-XX-XX-XX" +msgstr "MAC адреса має бути у форматі XX:XX:XX:XX:XX:XX або XX-XX-XX-XX-XX-XX" #: core/utils/validators.py:114 msgid "Украинский налоговый номер должен содержать 10 цифр" -msgstr "" +msgstr "Український податковий номер має містити 10 цифр" #: core/utils/validators.py:132 msgid "Неверная контрольная сумма украинского налогового номера" -msgstr "" +msgstr "Невірна контрольна сума українського податкового номера" #: core/utils/validators.py:142 msgid "ИНН должен содержать 10 или 12 цифр" -msgstr "" +msgstr "ІПН має містити 10 або 12 цифр" #: core/utils/validators.py:159 core/utils/validators.py:176 msgid "Неверная контрольная сумма ИНН" -msgstr "" +msgstr "Невірна контрольна сума ІПН" -#: core/views.py:351 +#: core/views.py:395 msgid "Рабочих дней" msgstr "Робочих днів" -#: core/views.py:351 +#: core/views.py:395 msgid "с отметками" msgstr "з відмітками" -#: core/views.py:352 +#: core/views.py:396 msgid "отмечались" msgstr "відмічались" -#: core/views.py:353 +#: core/views.py:397 msgid "Смен" msgstr "Змін" -#: core/views.py:353 +#: core/views.py:397 msgid "за месяц" msgstr "за місяць" -#: core/views.py:354 templates/pages/timetrack.html:62 +#: core/views.py:398 templates/pages/timetrack.html:62 msgid "Незакрытых" msgstr "Незакритих" -#: core/views.py:354 +#: core/views.py:398 msgid "без ухода" msgstr "без виходу" -#: core/views.py:355 +#: core/views.py:399 msgid "Всего часов" msgstr "Всього годин" -#: core/views.py:355 +#: core/views.py:399 msgid "отработано" msgstr "відпрацьовано" #: osc/apps/employees/models.py:34 msgid "Логин для входа, совпадает с учётной записью AD" -msgstr "" +msgstr "Логін для входу, збігається з обліковим записом AD" #: osc/apps/employees/models.py:105 msgid "Логин AD (sAMAccountName)" -msgstr "" +msgstr "Логін AD (sAMAccountName)" #: osc/apps/employees/models.py:107 msgid "AD User DN" -msgstr "" +msgstr "AD User DN" #: osc/apps/employees/models.py:151 msgid "Не может быть раньше даты приёма" -msgstr "" +msgstr "Не може бути раніше дати прийому" #: osc/apps/employees/models.py:153 msgid "Сотрудник не может быть своим руководителем" -msgstr "" +msgstr "Співробітник не може бути своїм керівником" #: osc/apps/mattermost_connector/models.py:23 msgid "Запись синхронизации Mattermost" -msgstr "" +msgstr "Запис синхронізації Mattermost" #: osc/apps/mattermost_connector/models.py:24 msgid "Журнал синхронизации Mattermost" -msgstr "" +msgstr "Журнал синхронізації Mattermost" #: osc/core/models/connector_settings.py:10 msgid "Требовать смену пароля при создании" -msgstr "" +msgstr "Вимагати зміну пароля при створенні" #: osc/core/models/connector_settings.py:12 -msgid "Новым учётным записям в AD выставляется флаг «сменить пароль при следующем входе». Если выключено — выданный пароль действует сразу, без принудительной смены." +msgid "" +"Новым учётным записям в AD выставляется флаг «сменить пароль при следующем " +"входе». Если выключено — выданный пароль действует сразу, без принудительной" +" смены." msgstr "" +"Новим обліковим записам в AD виставляється прапорець «змінити пароль при " +"наступному вході». Якщо вимкнено — виданий пароль діє одразу, без примусової" +" зміни." #: osc/core/models/connector_settings.py:17 msgid "Пароль не имеет срока действия" -msgstr "" +msgstr "Пароль не має терміну дії" #: osc/core/models/connector_settings.py:19 -msgid "Новым и существующим учётным записям в AD выставляется флаг «Password never expires». Если выключено — действует обычная парольная политика домена." +msgid "" +"Новым и существующим учётным записям в AD выставляется флаг «Password never " +"expires». Если выключено — действует обычная парольная политика домена." msgstr "" +"Новим і наявним обліковим записам в AD виставляється прапорець «Password " +"never expires». Якщо вимкнено — діє звичайна парольна політика домену." #: osc/core/models/connector_settings.py:24 msgid "Команда Mattermost по умолчанию" -msgstr "" +msgstr "Команда Mattermost за замовчуванням" #: osc/core/models/connector_settings.py:25 -msgid "Сотрудники автоматически добавляются в эту команду при синхронизации с Mattermost." +msgid "" +"Сотрудники автоматически добавляются в эту команду при синхронизации с " +"Mattermost." msgstr "" +"Співробітники автоматично додаються до цієї команди при синхронізації з " +"Mattermost." #: osc/core/models/connector_settings.py:28 msgid "Каналы по умолчанию" -msgstr "" +msgstr "Канали за замовчуванням" #: osc/core/models/connector_settings.py:29 -msgid "Сотрудники автоматически добавляются в эти каналы при синхронизации с Mattermost." +msgid "" +"Сотрудники автоматически добавляются в эти каналы при синхронизации с " +"Mattermost." msgstr "" +"Співробітники автоматично додаються до цих каналів при синхронізації з " +"Mattermost." #: osc/core/models/directory.py:29 msgid "Родительский элемент" -msgstr "" +msgstr "Батьківський елемент" #: osc/core/models/directory.py:30 msgid "Должен принадлежать тому же типу справочника" -msgstr "" +msgstr "Має належати до того самого типу довідника" #: osc/core/models/directory.py:35 msgid "OU в Active Directory" -msgstr "" +msgstr "OU в Active Directory" #: osc/core/models/directory.py:37 -msgid "Используется только для справочника отделов (department): сотрудники этого отдела создаются/переносятся в указанную OU. Например: OU=IT,OU=OL_Users,DC=ol,DC=local. Если не задано — используется AD_USERS_OU." +msgid "" +"Используется только для справочника отделов (department): сотрудники этого " +"отдела создаются/переносятся в указанную OU. Например: " +"OU=IT,OU=OL_Users,DC=ol,DC=local. Если не задано — используется AD_USERS_OU." msgstr "" +"Використовується лише для довідника відділів (department): співробітники " +"цього відділу створюються/переносяться до вказаної OU. Наприклад: " +"OU=IT,OU=OL_Users,DC=ol,DC=local. Якщо не задано — використовується " +"AD_USERS_OU." #: osc/core/models/directory.py:44 templates/pages/dashboard.html:163 msgid "Справочник" @@ -1888,65 +2134,69 @@ msgstr "Довідник" #: osc/core/models/directory.py:45 msgid "Справочники" -msgstr "" +msgstr "Довідники" #: osc/core/models/directory.py:56 msgid "Элемент не может быть родителем самому себе" -msgstr "" +msgstr "Елемент не може бути батьком самому собі" #: osc/core/models/directory.py:58 msgid "Родитель должен принадлежать тому же типу справочника" -msgstr "" +msgstr "Батько має належати до того самого типу довідника" #: osc/core/models/directory.py:62 msgid "Циклическая вложенность недопустима" -msgstr "" +msgstr "Циклічна вкладеність неприпустима" #: osc/core/models/site_settings.py:9 msgid "Название портала" -msgstr "" +msgstr "Назва порталу" #: osc/core/models/site_settings.py:10 msgid "Логотип" -msgstr "" +msgstr "Логотип" #: osc/core/models/site_settings.py:13 osc/core/models/site_settings.py:14 #: osc/core/models/site_settings.py:17 msgid "Настройки сайта" -msgstr "" +msgstr "Налаштування сайту" #: osc/core/utils/validators.py:16 msgid "Введите корректный украинский номер телефона, например +380501234567" -msgstr "" +msgstr "Введіть коректний український номер телефону, наприклад +380501234567" #: osc/core/utils/validators.py:24 msgid "Дата рождения должна быть в прошлом" -msgstr "" +msgstr "Дата народження має бути в минулому" #: osc/core/utils/validators.py:27 msgid "Сотруднику должно быть не менее 16 лет" -msgstr "" +msgstr "Співробітнику має бути не менше 16 років" #: osc/core/utils/validators.py:29 msgid "Проверьте дату рождения — возраст превышает 100 лет" -msgstr "" +msgstr "Перевірте дату народження — вік перевищує 100 років" #: osc/core/utils/validators.py:35 msgid "ИНН должен состоять из 10 цифр" -msgstr "" +msgstr "ІПН має складатися з 10 цифр" #: osc/core/utils/validators.py:38 msgid "Некорректный ИНН: неверная контрольная сумма" -msgstr "" +msgstr "Некоректний ІПН: невірна контрольна сума" #: osc/templates/admin/employees/employee/delete_confirmation.html:10 #, python-format -msgid "Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"? All of the following related items will be deleted:" +msgid "" +"Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"? " +"All of the following related items will be deleted:" msgstr "" +"Ви впевнені, що хочете видалити %(object_name)s «%(escaped_object)s»? Усі " +"наведені нижче пов’язані об’єкти буде видалено:" #: osc/templates/admin/employees/employee/delete_confirmation.html:17 msgid "Objects" -msgstr "" +msgstr "Об’єкти" #: templates/index.html:211 msgid "Сеть аптек" @@ -2032,7 +2282,7 @@ msgstr "Комунальні послуги" msgid "Рекрутинг" msgstr "Рекрутинг" -#: templates/index.html:411 templates/pages/hr-portal.html:292 +#: templates/index.html:411 templates/pages/hr-portal.html:428 #: templates/pages/timetrack.html:7 msgid "Учёт рабочего времени" msgstr "Облік робочого часу" @@ -2041,7 +2291,7 @@ msgstr "Облік робочого часу" msgid "Поддержка" msgstr "Підтримка" -#: templates/index.html:429 templates/pages/hr-portal.html:284 +#: templates/index.html:429 templates/pages/hr-portal.html:420 msgid "Справка" msgstr "Довідка" @@ -2065,7 +2315,8 @@ msgstr "Пошук..." msgid "Язык" msgstr "Мова" -#: templates/index.html:608 templates/pages/hr-portal.html:199 +#: templates/index.html:608 templates/pages/hr-portal.html:230 +#: templates/pages/hr-portal.html:392 msgid "Профиль" msgstr "Профіль" @@ -2083,11 +2334,11 @@ msgstr "Завантаження..." #: templates/index.html:675 msgid "Люди" -msgstr "" +msgstr "Люди" #: templates/index.html:691 msgid "Меню" -msgstr "" +msgstr "Меню" #: templates/pages/chat.html:6 msgid "Внутренний корпоративный мессенджер (Mattermost)" @@ -2155,8 +2406,11 @@ msgid "Ваша учётная запись ещё не синхронизиро msgstr "Ваш акаунт ще не синхронізовано з Mattermost" #: templates/pages/chat.html:132 -msgid "Нажмите кнопку ниже, чтобы создать или привязать вашу учётную запись в Mattermost." -msgstr "Натисніть кнопку нижче, щоб створити або прив'язати ваш акаунт у Mattermost." +msgid "" +"Нажмите кнопку ниже, чтобы создать или привязать вашу учётную запись в " +"Mattermost." +msgstr "" +"Натисніть кнопку нижче, щоб створити або прив'язати ваш акаунт у Mattermost." #: templates/pages/chat.html:135 msgid "Синхронизировать сейчас" @@ -2167,16 +2421,26 @@ msgid "Не удалось получить доступ к Mattermost" msgstr "Не вдалося отримати доступ до Mattermost" #: templates/pages/chat.html:146 -msgid "Обратитесь в IT-отдел — возможно, отключена выдача Personal Access Token на сервере Mattermost." -msgstr "Зверніться до IT-відділу — можливо, вимкнено видачу Personal Access Token на сервері Mattermost." +msgid "" +"Обратитесь в IT-отдел — возможно, отключена выдача Personal Access Token на " +"сервере Mattermost." +msgstr "" +"Зверніться до IT-відділу — можливо, вимкнено видачу Personal Access Token на" +" сервері Mattermost." #: templates/pages/chat.html:155 msgid "Чат ещё не подключён" msgstr "Чат ще не підключено" #: templates/pages/chat.html:157 -msgid "Укажите публичный URL Mattermost (поле public_url в «Дополнительные параметры» коннектора Mattermost в админке) — и он появится здесь." -msgstr "Вкажіть публічну URL-адресу Mattermost (поле public_url у «Додаткові параметри» коннектора Mattermost в адмінці) — і він з'явиться тут." +msgid "" +"Укажите публичный URL Mattermost (поле public_url в " +"«Дополнительные параметры» коннектора Mattermost в админке) — и он появится " +"здесь." +msgstr "" +"Вкажіть публічну URL-адресу Mattermost (поле public_url у " +"«Додаткові параметри» коннектора Mattermost в адмінці) — і він з'явиться " +"тут." #: templates/pages/dashboard.html:7 msgid "Добрый день" @@ -2206,7 +2470,7 @@ msgstr "Повідомлення" msgid "Сотрудников онлайн" msgstr "Співробітників онлайн" -#: templates/pages/dashboard.html:22 templates/pages/dislocation.html:720 +#: templates/pages/dashboard.html:22 templates/pages/dislocation.html:791 msgid "из" msgstr "із" @@ -2337,8 +2601,8 @@ msgid "Все предприятия" msgstr "Всі підприємства" #: templates/pages/dislocation.html:76 templates/pages/dislocation.html:81 -#: templates/pages/dislocation.html:82 templates/pages/dislocation.html:437 -#: templates/pages/dislocation.html:454 templates/pages/pharmacies.html:72 +#: templates/pages/dislocation.html:82 templates/pages/dislocation.html:450 +#: templates/pages/dislocation.html:467 templates/pages/pharmacies.html:72 msgid "Все города/регионы" msgstr "Всі міста/регіони" @@ -2346,126 +2610,136 @@ msgstr "Всі міста/регіони" msgid "Все статусы" msgstr "Всі статуси" -#: templates/pages/dislocation.html:98 templates/pages/hr-portal.html:110 +#: templates/pages/dislocation.html:103 templates/pages/hr-portal.html:139 msgid "Таблица" msgstr "Таблиця" -#: templates/pages/dislocation.html:103 +#: templates/pages/dislocation.html:108 msgid "Карта" msgstr "Карта" -#: templates/pages/dislocation.html:118 templates/pages/pharmacies.html:115 -#: templates/pages/pharmacy-detail.html:49 -#: templates/pages/pharmacy-detail.html:141 -msgid "Заведующий" -msgstr "Завідуючий" +#: templates/pages/dislocation.html:115 +msgid "Колонки таблицы" +msgstr "Колонки таблиці" -#: templates/pages/dislocation.html:121 templates/pages/pharmacies.html:118 -#: templates/pages/pharmacy-detail.html:59 -msgid "Режим работы" -msgstr "Режим роботи" +#: templates/pages/dislocation.html:117 +msgid "Пресет" +msgstr "Пресет" -#: templates/pages/dislocation.html:159 -msgid "не назначен" -msgstr "не призначений" +#: templates/pages/dislocation.html:119 +msgid "— свой набор —" +msgstr "— свій набір —" -#: templates/pages/dislocation.html:166 -msgid "Нехватка" -msgstr "Нестача" +#: templates/pages/dislocation.html:137 +msgid "обяз." +msgstr "обов." -#: templates/pages/dislocation.html:168 -msgid "Внимание" -msgstr "Увага" +#: templates/pages/dislocation.html:138 +msgid "Выше" +msgstr "Вище" -#: templates/pages/dislocation.html:170 -msgid "Норма" -msgstr "Норма" +#: templates/pages/dislocation.html:139 +msgid "Ниже" +msgstr "Нижче" -#: templates/pages/dislocation.html:182 templates/pages/dislocation.html:559 -#: templates/pages/pharmacies.html:158 templates/pages/pharmacy-detail.html:36 -msgid "Неактивна" -msgstr "Неактивна" +#: templates/pages/dislocation.html:144 templates/pages/it-manager.html:290 +#: templates/pages/it-manager.html:423 templates/pages/it-manager.html:735 +#: templates/pages/it-manager.html:1013 templates/pages/settings.html:43 +#: templates/pages/settings.html:60 +msgid "Сохранить" +msgstr "Зберегти" -#: templates/pages/dislocation.html:208 +#: templates/pages/dislocation.html:145 +#| msgid "Отменено" +msgid "Отмена" +msgstr "Скасувати" + +#: templates/pages/dislocation.html:221 #: templates/pages/pharmacy-detail.html:149 msgid "В этой аптеке пока нет активных сотрудников" msgstr "У цій аптеці поки немає активних співробітників" -#: templates/pages/dislocation.html:214 templates/pages/pharmacies.html:164 +#: templates/pages/dislocation.html:227 templates/pages/pharmacies.html:164 msgid "Аптеки не найдены" msgstr "Аптеки не знайдені" -#: templates/pages/dislocation.html:221 templates/pages/pharmacies.html:171 +#: templates/pages/dislocation.html:234 templates/pages/pharmacies.html:171 msgid "Показано 0–0 из 0" msgstr "Показано 0–0 з 0" -#: templates/pages/dislocation.html:224 templates/pages/pharmacies.html:174 +#: templates/pages/dislocation.html:237 templates/pages/pharmacies.html:174 msgid "На странице:" msgstr "На сторінці:" -#: templates/pages/dislocation.html:233 +#: templates/pages/dislocation.html:246 msgid "Предыдущая" msgstr "Попередня" -#: templates/pages/dislocation.html:234 templates/pages/pharmacies.html:184 +#: templates/pages/dislocation.html:247 templates/pages/pharmacies.html:184 msgid "Стр. 1 из 1" msgstr "Стор. 1 з 1" -#: templates/pages/dislocation.html:235 templates/pages/pharmacies.html:185 +#: templates/pages/dislocation.html:248 templates/pages/pharmacies.html:185 msgid "Следующая" msgstr "Наступна" -#: templates/pages/dislocation.html:246 +#: templates/pages/dislocation.html:259 msgid "апт. без указанного адреса не показаны на карте (нет координат)" msgstr "апт. без вказаної адреси не показані на карті (немає координат)" -#: templates/pages/dislocation.html:255 +#: templates/pages/dislocation.html:268 msgid "Список аптек" msgstr "Список аптек" -#: templates/pages/dislocation.html:288 +#: templates/pages/dislocation.html:301 msgid "сотрудников офиса и склада, сгруппировано по отделам" msgstr "співробітників офісу та складу, згруповано по відділах" -#: templates/pages/dislocation.html:296 +#: templates/pages/dislocation.html:309 msgid "Внутр. номер" msgstr "Внутр. номер" -#: templates/pages/dislocation.html:298 +#: templates/pages/dislocation.html:311 msgid "Ф.И.О." msgstr "П.І.Б." -#: templates/pages/dislocation.html:299 +#: templates/pages/dislocation.html:312 msgid "Корпоративный телефон" msgstr "Корпоративний телефон" -#: templates/pages/dislocation.html:320 templates/pages/structure.html:35 +#: templates/pages/dislocation.html:333 templates/pages/structure.html:35 msgid "Без отдела" msgstr "Без відділу" -#: templates/pages/dislocation.html:335 +#: templates/pages/dislocation.html:348 msgid "Сотрудников офиса/склада не найдено" msgstr "Співробітників офісу/складу не знайдено" -#: templates/pages/dislocation.html:561 +#: templates/pages/dislocation.html:572 templates/pages/pharmacies.html:158 +#: templates/pages/pharmacy-detail.html:36 +#: templates/partials/dislo_cell.html:42 +msgid "Неактивна" +msgstr "Неактивна" + +#: templates/pages/dislocation.html:574 msgid "Подробнее" msgstr "Детальніше" -#: templates/pages/dislocation.html:720 +#: templates/pages/dislocation.html:791 msgid "Показано" msgstr "Показано" -#: templates/pages/dislocation.html:720 +#: templates/pages/dislocation.html:791 msgid "Нет записей" msgstr "Немає записів" #: templates/pages/documents.html:6 msgid "Файловое хранилище: общие документы, обучение, отделы" -msgstr "" +msgstr "Файлове сховище: спільні документи, навчання, відділи" #: templates/pages/documents.html:29 msgid "Нет доступных разделов документов." -msgstr "" +msgstr "Немає доступних розділів документів." #: templates/pages/employee-detail.html:28 templates/pages/news-detail.html:17 #: templates/pages/task-detail.html:41 templates/pages/tasks.html:127 @@ -2476,14 +2750,6 @@ msgstr "Редагувати" msgid "Не активный" msgstr "Не активний" -#: templates/pages/employee-detail.html:49 -msgid "Испытательный срок" -msgstr "Випробувальний термін" - -#: templates/pages/employee-detail.html:70 -msgid "Рабочий email" -msgstr "Робочий email" - #: templates/pages/employee-detail.html:78 msgid "Внутренний номер" msgstr "Внутрішній номер" @@ -2492,7 +2758,7 @@ msgstr "Внутрішній номер" msgid "Подчинённых" msgstr "Підлеглих" -#: templates/pages/employee-detail.html:112 templates/pages/hr-portal.html:219 +#: templates/pages/employee-detail.html:112 templates/pages/hr-portal.html:250 msgid "Дата приёма" msgstr "Дата прийняття" @@ -2546,7 +2812,7 @@ msgstr "Додати співробітника" msgid "сотрудников, из них" msgstr "співробітників, з них" -#: templates/pages/employees.html:48 templates/pages/hr-portal.html:122 +#: templates/pages/employees.html:48 templates/pages/hr-portal.html:152 #: templates/pages/news.html:111 templates/pages/tasks.html:60 #: templates/pages/timetrack.html:45 msgid "Все" @@ -2557,11 +2823,11 @@ msgstr "Всі" msgid "Все аптеки" msgstr "Всі аптеки" -#: templates/pages/employees.html:105 templates/pages/hr-portal.html:217 +#: templates/pages/employees.html:105 templates/pages/hr-portal.html:248 msgid "Отдел / Аптека" msgstr "Відділ / Аптека" -#: templates/pages/employees.html:108 templates/pages/it-manager.html:914 +#: templates/pages/employees.html:108 templates/pages/it-manager.html:917 msgid "Дата начала" msgstr "Дата початку" @@ -2577,324 +2843,407 @@ msgstr "HR Портал" msgid "Управление персоналом и кадровые процессы" msgstr "Управління персоналом та кадрові процеси" +#: templates/pages/hr-portal.html:13 +#| msgid "Заявок пока нет" +msgid "Заявку створено" +msgstr "Заявку створено" + #: templates/pages/hr-portal.html:16 +#| msgid "Интерфейс" +msgid "Інтерн-слот" +msgstr "Інтерн-слот" + +#: templates/pages/hr-portal.html:16 +msgid "логін" +msgstr "логін" + +#: templates/pages/hr-portal.html:16 +msgid "пароль" +msgstr "пароль" + +#: templates/pages/hr-portal.html:20 +#| msgid "Заявка IT:" +msgid "Заявка IT" +msgstr "Заявка IT" + +#: templates/pages/hr-portal.html:27 +#| msgid "Подать заявку" +msgid "Не удалось создать заявку" +msgstr "Не вдалося створити заявку" + +#: templates/pages/hr-portal.html:43 msgid "Всего сотрудников" msgstr "Всього співробітників" -#: templates/pages/hr-portal.html:18 +#: templates/pages/hr-portal.html:45 msgid "за последний месяц" msgstr "за останній місяць" -#: templates/pages/hr-portal.html:31 +#: templates/pages/hr-portal.html:56 +#| msgid "Срок испытания" +msgid "Открыть список на испытании" +msgstr "Відкрити список на випробуванні" + +#: templates/pages/hr-portal.html:58 msgid "На испытательном сроке" msgstr "На випробувальному терміні" -#: templates/pages/hr-portal.html:33 +#: templates/pages/hr-portal.html:60 msgid "требуют оценки" msgstr "потребують оцінки" -#: templates/pages/hr-portal.html:44 +#: templates/pages/hr-portal.html:71 msgid "Запланированные тренинги" msgstr "Заплановані тренінги" -#: templates/pages/hr-portal.html:46 +#: templates/pages/hr-portal.html:73 msgid "раздел в разработке" msgstr "розділ у розробці" -#: templates/pages/hr-portal.html:57 +#: templates/pages/hr-portal.html:84 msgid "Заявки на отпуск" msgstr "Заявки на відпустку" -#: templates/pages/hr-portal.html:59 +#: templates/pages/hr-portal.html:86 msgid "ожидает решения" msgstr "очікує рішення" -#: templates/pages/hr-portal.html:76 +#: templates/pages/hr-portal.html:103 msgid "Поиск работников, вакансий, тренингов..." msgstr "Пошук працівників, вакансій, тренінгів..." -#: templates/pages/hr-portal.html:83 +#: templates/pages/hr-portal.html:110 msgid "Создать" msgstr "Створити" -#: templates/pages/hr-portal.html:94 +#: templates/pages/hr-portal.html:123 msgid "Заявки и отпуска" msgstr "Заявки та відпустки" -#: templates/pages/hr-portal.html:96 +#: templates/pages/hr-portal.html:125 msgid "Тренинги" msgstr "Тренінги" -#: templates/pages/hr-portal.html:98 +#: templates/pages/hr-portal.html:127 msgid "Вакансии" msgstr "Вакансії" -#: templates/pages/hr-portal.html:105 +#: templates/pages/hr-portal.html:134 msgid "Карточки" msgstr "Картки" -#: templates/pages/hr-portal.html:136 +#: templates/pages/hr-portal.html:166 msgid "Все отделы" msgstr "Всі відділи" -#: templates/pages/hr-portal.html:143 +#: templates/pages/hr-portal.html:173 msgid "Все должности" msgstr "Всі посади" -#: templates/pages/hr-portal.html:176 +#: templates/pages/hr-portal.html:207 templates/pages/hr-portal.html:390 msgid "На испытании до" msgstr "На випробуванні до" -#: templates/pages/hr-portal.html:182 +#: templates/pages/hr-portal.html:213 msgid "Отдел:" msgstr "Відділ:" -#: templates/pages/hr-portal.html:186 +#: templates/pages/hr-portal.html:217 msgid "Дата приёма:" msgstr "Дата прийняття:" -#: templates/pages/hr-portal.html:194 +#: templates/pages/hr-portal.html:225 msgid "Телефон:" msgstr "Телефон:" -#: templates/pages/hr-portal.html:202 templates/pages/hr-portal.html:255 +#: templates/pages/hr-portal.html:233 templates/pages/hr-portal.html:287 msgid "Активных сотрудников пока нет" msgstr "Активних співробітників поки немає" -#: templates/pages/hr-portal.html:206 templates/pages/hr-portal.html:261 +#: templates/pages/hr-portal.html:237 templates/pages/hr-portal.html:293 msgid "Ничего не найдено по заданным фильтрам" msgstr "Нічого не знайдено за заданими фільтрами" -#: templates/pages/hr-portal.html:242 templates/pages/hr-portal.html:559 -msgid "На испытании" -msgstr "На випробуванні" - -#: templates/pages/hr-portal.html:265 +#: templates/pages/hr-portal.html:297 msgid "Полный список сотрудников →" msgstr "Повний список співробітників →" -#: templates/pages/hr-portal.html:277 -msgid "Отпуск" -msgstr "Відпустка" - -#: templates/pages/hr-portal.html:278 -msgid "Подать заявку" -msgstr "Подати заявку" - -#: templates/pages/hr-portal.html:285 -msgid "Заказать документ" -msgstr "Замовити документ" - -#: templates/pages/hr-portal.html:291 -msgid "Табель" -msgstr "Табель" - -#: templates/pages/hr-portal.html:298 -msgid "Начисления" -msgstr "Нарахування" - -#: templates/pages/hr-portal.html:299 -msgid "Зарплата и премии" -msgstr "Зарплата та премії" - #: templates/pages/hr-portal.html:308 -msgid "Мои заявки" -msgstr "Мої заявки" +#| msgid "На испытании" +msgid "Кандидаты на испытании" +msgstr "Кандидати на випробуванні" -#: templates/pages/hr-portal.html:309 templates/pages/it-manager.html:373 -msgid "Новая" -msgstr "Нова" +#: templates/pages/hr-portal.html:309 +msgid "Заявки на найм до утверждения персонального аккаунта" +msgstr "Заявки на найм до затвердження персонального облікового запису" -#: templates/pages/hr-portal.html:314 -msgid "Отпуск 01–14 мая" -msgstr "Відпустка 01–14 травня" +#: templates/pages/hr-portal.html:311 +#| msgid "Новая задача" +msgid "Новая заявка" +msgstr "Нова заявка" -#: templates/pages/hr-portal.html:315 -msgid "Подано 10.04.2026" -msgstr "Подано 10.04.2026" - -#: templates/pages/hr-portal.html:317 templates/pages/hr-portal.html:374 -msgid "На рассмотрении" -msgstr "На розгляді" - -#: templates/pages/hr-portal.html:321 -msgid "Справка о зарплате" -msgstr "Довідка про зарплату" - -#: templates/pages/hr-portal.html:322 -msgid "Подано 05.04.2026" -msgstr "Подано 05.04.2026" - -#: templates/pages/hr-portal.html:324 -msgid "Готово" -msgstr "Готово" - -#: templates/pages/hr-portal.html:328 -msgid "Больничный 28–29 марта" -msgstr "Лікарняний 28–29 березня" - -#: templates/pages/hr-portal.html:329 -msgid "Подано 01.04.2026" -msgstr "Подано 01.04.2026" - -#: templates/pages/hr-portal.html:331 templates/pages/hr-portal.html:562 -msgid "Утверждено" -msgstr "Затверджено" - -#: templates/pages/hr-portal.html:339 -msgid "Баланс отпусков — 2026" -msgstr "Баланс відпусток — 2026" - -#: templates/pages/hr-portal.html:345 -msgid "Основной отпуск" -msgstr "Основна відпустка" - -#: templates/pages/hr-portal.html:346 templates/pages/hr-portal.html:355 -#: templates/pages/hr-portal.html:364 templates/pages/hr-portal.html:483 -msgid "дн." -msgstr "дн." - -#: templates/pages/hr-portal.html:349 -msgid "Остаток: 10 дней" -msgstr "Залишок: 10 днів" - -#: templates/pages/hr-portal.html:354 -msgid "Дополнительный отпуск" -msgstr "Додаткова відпустка" - -#: templates/pages/hr-portal.html:358 -msgid "Остаток: 7 дней" -msgstr "Залишок: 7 днів" - -#: templates/pages/hr-portal.html:363 -msgid "Больничные" -msgstr "Лікарняні" - -#: templates/pages/hr-portal.html:367 -msgid "Остаток: 28 дней" -msgstr "Залишок: 28 днів" - -#: templates/pages/hr-portal.html:371 -msgid "Следующий отпуск" -msgstr "Наступна відпустка" - -#: templates/pages/hr-portal.html:389 templates/pages/hr-portal.html:400 -msgid "Раздел в разработке" -msgstr "Розділ у розробці" - -#: templates/pages/hr-portal.html:390 -msgid "Планирование тренингов и обучения появится в ближайшее время" -msgstr "Планування тренінгів та навчання з'явиться найближчим часом" - -#: templates/pages/hr-portal.html:401 -msgid "Управление открытыми вакансиями появится в ближайшее время" -msgstr "Управління відкритими вакансіями з'явиться найближчим часом" - -#: templates/pages/hr-portal.html:415 -msgid "Новая заявка на найм" -msgstr "Нова заявка на найм" - -#: templates/pages/hr-portal.html:416 -msgid "Сразу создаёт личный учётный запис кандидата на срок испытания." -msgstr "Одразу створює особистий обліковий запис кандидата на термін випробування." - -#: templates/pages/hr-portal.html:422 -msgid "Фамилия " -msgstr "Прізвище " - -#: templates/pages/hr-portal.html:426 -msgid "Имя " -msgstr "Ім'я " - -#: templates/pages/hr-portal.html:437 -msgid "Тип локации " -msgstr "Тип локації " - -#: templates/pages/hr-portal.html:439 -msgid "Выберите..." -msgstr "Оберіть..." - -#: templates/pages/hr-portal.html:476 -msgid "Дата выхода" -msgstr "Дата виходу" - -#: templates/pages/hr-portal.html:480 -msgid "Срок испытания" -msgstr "Термін випробування" - -#: templates/pages/hr-portal.html:491 -msgid "Email кандидата" -msgstr "Email кандидата" - -#: templates/pages/hr-portal.html:501 -msgid "Требуются доступы (после утверждения — заявка в IT)" -msgstr "Потрібні доступи (після затвердження — заявка в IT)" - -#: templates/pages/hr-portal.html:513 -msgid "Примечание по доступам" -msgstr "Примітка щодо доступів" - -#: templates/pages/hr-portal.html:514 -msgid "Например: 1С Бухгалтерия — только просмотр" -msgstr "Наприклад: 1С Бухгалтерія — лише перегляд" - -#: templates/pages/hr-portal.html:520 -msgid "Создать заявку и временный аккаунт" -msgstr "Створити заявку та тимчасовий акаунт" - -#: templates/pages/hr-portal.html:521 -msgid "Создание учётной записи…" -msgstr "Створення облікового запису…" - -#: templates/pages/hr-portal.html:533 -msgid "Кандидат" -msgstr "Кандидат" - -#: templates/pages/hr-portal.html:534 +#: templates/pages/hr-portal.html:319 templates/pages/hr-portal.html:687 msgid "Должность / локация" msgstr "Посада / локація" -#: templates/pages/hr-portal.html:536 +#: templates/pages/hr-portal.html:320 templates/pages/hr-portal.html:688 +msgid "Інтерн / IT" +msgstr "Інтерн / IT" + +#: templates/pages/hr-portal.html:321 templates/pages/hr-portal.html:690 msgid "Испытание до" msgstr "Випробування до" -#: templates/pages/hr-portal.html:537 templates/pages/pharmacy-detail.html:302 -msgid "Подал" -msgstr "Подав" - -#: templates/pages/hr-portal.html:538 templates/pages/task-detail.html:138 +#: templates/pages/hr-portal.html:322 templates/pages/hr-portal.html:692 +#: templates/pages/task-detail.html:138 msgid "Действия" msgstr "Дії" -#: templates/pages/hr-portal.html:557 +#: templates/pages/hr-portal.html:331 templates/pages/hr-portal.html:725 msgid "Просрочено" msgstr "Прострочено" -#: templates/pages/hr-portal.html:564 -msgid "Отклонено" -msgstr "Відхилено" - -#: templates/pages/hr-portal.html:572 +#: templates/pages/hr-portal.html:351 templates/pages/hr-portal.html:740 msgid "Утвердить кандидата" msgstr "Затвердити кандидата" -#: templates/pages/hr-portal.html:575 +#: templates/pages/hr-portal.html:354 templates/pages/hr-portal.html:743 msgid "Утвердить" msgstr "Затвердити" -#: templates/pages/hr-portal.html:581 +#: templates/pages/hr-portal.html:360 templates/pages/hr-portal.html:749 msgid "Причина отказа" msgstr "Причина відмови" -#: templates/pages/hr-portal.html:583 +#: templates/pages/hr-portal.html:362 templates/pages/hr-portal.html:751 msgid "Отклонить" msgstr "Відхилити" -#: templates/pages/hr-portal.html:591 -msgid "Заявка IT:" -msgstr "Заявка IT:" +#: templates/pages/hr-portal.html:371 +#| msgid "На испытании" +msgid "Нет кандидатов на испытании" +msgstr "Немає кандидатів на випробуванні" -#: templates/pages/hr-portal.html:598 +#: templates/pages/hr-portal.html:383 +#| msgid "На испытательном сроке" +msgid "Сотрудники с испытательным сроком" +msgstr "Співробітники з випробувальним терміном" + +#: templates/pages/hr-portal.html:384 +msgid "Учётные записи с заполненной датой окончания испытания" +msgstr "Облікові записи із заповненою датою закінчення випробування" + +#: templates/pages/hr-portal.html:399 +#| msgid "На испытательном сроке" +msgid "Нет сотрудников на испытательном сроке" +msgstr "Немає співробітників на випробувальному терміні" + +#: templates/pages/hr-portal.html:413 +msgid "Отпуск" +msgstr "Відпустка" + +#: templates/pages/hr-portal.html:414 +msgid "Подать заявку" +msgstr "Подати заявку" + +#: templates/pages/hr-portal.html:421 +msgid "Заказать документ" +msgstr "Замовити документ" + +#: templates/pages/hr-portal.html:427 +msgid "Табель" +msgstr "Табель" + +#: templates/pages/hr-portal.html:434 +msgid "Начисления" +msgstr "Нарахування" + +#: templates/pages/hr-portal.html:435 +msgid "Зарплата и премии" +msgstr "Зарплата та премії" + +#: templates/pages/hr-portal.html:444 +msgid "Мои заявки" +msgstr "Мої заявки" + +#: templates/pages/hr-portal.html:445 templates/pages/it-manager.html:373 +msgid "Новая" +msgstr "Нова" + +#: templates/pages/hr-portal.html:450 +msgid "Отпуск 01–14 мая" +msgstr "Відпустка 01–14 травня" + +#: templates/pages/hr-portal.html:451 +msgid "Подано 10.04.2026" +msgstr "Подано 10.04.2026" + +#: templates/pages/hr-portal.html:453 templates/pages/hr-portal.html:510 +msgid "На рассмотрении" +msgstr "На розгляді" + +#: templates/pages/hr-portal.html:457 +msgid "Справка о зарплате" +msgstr "Довідка про зарплату" + +#: templates/pages/hr-portal.html:458 +msgid "Подано 05.04.2026" +msgstr "Подано 05.04.2026" + +#: templates/pages/hr-portal.html:460 +msgid "Готово" +msgstr "Готово" + +#: templates/pages/hr-portal.html:464 +msgid "Больничный 28–29 марта" +msgstr "Лікарняний 28–29 березня" + +#: templates/pages/hr-portal.html:465 +msgid "Подано 01.04.2026" +msgstr "Подано 01.04.2026" + +#: templates/pages/hr-portal.html:475 +msgid "Баланс отпусков — 2026" +msgstr "Баланс відпусток — 2026" + +#: templates/pages/hr-portal.html:481 +msgid "Основной отпуск" +msgstr "Основна відпустка" + +#: templates/pages/hr-portal.html:485 +msgid "Остаток: 10 дней" +msgstr "Залишок: 10 днів" + +#: templates/pages/hr-portal.html:490 +msgid "Дополнительный отпуск" +msgstr "Додаткова відпустка" + +#: templates/pages/hr-portal.html:494 +msgid "Остаток: 7 дней" +msgstr "Залишок: 7 днів" + +#: templates/pages/hr-portal.html:499 +msgid "Больничные" +msgstr "Лікарняні" + +#: templates/pages/hr-portal.html:503 +msgid "Остаток: 28 дней" +msgstr "Залишок: 28 днів" + +#: templates/pages/hr-portal.html:507 +msgid "Следующий отпуск" +msgstr "Наступна відпустка" + +#: templates/pages/hr-portal.html:525 templates/pages/hr-portal.html:536 +msgid "Раздел в разработке" +msgstr "Розділ у розробці" + +#: templates/pages/hr-portal.html:526 +msgid "Планирование тренингов и обучения появится в ближайшее время" +msgstr "Планування тренінгів та навчання з'явиться найближчим часом" + +#: templates/pages/hr-portal.html:537 +msgid "Управление открытыми вакансиями появится в ближайшее время" +msgstr "Управління відкритими вакансіями з'явиться найближчим часом" + +#: templates/pages/hr-portal.html:550 +msgid "Новая заявка на найм" +msgstr "Нова заявка на найм" + +#: templates/pages/hr-portal.html:551 +msgid "" +"Создаёт IT-заявку и выдаёт интерн-учётку на срок испытания. Персональный " +"аккаунт — после утверждения." +msgstr "" +"Створює IT-заявку і видає інтерн-обліковку на строк випробування. " +"Персональний обліковий запис — після затвердження." + +#: templates/pages/hr-portal.html:553 +#, python-format +msgid "Свободных интерн-слотов: %(free)s из %(total)s" +msgstr "Вільних інтерн-слотів: %(free)s із %(total)s" + +#: templates/pages/hr-portal.html:555 +msgid "" +"добавьте слоты в админке (Пул інтерн-облікових записів) или дождитесь " +"освобождения" +msgstr "" +"додайте слоти в адмінці (Пул інтерн-облікових записів) або дочекайтеся " +"звільнення" + +#: templates/pages/hr-portal.html:563 +msgid "Фамилия " +msgstr "Прізвище " + +#: templates/pages/hr-portal.html:567 +msgid "Имя " +msgstr "Ім'я " + +#: templates/pages/hr-portal.html:578 +msgid "Тип локации " +msgstr "Тип локації " + +#: templates/pages/hr-portal.html:580 +msgid "Выберите..." +msgstr "Оберіть..." + +#: templates/pages/hr-portal.html:587 +#| msgid "Отдел" +msgid "Отдел " +msgstr "Відділ " + +#: templates/pages/hr-portal.html:596 +#| msgid "Аптека" +msgid "Аптека " +msgstr "Аптека " + +#: templates/pages/hr-portal.html:605 +#| msgid "Офис / Склад" +msgid "Офис / склад " +msgstr "Офіс / склад " + +#: templates/pages/hr-portal.html:630 +msgid "Срок испытания" +msgstr "Термін випробування" + +#: templates/pages/hr-portal.html:641 +msgid "Email кандидата" +msgstr "Email кандидата" + +#: templates/pages/hr-portal.html:651 +#| msgid "Быстрый доступ" +msgid "Требуются доступы" +msgstr "Потрібні доступи" + +#: templates/pages/hr-portal.html:652 +msgid "" +"Створює заявку IT на підготовку робочого місця. Список залежить від типу " +"локації." +msgstr "" +"Створює заявку IT на підготовку робочого місця. Список залежить від типу " +"локації." + +#: templates/pages/hr-portal.html:667 +msgid "Например: 1С Бухгалтерия — только просмотр" +msgstr "Наприклад: 1С Бухгалтерія — лише перегляд" + +#: templates/pages/hr-portal.html:673 +msgid "Створити заявку (інтерн + IT)" +msgstr "Створити заявку (інтерн + IT)" + +#: templates/pages/hr-portal.html:674 +msgid "Створення…" +msgstr "Створення…" + +#: templates/pages/hr-portal.html:691 templates/pages/pharmacy-detail.html:302 +msgid "Подал" +msgstr "Подав" + +#: templates/pages/hr-portal.html:762 +msgid "PDF з даними" +msgstr "PDF з даними" + +#: templates/pages/hr-portal.html:772 msgid "Заявок на найм пока нет" msgstr "Заявок на найм поки немає" @@ -2914,7 +3263,7 @@ msgstr "Виконані роботи" msgid "Расходные материалы" msgstr "Витратні матеріали" -#: templates/pages/it-manager.html:21 templates/pages/it-manager.html:791 +#: templates/pages/it-manager.html:21 templates/pages/it-manager.html:794 msgid "Провайдеры" msgstr "Провайдери" @@ -2928,7 +3277,8 @@ msgid "На складе" msgstr "На складі" #: templates/pages/it-manager.html:39 templates/pages/it-manager.html:236 -#: templates/pages/it-manager.html:260 templates/pages/pharmacy-detail.html:198 +#: templates/pages/it-manager.html:260 +#: templates/pages/pharmacy-detail.html:198 #: templates/pages/pharmacy-detail.html:214 msgid "Выдано" msgstr "Видано" @@ -2970,10 +3320,14 @@ msgid "По одному в строку — вставьте список се msgstr "По одному в рядку — вставте список серійників, наприклад" #: templates/pages/it-manager.html:96 -msgid "Сколько серийников — столько единиц будет создано. Дубликаты и уже имеющиеся пропускаются." +msgid "" +"Сколько серийников — столько единиц будет создано. Дубликаты и уже имеющиеся" +" пропускаются." msgstr "" +"Скільки серійників — стільки одиниць буде створено. Дублікати та вже наявні " +"пропускаються." -#: templates/pages/it-manager.html:100 templates/pages/it-manager.html:670 +#: templates/pages/it-manager.html:100 templates/pages/it-manager.html:673 msgid "Дата покупки" msgstr "Дата покупки" @@ -2990,8 +3344,8 @@ msgid "(необязательно)" msgstr "(необов'язково)" #: templates/pages/it-manager.html:110 templates/pages/it-manager.html:202 -#: templates/pages/it-manager.html:500 templates/pages/it-manager.html:656 -#: templates/pages/it-manager.html:881 +#: templates/pages/it-manager.html:503 templates/pages/it-manager.html:659 +#: templates/pages/it-manager.html:884 msgid "Поиск по адресу или номеру аптеки..." msgstr "Пошук за адресою чи номером аптеки..." @@ -3004,8 +3358,8 @@ msgid "Создать экземпляры" msgstr "Створити екземпляри" #: templates/pages/it-manager.html:122 templates/pages/it-manager.html:172 -#: templates/pages/it-manager.html:542 templates/pages/it-manager.html:680 -#: templates/pages/it-manager.html:1086 +#: templates/pages/it-manager.html:545 templates/pages/it-manager.html:683 +#: templates/pages/it-manager.html:1089 msgid "Сохранение…" msgstr "Збереження…" @@ -3017,8 +3371,8 @@ msgstr "Скасувати" msgid "Новая модель" msgstr "Нова модель" -#: templates/pages/it-manager.html:149 templates/pages/it-manager.html:625 -#: templates/pages/it-manager.html:900 +#: templates/pages/it-manager.html:149 templates/pages/it-manager.html:628 +#: templates/pages/it-manager.html:903 msgid "+ Добавить новый тип..." msgstr "+ Додати новий тип..." @@ -3056,7 +3410,7 @@ msgstr "Каталог порожній — додайте першу модел #: templates/pages/it-manager.html:199 msgid "ед.:" -msgstr "" +msgstr "од.:" #: templates/pages/it-manager.html:201 msgid "На локацию" @@ -3082,7 +3436,8 @@ msgstr "Тип / Бренд / Модель" msgid "С/н · Инв. №" msgstr "С/н · Інв. №" -#: templates/pages/it-manager.html:261 templates/pages/pharmacy-detail.html:215 +#: templates/pages/it-manager.html:261 +#: templates/pages/pharmacy-detail.html:215 msgid "В ремонте" msgstr "В ремонті" @@ -3098,13 +3453,7 @@ msgstr "Видано кому" msgid "— (на складе)" msgstr "— (на складі)" -#: templates/pages/it-manager.html:290 templates/pages/it-manager.html:420 -#: templates/pages/it-manager.html:732 templates/pages/it-manager.html:1010 -#: templates/pages/settings.html:43 templates/pages/settings.html:60 -msgid "Сохранить" -msgstr "Зберегти" - -#: templates/pages/it-manager.html:294 templates/pages/it-manager.html:739 +#: templates/pages/it-manager.html:294 templates/pages/it-manager.html:742 msgid "Примечания:" msgstr "Примітки" @@ -3141,12 +3490,12 @@ msgid "Заявитель" msgstr "Заявник" #: templates/pages/it-manager.html:348 templates/pages/it-manager.html:411 -#: templates/pages/it-manager.html:556 +#: templates/pages/it-manager.html:559 msgid "Исполнитель" msgstr "Виконавець" -#: templates/pages/it-manager.html:349 templates/pages/it-manager.html:486 -#: templates/pages/it-manager.html:555 templates/pages/it-manager.html:1098 +#: templates/pages/it-manager.html:349 templates/pages/it-manager.html:489 +#: templates/pages/it-manager.html:558 templates/pages/it-manager.html:1101 #: templates/pages/pharmacy-detail.html:299 msgid "Дата" msgstr "Дата" @@ -3163,268 +3512,273 @@ msgstr "Критичний" msgid "Решена" msgstr "Вирішена" -#: templates/pages/it-manager.html:391 templates/pages/it-manager.html:430 +#: templates/pages/it-manager.html:391 templates/pages/it-manager.html:433 msgid "внутренний" msgstr "внутрішній" -#: templates/pages/it-manager.html:413 +#: templates/pages/it-manager.html:414 msgid "— не назначено" msgstr "— не призначено" -#: templates/pages/it-manager.html:428 +#: templates/pages/it-manager.html:418 +msgid "Нет сотрудников IT / staff — добавьте в группу IT" +msgstr "Немає співробітників IT / staff — додайте до групи IT" + +#: templates/pages/it-manager.html:431 msgid "Комментарий..." msgstr "Коментар..." -#: templates/pages/it-manager.html:433 templates/pages/it-manager.html:679 +#: templates/pages/it-manager.html:436 templates/pages/it-manager.html:682 msgid "Добавить" msgstr "Додати" -#: templates/pages/it-manager.html:440 +#: templates/pages/it-manager.html:443 msgid "Заявок пока нет" msgstr "Заявок поки немає" -#: templates/pages/it-manager.html:455 +#: templates/pages/it-manager.html:458 msgid "Записей в этом месяце" msgstr "Записів цього місяця" -#: templates/pages/it-manager.html:459 +#: templates/pages/it-manager.html:462 msgid "Часов в этом месяце" msgstr "Годин цього місяця" -#: templates/pages/it-manager.html:469 templates/pages/it-manager.html:541 +#: templates/pages/it-manager.html:472 templates/pages/it-manager.html:544 msgid "Добавить запись" msgstr "Додати запис" -#: templates/pages/it-manager.html:479 +#: templates/pages/it-manager.html:482 msgid "Добавить запись о выполненной работе" msgstr "Додати запис про виконану роботу" -#: templates/pages/it-manager.html:495 +#: templates/pages/it-manager.html:498 msgid "+ Добавить новый вид работ..." msgstr "+ Додати новий вид робіт..." -#: templates/pages/it-manager.html:510 +#: templates/pages/it-manager.html:513 msgid "Затрачено часов" msgstr "Витрачено годин" -#: templates/pages/it-manager.html:532 +#: templates/pages/it-manager.html:535 msgid "Описание работ" msgstr "Опис робіт" -#: templates/pages/it-manager.html:536 +#: templates/pages/it-manager.html:539 msgid "Результат" msgstr "Результат" -#: templates/pages/it-manager.html:560 templates/pages/timetrack.html:59 +#: templates/pages/it-manager.html:563 templates/pages/timetrack.html:59 msgid "Часов" msgstr "Годин" -#: templates/pages/it-manager.html:574 +#: templates/pages/it-manager.html:577 msgid "Записей ещё нет" msgstr "Записів ще немає" -#: templates/pages/it-manager.html:589 +#: templates/pages/it-manager.html:592 msgid "Всего позиций" msgstr "Всього позицій" -#: templates/pages/it-manager.html:593 +#: templates/pages/it-manager.html:596 msgid "Заканчивается / на замену" msgstr "Закінчується / на заміну" -#: templates/pages/it-manager.html:603 +#: templates/pages/it-manager.html:606 msgid "Добавить материал" msgstr "Додати матеріал" -#: templates/pages/it-manager.html:613 +#: templates/pages/it-manager.html:616 msgid "Добавить расходный материал" msgstr "Додати витратний матеріал" -#: templates/pages/it-manager.html:634 +#: templates/pages/it-manager.html:637 msgid "+ Добавить новый цвет..." msgstr "+ Додати новий колір..." -#: templates/pages/it-manager.html:638 +#: templates/pages/it-manager.html:641 msgid "Производитель" msgstr "Виробник" -#: templates/pages/it-manager.html:642 +#: templates/pages/it-manager.html:645 msgid "Модель / артикул" msgstr "Модель / артикул" -#: templates/pages/it-manager.html:646 templates/pages/it-manager.html:695 +#: templates/pages/it-manager.html:649 templates/pages/it-manager.html:698 msgid "Принтер" msgstr "Принтер" -#: templates/pages/it-manager.html:655 +#: templates/pages/it-manager.html:658 msgid "Локация хранения" msgstr "Локація зберігання" -#: templates/pages/it-manager.html:666 +#: templates/pages/it-manager.html:669 msgid "Ресурс (страниц)" msgstr "Ресурс (сторінок)" -#: templates/pages/it-manager.html:693 +#: templates/pages/it-manager.html:696 msgid "Тип / Модель" msgstr "Тип / Модель" -#: templates/pages/it-manager.html:736 +#: templates/pages/it-manager.html:739 msgid "Ресурс:" msgstr "Ресурс:" -#: templates/pages/it-manager.html:736 +#: templates/pages/it-manager.html:739 msgid "стр." msgstr "стор." -#: templates/pages/it-manager.html:745 +#: templates/pages/it-manager.html:748 msgid "Расходных материалов ещё не добавлено" msgstr "Витратних матеріалів ще не додано" -#: templates/pages/it-manager.html:760 +#: templates/pages/it-manager.html:763 msgid "Провайдеров" msgstr "Провайдерів" -#: templates/pages/it-manager.html:764 +#: templates/pages/it-manager.html:767 msgid "Договоров" msgstr "Договорів" -#: templates/pages/it-manager.html:768 +#: templates/pages/it-manager.html:771 msgid "Заканчиваются через 30 дней" msgstr "Закінчуються за 30 днів" -#: templates/pages/it-manager.html:778 templates/pages/it-manager.html:850 +#: templates/pages/it-manager.html:781 templates/pages/it-manager.html:853 msgid "Добавить провайдера" msgstr "Додати провайдера" -#: templates/pages/it-manager.html:784 templates/pages/it-manager.html:955 +#: templates/pages/it-manager.html:787 templates/pages/it-manager.html:958 msgid "Добавить договор" msgstr "Додати договір" -#: templates/pages/it-manager.html:803 +#: templates/pages/it-manager.html:806 msgid "Провайдеров ещё нет" msgstr "Провайдерів ще немає" -#: templates/pages/it-manager.html:814 +#: templates/pages/it-manager.html:817 msgid "Новый провайдер" msgstr "Новий провайдер" -#: templates/pages/it-manager.html:833 +#: templates/pages/it-manager.html:836 msgid "Сайт" msgstr "Сайт" -#: templates/pages/it-manager.html:837 +#: templates/pages/it-manager.html:840 msgid "Телефон техподдержки" msgstr "Телефон техпідтримки" -#: templates/pages/it-manager.html:841 +#: templates/pages/it-manager.html:844 msgid "Email техподдержки" msgstr "Email техпідтримки" -#: templates/pages/it-manager.html:864 +#: templates/pages/it-manager.html:867 msgid "Новый договор" msgstr "Новий договір" -#: templates/pages/it-manager.html:891 +#: templates/pages/it-manager.html:894 msgid "Номер договора" msgstr "Номер договору" -#: templates/pages/it-manager.html:895 templates/pages/it-manager.html:970 +#: templates/pages/it-manager.html:898 templates/pages/it-manager.html:973 #: templates/pages/pharmacy-detail.html:244 msgid "Тип услуги" msgstr "Тип послуги" -#: templates/pages/it-manager.html:904 +#: templates/pages/it-manager.html:907 msgid "Технология подключения" msgstr "Технологія підключення" -#: templates/pages/it-manager.html:910 +#: templates/pages/it-manager.html:913 msgid "+ Добавить новую технологию..." msgstr "+ Додати нову технологію..." -#: templates/pages/it-manager.html:918 +#: templates/pages/it-manager.html:921 msgid "Дата окончания" msgstr "Дата закінчення" -#: templates/pages/it-manager.html:922 +#: templates/pages/it-manager.html:925 msgid "Стоимость/мес." msgstr "Вартість/міс." -#: templates/pages/it-manager.html:926 +#: templates/pages/it-manager.html:929 msgid "Скорость ↓ Мбит/с" msgstr "Швидкість ↓ Мбіт/с" -#: templates/pages/it-manager.html:930 +#: templates/pages/it-manager.html:933 msgid "Скорость ↑ Мбит/с" msgstr "Швидкість ↑ Мбіт/с" -#: templates/pages/it-manager.html:934 +#: templates/pages/it-manager.html:937 msgid "Лицевой счёт" msgstr "Особовий рахунок" -#: templates/pages/it-manager.html:941 +#: templates/pages/it-manager.html:944 msgid "Выделенный IP" msgstr "Виділений IP" -#: templates/pages/it-manager.html:945 +#: templates/pages/it-manager.html:948 msgid "По одному IP-адресу на строку" msgstr "По одній IP-адресі на рядок" -#: templates/pages/it-manager.html:969 +#: templates/pages/it-manager.html:972 msgid "Провайдер / Локация" msgstr "Провайдер / Локація" -#: templates/pages/it-manager.html:971 templates/pages/pharmacy-detail.html:246 +#: templates/pages/it-manager.html:974 +#: templates/pages/pharmacy-detail.html:246 msgid "Скорость" msgstr "Швидкість" -#: templates/pages/it-manager.html:1013 +#: templates/pages/it-manager.html:1016 msgid "Счёт:" msgstr "Рахунок:" -#: templates/pages/it-manager.html:1013 +#: templates/pages/it-manager.html:1016 msgid "грн/мес" msgstr "грн/міс" -#: templates/pages/it-manager.html:1018 +#: templates/pages/it-manager.html:1021 msgid "Договоров ещё нет" msgstr "Договорів ще немає" -#: templates/pages/it-manager.html:1032 +#: templates/pages/it-manager.html:1035 msgid "обращений" msgstr "звернень" -#: templates/pages/it-manager.html:1037 templates/pages/it-manager.html:1047 +#: templates/pages/it-manager.html:1040 templates/pages/it-manager.html:1050 msgid "Новое обращение" msgstr "Нове звернення" -#: templates/pages/it-manager.html:1054 templates/pages/it-manager.html:1099 +#: templates/pages/it-manager.html:1057 templates/pages/it-manager.html:1102 msgid "Кто обратился" msgstr "Хто звернувся" -#: templates/pages/it-manager.html:1068 +#: templates/pages/it-manager.html:1071 msgid "+ Добавить новый канал..." msgstr "+ Додати новий канал..." -#: templates/pages/it-manager.html:1072 +#: templates/pages/it-manager.html:1075 msgid "Затрачено минут" msgstr "Витрачено хвилин" -#: templates/pages/it-manager.html:1076 +#: templates/pages/it-manager.html:1079 msgid "Описание обращения" msgstr "Опис звернення" -#: templates/pages/it-manager.html:1080 +#: templates/pages/it-manager.html:1083 msgid "Решение" msgstr "Рішення" -#: templates/pages/it-manager.html:1085 +#: templates/pages/it-manager.html:1088 msgid "Добавить обращение" msgstr "Додати звернення" -#: templates/pages/it-manager.html:1101 +#: templates/pages/it-manager.html:1104 msgid "Принял" msgstr "Прийняв" -#: templates/pages/it-manager.html:1117 +#: templates/pages/it-manager.html:1120 msgid "Обращений ещё нет" msgstr "Звернень ще немає" @@ -3441,7 +3795,8 @@ msgid "Это необратимо." msgstr "Це незворотньо." #: templates/pages/news-detail.html:24 templates/pages/task-detail.html:48 -#: templates/pages/tasks.html:135 templates/partials/pharmacy-documents.html:51 +#: templates/pages/tasks.html:135 +#: templates/partials/pharmacy-documents.html:51 msgid "Удалить" msgstr "Видалити" @@ -3572,14 +3927,6 @@ msgstr "Інтернет-провайдер" msgid "Коммунальный отдел" msgstr "Комунальний відділ" -#: templates/pages/pharmacy-detail.html:88 -msgid "Регион" -msgstr "Регіон" - -#: templates/pages/pharmacy-detail.html:90 -msgid "Категория" -msgstr "Категорія" - #: templates/pages/pharmacy-detail.html:91 msgid "Категория НДС" msgstr "Категорія ПДВ" @@ -3681,8 +4028,12 @@ msgid "Количество записей в таблицах по умолча msgstr "Кількість записів у таблицях за замовчуванням" #: templates/pages/profile.html:100 -msgid "Применяется ко всем таблицам портала, если для конкретной таблицы размер не выбран вручную." -msgstr "Застосовується до всіх таблиць порталу, якщо для конкретної таблиці розмір не вибрано вручну." +msgid "" +"Применяется ко всем таблицам портала, если для конкретной таблицы размер не " +"выбран вручную." +msgstr "" +"Застосовується до всіх таблиць порталу, якщо для конкретної таблиці розмір " +"не вибрано вручну." #: templates/pages/settings.html:6 msgid "Конфигурация портала" @@ -3860,33 +4211,62 @@ msgstr "Немає відміток робочого часу за" msgid "сотрудников" msgstr "співробітників" +#: templates/partials/dislo_cell.html:15 +msgid "не назначен" +msgstr "не призначений" + +#: templates/partials/dislo_cell.html:23 +msgid "Нехватка" +msgstr "Нестача" + +#: templates/partials/dislo_cell.html:25 +msgid "Внимание" +msgstr "Увага" + +#: templates/partials/dislo_cell.html:27 +msgid "Норма" +msgstr "Норма" + #: templates/partials/documents-browser.html:28 #: templates/partials/pharmacy-documents.html:18 msgid "Файловое хранилище пока не настроено." -msgstr "" +msgstr "Файлове сховище поки не налаштоване." #: templates/partials/documents-browser.html:32 #: templates/partials/pharmacy-documents.html:22 msgid "Не удалось получить список документов." -msgstr "" +msgstr "Не вдалося отримати список документів." #: templates/partials/documents-browser.html:61 msgid "В этой папке пока нет файлов." -msgstr "" +msgstr "У цій папці поки немає файлів." #: templates/partials/documents-browser.html:74 #: templates/partials/pharmacy-documents.html:76 msgid "Загрузить" -msgstr "" +msgstr "Завантажити" #: templates/partials/pharmacy-documents.html:4 msgid "Документы аптеки" -msgstr "" +msgstr "Документи аптеки" #: templates/partials/pharmacy-documents.html:48 msgid "Удалить файл?" -msgstr "" +msgstr "Видалити файл?" #: templates/partials/pharmacy-documents.html:63 msgid "В папке аптеки пока нет файлов." -msgstr "" +msgstr "У папці аптеки поки немає файлів." + +#~ msgid "Сразу создаёт личный учётный запис кандидата на срок испытания." +#~ msgstr "" +#~ "Одразу створює особистий обліковий запис кандидата на термін випробування." + +#~ msgid "Требуются доступы (после утверждения — заявка в IT)" +#~ msgstr "Потрібні доступи (після затвердження — заявка в IT)" + +#~ msgid "Создать заявку и временный аккаунт" +#~ msgstr "Створити заявку та тимчасовий акаунт" + +#~ msgid "Создание учётной записи…" +#~ msgstr "Створення облікового запису…" diff --git a/requirements.txt b/requirements.txt index b6b7a4b..6af8368 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,6 +28,7 @@ django-tinymce==5.0.0 django-unfold==0.87.0 Django==6.0.3 djangorestframework==3.15.2 +fpdf2==2.8.7 et_xmlfile==2.0.0 frozenlist==1.8.0 h11==0.16.0 diff --git a/static/assets/js/hr-portal.js b/static/assets/js/hr-portal.js index f1a5718..4dea643 100644 --- a/static/assets/js/hr-portal.js +++ b/static/assets/js/hr-portal.js @@ -9,6 +9,12 @@ function hrTabSwitch(tab) { btn.style.color = active ? 'var(--color-card-foreground)' : 'var(--color-muted-foreground)'; btn.style.boxShadow = active ? '0 1px 2px rgb(0 0 0/0.08)' : ''; }); + var viewToggle = document.getElementById('hr-view-toggle'); + if (viewToggle) { + viewToggle.style.display = (tab === 'employees') ? '' : 'none'; + } + if (tab === 'hiring') hrHiringInit(); + if (tab === 'employees') hrFilterEmployees(); } function hrViewSwitch(view) { @@ -27,7 +33,6 @@ function hrViewSwitch(view) { } function hrLocFilter(loc) { - var toggle = document.getElementById('hr-view-toggle'); document.querySelectorAll('.hr-loc-btn').forEach(function (btn) { var active = btn.getAttribute('data-loc') === loc; btn.classList.toggle('hr-loc-active', active); @@ -35,9 +40,30 @@ function hrLocFilter(loc) { btn.style.color = active ? 'var(--color-card-foreground)' : 'var(--color-muted-foreground)'; btn.style.boxShadow = active ? '0 1px 2px rgb(0 0 0/0.08)' : ''; }); + hrFilterListPosts(loc); hrFilterEmployees(); } +/** У фільтрі списку працівників показує лише посади для обраного типу локації. */ +function hrFilterListPosts(locationType) { + var select = document.getElementById('hr-post-select'); + var dataEl = document.getElementById('hr-post-loc-types-data'); + if (!select || !dataEl) return; + var map = {}; + try { map = JSON.parse(dataEl.textContent); } catch (e) { map = {}; } + Array.prototype.forEach.call(select.options, function (opt) { + if (!opt.value) return; + var types = map[opt.value] || []; + var ok = !locationType || types.length === 0 || types.indexOf(locationType) !== -1; + opt.hidden = !ok; + opt.disabled = !ok; + }); + var sel = select.options[select.selectedIndex]; + if (select.value && sel && (sel.hidden || sel.disabled)) { + select.value = ''; + } +} + function hrFilterEmployees() { var searchInput = document.getElementById('hr-search-input'); var deptSelect = document.getElementById('hr-department-select'); @@ -232,12 +258,64 @@ function hrFilterEmployees() { function hrHiringLocToggle(locationType) { var deptField = document.getElementById('hr-hiring-department-field'); var pharmField = document.getElementById('hr-hiring-pharmacy-field'); - if (deptField && pharmField) { - var isPharmacy = locationType === 'pharmacy'; - deptField.style.display = isPharmacy ? 'none' : ''; - pharmField.style.display = isPharmacy ? '' : 'none'; + var locField = document.getElementById('hr-hiring-location-field'); + var deptSelect = document.getElementById('hr-hiring-department-select'); + var pharmSelect = document.getElementById('hr-hiring-pharmacy-select'); + var locSelect = document.getElementById('hr-hiring-location-select'); + var reqOffice = document.querySelectorAll('.hr-req-office'); + + var isPharmacy = locationType === 'pharmacy'; + var isOfficeLike = locationType === 'office' || locationType === 'warehouse'; + + if (deptField) deptField.style.display = isPharmacy ? 'none' : ''; + if (pharmField) pharmField.style.display = isPharmacy ? '' : 'none'; + if (locField) locField.style.display = isOfficeLike ? '' : 'none'; + + Array.prototype.forEach.call(reqOffice, function (el) { + el.style.display = isOfficeLike ? '' : 'none'; + }); + + if (deptSelect) { + deptSelect.required = isOfficeLike; + if (isPharmacy) deptSelect.value = ''; } + if (pharmSelect) { + pharmSelect.required = isPharmacy; + if (!isPharmacy) pharmSelect.value = ''; + } + if (locSelect) { + locSelect.required = isOfficeLike; + Array.prototype.forEach.call(locSelect.options, function (opt) { + if (!opt.value) return; + var ok = !locationType || opt.getAttribute('data-type') === locationType; + opt.hidden = !ok; + opt.disabled = !ok; + }); + var sel = locSelect.options[locSelect.selectedIndex]; + if (locSelect.value && sel && sel.hidden) locSelect.value = ''; + if (!isOfficeLike) locSelect.value = ''; + } + hrHiringFilterPosts(locationType); + hrHiringFilterAccesses(locationType); +} + +// Показує чекбокси доступів лише для обраного типу локації. +// Порожній список типів у системи = доступна скрізь (як у посад). +function hrHiringFilterAccesses(locationType) { + var dataEl = document.getElementById('hr-access-loc-types-data'); + var options = document.querySelectorAll('.hr-access-option'); + if (!dataEl || !options.length) return; + var map = {}; + try { map = JSON.parse(dataEl.textContent); } catch (e) { map = {}; } + Array.prototype.forEach.call(options, function (label) { + var id = label.getAttribute('data-access-id'); + var types = map[id] || []; + var ok = !locationType || types.length === 0 || types.indexOf(locationType) !== -1; + label.style.display = ok ? 'flex' : 'none'; + var cb = label.querySelector('input[type=checkbox]'); + if (cb && !ok) cb.checked = false; + }); } // Показує в селекті «Посада» лише посади, доступні для обраного типу локації. @@ -258,3 +336,61 @@ function hrHiringFilterPosts(locationType) { var sel = select.options[select.selectedIndex]; if (select.value && sel && sel.hidden) select.value = ''; } + +/** Ініціалізація форми найму: за замовчуванням «Аптека» + застосування фільтрів. */ +function hrHiringInit() { + var sel = document.getElementById('hr-hiring-loc-type') + || document.querySelector('#hr-panel-hiring select[name="user_location_type"]'); + if (!sel) return; + if (!sel.value) sel.value = 'pharmacy'; + hrHiringLocToggle(sel.value); +} + +/** Ініціалізація HR-порталу після завантаження / HTMX-swap. */ +function hrPortalInit() { + var root = document.querySelector('.page-content[data-hr-active-tab]'); + var activeTab = root ? root.getAttribute('data-hr-active-tab') : ''; + if (activeTab && activeTab !== 'employees') { + hrTabSwitch(activeTab); + } else { + // Сбрасываем визуальное состояние вкладок на «Сотрудники» + document.querySelectorAll('.hr-tab-btn').forEach(function (btn) { + var active = btn.getAttribute('data-tab') === 'employees'; + btn.classList.toggle('hr-tab-active', active); + btn.style.background = active ? 'var(--color-card)' : 'transparent'; + btn.style.color = active ? 'var(--color-card-foreground)' : 'var(--color-muted-foreground)'; + btn.style.boxShadow = active ? '0 1px 2px rgb(0 0 0/0.08)' : ''; + }); + document.querySelectorAll('.hr-panel').forEach(function (panel) { + panel.style.display = panel.id === 'hr-panel-employees' ? '' : 'none'; + }); + var viewToggle = document.getElementById('hr-view-toggle'); + if (viewToggle) viewToggle.style.display = ''; + } + + var activeLoc = document.querySelector('.hr-loc-btn.hr-loc-active'); + // За замовчуванням — «Всі» (порожній data-loc), не «Аптека». + var loc = activeLoc ? activeLoc.getAttribute('data-loc') : ''; + if (!activeLoc) { + hrLocFilter(''); + } else { + hrFilterListPosts(loc || ''); + hrFilterEmployees(); + } + var hiringPanel = document.getElementById('hr-panel-hiring'); + if (hiringPanel && hiringPanel.style.display !== 'none') { + hrHiringInit(); + } +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', hrPortalInit); +} else { + hrPortalInit(); +} +// Слушаем на document: скрипт в , document.body ещё может быть null. +document.addEventListener('htmx:afterSwap', function (ev) { + if (ev.target && ev.target.id === 'main-content') { + hrPortalInit(); + } +}); diff --git a/templates/index.html b/templates/index.html index 8c485b5..a17d9d0 100644 --- a/templates/index.html +++ b/templates/index.html @@ -278,6 +278,7 @@ {% trans "Документы" %} + {% if request.user|can_section:'email' %} @@ -287,6 +288,7 @@ {% trans "Электронная почта" %} + {% endif %} {% trans "Календарь" %} + {% if request.user|can_section:'databases' %} @@ -314,7 +317,9 @@ {% trans "Базы данных" %} + {% endif %} + {% if request.user|can_section:'reports' %} @@ -323,7 +328,9 @@ {% trans "Отчёты" %} + {% endif %} + {% if request.user|can_section:'purchases' %} @@ -333,7 +340,9 @@ {% trans "Закупки" %} + {% endif %} + {% if request.user|can_section:'hr_portal' %} @@ -342,6 +351,7 @@ {% trans "HR портал" %} + {% endif %} @@ -371,6 +382,7 @@ {% trans "Структура" %} + {% endif %} {% trans "Аптеки" %} + + + + + {% trans "РРО / ПРРО" %} + + @@ -391,6 +412,7 @@ {% trans "Коммунальные услуги" %} + {% if request.user|can_section:'recruiting' %} @@ -401,7 +423,9 @@ {% trans "Рекрутинг" %} + {% endif %} + {% if request.user|can_section:'timetrack' %} @@ -410,6 +434,7 @@ {% trans "Учёт рабочего времени" %} + {% endif %} @@ -439,7 +464,7 @@ {% trans "Поддержка IT" %} - {% if request.user|is_it_staff %} + {% if request.user|can_section:'it_manager' %} @@ -450,7 +475,7 @@ {% endif %} - {% if request.user.is_staff %} + {% if request.user|can_section:'ad_mapping' %} diff --git a/templates/pages/dislocation.html b/templates/pages/dislocation.html index 01c11e8..a6bc653 100644 --- a/templates/pages/dislocation.html +++ b/templates/pages/dislocation.html @@ -91,7 +91,12 @@ -
+
+
+ +
@@ -113,13 +156,9 @@ - {% trans "Аптека" %} ⇅ - {% trans "Организация" %} ⇅ - {% trans "Заведующий" %} ⇅ - {% trans "Персонал" %} ⇅ - {% trans "Телефон" %} ⇅ - {% trans "Режим работы" %} ⇅ - {% trans "Статус" %} ⇅ + {% for col in visible_columns %} + {{ col.label }} ⇅ + {% endfor %} @@ -137,55 +176,29 @@ data-enterprise="{{ pharmacy.organization.name|escape }}" data-name="{{ pharmacy.organization.name }} #{{ pharmacy.pharmacy_number }}" data-sort-pharmacy="{{ pharmacy.pharmacy_number|default:0 }}" - data-sort-org="{{ pharmacy.organization.name|default:""|escape }}" + data-sort-organization="{{ pharmacy.organization.name|default:""|escape }}" data-sort-head="{% if pharmacy.head_pharmacist %}{{ pharmacy.head_pharmacist.last_name }} {{ pharmacy.head_pharmacist.first_name }}{% endif %}" data-sort-staff="{{ pharmacy.staff_count|default:0 }}" data-sort-phone="{{ pharmacy.phone|default:"" }}" data-sort-hours="{{ pharmacy.working_hours|default:"" }}" - data-sort-status="{% if pharmacy.is_active %}1{% else %}0{% endif %}"> + data-sort-status="{% if pharmacy.is_active %}1{% else %}0{% endif %}" + data-sort-region="{{ pharmacy.location_region.name|default:""|escape }}" + data-sort-category="{{ pharmacy.category|default:"" }}" + data-sort-curator="{% if pharmacy.curator %}{{ pharmacy.curator.last_name }} {{ pharmacy.curator.first_name }}{% endif %}" + data-sort-manager="{% if pharmacy.responsible_manager %}{{ pharmacy.responsible_manager.last_name }} {{ pharmacy.responsible_manager.first_name }}{% endif %}" + data-sort-license="{{ pharmacy.license_number|default:"" }}" + data-sort-vat="{% if pharmacy.vat_category %}{{ pharmacy.get_vat_category_display }}{% endif %}" + data-sort-email="{{ pharmacy.carbonio_email|default:"" }}"> ▸ - - № {{ pharmacy.pharmacy_number }} -
{{ pharmacy.address|default:no_addr_short }}
- - {{ pharmacy.organization.name|default:"—" }} - - {% if pharmacy.head_pharmacist %} - {{ pharmacy.head_pharmacist.first_name }} {{ pharmacy.head_pharmacist.last_name|default:'' }} - {% else %} - {% trans "не назначен" %} - {% endif %} - - -
- {{ pharmacy.staff_count }} - {% if pharmacy.staff_status == 'danger' %} - {% trans "Нехватка" %} - {% elif pharmacy.staff_status == 'warning' %} - {% trans "Внимание" %} - {% else %} - {% trans "Норма" %} - {% endif %} -
- - - {% if pharmacy.phone %}{{ pharmacy.phone }}{% else %}-{% endif %} - - {{ pharmacy.working_hours }} - - {% if pharmacy.is_active %} - {% trans "Активна" %} - {% else %} - {% trans "Неактивна" %} - {% endif %} - + {% for col in visible_columns %} + {% include "partials/dislo_cell.html" with column=col pharmacy=pharmacy no_addr_short=no_addr_short %} + {% endfor %} - + {% if pharmacy.active_staff %}
{% for employee in pharmacy.active_staff %} @@ -211,7 +224,7 @@ {% empty %} - {% trans "Аптеки не найдены" %} + {% trans "Аптеки не найдены" %} {% endfor %} @@ -603,6 +616,7 @@ function disloView(view) { const mapView = document.getElementById('dislo-view-map'); const btnTable = document.getElementById('dislo-btn-table'); const btnMap = document.getElementById('dislo-btn-map'); + const columnsPanel = document.getElementById('dislo-columns-panel'); if (view === 'table') { tableView.style.display = 'block'; @@ -614,6 +628,7 @@ function disloView(view) { } else { tableView.style.display = 'none'; mapView.style.display = 'block'; + if (columnsPanel) columnsPanel.style.display = 'none'; btnTable.style.background = 'transparent'; btnTable.style.color = 'var(--color-muted-foreground)'; btnMap.style.background = 'var(--color-card)'; @@ -626,6 +641,62 @@ function disloView(view) { } } +function disloToggleColumnsPanel() { + const panel = document.getElementById('dislo-columns-panel'); + if (!panel) return; + const opening = panel.style.display === 'none' || !panel.style.display; + panel.style.display = opening ? 'block' : 'none'; + if (opening) disloView('table'); +} + +function disloColMove(btn, dir) { + const row = btn.closest('.dislo-col-row'); + const container = document.getElementById('dislo-col-order'); + if (!row || !container) return; + const siblings = Array.from(container.children); + const idx = siblings.indexOf(row); + const targetIdx = idx + dir; + if (targetIdx < 0 || targetIdx >= siblings.length) return; + const target = siblings[targetIdx]; + if (dir < 0) { + container.insertBefore(row, target); + } else { + container.insertBefore(target, row); + } +} + +function disloApplyPreset(selectEl) { + const code = selectEl.value; + if (!code) return; + const csrf = document.querySelector('#dislo-columns-panel input[name=csrfmiddlewaretoken]'); + if (window.htmx) { + htmx.ajax('POST', '/dislocation/columns/', { + target: '#main-content', + swap: 'innerHTML', + values: { preset_code: code, csrfmiddlewaretoken: csrf ? csrf.value : '' }, + headers: csrf ? { 'X-CSRFToken': csrf.value } : {}, + }); + return; + } + const form = document.createElement('form'); + form.method = 'POST'; + form.action = '/dislocation/columns/'; + if (csrf) { + const csrfInput = document.createElement('input'); + csrfInput.type = 'hidden'; + csrfInput.name = 'csrfmiddlewaretoken'; + csrfInput.value = csrf.value; + form.appendChild(csrfInput); + } + const presetInput = document.createElement('input'); + presetInput.type = 'hidden'; + presetInput.name = 'preset_code'; + presetInput.value = code; + form.appendChild(presetInput); + document.body.appendChild(form); + form.submit(); +} + function disloGetVisiblePharmacyIds() { const searchText = document.getElementById('dislo-search').value.toLowerCase(); const regionFilter = document.getElementById('dislo-filter-region').value; diff --git a/templates/pages/hr-portal.html b/templates/pages/hr-portal.html index dd7316a..0e56b29 100644 --- a/templates/pages/hr-portal.html +++ b/templates/pages/hr-portal.html @@ -1,13 +1,40 @@ {% load common_tags %} {% load i18n %} {% trans "Должность не указана" as no_post %} -
+

{% trans "HR Портал" %}

{% trans "Управление персоналом и кадровые процессы" %}

+ {% if hiring_just_created %} +
+
{% trans "Заявку створено" %}
+ {% with intern=hiring_just_created.intern_account %} + {% if intern %} +
{% trans "Інтерн-слот" %}: {{ intern.label }} · {% trans "логін" %} {{ intern.username }} · {% trans "пароль" %} {{ intern.password }}
+ {% endif %} + {% endwith %} + {% if hiring_just_created.granted_ticket %} +
{% trans "Заявка IT" %}: {{ hiring_just_created.granted_ticket.number }}
+ {% endif %} +
+ {% endif %} + + {% if hiring_form_errors %} +
+
{% trans "Не удалось создать заявку" %}
+
    + {% for field, messages in hiring_form_errors.items %} + {% for msg in messages %} +
  • {{ msg }}
  • + {% endfor %} + {% endfor %} +
+
+ {% endif %} +
@@ -26,7 +53,7 @@
-
+
{% trans "На испытательном сроке" %}
{{ employees_probation }}
@@ -90,6 +117,8 @@
+ @@ -159,6 +189,7 @@ data-department="{{ employee.department_id|default:'' }}" data-post="{{ employee.post_id|default:'' }}" data-loc="{{ employee.user_location_type|default:'' }}" + data-probation="{% if employee.is_on_probation %}1{% else %}0{% endif %}" style="background:var(--color-card); border-radius:0.75rem; border:1px solid var(--color-border); box-shadow:0 1px 2px rgb(0 0 0/0.05); padding:1.25rem;">
{% if employee.photo %} @@ -226,6 +257,7 @@ data-department="{{ employee.department_id|default:'' }}" data-post="{{ employee.post_id|default:'' }}" data-loc="{{ employee.user_location_type|default:'' }}" + data-probation="{% if employee.is_on_probation %}1{% else %}0{% endif %}" style="border-bottom:1px solid var(--color-border); cursor:pointer;" hx-get="/employees/{{ employee.pk }}/" hx-target="#main-content" hx-push-url="/employees/{{ employee.pk }}/" hx-indicator="#loader"> @@ -266,6 +298,110 @@
+ + +
- {% for emp in it_staff %} + {% empty %} + {% endfor %}
diff --git a/templates/pages/pharmacy-detail.html b/templates/pages/pharmacy-detail.html index 6452b84..7c3b513 100644 --- a/templates/pages/pharmacy-detail.html +++ b/templates/pages/pharmacy-detail.html @@ -69,6 +69,8 @@ style="padding:0.375rem 1rem; font-size:0.8125rem; font-weight:500; border:none; border-radius:calc(var(--radius) - 2px); cursor:pointer; background:transparent; color:var(--color-muted-foreground);">{% trans "Сетевое и компьютерное оборудование" %} + + {% if is_staff %} + {% trans "Добавить в админке" %} + {% endif %} + + +
+
+ + + + + + + + + + + + + + {% for reg in registers %} + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Аптека" %}{% trans "Тип" %}{% trans "Фискальный номер" %}{% trans "Модель" %}{% trans "Статус" %}{% trans "Сертификат до" %}
+ + {{ reg.pharmacy.organization.name }} №{{ reg.pharmacy.pharmacy_number }} + + {% if reg.is_primary %}{% trans "осн." %}{% endif %} + {{ reg.get_kind_display }}{{ reg.fiscal_number }} + {% if reg.vendor %}{{ reg.vendor.name }} {% endif %}{{ reg.model_name|default:"—" }} + + {% if reg.status.code == 'active' %}{{ reg.status.name }} + {% elif reg.status.code == 'repair' %}{{ reg.status.name }} + {% elif reg.status.code == 'decommissioned' %}{{ reg.status.name }} + {% else %}{{ reg.status.name }}{% endif %} + + {% if reg.cert_expires_at %}{{ reg.cert_expires_at|date:"d.m.Y" }}{% else %}—{% endif %} + + {% if is_staff %} + {% trans "Изменить" %} + {% endif %} +
+ {% trans "Кассовые аппараты не найдены" %} +
+
+
+
diff --git a/templates/partials/dislo_cell.html b/templates/partials/dislo_cell.html new file mode 100644 index 0000000..034409a --- /dev/null +++ b/templates/partials/dislo_cell.html @@ -0,0 +1,65 @@ +{% load i18n %} +{% if column.key == 'pharmacy' %} + + № {{ pharmacy.pharmacy_number }} +
{{ pharmacy.address|default:no_addr_short }}
+ +{% elif column.key == 'organization' %} +{{ pharmacy.organization.name|default:"—" }} +{% elif column.key == 'head' %} + + {% if pharmacy.head_pharmacist %} + {{ pharmacy.head_pharmacist.first_name }} {{ pharmacy.head_pharmacist.last_name|default:'' }} + {% else %} + {% trans "не назначен" %} + {% endif %} + +{% elif column.key == 'staff' %} + +
+ {{ pharmacy.staff_count }} + {% if pharmacy.staff_status == 'danger' %} + {% trans "Нехватка" %} + {% elif pharmacy.staff_status == 'warning' %} + {% trans "Внимание" %} + {% else %} + {% trans "Норма" %} + {% endif %} +
+ +{% elif column.key == 'phone' %} + + {% if pharmacy.phone %}{{ pharmacy.phone }}{% else %}-{% endif %} + +{% elif column.key == 'hours' %} +{{ pharmacy.working_hours }} +{% elif column.key == 'status' %} + + {% if pharmacy.is_active %} + {% trans "Активна" %} + {% else %} + {% trans "Неактивна" %} + {% endif %} + +{% elif column.key == 'region' %} +{{ pharmacy.location_region.name|default:"—" }} +{% elif column.key == 'category' %} +{{ pharmacy.category|default:"—" }} +{% elif column.key == 'curator' %} + + {% if pharmacy.curator %}{{ pharmacy.curator.last_name }} {{ pharmacy.curator.first_name }}{% else %}—{% endif %} + +{% elif column.key == 'manager' %} + + {% if pharmacy.responsible_manager %}{{ pharmacy.responsible_manager.last_name }} {{ pharmacy.responsible_manager.first_name }}{% else %}—{% endif %} + +{% elif column.key == 'license' %} +{{ pharmacy.license_number|default:"—" }} +{% elif column.key == 'vat' %} +{% if pharmacy.vat_category %}{{ pharmacy.get_vat_category_display }}{% else %}—{% endif %} +{% elif column.key == 'email' %} +{{ pharmacy.carbonio_email|default:"—" }} +{% else %} +— +{% endif %}