This commit is contained in:
0
manufacturing/__init__.py
Normal file
0
manufacturing/__init__.py
Normal file
43
manufacturing/admin.py
Normal file
43
manufacturing/admin.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import BOM, ProductEntity, RouteStub
|
||||
|
||||
|
||||
@admin.register(RouteStub)
|
||||
class RouteStubAdmin(admin.ModelAdmin):
|
||||
list_display = ('name',)
|
||||
search_fields = ('name',)
|
||||
|
||||
|
||||
class BOMChildInline(admin.TabularInline):
|
||||
"""Состав изделия/сборки (строки BOM) прямо в карточке ProductEntity."""
|
||||
|
||||
model = BOM
|
||||
fk_name = 'parent'
|
||||
fields = ('child', 'quantity')
|
||||
autocomplete_fields = ('child',)
|
||||
extra = 10
|
||||
|
||||
|
||||
@admin.register(ProductEntity)
|
||||
class ProductEntityAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
'drawing_number',
|
||||
'name',
|
||||
'entity_type',
|
||||
'planned_material',
|
||||
'blank_area_m2',
|
||||
'blank_length_mm',
|
||||
)
|
||||
list_filter = ('entity_type', 'planned_material__category')
|
||||
search_fields = ('drawing_number', 'name', 'planned_material__name', 'planned_material__full_name')
|
||||
autocomplete_fields = ('planned_material', 'route')
|
||||
inlines = (BOMChildInline,)
|
||||
|
||||
|
||||
@admin.register(BOM)
|
||||
class BOMAdmin(admin.ModelAdmin):
|
||||
list_display = ('parent', 'child', 'quantity')
|
||||
search_fields = ('parent__name', 'parent__drawing_number', 'child__name', 'child__drawing_number')
|
||||
list_filter = ('parent',)
|
||||
autocomplete_fields = ('parent', 'child')
|
||||
7
manufacturing/apps.py
Normal file
7
manufacturing/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ManufacturingConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'manufacturing'
|
||||
verbose_name = 'Производство (КД/BOM)'
|
||||
61
manufacturing/migrations/0001_initial.py
Normal file
61
manufacturing/migrations/0001_initial.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# Generated by Django 6.0.3 on 2026-04-04 15:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('warehouse', '0003_alter_material_full_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='RouteStub',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200, unique=True, verbose_name='Маршрут')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Маршрут',
|
||||
'verbose_name_plural': 'Маршруты',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProductEntity',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Наименование')),
|
||||
('drawing_number', models.CharField(blank=True, default='', max_length=100, verbose_name='Обозначение/Чертёж')),
|
||||
('entity_type', models.CharField(choices=[('product', 'Готовое изделие'), ('assembly', 'Сборочная единица'), ('part', 'Деталь')], default='part', max_length=15, verbose_name='Тип')),
|
||||
('blank_area_m2', models.FloatField(blank=True, null=True, verbose_name='Норма: площадь заготовки (м²/шт)')),
|
||||
('blank_length_mm', models.FloatField(blank=True, null=True, verbose_name='Норма: длина заготовки (мм/шт)')),
|
||||
('dxf_file', models.FileField(blank=True, null=True, upload_to='drawings/%Y/%m/', verbose_name='Исходник (DXF/IGES/STEP)')),
|
||||
('pdf_main', models.FileField(blank=True, null=True, upload_to='drawings_pdf/%Y/%m/', verbose_name='Чертёж (PDF)')),
|
||||
('preview', models.ImageField(blank=True, null=True, upload_to='previews/%Y/%m/', verbose_name='Превью')),
|
||||
('planned_material', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='warehouse.material', verbose_name='Заложенный материал')),
|
||||
('route', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='manufacturing.routestub', verbose_name='Маршрут')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'КД (изделие/деталь)',
|
||||
'verbose_name_plural': 'КД (изделия/детали)',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BOM',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('quantity', models.PositiveIntegerField(default=1, verbose_name='Кол-во в сборке')),
|
||||
('child', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='used_in', to='manufacturing.productentity', verbose_name='Что входит (деталь)')),
|
||||
('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='components', to='manufacturing.productentity', verbose_name='Куда входит (сборка)')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Спецификация (BOM)',
|
||||
'verbose_name_plural': 'Спецификации (BOM)',
|
||||
'unique_together': {('parent', 'child')},
|
||||
},
|
||||
),
|
||||
]
|
||||
0
manufacturing/migrations/__init__.py
Normal file
0
manufacturing/migrations/__init__.py
Normal file
97
manufacturing/models.py
Normal file
97
manufacturing/models.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class RouteStub(models.Model):
|
||||
"""Маршрут (пока заглушка под техпроцессы)."""
|
||||
|
||||
name = models.CharField("Маршрут", max_length=200, unique=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Маршрут"
|
||||
verbose_name_plural = "Маршруты"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class ProductEntity(models.Model):
|
||||
"""Паспорт детали/сборки/изделия (КД).
|
||||
|
||||
planned_material:
|
||||
- материал, заложенный в КД (для расчёта потребности и контроля замен при раскрое).
|
||||
|
||||
Нормы расхода (для BOM Explosion и MaterialRequirement):
|
||||
- для листовой детали: blank_area_m2 (м² на 1 шт)
|
||||
- для линейной (профиль/труба/круг): blank_length_mm (мм на 1 шт)
|
||||
|
||||
Примечание:
|
||||
- категорию типа (лист/профиль) определяем по planned_material.category.
|
||||
"""
|
||||
|
||||
ENTITY_TYPE = [
|
||||
('product', 'Готовое изделие'),
|
||||
('assembly', 'Сборочная единица'),
|
||||
('part', 'Деталь'),
|
||||
]
|
||||
|
||||
name = models.CharField("Наименование", max_length=255)
|
||||
drawing_number = models.CharField("Обозначение/Чертёж", max_length=100, blank=True, default="")
|
||||
entity_type = models.CharField("Тип", max_length=15, choices=ENTITY_TYPE, default='part')
|
||||
|
||||
planned_material = models.ForeignKey(
|
||||
'warehouse.Material',
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Заложенный материал",
|
||||
)
|
||||
route = models.ForeignKey(
|
||||
RouteStub,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Маршрут",
|
||||
)
|
||||
|
||||
blank_area_m2 = models.FloatField("Норма: площадь заготовки (м²/шт)", null=True, blank=True)
|
||||
blank_length_mm = models.FloatField("Норма: длина заготовки (мм/шт)", null=True, blank=True)
|
||||
|
||||
dxf_file = models.FileField("Исходник (DXF/IGES/STEP)", upload_to="drawings/%Y/%m/", blank=True, null=True)
|
||||
pdf_main = models.FileField("Чертёж (PDF)", upload_to="drawings_pdf/%Y/%m/", blank=True, null=True)
|
||||
preview = models.ImageField("Превью", upload_to="previews/%Y/%m/", blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "КД (изделие/деталь)"
|
||||
verbose_name_plural = "КД (изделия/детали)"
|
||||
|
||||
def __str__(self):
|
||||
base = f"{self.drawing_number} {self.name}".strip()
|
||||
return base if base else self.name
|
||||
|
||||
|
||||
class BOM(models.Model):
|
||||
"""Спецификация (BOM): parent состоит из child в количестве quantity."""
|
||||
|
||||
parent = models.ForeignKey(
|
||||
ProductEntity,
|
||||
related_name='components',
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Куда входит (сборка)",
|
||||
)
|
||||
child = models.ForeignKey(
|
||||
ProductEntity,
|
||||
related_name='used_in',
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Что входит (деталь)",
|
||||
)
|
||||
quantity = models.PositiveIntegerField("Кол-во в сборке", default=1)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('parent', 'child')
|
||||
verbose_name = "Спецификация (BOM)"
|
||||
verbose_name_plural = "Спецификации (BOM)"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.parent} -> {self.child} x{self.quantity}"
|
||||
|
||||
# Create your models here.
|
||||
3
manufacturing/tests.py
Normal file
3
manufacturing/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
manufacturing/views.py
Normal file
3
manufacturing/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
Reference in New Issue
Block a user