This commit is contained in:
Flatlogic Bot 2026-01-27 20:26:20 +00:00
parent 0e01a24380
commit d58a583044
30 changed files with 756 additions and 146 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -149,6 +149,9 @@ STATIC_URL = 'static/'
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
STATIC_ROOT = BASE_DIR / 'staticfiles'
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
STATICFILES_DIRS = [
BASE_DIR / 'static',
BASE_DIR / 'assets',

View File

@ -27,3 +27,4 @@ urlpatterns = [
if settings.DEBUG:
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -1,15 +1,36 @@
from django.contrib import admin
from .models import Category, FleetUnit, Maintenance, Breakdown, PartRequest, Document
from .models import Category, FleetUnit, Maintenance, Breakdown, PartRequest, Document, Supplier
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name',)
@admin.register(Supplier)
class SupplierAdmin(admin.ModelAdmin):
list_display = ('name', 'representative_name', 'phone', 'email', 'contract_number')
search_fields = ('name', 'representative_name', 'contract_number')
@admin.register(FleetUnit)
class FleetUnitAdmin(admin.ModelAdmin):
list_display = ('name', 'category', 'plate_number', 'status', 'year')
list_filter = ('status', 'category')
search_fields = ('name', 'vin', 'plate_number')
fieldsets = (
('Основная информация', {
'fields': ('name', 'category', 'model_name', 'vin', 'plate_number', 'year', 'photo', 'status', 'commissioning_date', 'notes')
}),
('Страховка', {
'fields': ('insurance_company', 'insurance_policy_number', 'insurance_start_date', 'insurance_end_date')
}),
('Снабжение и Поставщик', {
'fields': ('supplier', 'vehicle_documents', 'supplier_name', 'supplier_contacts')
}),
('QR-код', {
'fields': ('qr_code',),
'classes': ('collapse',)
}),
)
@admin.register(Maintenance)
class MaintenanceAdmin(admin.ModelAdmin):

View File

@ -0,0 +1,43 @@
# Generated by Django 5.2.7 on 2026-01-27 20:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0003_setup_groups'),
]
operations = [
migrations.AddField(
model_name='fleetunit',
name='insurance_company',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Страховая компания'),
),
migrations.AddField(
model_name='fleetunit',
name='insurance_end_date',
field=models.DateField(blank=True, null=True, verbose_name='Дата окончания страховки'),
),
migrations.AddField(
model_name='fleetunit',
name='insurance_start_date',
field=models.DateField(blank=True, null=True, verbose_name='Дата начала страховки'),
),
migrations.AddField(
model_name='fleetunit',
name='supplier_contacts',
field=models.TextField(blank=True, null=True, verbose_name='Контакты поставщика'),
),
migrations.AddField(
model_name='fleetunit',
name='supplier_name',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Поставщик / Контрагент'),
),
migrations.AddField(
model_name='fleetunit',
name='vehicle_documents',
field=models.FileField(blank=True, null=True, upload_to='fleet_docs/', verbose_name='Документы на авто'),
),
]

View File

@ -0,0 +1,49 @@
# Generated by Django 5.2.7 on 2026-01-27 20:11
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0004_fleetunit_insurance_company_and_more'),
]
operations = [
migrations.CreateModel(
name='Supplier',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Наименование компании')),
('representative_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='ФИО представителя/менеджера')),
('phone', models.CharField(blank=True, max_length=50, null=True, verbose_name='Телефон')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Электронная почта')),
('contract_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='Номер договора')),
],
options={
'verbose_name': 'Поставщик / Контрагент',
'verbose_name_plural': 'Поставщики / Контрагенты',
},
),
migrations.AddField(
model_name='fleetunit',
name='insurance_policy_number',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Номер страхового полиса'),
),
migrations.AlterField(
model_name='fleetunit',
name='supplier_contacts',
field=models.TextField(blank=True, null=True, verbose_name='Контакты поставщика (старое)'),
),
migrations.AlterField(
model_name='fleetunit',
name='supplier_name',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Поставщик (старое)'),
),
migrations.AddField(
model_name='fleetunit',
name='supplier',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.supplier', verbose_name='Поставщик / Контрагент'),
),
]

View File

@ -16,6 +16,20 @@ class Category(models.Model):
def __str__(self):
return self.name
class Supplier(models.Model):
name = models.CharField(max_length=255, verbose_name="Наименование компании")
representative_name = models.CharField(max_length=255, blank=True, null=True, verbose_name="ФИО представителя/менеджера")
phone = models.CharField(max_length=50, blank=True, null=True, verbose_name="Телефон")
email = models.EmailField(blank=True, null=True, verbose_name="Электронная почта")
contract_number = models.CharField(max_length=100, blank=True, null=True, verbose_name="Номер договора")
class Meta:
verbose_name = "Поставщик / Контрагент"
verbose_name_plural = "Поставщики / Контрагенты"
def __str__(self):
return self.name
class FleetUnit(models.Model):
STATUS_CHOICES = [
('active', 'В работе'),
@ -37,6 +51,18 @@ class FleetUnit(models.Model):
notes = models.TextField(blank=True, null=True, verbose_name="Примечания")
qr_code = models.ImageField(upload_to='qrcodes/', blank=True, null=True, verbose_name="QR-код")
# Insurance
insurance_company = models.CharField(max_length=255, blank=True, null=True, verbose_name="Страховая компания")
insurance_policy_number = models.CharField(max_length=100, blank=True, null=True, verbose_name="Номер страхового полиса")
insurance_start_date = models.DateField(blank=True, null=True, verbose_name="Дата начала страховки")
insurance_end_date = models.DateField(blank=True, null=True, verbose_name="Дата окончания страховки")
# Supplier
supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Поставщик / Контрагент")
supplier_name = models.CharField(max_length=255, blank=True, null=True, verbose_name="Поставщик (старое)")
supplier_contacts = models.TextField(blank=True, null=True, verbose_name="Контакты поставщика (старое)")
vehicle_documents = models.FileField(upload_to="fleet_docs/", blank=True, null=True, verbose_name="Документы на авто")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@ -68,13 +94,8 @@ class FleetUnit(models.Model):
self.generate_qr_code()
def generate_qr_code(self):
# We need the full URL. In a real app, we'd use the site domain.
# For now, we'll use a relative path or a placeholder if domain is unknown.
path = self.get_absolute_url()
# You might want to use a full URL here if you have a domain
# base_url = "https://yourdomain.com"
# qr_data = f"{base_url}{path}"
qr_data = path # Using path for now, or you can try to get site domain
qr_data = path
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(qr_data)
@ -194,4 +215,4 @@ class Document(models.Model):
verbose_name_plural = "Документы"
def __str__(self):
return f"{self.get_doc_type_display()} - {self.uploaded_at}"
return f"{self.get_doc_type_display()} - {self.uploaded_at}"

View File

@ -46,6 +46,9 @@
<li class="nav-item">
<a class="nav-link {% if 'part-request' in request.path %}active{% endif %}" href="{% url 'part_request_list' %}">Заявки</a>
</li>
<li class="nav-item">
<a class="nav-link {% if 'supply' in request.path or 'supplier' in request.path %}active{% endif %}" href="{% url 'supply_list' %}">Снабжение</a>
</li>
</ul>
<div class="d-flex align-items-center">
<span class="text-white-50 me-3 d-none d-md-inline small">
@ -81,4 +84,4 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
</html>

View File

@ -0,0 +1,62 @@
{% extends 'base.html' %}
{% load static %}
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h3 class="fw-bold mb-0">Журнал поломок</h3>
<a href="{% url 'breakdown_add' %}" class="btn btn-danger rounded-pill px-4">
<i class="bi bi-exclamation-triangle me-2"></i>Сообщить о поломке
</a>
</div>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="border-0 px-4">Техника</th>
<th class="border-0">Дата</th>
<th class="border-0">Узел / Система</th>
<th class="border-0">Статус</th>
<th class="border-0 text-end px-4">Действия</th>
</tr>
</thead>
<tbody>
{% for b in breakdowns %}
<tr>
<td class="px-4">
<div class="fw-bold"><a href="{{ b.fleet_unit.get_absolute_url }}">{{ b.fleet_unit.name }}</a></div>
<div class="small text-muted">{{ b.fleet_unit.plate_number|default:b.fleet_unit.vin }}</div>
</td>
<td>{{ b.date }}</td>
<td>{{ b.system_node }}</td>
<td>
{% if b.status == 'reported' %}
<span class="badge bg-danger rounded-pill">Заявлено</span>
{% elif b.status == 'repaired' %}
<span class="badge bg-success rounded-pill">Отремонтировано</span>
{% elif b.status == 'need_part' %}
<span class="badge bg-warning text-dark rounded-pill">Нужна деталь</span>
{% else %}
<span class="badge bg-secondary rounded-pill">{{ b.status }}</span>
{% endif %}
</td>
<td class="text-end px-4">
<a href="{{ b.fleet_unit.get_absolute_url }}" class="btn btn-sm btn-outline-primary rounded-pill px-3">К технике</a>
{% if b.status == 'need_part' %}
<a href="{% url 'part_request_add' %}?breakdown={{ b.pk }}" class="btn btn-sm btn-outline-warning rounded-pill px-3">Заказать запчасть</a>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center py-5 text-muted">Нет зафиксированных поломок</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -40,6 +40,15 @@
</div>
</div>
{% if unit.insurance_company %}
<div class="bg-light p-3 rounded mb-4">
<h6 class="fw-bold small mb-2"><i class="bi bi-shield-check me-2"></i>Страховка</h6>
<p class="small mb-1"><strong>Компания:</strong> {{ unit.insurance_company }}</p>
<p class="small mb-1"><strong>Полис:</strong> {{ unit.insurance_policy_number|default:"-" }}</p>
<p class="small mb-0"><strong>Срок:</strong> {{ unit.insurance_start_date|date:"d.m.Y" }} — {{ unit.insurance_end_date|date:"d.m.Y" }}</p>
</div>
{% endif %}
<div class="d-grid gap-2">
<a href="{% url 'fleet_edit' unit.pk %}" class="btn btn-outline-primary rounded-pill">Редактировать</a>
<a href="{% url 'breakdown_add' %}?fleet_unit={{ unit.pk }}" class="btn btn-danger rounded-pill">Заявить о поломке</a>
@ -73,6 +82,9 @@
<li class="nav-item">
<button class="nav-link rounded-pill px-4" id="parts-tab" data-bs-toggle="tab" data-bs-target="#parts" type="button">Заявки на запчасти</button>
</li>
<li class="nav-item">
<button class="nav-link rounded-pill px-4" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply" type="button">Снабжение</button>
</li>
</ul>
</div>
<div class="card-body">
@ -169,6 +181,64 @@
</table>
</div>
</div>
<!-- Supply Tab -->
<div class="tab-pane fade" id="supply">
<h6 class="fw-bold mb-3">Данные по снабжению и поставщику</h6>
<div class="row g-4">
<div class="col-md-6">
{% if unit.supplier %}
<p class="text-muted small mb-1">Поставщик / Контрагент</p>
<p class="fw-bold mb-1">{{ unit.supplier.name }}</p>
<p class="small text-muted mb-3">Договор №{{ unit.supplier.contract_number|default:"-" }}</p>
<p class="text-muted small mb-1">Представитель</p>
<p class="small mb-1">{{ unit.supplier.representative_name|default:"-" }}</p>
<p class="small mb-1"><i class="bi bi-telephone me-2"></i>{{ unit.supplier.phone|default:"-" }}</p>
<p class="small"><i class="bi bi-envelope me-2"></i>{{ unit.supplier.email|default:"-" }}</p>
{% else %}
<p class="text-muted small">Поставщик не привязан</p>
<p class="small text-muted">Вы можете выбрать поставщика в режиме редактирования техники.</p>
{% endif %}
</div>
<div class="col-md-6 text-end">
<p class="text-muted small mb-1">Документы на авто</p>
{% if unit.vehicle_documents %}
<a href="{{ unit.vehicle_documents.url }}" class="btn btn-outline-secondary btn-sm rounded-pill" target="_blank">
<i class="bi bi-file-earmark-text me-2"></i>Открыть документы
</a>
{% else %}
<p class="text-muted small">Не загружены</p>
{% endif %}
</div>
</div>
<hr class="my-4">
<h6 class="fw-bold mb-3">Связанные заявки на запчасти</h6>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr>
<th>Деталь</th>
<th>Статус</th>
<th>Дата</th>
</tr>
</thead>
<tbody>
{% for r in part_requests %}
<tr>
<td>{{ r.part_name }}</td>
<td><span class="badge bg-light text-dark border rounded-pill small">{{ r.get_status_display }}</span></td>
<td class="small">{{ r.created_at|date:"d.m.Y" }}</td>
</tr>
{% empty %}
<tr><td colspan="3" class="text-center py-4 text-muted">Нет заявок</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
@ -182,4 +252,4 @@
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -1,115 +1,112 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}
{% if object %}Редактирование {{ object.name }}{% else %}Добавление техники{% endif %} | Fleet Manager
{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="mb-4">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-2">
<li class="breadcrumb-item"><a href="{% url 'index' %}" class="text-decoration-none">Дашборд</a></li>
<li class="breadcrumb-item"><a href="{% url 'fleet_list' %}" class="text-decoration-none">Техника</a></li>
<li class="breadcrumb-item active" aria-current="page">
{% if object %}Редактирование{% else %}Добавление{% endif %}
</li>
</ol>
</nav>
<h2 class="mb-0">{% if object %}Редактирование {{ object.name }}{% else %}Новая единица техники{% endif %}</h2>
</div>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="card shadow-sm border-0">
<div class="card-header bg-white border-0 py-4">
<h2 class="h4 mb-0 text-center">
{% if object %}Редактировать технику{% else %}Добавить новую технику{% endif %}
</h2>
</div>
<div class="card-body p-4">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<!-- Section: Basic Info -->
<h5 class="mb-3 text-primary border-bottom pb-2">Основная информация</h5>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label">Наименование *</label>
{{ form.name }}
</div>
<div class="col-md-6">
<label class="form-label">Модель *</label>
{{ form.model_name }}
</div>
<div class="col-md-4">
<label class="form-label">Категория</label>
{{ form.category }}
</div>
<div class="col-md-4">
<label class="form-label">VIN / Серийный номер *</label>
{{ form.vin }}
</div>
<div class="col-md-4">
<label class="form-label">Госномер</label>
{{ form.plate_number }}
</div>
<div class="col-md-4">
<label class="form-label">Год выпуска *</label>
{{ form.year }}
</div>
<div class="col-md-4">
<label class="form-label">Дата ввода в эксплуатацию *</label>
{{ form.commissioning_date }}
</div>
<div class="col-md-4">
<label class="form-label">Статус</label>
{{ form.status }}
</div>
</div>
<div class="card shadow-sm">
<div class="card-body p-4">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="row g-3">
<div class="col-md-12">
<label for="{{ form.name.id_for_label }}" class="form-label fw-medium">{{ form.name.label }}</label>
{{ form.name }}
{% if form.name.errors %}<div class="text-danger small">{{ form.name.errors }}</div>{% endif %}
<!-- Section: Insurance -->
<h5 class="mb-3 text-primary border-bottom pb-2">Страховка</h5>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label">Страховая компания</label>
{{ form.insurance_company }}
</div>
<div class="col-md-6">
<label class="form-label">Номер полиса</label>
{{ form.insurance_policy_number }}
</div>
<div class="col-md-6">
<label class="form-label">Дата начала</label>
{{ form.insurance_start_date }}
</div>
<div class="col-md-6">
<label class="form-label">Дата окончания</label>
{{ form.insurance_end_date }}
</div>
</div>
<div class="col-md-6">
<label for="{{ form.category.id_for_label }}" class="form-label fw-medium">{{ form.category.label }}</label>
{{ form.category }}
{% if form.category.errors %}<div class="text-danger small">{{ form.category.errors }}</div>{% endif %}
<!-- Section: Supply -->
<h5 class="mb-3 text-primary border-bottom pb-2">Снабжение</h5>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label">Поставщик / Контрагент</label>
{{ form.supplier }}
<div class="form-text">Выберите поставщика из списка. Создать нового можно в разделе "Снабжение".</div>
</div>
<div class="col-md-6">
<label class="form-label">Документы на авто (PDF/Image)</label>
{{ form.vehicle_documents }}
</div>
</div>
<div class="col-md-6">
<label for="{{ form.model_name.id_for_label }}" class="form-label fw-medium">{{ form.model_name.label }}</label>
{{ form.model_name }}
{% if form.model_name.errors %}<div class="text-danger small">{{ form.model_name.errors }}</div>{% endif %}
<!-- Section: Media & Notes -->
<h5 class="mb-3 text-primary border-bottom pb-2">Медиа и примечания</h5>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label">Фото техники</label>
{{ form.photo }}
</div>
<div class="col-12">
<label class="form-label">Примечания</label>
{{ form.notes }}
</div>
</div>
<div class="col-md-6">
<label for="{{ form.vin.id_for_label }}" class="form-label fw-medium">{{ form.vin.label }}</label>
{{ form.vin }}
{% if form.vin.errors %}<div class="text-danger small">{{ form.vin.errors }}</div>{% endif %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-5">
<a href="{% if object %}{% url 'fleet_detail' object.pk %}{% else %}{% url 'fleet_list' %}{% endif %}" class="btn btn-light px-4">Отмена</a>
<button type="submit" class="btn btn-primary px-5">Сохранить</button>
</div>
<div class="col-md-6">
<label for="{{ form.plate_number.id_for_label }}" class="form-label fw-medium">{{ form.plate_number.label }}</label>
{{ form.plate_number }}
{% if form.plate_number.errors %}<div class="text-danger small">{{ form.plate_number.errors }}</div>{% endif %}
</div>
<div class="col-md-4">
<label for="{{ form.year.id_for_label }}" class="form-label fw-medium">{{ form.year.label }}</label>
{{ form.year }}
{% if form.year.errors %}<div class="text-danger small">{{ form.year.errors }}</div>{% endif %}
</div>
<div class="col-md-4">
<label for="{{ form.status.id_for_label }}" class="form-label fw-medium">{{ form.status.label }}</label>
{{ form.status }}
{% if form.status.errors %}<div class="text-danger small">{{ form.status.errors }}</div>{% endif %}
</div>
<div class="col-md-4">
<label for="{{ form.commissioning_date.id_for_label }}" class="form-label fw-medium">{{ form.commissioning_date.label }}</label>
{{ form.commissioning_date }}
<div class="form-text small">ГГГГ-ММ-ДД</div>
{% if form.commissioning_date.errors %}<div class="text-danger small">{{ form.commissioning_date.errors }}</div>{% endif %}
</div>
<div class="col-md-12">
<label for="{{ form.photo.id_for_label }}" class="form-label fw-medium">{{ form.photo.label }}</label>
{{ form.photo }}
{% if form.photo.errors %}<div class="text-danger small">{{ form.photo.errors }}</div>{% endif %}
</div>
<div class="col-md-12">
<label for="{{ form.notes.id_for_label }}" class="form-label fw-medium">{{ form.notes.label }}</label>
{{ form.notes }}
{% if form.notes.errors %}<div class="text-danger small">{{ form.notes.errors }}</div>{% endif %}
</div>
</div>
<div class="d-flex gap-2 mt-5">
<button type="submit" class="btn btn-primary px-5">
{% if object %}Сохранить изменения{% else %}Добавить технику{% endif %}
</button>
<a href="{% if object %}{{ object.get_absolute_url }}{% else %}{% url 'fleet_list' %}{% endif %}" class="btn btn-outline-secondary">Отмена</a>
</div>
</form>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
// Add bootstrap classes to form fields
document.querySelectorAll('input, select, textarea').forEach(el => {
if (el.type !== 'checkbox' && el.type !== 'radio') {
el.classList.add('form-control');
}
if (el.tagName === 'SELECT') {
el.classList.add('form-select');
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,70 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow-sm border-0">
<div class="card-header bg-white border-0 py-4">
<h2 class="h4 mb-0 text-center">
{% if object %}Редактировать поставщика{% else %}Новый поставщик{% endif %}
</h2>
</div>
<div class="card-body p-4">
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label class="form-label">Наименование компании *</label>
{{ form.name }}
{% if form.name.errors %}<div class="text-danger small">{{ form.name.errors }}</div>{% endif %}
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">ФИО представителя</label>
{{ form.representative_name }}
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Телефон</label>
{{ form.phone }}
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Электронная почта</label>
{{ form.email }}
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Номер договора</label>
{{ form.contract_number }}
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-4">
<a href="{% url 'supply_list' %}" class="btn btn-light px-4">Отмена</a>
<button type="submit" class="btn btn-primary px-5">Сохранить</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<style>
.form-control {
border-radius: 8px;
padding: 0.75rem;
border: 1px solid #e0e0e0;
}
.form-control:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.1);
}
label {
font-weight: 500;
color: #495057;
margin-bottom: 0.5rem;
}
</style>
{% endblock %}

View File

@ -0,0 +1,96 @@
{% extends 'base.html' %}
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">Снабжение</h1>
</div>
<div class="row">
<!-- Part Requests Section -->
<div class="col-lg-8">
<div class="card shadow-sm mb-4">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Заявки на запчасти</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Дата</th>
<th>Техника</th>
<th>Деталь</th>
<th>Кол-во</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for req in requests %}
<tr>
<td>{{ req.created_at|date:"d.m.Y" }}</td>
<td>
<a href="{% url 'fleet_detail' req.fleet_unit.pk %}" class="text-decoration-none">
{{ req.fleet_unit.name }}
</a>
</td>
<td>{{ req.part_name }}</td>
<td>{{ req.quantity }}</td>
<td>
<span class="badge rounded-pill bg-{{ req.status|yesno:'info,secondary' }}">
{{ req.get_status_display }}
</span>
</td>
<td>
<a href="#" class="btn btn-sm btn-outline-primary">Управление</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center py-4">Нет активных заявок</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Suppliers Section -->
<div class="col-lg-4">
<div class="card shadow-sm">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Поставщики</h5>
<a href="{% url 'supplier_add' %}" class="btn btn-sm btn-primary">
<i class="bi bi-plus-lg"></i> Добавить
</a>
</div>
<div class="card-body">
<div class="list-group list-group-flush">
{% for supplier in suppliers %}
<div class="list-group-item px-0 py-3">
<div class="d-flex w-100 justify-content-between align-items-center">
<h6 class="mb-1">{{ supplier.name }}</h6>
<a href="{% url 'supplier_edit' supplier.pk %}" class="btn btn-sm btn-link p-0 text-muted">
<i class="bi bi-pencil-square"></i>
</a>
</div>
<p class="mb-1 small text-muted">
<strong>Менеджер:</strong> {{ supplier.representative_name|default:"-" }}<br>
<strong>Тел:</strong> {{ supplier.phone|default:"-" }}<br>
<strong>Email:</strong> {{ supplier.email|default:"-" }}<br>
<strong>Договор:</strong> {{ supplier.contract_number|default:"-" }}
</p>
</div>
{% empty %}
<div class="text-center py-3 text-muted">Список поставщиков пуст</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -26,4 +26,9 @@ urlpatterns = [
# Part Request
path('part-request/', views.PartRequestListView.as_view(), name='part_request_list'),
path('part-request/add/', views.PartRequestCreateView.as_view(), name='part_request_add'),
# Supply
path('supply/', views.SupplyListView.as_view(), name='supply_list'),
path('supplier/add/', views.SupplierCreateView.as_view(), name='supplier_add'),
path('supplier/<int:pk>/edit/', views.SupplierUpdateView.as_view(), name='supplier_edit'),
]

View File

@ -11,16 +11,47 @@ from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from .models import FleetUnit, Maintenance, Breakdown, PartRequest, Category, Document
from .models import FleetUnit, Maintenance, Breakdown, PartRequest, Category, Document, Supplier
# Forms
class FleetUnitForm(forms.ModelForm):
class Meta:
model = FleetUnit
fields = ['name', 'category', 'model_name', 'vin', 'plate_number', 'year', 'photo', 'status', 'commissioning_date', 'notes']
fields = [
'name', 'category', 'model_name', 'vin', 'plate_number', 'year', 'photo', 'status',
'commissioning_date', 'notes',
'insurance_company', 'insurance_policy_number', 'insurance_start_date', 'insurance_end_date',
'supplier', 'vehicle_documents'
]
widgets = {
'commissioning_date': forms.DateInput(attrs={'type': 'date'}),
'notes': forms.Textarea(attrs={'rows': 3}),
'name': forms.TextInput(attrs={'class': 'form-control'}),
'category': forms.Select(attrs={'class': 'form-select'}),
'model_name': forms.TextInput(attrs={'class': 'form-control'}),
'vin': forms.TextInput(attrs={'class': 'form-control'}),
'plate_number': forms.TextInput(attrs={'class': 'form-control'}),
'year': forms.NumberInput(attrs={'class': 'form-control'}),
'photo': forms.FileInput(attrs={'class': 'form-control'}),
'status': forms.Select(attrs={'class': 'form-select'}),
'commissioning_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
'insurance_company': forms.TextInput(attrs={'class': 'form-control'}),
'insurance_policy_number': forms.TextInput(attrs={'class': 'form-control'}),
'insurance_start_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'insurance_end_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'supplier': forms.Select(attrs={'class': 'form-select'}),
'vehicle_documents': forms.FileInput(attrs={'class': 'form-control'}),
}
class SupplierForm(forms.ModelForm):
class Meta:
model = Supplier
fields = ['name', 'representative_name', 'phone', 'email', 'contract_number']
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}),
'representative_name': forms.TextInput(attrs={'class': 'form-control'}),
'phone': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'contract_number': forms.TextInput(attrs={'class': 'form-control'}),
}
class MaintenanceForm(forms.ModelForm):
@ -28,8 +59,12 @@ class MaintenanceForm(forms.ModelForm):
model = Maintenance
fields = ['fleet_unit', 'm_type', 'planned_date', 'planned_runtime', 'mechanic', 'notes']
widgets = {
'planned_date': forms.DateInput(attrs={'type': 'date'}),
'notes': forms.Textarea(attrs={'rows': 3}),
'fleet_unit': forms.Select(attrs={'class': 'form-select'}),
'm_type': forms.Select(attrs={'class': 'form-select'}),
'planned_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'planned_runtime': forms.NumberInput(attrs={'class': 'form-control'}),
'mechanic': forms.Select(attrs={'class': 'form-select'}),
'notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
}
class BreakdownForm(forms.ModelForm):
@ -37,16 +72,27 @@ class BreakdownForm(forms.ModelForm):
model = Breakdown
fields = ['fleet_unit', 'system_node', 'description', 'photo', 'status', 'notes']
widgets = {
'description': forms.Textarea(attrs={'rows': 3}),
'notes': forms.Textarea(attrs={'rows': 3}),
'fleet_unit': forms.Select(attrs={'class': 'form-select'}),
'system_node': forms.TextInput(attrs={'class': 'form-control'}),
'description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
'photo': forms.FileInput(attrs={'class': 'form-control'}),
'status': forms.Select(attrs={'class': 'form-select'}),
'notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
}
class PartRequestForm(forms.ModelForm):
class Meta:
model = PartRequest
fields = ['fleet_unit', 'breakdown', 'part_name', 'article_number', 'quantity', 'photo', 'notes']
fields = ['fleet_unit', 'breakdown', 'part_name', 'article_number', 'quantity', 'status', 'photo', 'notes']
widgets = {
'notes': forms.Textarea(attrs={'rows': 3}),
'fleet_unit': forms.Select(attrs={'class': 'form-select'}),
'breakdown': forms.Select(attrs={'class': 'form-select'}),
'part_name': forms.TextInput(attrs={'class': 'form-control'}),
'article_number': forms.TextInput(attrs={'class': 'form-control'}),
'quantity': forms.NumberInput(attrs={'class': 'form-control'}),
'status': forms.Select(attrs={'class': 'form-select'}),
'photo': forms.FileInput(attrs={'class': 'form-control'}),
'notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
}
# Views
@ -194,15 +240,21 @@ class MaintenancePDFView(View):
buffer = BytesIO()
p = canvas.Canvas(buffer, pagesize=A4)
# Draw PDF content (basic for now as we don't have Cyrillic fonts loaded by default in reportlab without setup)
# Note: For real Cyrillic support we need to register a TTF font.
p.drawString(100, 800, f"Maintenance Act - {maintenance.m_type}")
p.drawString(100, 780, f"Unit: {maintenance.fleet_unit.name}")
p.drawString(100, 760, f"Date: {maintenance.actual_date}")
p.drawString(100, 740, f"Runtime: {maintenance.actual_runtime}")
# Register Cyrillic font
font_path = "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf"
try:
pdfmetrics.registerFont(TTFont('LiberationSans', font_path))
p.setFont('LiberationSans', 12)
except:
pass # Fallback to default if font not found
p.drawString(100, 800, f"Акт технического обслуживания - {maintenance.m_type}")
p.drawString(100, 780, f"Техника: {maintenance.fleet_unit.name}")
p.drawString(100, 760, f"Дата: {maintenance.actual_date}")
p.drawString(100, 740, f"Наработка: {maintenance.actual_runtime}")
y = 700
p.drawString(100, y, "Checklist:")
p.drawString(100, y, "Чек-лист:")
y -= 20
for item in maintenance.checklist:
status = "[x]" if item['done'] else "[ ]"
@ -271,4 +323,26 @@ class PartRequestCreateView(CreateView):
return initial
def get_success_url(self):
return reverse('fleet_detail', kwargs={'pk': self.object.fleet_unit.pk})
return reverse('fleet_detail', kwargs={'pk': self.object.fleet_unit.pk})
class SupplyListView(ListView):
model = PartRequest
template_name = 'core/supply_list.html'
context_object_name = 'requests'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['suppliers'] = Supplier.objects.all()
return context
class SupplierCreateView(CreateView):
model = Supplier
form_class = SupplierForm
template_name = 'core/supplier_form.html'
success_url = reverse_lazy('supply_list')
class SupplierUpdateView(UpdateView):
model = Supplier
form_class = SupplierForm
template_name = 'core/supplier_form.html'
success_url = reverse_lazy('supply_list')

BIN
media/qrcodes/qr-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

BIN
qrcodes/qr-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

View File

@ -1,21 +1,116 @@
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
--bg-slate-900: #1e293b;
--text-blue-500: #3b82f6;
--primary: #3b82f6;
--secondary: #64748b;
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
--info: #06b6d4;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
color: #334155;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Montserrat', sans-serif;
font-weight: 700;
}
.bg-slate-900 {
background-color: var(--bg-slate-900) !important;
}
.text-blue-500 {
color: var(--text-blue-500) !important;
}
.tracking-wider {
letter-spacing: 0.05em;
}
/* Card Styling */
.card {
border: none;
border-radius: 12px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important;
}
/* Stat Cards */
.stat-card {
padding: 1.5rem;
display: flex;
align-items: center;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
margin-right: 1rem;
}
/* Badges */
.badge {
padding: 0.5em 0.8em;
font-weight: 500;
border-radius: 6px;
}
/* Hero Section */
.hero-gradient {
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
color: white;
padding: 3rem 0;
border-radius: 16px;
margin-bottom: 2rem;
}
/* Form Styling */
.form-control, .form-select {
border-radius: 8px;
padding: 0.625rem 0.75rem;
border: 1px solid #e2e8f0;
}
.form-control:focus, .form-select:focus {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
border-color: #3b82f6;
}
.btn {
border-radius: 8px;
padding: 0.625rem 1.25rem;
font-weight: 500;
}
.btn-primary {
background-color: var(--primary);
border-color: var(--primary);
}
.btn-primary:hover {
background-color: #2563eb;
border-color: #2563eb;
}
/* Mobile adjustments */
@media (max-width: 768px) {
.stat-card {
padding: 1rem;
}
.hero-gradient {
padding: 2rem 1rem;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB