Первая попытка модельки

This commit is contained in:
ack_ik
2026-02-10 13:40:12 +03:00
parent 8c38cb68c4
commit b27c685eb1
10 changed files with 285 additions and 0 deletions

33
.ignore/filters.py Normal file
View File

@@ -0,0 +1,33 @@
from django_filters import FilterSet, CharFilter, ModelChoiceFilter
from .models import Part
class PartFilter(FilterSet):
type = ModelChoiceFilter(
field_name='type',
choices=Part.TYPE_CHOICES,
label='Тип заготовки'
)
thickness_min = CharFilter(
field_name='thickness',
label='Минимальная толщина',
widget=forms.NumberInput(attrs={'placeholder': 'От'})
)
thickness_max = CharFilter(
field_name='thickness',
label='Максимальная толщина',
widget=forms.NumberInput(attrs={'placeholder': 'До'})
)
search = CharFilter(
field_name='name',
label='Поиск по наименованию',
widget=forms.TextInput(attrs={'placeholder': 'Введите текст'})
)
decimal_number = CharFilter(
field_name='decimal_number',
label='Поиск по децимальному номеру',
widget=forms.TextInput(attrs={'placeholder': 'Введите номер'})
)
class Meta:
model = Part
fields = ['type', 'thickness_min', 'thickness_max', 'search', 'decimal_number']

39
.ignore/models.py Normal file
View File

@@ -0,0 +1,39 @@
from django.db import models
from mptt.models import MPTTModel, TreeForeignKey
from django.utils.text import slugify
TYPE_CHOICES = (
('Лист', 'Лист'),
('Труба', 'Труба'),
('Круг', 'Круг'),
('Уголок', 'Уголок'),
('Профиль', 'Профиль'),
('Плита', 'Плита'),
('Швеллер', 'Швеллер'),
('Ребро', 'Ребро'),
('Тонкий', 'Тонкий'),
('Толстый', 'Толстый'),
)
class Part(models.Model):
name = models.CharField(max_length=255)
decimal_number = models.CharField(max_length=50, unique=True)
type = models.CharField(max_length=50, choices=TYPE_CHOICES)
thickness = models.FloatField(null=True, blank=True)
length = models.FloatField(null=True, blank=True)
weight = models.FloatField(null=True, blank=True)
cut_length = models.FloatField(null=True, blank=True)
number_of_punches = models.IntegerField(null=True, blank=True)
slug = models.SlugField(unique=True, max_length=255)
def save(self, *args, **kwargs):
self.slug = slugify(f"{self.name}-{self.decimal_number}")
super().save(*args, **kwargs)
def __str__(self):
return f"{self.name} ({self.decimal_number})"
class ProductStructure(MPTTModel):
item = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='structures')
quantity = models.FloatField()
parent = TreeForeignKey('self', on_delete=models.CASCADE)

8
.ignore/urls.py Normal file
View File

@@ -0,0 +1,8 @@
from django.contrib import admin
from django.urls import path
from .views import PartList
urlpatterns = [
path('admin/', admin.site.urls),
path('api/parts/', PartList.as_view(), name='part-list'),
]

58
.ignore/views.py Normal file
View File

@@ -0,0 +1,58 @@
from django.shortcuts import render
from django.views import View
from django.http import JsonResponse
from .models import Part, ProductStructure
from .filters import PartFilter
from django.contrib import admin
from django.db import models
from django.forms import ModelForm
from django.utils.html import format_html
class PartList(View):
def get(self, request):
filter = PartFilter(request.GET, queryset=Part.objects.all())
return JsonResponse({
'results': [self.part_to_json(part) for part in filter.qs],
'filters': filter.filters.items()
})
def part_to_json(self, part):
return {
'id': part.id,
'name': part.name,
'decimal_number': part.decimal_number,
'type': part.get_type_display(),
'thickness': part.thickness,
'length': part.length,
'weight': part.weight,
'cut_length': part.cut_length,
'number_of_punches': part.number_of_punches,
'slug': part.slug
}
class PartAdmin(admin.ModelAdmin):
list_display = ('name', 'decimal_number', 'type', 'thickness', 'weight')
search_fields = ('name', 'decimal_number')
list_filter = ('type',)
inlines = [
ProductionOperationInline
]
class ProductionOperationInline(admin.TabularInline):
model = ProductionOperation
extra = 1
class ProductionOperation(models.Model):
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='operations')
operation_type = models.CharField(max_length=50, choices=[
('Лазер', 'Лазер'),
('Сварка', 'Сварка'),
('Покраска', 'Покраска'),
('Обработка', 'Обработка'),
])
time = models.FloatField()
description = models.TextField(blank=True)
def __str__(self):
return f"{self.part.name} - {self.operation_type}"

