Добавил приложение склад и модели заготовок
All checks were successful
Auto-Deploy-prodman / deploy (push) Successful in 6s
All checks were successful
Auto-Deploy-prodman / deploy (push) Successful in 6s
This commit is contained in:
17
.clinerules
Normal file
17
.clinerules
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Role: Senior Django MES Engineer
|
||||||
|
Answer in RUSSIAN (ОБЯЗАТЕЛЬНО НА РУССКОМ).
|
||||||
|
|
||||||
|
# Project Context: Prodman
|
||||||
|
- Web-based MES/BOM management.
|
||||||
|
- Stack: Django 6.0, PostgreSQL, django-mptt, django-polymorphic.
|
||||||
|
- Logic: Item -> BOMNode (Tree) -> BaseOperation (Polymorphic).
|
||||||
|
|
||||||
|
# Core Rules:
|
||||||
|
1. MANDATORY: Respond in Russian only. Code comments in Russian.
|
||||||
|
2. Logic: Units/Assemblies can have children; Parts cannot.
|
||||||
|
3. Operations: LaserCutSheet, LaserCutTube, Turning, Weld, Paint. All need a WorkCenter.
|
||||||
|
4. Calculations: When writing functions, always consider quantity in BOMNode.
|
||||||
|
|
||||||
|
# Environment:
|
||||||
|
- OS: Windows 11. Use Windows paths.
|
||||||
|
- Hardware: Limited VRAM. Be concise. No yapping.
|
||||||
35
agent.md.old
Normal file
35
agent.md.old
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Role and Context
|
||||||
|
You are a Senior Fullstack Engineer specialized in Django 6.0, Manufacturing Execution Systems (MES), and CAD integration.
|
||||||
|
Project: **Prodman** - A system for BOM management and production routing.
|
||||||
|
|
||||||
|
# MANDATORY LANGUAGE RULE (ОБЯЗАТЕЛЬНОЕ ПРАВИЛО ЯЗЫКА)
|
||||||
|
- **ALWAYS respond in Russian**, even if the user's prompt is in English.
|
||||||
|
- **ВСЕГДА отвечай только на русском языке**, даже если запрос на английском.
|
||||||
|
- Use English ONLY for code, technical terms, and file paths.
|
||||||
|
- If you notice you are speaking English, STOP and translate your last thought into Russian.
|
||||||
|
|
||||||
|
# Core Technical Principles
|
||||||
|
1. **Architecture Awareness**:
|
||||||
|
- Use `django-mptt` for Bill of Materials (BOM) trees.
|
||||||
|
- Use `django-polymorphic` for production operations (Laser, Weld, etc.).
|
||||||
|
- Model relationships: `Item` -> `BOMNode` (tree) -> `BaseOperation` (process).
|
||||||
|
|
||||||
|
2. **Code Standards**:
|
||||||
|
- Strictly follow PEP 8.
|
||||||
|
- Use Type Hinting (typing_extensions).
|
||||||
|
- Use English for naming variables/classes, but Russian for `verbose_name` and docstrings.
|
||||||
|
|
||||||
|
3. **Engineering Specifics**:
|
||||||
|
- Validation: Parts (`PART`) cannot have children in BOM. Only `ASSEMBLY` or `UNIT` can.
|
||||||
|
- Operations: Every operation must be linked to a `WorkCenter`.
|
||||||
|
|
||||||
|
# System Environment
|
||||||
|
- OS: Windows 11.
|
||||||
|
- Paths: Always use Windows-style paths (C:\...) or relative project paths.
|
||||||
|
- Python: 3.12+
|
||||||
|
- Django: 6.0.2
|
||||||
|
|
||||||
|
# Performance & Context Management
|
||||||
|
- Your current hardware (GTX 1050) has limited VRAM.
|
||||||
|
- **KEEP RESPONSES CONCISE.** Avoid long preambles.
|
||||||
|
- If the task is large, split it into small, verifiable steps.
|
||||||
@@ -50,6 +50,7 @@ INSTALLED_APPS = [
|
|||||||
'polymorphic', # added app Polymorphic
|
'polymorphic', # added app Polymorphic
|
||||||
'mptt', # added app MPTT
|
'mptt', # added app MPTT
|
||||||
'bom_manager', # added app Bom Manager
|
'bom_manager', # added app Bom Manager
|
||||||
|
'stock', # added app Stock
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|||||||
16749
media/gosts/ГОСТ_32931-2015.pdf
Normal file
16749
media/gosts/ГОСТ_32931-2015.pdf
Normal file
File diff suppressed because it is too large
Load Diff
0
stock/__init__.py
Normal file
0
stock/__init__.py
Normal file
101
stock/admin copy.py
Normal file
101
stock/admin copy.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from polymorphic.admin import (
|
||||||
|
PolymorphicChildModelAdmin,
|
||||||
|
PolymorphicParentModelAdmin,
|
||||||
|
PolymorphicChildModelFilter
|
||||||
|
)
|
||||||
|
from .models import Gost, MaterialGrade, BaseMaterial, SheetMaterial, ProfileMaterial, StockItem
|
||||||
|
|
||||||
|
@admin.register(Gost)
|
||||||
|
class GostAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'description', 'get_pdf_link')
|
||||||
|
search_fields = ('name',)
|
||||||
|
|
||||||
|
def get_pdf_link(self, obj):
|
||||||
|
if obj.pdf_file:
|
||||||
|
return format_html('<a href="{}" target="_blank">📄 PDF</a>', obj.pdf_file.url)
|
||||||
|
return "—"
|
||||||
|
get_pdf_link.short_description = "Файл"
|
||||||
|
|
||||||
|
@admin.register(MaterialGrade)
|
||||||
|
class MaterialGradeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'gost', 'density')
|
||||||
|
search_fields = ('name', 'gost__name')
|
||||||
|
|
||||||
|
class BaseChildAdmin(PolymorphicChildModelAdmin):
|
||||||
|
base_model = BaseMaterial
|
||||||
|
|
||||||
|
@admin.register(SheetMaterial)
|
||||||
|
class SheetMaterialAdmin(BaseChildAdmin):
|
||||||
|
list_display = ('title', 'thickness', 'grade', 'gost')
|
||||||
|
|
||||||
|
@admin.register(ProfileMaterial)
|
||||||
|
class ProfileMaterialAdmin(BaseChildAdmin):
|
||||||
|
list_display = ('title', 'profile_type', 'weight_per_meter', 'grade', 'gost')
|
||||||
|
|
||||||
|
@admin.register(BaseMaterial)
|
||||||
|
class BaseMaterialParentAdmin(PolymorphicParentModelAdmin):
|
||||||
|
base_model = BaseMaterial
|
||||||
|
child_models = (SheetMaterial, ProfileMaterial)
|
||||||
|
list_display = ('get_icon', 'title', 'grade', 'gost', 'get_specs')
|
||||||
|
list_filter = (PolymorphicChildModelFilter, 'grade', 'gost')
|
||||||
|
search_fields = ('title',)
|
||||||
|
|
||||||
|
def get_icon(self, obj):
|
||||||
|
if isinstance(obj, SheetMaterial):
|
||||||
|
return format_html('<span title="Лист" style="font-size: 1.2rem;">📄</span>')
|
||||||
|
if isinstance(obj, ProfileMaterial):
|
||||||
|
return format_html('<span title="Профиль" style="font-size: 1.2rem;">🏗️</span>')
|
||||||
|
return "❓"
|
||||||
|
get_icon.short_description = ""
|
||||||
|
|
||||||
|
def get_specs(self, obj):
|
||||||
|
real_obj = obj.get_real_instance()
|
||||||
|
if isinstance(real_obj, SheetMaterial):
|
||||||
|
return f"t = {real_obj.thickness} мм"
|
||||||
|
if isinstance(real_obj, ProfileMaterial):
|
||||||
|
return f"{real_obj.weight_per_meter} кг/м"
|
||||||
|
return "-"
|
||||||
|
get_specs.short_description = "Характеристики"
|
||||||
|
|
||||||
|
@admin.register(StockItem)
|
||||||
|
class StockItemAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
'get_material_name',
|
||||||
|
'display_dimensions',
|
||||||
|
'quantity',
|
||||||
|
'colored_status',
|
||||||
|
'order_reference',
|
||||||
|
'location'
|
||||||
|
)
|
||||||
|
list_filter = ('is_scrap', 'material__grade', 'material')
|
||||||
|
search_fields = ('order_reference', 'material__title', 'location')
|
||||||
|
|
||||||
|
def get_material_name(self, obj):
|
||||||
|
return obj.material.title
|
||||||
|
get_material_name.short_description = "Заготовка"
|
||||||
|
|
||||||
|
def display_dimensions(self, obj):
|
||||||
|
if obj.width:
|
||||||
|
return format_html(f"<b>{obj.length} × {obj.width}</b>")
|
||||||
|
return format_html(f"L = <b>{obj.length}</b>")
|
||||||
|
display_dimensions.short_description = "Размеры (мм)"
|
||||||
|
|
||||||
|
def colored_status(self, obj):
|
||||||
|
if obj.is_scrap:
|
||||||
|
return format_html('<b style="color: #ca8a04;">ОБРЕЗОК</b>')
|
||||||
|
return format_html('<b style="color: #16a34a;">ЦЕЛЫЙ</b>')
|
||||||
|
colored_status.short_description = "Статус"
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ('material', 'quantity')
|
||||||
|
}),
|
||||||
|
('Габариты (мм)', {
|
||||||
|
'fields': (('length', 'width'),)
|
||||||
|
}),
|
||||||
|
('Учет и хранение', {
|
||||||
|
'fields': ('is_scrap', 'order_reference', 'location')
|
||||||
|
}),
|
||||||
|
)
|
||||||
135
stock/admin.py
Normal file
135
stock/admin.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from polymorphic.admin import (
|
||||||
|
PolymorphicChildModelAdmin,
|
||||||
|
PolymorphicParentModelAdmin,
|
||||||
|
PolymorphicChildModelFilter
|
||||||
|
)
|
||||||
|
from .models import Gost, MaterialGrade, BaseMaterial, SheetMaterial, ProfileMaterial, StockItem
|
||||||
|
|
||||||
|
# --- 1. Справочники (ГОСТ и Марки) ---
|
||||||
|
|
||||||
|
@admin.register(Gost)
|
||||||
|
class GostAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'description', 'get_pdf_link')
|
||||||
|
search_fields = ('name',)
|
||||||
|
|
||||||
|
def get_pdf_link(self, obj):
|
||||||
|
if obj.pdf_file:
|
||||||
|
return format_html('<a href="{}" target="_blank">📄 PDF</a>', obj.pdf_file.url)
|
||||||
|
return "—"
|
||||||
|
get_pdf_link.short_description = "Файл"
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(MaterialGrade)
|
||||||
|
class MaterialGradeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'gost', 'density')
|
||||||
|
search_fields = ('name', 'gost__name')
|
||||||
|
|
||||||
|
|
||||||
|
# --- 2. Дочерние админки для заготовок ---
|
||||||
|
|
||||||
|
class BaseChildAdmin(PolymorphicChildModelAdmin):
|
||||||
|
base_model = BaseMaterial
|
||||||
|
# Делаем название кликабельным и в дочерних списках
|
||||||
|
list_display_links = ('title',)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(SheetMaterial)
|
||||||
|
class SheetMaterialAdmin(BaseChildAdmin):
|
||||||
|
list_display = ('title', 'thickness', 'grade', 'gost')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ProfileMaterial)
|
||||||
|
class ProfileMaterialAdmin(BaseChildAdmin):
|
||||||
|
list_display = ('title', 'profile_type', 'weight_per_meter', 'grade', 'gost')
|
||||||
|
|
||||||
|
|
||||||
|
# --- 3. Главная (родительская) админка заготовок ---
|
||||||
|
|
||||||
|
@admin.register(BaseMaterial)
|
||||||
|
class BaseMaterialParentAdmin(PolymorphicParentModelAdmin):
|
||||||
|
base_model = BaseMaterial
|
||||||
|
child_models = (SheetMaterial, ProfileMaterial)
|
||||||
|
|
||||||
|
# Заменяем обычный title на наш кликабельный метод
|
||||||
|
list_display = ('clickable_title', 'grade', 'gost', 'get_specs')
|
||||||
|
list_display_links = ('clickable_title',)
|
||||||
|
list_filter = (PolymorphicChildModelFilter, 'grade', 'gost')
|
||||||
|
search_fields = ('title',)
|
||||||
|
|
||||||
|
def clickable_title(self, obj):
|
||||||
|
"""Объединяет иконку и название в одну ссылку"""
|
||||||
|
# Безопасно получаем реальный тип (Лист или Профиль)
|
||||||
|
real_obj = obj.get_real_instance()
|
||||||
|
|
||||||
|
icon = "❓"
|
||||||
|
if isinstance(real_obj, SheetMaterial):
|
||||||
|
icon = "📄"
|
||||||
|
elif isinstance(real_obj, ProfileMaterial):
|
||||||
|
icon = "🏗️"
|
||||||
|
|
||||||
|
return format_html(
|
||||||
|
'<span style="margin-right: 8px; font-size: 1.1rem;">{}</span> <b>{}</b>',
|
||||||
|
icon,
|
||||||
|
obj.title
|
||||||
|
)
|
||||||
|
clickable_title.short_description = "Наименование заготовки"
|
||||||
|
clickable_title.admin_order_field = 'title' # Чтобы работала сортировка
|
||||||
|
|
||||||
|
def get_specs(self, obj):
|
||||||
|
"""Вывод ключевых параметров в общий список"""
|
||||||
|
real_obj = obj.get_real_instance()
|
||||||
|
if isinstance(real_obj, SheetMaterial):
|
||||||
|
return f"t = {real_obj.thickness} мм"
|
||||||
|
if isinstance(real_obj, ProfileMaterial):
|
||||||
|
return f"{real_obj.get_profile_type_display()}: {real_obj.weight_per_meter} кг/м"
|
||||||
|
return "-"
|
||||||
|
get_specs.short_description = "Характеристики"
|
||||||
|
|
||||||
|
|
||||||
|
# --- 4. Складской учет ---
|
||||||
|
|
||||||
|
@admin.register(StockItem)
|
||||||
|
class StockItemAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
'get_material_name',
|
||||||
|
'display_dimensions',
|
||||||
|
'quantity',
|
||||||
|
'colored_status',
|
||||||
|
'order_reference',
|
||||||
|
'location'
|
||||||
|
)
|
||||||
|
list_display_links = ('get_material_name',)
|
||||||
|
list_filter = ('is_scrap', 'material__grade', 'material')
|
||||||
|
search_fields = ('order_reference', 'material__title', 'location')
|
||||||
|
|
||||||
|
def get_material_name(self, obj):
|
||||||
|
return obj.material.title
|
||||||
|
get_material_name.short_description = "Заготовка"
|
||||||
|
|
||||||
|
def display_dimensions(self, obj):
|
||||||
|
"""Красивое отображение габаритов"""
|
||||||
|
if obj.width:
|
||||||
|
return format_html(f"<b>{obj.length} × {obj.width}</b>")
|
||||||
|
return format_html(f"L = <b>{obj.length}</b>")
|
||||||
|
display_dimensions.short_description = "Размеры (мм)"
|
||||||
|
|
||||||
|
def colored_status(self, obj):
|
||||||
|
"""Цветовая маркировка остатков"""
|
||||||
|
if obj.is_scrap:
|
||||||
|
return format_html('<b style="color: #ca8a04;">ОБРЕЗОК</b>')
|
||||||
|
return format_html('<b style="color: #16a34a;">ЦЕЛЫЙ</b>')
|
||||||
|
colored_status.short_description = "Статус"
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ('material', 'quantity')
|
||||||
|
}),
|
||||||
|
('Габариты (мм)', {
|
||||||
|
'fields': (('length', 'width'),)
|
||||||
|
}),
|
||||||
|
('Учет и хранение', {
|
||||||
|
'fields': ('is_scrap', 'order_reference', 'location')
|
||||||
|
}),
|
||||||
|
)
|
||||||
5
stock/apps.py
Normal file
5
stock/apps.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class StockConfig(AppConfig):
|
||||||
|
name = 'stock'
|
||||||
89
stock/migrations/0001_initial.py
Normal file
89
stock/migrations/0001_initial.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# Generated by Django 6.0.2 on 2026-02-16 04:28
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BaseMaterial',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(help_text='Лист 10мм или Труба 40х40х2', max_length=255, verbose_name='Наименование заготовки')),
|
||||||
|
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Заготовка',
|
||||||
|
'verbose_name_plural': 'Заготовки',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MaterialGrade',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=50, verbose_name='Марка стали')),
|
||||||
|
('gost', models.CharField(blank=True, max_length=100, null=True, verbose_name='ГОСТ/ТУ')),
|
||||||
|
('density', models.PositiveIntegerField(default=7850.0, verbose_name='Плотность, кг/м³')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Марка материала',
|
||||||
|
'verbose_name_plural': 'Марки материалов',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ProfileMaterial',
|
||||||
|
fields=[
|
||||||
|
('basematerial_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='stock.basematerial')),
|
||||||
|
('profile_type', models.CharField(choices=[('round_tube', 'Труба круглая'), ('square_tube', 'Труба профильная'), ('channel', 'Швеллер'), ('angle', 'Уголок'), ('bar', 'Круг/Пруток'), ('other', 'Прочее')], max_length=20, verbose_name='Тип сечения')),
|
||||||
|
('weight_per_meter', models.FloatField(help_text='Табличный вес по ГОСТ', verbose_name='Вес 1 м.п., кг')),
|
||||||
|
('max_dimension', models.PositiveIntegerField(help_text='Для проверки входимости детали', verbose_name='Макс. габарит сечения, мм')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Профильный материал',
|
||||||
|
'verbose_name_plural': 'Профильные материалы',
|
||||||
|
},
|
||||||
|
bases=('stock.basematerial',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SheetMaterial',
|
||||||
|
fields=[
|
||||||
|
('basematerial_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='stock.basematerial')),
|
||||||
|
('thickness', models.PositiveIntegerField(verbose_name='Толщина, мм')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Листовой материал',
|
||||||
|
'verbose_name_plural': 'Листовые материалы',
|
||||||
|
},
|
||||||
|
bases=('stock.basematerial',),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='basematerial',
|
||||||
|
name='grade',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='stock.materialgrade', verbose_name='Материал'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='StockItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('length', models.PositiveIntegerField(verbose_name='Длина, мм')),
|
||||||
|
('width', models.PositiveIntegerField(blank=True, null=True, verbose_name='Ширина, мм')),
|
||||||
|
('quantity', models.PositiveIntegerField(default=1, verbose_name='Количество, шт')),
|
||||||
|
('order_reference', models.CharField(blank=True, max_length=100, null=True, verbose_name='Заказ/Сделка')),
|
||||||
|
('is_scrap', models.BooleanField(default=False, verbose_name='Деловой остаток')),
|
||||||
|
('location', models.CharField(blank=True, max_length=100, null=True, verbose_name='Место хранения')),
|
||||||
|
('material', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stock_items', to='stock.basematerial', verbose_name='Тип заготовки')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Складская единица',
|
||||||
|
'verbose_name_plural': 'Склад налицо',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Generated by Django 6.0.2 on 2026-02-16 04:40
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stock', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Gost',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text='Например, ГОСТ 19903-2015', max_length=100, verbose_name='Название')),
|
||||||
|
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
|
||||||
|
('pdf_file', models.FileField(blank=True, null=True, upload_to='gosts/', verbose_name='Файл (PDF)')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'ГОСТ/ТУ',
|
||||||
|
'verbose_name_plural': 'ГОСТы и ТУ',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='basematerial',
|
||||||
|
name='grade',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='stock.materialgrade', verbose_name='Марка стали'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='basematerial',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(help_text='Пример: Лист 10мм или Труба 40х40х2', max_length=255, verbose_name='Наименование заготовки'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='basematerial',
|
||||||
|
name='gost',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stock.gost', verbose_name='ГОСТ на сортамент'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='materialgrade',
|
||||||
|
name='gost',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stock.gost', verbose_name='ГОСТ на марку'),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
stock/migrations/__init__.py
Normal file
0
stock/migrations/__init__.py
Normal file
93
stock/models.py
Normal file
93
stock/models.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
from django.db import models
|
||||||
|
from polymorphic.models import PolymorphicModel
|
||||||
|
|
||||||
|
class Gost(models.Model):
|
||||||
|
"""Справочник нормативных документов (ГОСТ, ТУ, ОСТ)"""
|
||||||
|
name = models.CharField("Название", max_length=100, help_text="Например, ГОСТ 19903-2015")
|
||||||
|
description = models.TextField("Описание", blank=True, null=True)
|
||||||
|
# Файлы будут загружаться в /media/gosts/
|
||||||
|
pdf_file = models.FileField("Файл (PDF)", upload_to='gosts/', blank=True, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "ГОСТ/ТУ"
|
||||||
|
verbose_name_plural = "ГОСТы и ТУ"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class MaterialGrade(models.Model):
|
||||||
|
"""Справочник марок стали"""
|
||||||
|
name = models.CharField("Марка стали", max_length=50)
|
||||||
|
# Теперь ссылка на таблицу ГОСТов (например, ГОСТ на хим. состав)
|
||||||
|
gost = models.ForeignKey(Gost, on_delete=models.SET_NULL, verbose_name="ГОСТ на марку",
|
||||||
|
blank=True, null=True)
|
||||||
|
density = models.PositiveIntegerField("Плотность, кг/м³", default=7850)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Марка материала"
|
||||||
|
verbose_name_plural = "Марки материалов"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} ({self.gost.name})" if self.gost else self.name
|
||||||
|
|
||||||
|
class BaseMaterial(PolymorphicModel):
|
||||||
|
"""Базовая модель для всех типов заготовок"""
|
||||||
|
title = models.CharField("Наименование заготовки", max_length=255,
|
||||||
|
help_text="Пример: Лист 10мм или Труба 40х40х2")
|
||||||
|
grade = models.ForeignKey(MaterialGrade, on_delete=models.PROTECT, verbose_name="Марка стали")
|
||||||
|
# ГОСТ на сортамент (например, ГОСТ на прокат листа или трубы)
|
||||||
|
gost = models.ForeignKey(Gost, on_delete=models.SET_NULL, verbose_name="ГОСТ на сортамент",
|
||||||
|
blank=True, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Заготовка"
|
||||||
|
verbose_name_plural = "Заготовки"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.title} [{self.grade.name}]"
|
||||||
|
|
||||||
|
class SheetMaterial(BaseMaterial):
|
||||||
|
"""Листовой прокат"""
|
||||||
|
thickness = models.PositiveIntegerField("Толщина, мм")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Листовой материал"
|
||||||
|
verbose_name_plural = "Листовые материалы"
|
||||||
|
|
||||||
|
class ProfileMaterial(BaseMaterial):
|
||||||
|
"""Линейный прокат"""
|
||||||
|
SECTION_TYPES = [
|
||||||
|
('round_tube', 'Труба круглая'),
|
||||||
|
('square_tube', 'Труба профильная'),
|
||||||
|
('channel', 'Швеллер'),
|
||||||
|
('angle', 'Уголок'),
|
||||||
|
('bar', 'Круг/Пруток'),
|
||||||
|
('other', 'Прочее'),
|
||||||
|
]
|
||||||
|
profile_type = models.CharField("Тип сечения", max_length=20, choices=SECTION_TYPES)
|
||||||
|
weight_per_meter = models.FloatField("Вес 1 м.п., кг", help_text="Табличный вес по ГОСТ")
|
||||||
|
max_dimension = models.PositiveIntegerField("Макс. габарит сечения, мм",
|
||||||
|
help_text="Для проверки входимости детали")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Профильный материал"
|
||||||
|
verbose_name_plural = "Профильные материалы"
|
||||||
|
|
||||||
|
class StockItem(models.Model):
|
||||||
|
"""Складская единица (налицо)"""
|
||||||
|
material = models.ForeignKey(BaseMaterial, on_delete=models.CASCADE,
|
||||||
|
related_name='stock_items', verbose_name="Тип заготовки")
|
||||||
|
length = models.PositiveIntegerField("Длина, мм")
|
||||||
|
width = models.PositiveIntegerField("Ширина, мм", blank=True, null=True)
|
||||||
|
quantity = models.PositiveIntegerField("Количество, шт", default=1)
|
||||||
|
order_reference = models.CharField("Заказ/Сделка", max_length=100, blank=True, null=True)
|
||||||
|
is_scrap = models.BooleanField("Деловой остаток", default=False)
|
||||||
|
location = models.CharField("Место хранения", max_length=100, blank=True, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Складская единица"
|
||||||
|
verbose_name_plural = "Склад налицо"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
dim = f"{self.length}x{self.width}" if self.width else f"L={self.length}"
|
||||||
|
return f"{self.material.title} ({dim})"
|
||||||
3
stock/tests.py
Normal file
3
stock/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
3
stock/views.py
Normal file
3
stock/views.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
Reference in New Issue
Block a user