mirror of
https://github.com/ada-dmitry/Project_WEB.git
synced 2026-09-23 23:50:20 +00:00
Add initial Django project structure with cleaning app
- Created .gitignore to exclude Python-generated files and virtual environments. - Added .python-version to specify Python version 3.13. - Implemented models for Client, Brigade, Service, Order, and CompletedWork. - Created Django admin configurations for managing models. - Added management command to seed the database with test data. - Created initial migrations for the models. - Developed views and templates for listing and detailing orders, clients, and brigades. - Set up URL routing for the cleaning app. - Configured Django settings and WSGI/ASGI entry points. - Added basic test structure in tests.py. - Included project metadata in pyproject.toml and uv.lock.
This commit is contained in:
+11
@@ -0,0 +1,11 @@
|
|||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
|
cache/
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import Client, Brigade, Service, Order, CompletedWork
|
||||||
|
|
||||||
|
|
||||||
|
class CompletedWorkInline(admin.TabularInline):
|
||||||
|
model = CompletedWork
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
@admin.register(Client)
|
||||||
|
class ClientAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['full_name', 'phone', 'address', 'preferred_time']
|
||||||
|
search_fields = ['full_name', 'phone', 'address']
|
||||||
|
list_filter = ['preferred_time']
|
||||||
|
|
||||||
|
@admin.register(Brigade)
|
||||||
|
class BrigadeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'supervisor', 'employee_count', 'specialization']
|
||||||
|
search_fields = ['name', 'supervisor', 'specialization']
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Service)
|
||||||
|
class ServiceAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'price_per_sqm', 'duration_per_sqm']
|
||||||
|
search_fields = ['name']
|
||||||
|
|
||||||
|
@admin.register(Order)
|
||||||
|
class OrderAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['id', 'client', 'brigade', 'date', 'start_time', 'end_time']
|
||||||
|
search_fields = ['client__full_name', 'brigade__name']
|
||||||
|
list_filter = ['date', 'brigade']
|
||||||
|
inlines = [CompletedWorkInline]
|
||||||
|
|
||||||
|
@admin.register(CompletedWork)
|
||||||
|
class CompletedWorkAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['order', 'service', 'area']
|
||||||
|
search_fields = ['order__client__full_name', 'service__name']
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CleaningConfig(AppConfig):
|
||||||
|
name = 'cleaning'
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import random
|
||||||
|
from datetime import date, timedelta, time
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from faker import Faker
|
||||||
|
|
||||||
|
from cleaning.models import Client, Brigade, Service, Order, CompletedWork
|
||||||
|
|
||||||
|
fake = Faker('ru_RU')
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Заполнить базу тестовыми данными'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
CompletedWork.objects.all().delete()
|
||||||
|
Order.objects.all().delete()
|
||||||
|
Service.objects.all().delete()
|
||||||
|
Brigade.objects.all().delete()
|
||||||
|
Client.objects.all().delete()
|
||||||
|
|
||||||
|
time_choices = ['morning', 'day', 'evening', 'any']
|
||||||
|
clients = []
|
||||||
|
for _ in range(12):
|
||||||
|
client = Client.objects.create(
|
||||||
|
full_name=fake.name(),
|
||||||
|
address=fake.address().replace('\n', ', '),
|
||||||
|
phone=fake.phone_number(),
|
||||||
|
preferred_time=random.choice(time_choices),
|
||||||
|
)
|
||||||
|
clients.append(client)
|
||||||
|
|
||||||
|
specializations = [
|
||||||
|
'Генеральная уборка',
|
||||||
|
'Уборка после ремонта',
|
||||||
|
'Мытьё окон',
|
||||||
|
'Химчистка мебели',
|
||||||
|
'Уборка офисов',
|
||||||
|
]
|
||||||
|
brigades = []
|
||||||
|
for i, spec in enumerate(specializations):
|
||||||
|
brigade = Brigade.objects.create(
|
||||||
|
name=f'Бригада «{fake.last_name()}»',
|
||||||
|
supervisor=fake.name(),
|
||||||
|
employee_count=random.randint(2, 8),
|
||||||
|
specialization=spec,
|
||||||
|
)
|
||||||
|
brigades.append(brigade)
|
||||||
|
|
||||||
|
services_data = [
|
||||||
|
('Мытьё полов', 25.00, 0.5),
|
||||||
|
('Мытьё окон', 80.00, 1.2),
|
||||||
|
('Уборка ванной комнаты', 120.00, 2.0),
|
||||||
|
('Пылесосинг ковров', 30.00, 0.8),
|
||||||
|
('Мытьё сантехники', 90.00, 1.5),
|
||||||
|
('Протирка мебели', 20.00, 0.3),
|
||||||
|
('Чистка кухонной плиты', 150.00, 3.0),
|
||||||
|
('Вынос мусора', 10.00, 0.1),
|
||||||
|
('Мытьё зеркал', 40.00, 0.4),
|
||||||
|
]
|
||||||
|
services = []
|
||||||
|
for name, price, duration in services_data:
|
||||||
|
service = Service.objects.create(
|
||||||
|
name=name,
|
||||||
|
price_per_sqm=price,
|
||||||
|
duration_per_sqm=duration,
|
||||||
|
)
|
||||||
|
services.append(service)
|
||||||
|
|
||||||
|
today = date.today()
|
||||||
|
for i in range(25):
|
||||||
|
order_date = today + timedelta(days=random.randint(-30, 15))
|
||||||
|
start_hour = random.randint(8, 16)
|
||||||
|
duration_hours = random.randint(2, 5)
|
||||||
|
order = Order.objects.create(
|
||||||
|
client=random.choice(clients),
|
||||||
|
brigade=random.choice(brigades),
|
||||||
|
date=order_date,
|
||||||
|
start_time=time(start_hour, 0),
|
||||||
|
end_time=time(start_hour + duration_hours, 0),
|
||||||
|
)
|
||||||
|
chosen_services = random.sample(services, k=random.randint(1, 4))
|
||||||
|
for service in chosen_services:
|
||||||
|
CompletedWork.objects.create(
|
||||||
|
order=order,
|
||||||
|
service=service,
|
||||||
|
area=round(random.uniform(10, 80), 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f'Готово: {Client.objects.count()} клиентов, '
|
||||||
|
f'{Brigade.objects.count()} бригад, '
|
||||||
|
f'{Service.objects.count()} услуг, '
|
||||||
|
f'{Order.objects.count()} заказов.'
|
||||||
|
))
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Client',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('full_name', models.CharField(max_length=200, verbose_name='ФИО')),
|
||||||
|
('address', models.CharField(max_length=300, verbose_name='Адрес')),
|
||||||
|
('phone', models.CharField(max_length=20, verbose_name='Телефон')),
|
||||||
|
('preferred_time', models.CharField(
|
||||||
|
choices=[('morning', 'Утро'), ('day', 'День'), ('evening', 'Вечер'), ('any', 'Не важно')],
|
||||||
|
default='any',
|
||||||
|
max_length=10,
|
||||||
|
verbose_name='Предпочтительное время',
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Клиент',
|
||||||
|
'verbose_name_plural': 'Клиенты',
|
||||||
|
'ordering': ['full_name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Brigade',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='Название')),
|
||||||
|
('supervisor', models.CharField(max_length=200, verbose_name='Руководитель')),
|
||||||
|
('employee_count', models.PositiveIntegerField(verbose_name='Количество сотрудников')),
|
||||||
|
('specialization', models.CharField(max_length=200, verbose_name='Специализация')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Бригада',
|
||||||
|
'verbose_name_plural': 'Бригады',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cleaning', '0001_clients_brigades'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Service',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name='Наименование')),
|
||||||
|
('price_per_sqm', models.DecimalField(decimal_places=2, max_digits=8, verbose_name='Цена за м2')),
|
||||||
|
('duration_per_sqm', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='Продолжительность (мин/м2)')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Услуга',
|
||||||
|
'verbose_name_plural': 'Услуги',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cleaning', '0002_services'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Order',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('date', models.DateField(verbose_name='Дата')),
|
||||||
|
('start_time', models.TimeField(verbose_name='Время начала')),
|
||||||
|
('end_time', models.TimeField(verbose_name='Время окончания')),
|
||||||
|
('client', models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to='cleaning.client',
|
||||||
|
verbose_name='Клиент',
|
||||||
|
)),
|
||||||
|
('brigade', models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to='cleaning.brigade',
|
||||||
|
verbose_name='Бригада',
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Заказ',
|
||||||
|
'verbose_name_plural': 'Заказы',
|
||||||
|
'ordering': ['date'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cleaning', '0003_orders'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CompletedWork',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('area', models.DecimalField(decimal_places=2, max_digits=8, verbose_name='Фактическая площадь (м2)')),
|
||||||
|
('order', models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to='cleaning.order',
|
||||||
|
verbose_name='Заказ',
|
||||||
|
)),
|
||||||
|
('service', models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to='cleaning.service',
|
||||||
|
verbose_name='Услуга',
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Выполненная работа',
|
||||||
|
'verbose_name_plural': 'Выполненные работы',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='services',
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
through='cleaning.CompletedWork',
|
||||||
|
to='cleaning.service',
|
||||||
|
verbose_name='Услуги',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Client(models.Model):
|
||||||
|
TIME_CHOICES = [
|
||||||
|
('morning', 'Утро'),
|
||||||
|
('day', 'День'),
|
||||||
|
('evening', 'Вечер'),
|
||||||
|
('any', 'Не важно'),
|
||||||
|
]
|
||||||
|
|
||||||
|
full_name = models.CharField('ФИО', max_length=200)
|
||||||
|
address = models.CharField('Адрес', max_length=300)
|
||||||
|
phone = models.CharField('Телефон', max_length=20)
|
||||||
|
preferred_time = models.CharField(
|
||||||
|
'Предпочтительное время',
|
||||||
|
max_length=10,
|
||||||
|
choices=TIME_CHOICES,
|
||||||
|
default='any',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Клиент'
|
||||||
|
verbose_name_plural = 'Клиенты'
|
||||||
|
ordering = ['full_name']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.full_name
|
||||||
|
|
||||||
|
|
||||||
|
class Brigade(models.Model):
|
||||||
|
name = models.CharField('Название', max_length=100)
|
||||||
|
supervisor = models.CharField('Руководитель', max_length=200)
|
||||||
|
employee_count = models.PositiveIntegerField('Количество сотрудников')
|
||||||
|
specialization = models.CharField('Специализация', max_length=200)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Бригада'
|
||||||
|
verbose_name_plural = 'Бригады'
|
||||||
|
ordering = ['name']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Service(models.Model):
|
||||||
|
name = models.CharField('Наименование', max_length=200)
|
||||||
|
price_per_sqm = models.DecimalField('Цена за м2', max_digits=8, decimal_places=2)
|
||||||
|
duration_per_sqm = models.DecimalField('Продолжительность (мин/м2)', max_digits=6, decimal_places=2)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Услуга'
|
||||||
|
verbose_name_plural = 'Услуги'
|
||||||
|
ordering = ['name']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Order(models.Model):
|
||||||
|
client = models.ForeignKey(Client, on_delete=models.CASCADE, verbose_name='Клиент', related_name='orders')
|
||||||
|
brigade = models.ForeignKey(Brigade, on_delete=models.CASCADE, verbose_name='Бригада', related_name='orders')
|
||||||
|
date = models.DateField('Дата')
|
||||||
|
start_time = models.TimeField('Время начала')
|
||||||
|
end_time = models.TimeField('Время окончания')
|
||||||
|
services = models.ManyToManyField(Service, through='CompletedWork', verbose_name='Услуги')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Заказ'
|
||||||
|
verbose_name_plural = 'Заказы'
|
||||||
|
ordering = ['date']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'Заказ №{self.pk} — {self.client} ({self.date})'
|
||||||
|
|
||||||
|
def total_cost(self):
|
||||||
|
total = 0
|
||||||
|
for work in self.completedwork_set.select_related('service').all(): # type: ignore
|
||||||
|
total += work.area * work.service.price_per_sqm
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
class CompletedWork(models.Model):
|
||||||
|
order = models.ForeignKey(Order, on_delete=models.CASCADE, verbose_name='Заказ')
|
||||||
|
service = models.ForeignKey(Service, on_delete=models.CASCADE, verbose_name='Услуга')
|
||||||
|
area = models.DecimalField('Фактическая площадь (м2)', max_digits=8, decimal_places=2)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Выполненная работа'
|
||||||
|
verbose_name_plural = 'Выполненные работы'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.service} — {self.area} м2'
|
||||||
|
|
||||||
|
def cost(self):
|
||||||
|
return self.area * self.service.price_per_sqm
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}Клининг{% endblock %}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="{% url 'cleaning:index' %}">Клининг</a>
|
||||||
|
<div>
|
||||||
|
<a class="nav-link text-white d-inline" href="{% url 'cleaning:order_list' %}">Заказы</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="container mt-4"> {% comment %} mt-4 - mergin-top {% endcomment %}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{% extends 'cleaning/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}О бригаде{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{# mb-4 — отступ снизу, чтобы карточка не прилипала к таблицам #}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header"><h4>{{ brigade.name }}</h4></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p><strong>Руководитель:</strong> {{ brigade.supervisor }}</p>
|
||||||
|
<p><strong>Сотрудников:</strong> {{ brigade.employee_count }}</p>
|
||||||
|
<p><strong>Специализация:</strong> {{ brigade.specialization }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>Активные заказы</h5>
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead><tr><th>Заказ</th><th>Клиент</th><th>Дата</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for order in active %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{% url 'cleaning:order_detail' order.pk %}">Заказ №{{ order.pk }}</a></td>
|
||||||
|
<td>{{ order.client }}</td>
|
||||||
|
<td>{{ order.date }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Выполненные заказы</h5>
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead><tr><th>Заказ</th><th>Клиент</th><th>Дата</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for order in completed %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{% url 'cleaning:order_detail' order.pk %}">Заказ №{{ order.pk }}</a></td>
|
||||||
|
<td>{{ order.client }}</td>
|
||||||
|
<td>{{ order.date }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{% extends 'cleaning/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}О клиенте{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{# card — карточка с основной информацией о клиенте #}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header"><h4>{{ client.full_name }}</h4></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p><strong>Адрес:</strong> {{ client.address }}</p>
|
||||||
|
<p><strong>Телефон:</strong> {{ client.phone }}</p>
|
||||||
|
{# get_preferred_time_display — возвращает читаемое значение choices ('morning' → 'Утро') #}
|
||||||
|
<p><strong>Предпочтительное время:</strong> {{ client.get_preferred_time_display }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>Заказы клиента</h5>
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead><tr><th>Заказ</th><th>Бригада</th><th>Дата</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for order in client.order_set.all %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{% url 'cleaning:order_detail' order.pk %}">Заказ №{{ order.pk }}</a></td>
|
||||||
|
<td>{{ order.brigade }}</td>
|
||||||
|
<td>{{ order.date }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{% extends 'cleaning/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Детали заказа{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{# card — Bootstrap-компонент для группировки информации #}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><h4>Заказ №{{ order.pk }}</h4></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p><strong>Клиент:</strong> <a href="{% url 'cleaning:client_detail' order.client.pk %}">{{ order.client }}</a></p>
|
||||||
|
<p><strong>Бригада:</strong> <a href="{% url 'cleaning:brigade_detail' order.brigade.pk %}">{{ order.brigade }}</a></p>
|
||||||
|
<p><strong>Дата:</strong> {{ order.date }}</p>
|
||||||
|
<p><strong>Время:</strong> {{ order.start_time }} — {{ order.end_time }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Выполненные работы</h5> {# mt-4 — отступ сверху #}
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Услуга</th><th>Площадь, м²</th><th>Стоимость, руб.</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for work in order.completedwork_set.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ work.service.name }}</td>
|
||||||
|
<td>{{ work.area }}</td>
|
||||||
|
<td>{{ work.cost }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
{# tfoot с table-group-divider — отделяет итоговую строку чертой #}
|
||||||
|
<tfoot class="table-group-divider">
|
||||||
|
<tr><td colspan="2"><strong>Итого</strong></td><td><strong>{{ order.total_cost }} руб.</strong></td></tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{% extends 'cleaning/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Заказы{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Список заказов</h1>
|
||||||
|
{# table-striped — чередование цвета строк, table-hover — подсветка при наведении #}
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>№</th>
|
||||||
|
<th>Клиент</th>
|
||||||
|
<th>Бригада</th>
|
||||||
|
<th>Детали</th>
|
||||||
|
<th>Дата</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for order in orders %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ forloop.counter }}</td>
|
||||||
|
{# ссылки ведут на детали клиента, бригады и заказа #}
|
||||||
|
<td><a href="{% url 'cleaning:client_detail' order.client.pk %}">{{ order.client }}</a></td>
|
||||||
|
<td><a href="{% url 'cleaning:brigade_detail' order.brigade.pk %}">{{ order.brigade }}</a></td>
|
||||||
|
<td><a href="{% url 'cleaning:order_detail' order.pk %}">Заказ №{{ order.pk }}</a></td>
|
||||||
|
<td>{{ order.date }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'cleaning'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.index, name='index'),
|
||||||
|
path('orders/', views.order_list, name='order_list'),
|
||||||
|
path('order/<int:order_id>/', views.order_detail, name='order_detail'),
|
||||||
|
path('brigades/<int:pk>/', views.brigade_detail, name='brigade_detail'),
|
||||||
|
path('clients/<int:pk>/', views.client_detail, name='client_detail'),
|
||||||
|
|
||||||
|
]
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.utils import timezone
|
||||||
|
from .models import Brigade, Order, Client
|
||||||
|
|
||||||
|
def index(request):
|
||||||
|
return redirect('cleaning:order_list')
|
||||||
|
|
||||||
|
def order_list(request):
|
||||||
|
orders = Order.objects.order_by('date')
|
||||||
|
return render(request, 'cleaning/order_list.html', {'orders': orders})
|
||||||
|
|
||||||
|
def order_detail(request, order_id):
|
||||||
|
order = get_object_or_404(Order.objects.prefetch_related('completedwork_set__service'), pk=order_id)
|
||||||
|
return render(request, 'cleaning/order_detail.html', {'order': order})
|
||||||
|
|
||||||
|
# def brigade_detail(request, pk):
|
||||||
|
# brigade = get_object_or_404(Brigade, pk=pk)
|
||||||
|
# return render(request, 'cleaning/brigade_detail.html', {'brigade': brigade})
|
||||||
|
def brigade_detail(request, pk):
|
||||||
|
brigade = get_object_or_404(Brigade, pk=pk)
|
||||||
|
today = timezone.now().date()
|
||||||
|
active = brigade.orders.filter(date__gte=today).order_by('date') # type: ignore
|
||||||
|
completed = brigade.orders.filter(date__lt=today).order_by('-date') # type: ignore
|
||||||
|
return render(request, 'cleaning/brigade_detail.html', {
|
||||||
|
'brigade': brigade,
|
||||||
|
'active': active,
|
||||||
|
'completed': completed,
|
||||||
|
})
|
||||||
|
|
||||||
|
def client_detail(request, pk):
|
||||||
|
client = get_object_or_404(Client, pk=pk)
|
||||||
|
orders = client.orders.order_by('date') # type: ignore
|
||||||
|
return render(request, 'cleaning/client_detail.html', {'client': client})
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for config project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
"""
|
||||||
|
Django settings for config project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 6.0.5.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/6.0/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/6.0/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = 'django-insecure-^*a-kotmr&dhhk))s6vyb2omn24l)llm)e0&0+invb7d@r3b3^'
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'cleaning',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'config.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'config.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/6.0/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'ru-ru'
|
||||||
|
|
||||||
|
TIME_ZONE = 'Europe/Moscow'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = 'static/'
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import include, path
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('', include('cleaning.urls')),
|
||||||
|
]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for config project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[project]
|
||||||
|
name = "project-web"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"django>=6.0.5",
|
||||||
|
"faker>=37.1.0",
|
||||||
|
]
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asgiref"
|
||||||
|
version = "3.11.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django"
|
||||||
|
version = "6.0.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "asgiref" },
|
||||||
|
{ name = "sqlparse" },
|
||||||
|
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5e/f1/bf85f0d29ef76abf901f193fe8fef4769d3da7794197832bc30151c071d8/django-6.0.5.tar.gz", hash = "sha256:bc6d6872e98a2864c836e42edd644b362db311147dd5aa8d5b82ba7a032f5269", size = 10924131, upload-time = "2026-05-05T13:54:39.329Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/5b/1328f8b84fce040c404f76822bf8c57d254e368e8cbd8bd67ec2b26d75f5/django-6.0.5-py3-none-any.whl", hash = "sha256:9d58a7cb49244e74c8e161d5e403a46d6209f1009ba40f5a66d6aa0d0786a8f0", size = 8368680, upload-time = "2026-05-05T13:54:33.532Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "faker"
|
||||||
|
version = "40.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7f/13/6741787bd91c4109c7bed047d68273965cd52ce8a5f773c471b949334b6d/faker-40.15.0.tar.gz", hash = "sha256:20f3a6ec8c266b74d4c554e34118b21c3c2056c0b4a519d15c8decb3a4e6e795", size = 1967447, upload-time = "2026-04-17T20:05:27.555Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/a7/a600f8f30d4505e89166de51dd121bd540ab8e560e8cf0901de00a81de8c/faker-40.15.0-py3-none-any.whl", hash = "sha256:71ab3c3370da9d2205ab74ffb0fd51273063ad562b3a3bb69d0026a20923e318", size = 2004447, upload-time = "2026-04-17T20:05:25.437Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "project-web"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
{ name = "faker" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "django", specifier = ">=6.0.5" },
|
||||||
|
{ name = "faker", specifier = ">=37.1.0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlparse"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2026.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user