49
plan.md Normal file
View File

@@ -0,0 +1,49 @@
Задача: Создать Django-проект для учета состава изделий (BOM) с иерархией, техпроцессами и системой фильтрации.
Технологии: Django 5.x, PostgreSQL, django-mptt, django-filter, Bootstrap 5.
игнорируй папки .ignore
1. Модель данных:
Part (Номенклатура):
Поля: Децимальный номер, Наименование, Тип заготовки (Choices: Лист, Труба, Круг, Уголок и др.), Толщина, Длина, Вес, Длина реза, Число проколов.
ProductStructure (Дерево состава): * MPTTModel. Поля: parent, item (FK на Part), quantity.
ProductionOperation (Техпроцесс):
FK на Part. Поля: Тип операции (Choices: Лазер, Сварка, Покраска и др.), Время, Описание.
2. Система фильтрации (Django-filter):
Создать PartFilter, который позволит фильтровать список деталей по:
Типу заготовки (выпадающий список).
Диапазону толщины (от и до).
Поиску по наименованию и децимальному номеру (регистронезависимый поиск).
3. Интерфейс и Шаблоны (Bootstrap 5):
Структура Layout: Разделить на base.html, _navbar.html (fixed-top), _footer.html (sticky footer через Flexbox min-vh-100).
Страница списка деталей: Слева или сверху — узкая панель с фильтрами, справа — таблица с результатами.
Страница изделия: Визуальное дерево состава (используя mptt-tags и рекурсию) с выводом суммарных характеристик.
Админка: Настроить TabularInline для ProductionOperation внутри PartAdmin.
4. Инструкции по коду:
создай проект с настройками в папке core
Создай приложение Prodman
Напиши models.py, filters.py, views.py и urls.py.
Создай шаблоны в папке templates/, соблюдая иерархию блоков.
В base.html добавь CDN для Bootstrap 5 и FontAwesome.
Напиши requirements.txt (включи django, django-mptt, django-filter, psycopg2-binary).

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
Django>=5.0
django-mptt
django-filter
psycopg2-binary
bootstrap-icons

26
templates/_navbar.html Normal file
View File

@@ -0,0 +1,26 @@
<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'part-list' %}">BOM Manager</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" href="{% url 'part-list' %}">Список деталей</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Структура изделия</a>
</li>
</ul>
<div class="d-flex">
<a href="{% url 'admin:index' %}" class="btn btn-outline-secondary me-2">
<i class="fas fa-tools"></i> Админка
</a>
<a href="#" class="btn btn-outline-secondary">
<i class="fas fa-sign-out-alt"></i> Выйти
</a>
</div>
</div>
</div>
</nav>

22
templates/base.html Normal file
View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock %}</title>
<!-- Bootstrap 5 CDN -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- FontAwesome CDN -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
{% include '_navbar.html' %}
</head>
<body>
<div class="container mt-4">
{% block content %}
{% endblock %}
</div>
{% include '_footer.html' %}
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

45
templates/parts_list.html Normal file
View File

@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block title %}Список деталей{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-3">
<h4>Фильтры</h4>
<form method="get">
{{ filter.form.as_p }}
<button type="submit" class="btn btn-primary">Применить</button>
</form>
</div>
<div class="col-md-9">
<h4>Результаты</h4>
<table class="table table-striped">
<thead>
<tr>
<th>Наименование</th>
<th>Децимальный номер</th>
<th>Тип</th>
<th>Толщина</th>
<th>Длина</th>
<th>Вес</th>
<th>Длина реза</th>
<th>Число проколов</th>
</tr>
</thead>
<tbody>
{% for part in filter.qs %}
<tr>
<td>{{ part.name }}</td>
<td>{{ part.decimal_number }}</td>
<td>{{ part.get_type_display }}</td>
<td>{{ part.thickness }}</td>
<td>{{ part.length }}</td>
<td>{{ part.weight }}</td>
<td>{{ part.cut_length }}</td>
<td>{{ part.number_of_punches }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}