ДОбавил изделия и заполнение спецификции изделия
All checks were successful
Deploy MES Core / deploy (push) Successful in 3m27s
All checks were successful
Deploy MES Core / deploy (push) Successful in 3m27s
This commit is contained in:
6
TODO.md
6
TODO.md
@@ -5,3 +5,9 @@
|
|||||||
- По клику на строку открывать карточку «Единица на складе» (read-only для observer, редактирование для admin/technologist/master/clerk):
|
- По клику на строку открывать карточку «Единица на складе» (read-only для observer, редактирование для admin/technologist/master/clerk):
|
||||||
- правка: сделка, давальческий, размеры (лист/хлыст), количество, примечание (если добавим)
|
- правка: сделка, давальческий, размеры (лист/хлыст), количество, примечание (если добавим)
|
||||||
- отображение: история перемещений/приходов/отгрузок (если потребуется).
|
- отображение: история перемещений/приходов/отгрузок (если потребуется).
|
||||||
|
|
||||||
|
## Списание (UI)
|
||||||
|
- Доработать страницу «Списание»: фильтры, удобная сводка по материалам/изделиям и отметка «внесено в 1С».
|
||||||
|
|
||||||
|
## Потребность (Материалы)
|
||||||
|
- Пересмотреть расчёт потребности: уйти от м²/мм, формировать пачки DXF по материалам/толщинам и прокат по длинам (для nesting/ручного расчёта).
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-04-07 04:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('manufacturing', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='productentity',
|
||||||
|
name='passport_filled',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Паспорт заполнен'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='productentity',
|
||||||
|
name='entity_type',
|
||||||
|
field=models.CharField(choices=[('product', 'Готовое изделие'), ('assembly', 'Сборочная единица'), ('part', 'Деталь'), ('purchased', 'Покупное'), ('casting', 'Литьё'), ('outsourced', 'Аутсорс')], default='part', max_length=15, verbose_name='Тип'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-04-07 08:56
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('manufacturing', '0002_productentity_passport_filled_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AssemblyPassport',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('weight_kg', models.FloatField(blank=True, null=True, verbose_name='Масса, кг')),
|
||||||
|
('coating', models.CharField(blank=True, default='', max_length=200, verbose_name='Покрытие')),
|
||||||
|
('coating_color', models.CharField(blank=True, default='', max_length=100, verbose_name='Цвет')),
|
||||||
|
('coating_area_m2', models.FloatField(blank=True, null=True, verbose_name='Площадь покрытия, м²')),
|
||||||
|
('technical_requirements', models.TextField(blank=True, default='', verbose_name='Технические требования')),
|
||||||
|
('entity', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='assembly_passport', to='manufacturing.productentity')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Паспорт сборки/изделия',
|
||||||
|
'verbose_name_plural': 'Паспорта сборок/изделий',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WeldingSeam',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255, verbose_name='Наименование')),
|
||||||
|
('leg_mm', models.FloatField(verbose_name='Катет, мм')),
|
||||||
|
('length_mm', models.FloatField(verbose_name='Длина, мм')),
|
||||||
|
('quantity', models.PositiveIntegerField(default=1, verbose_name='Кол-во')),
|
||||||
|
('passport', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='welding_seams', to='manufacturing.assemblypassport')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Сварной шов',
|
||||||
|
'verbose_name_plural': 'Сварные швы',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-04-07 09:07
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('manufacturing', '0003_assemblypassport_weldingseam'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CastingPassport',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('casting_material', models.CharField(blank=True, default='', max_length=200, verbose_name='Материал литья')),
|
||||||
|
('mass_kg', models.FloatField(blank=True, null=True, verbose_name='Масса, кг')),
|
||||||
|
('entity', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='casting_passport', to='manufacturing.productentity')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Паспорт литья',
|
||||||
|
'verbose_name_plural': 'Паспорта литья',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OutsourcedPassport',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('technical_requirements', models.TextField(blank=True, default='', verbose_name='Технические требования')),
|
||||||
|
('notes', models.TextField(blank=True, default='', verbose_name='Пояснения')),
|
||||||
|
('entity', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='outsourced_passport', to='manufacturing.productentity')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Паспорт аутсорса',
|
||||||
|
'verbose_name_plural': 'Паспорта аутсорса',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PartPassport',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('thickness_mm', models.FloatField(blank=True, null=True, verbose_name='Толщина, мм')),
|
||||||
|
('length_mm', models.FloatField(blank=True, null=True, verbose_name='Длина, мм')),
|
||||||
|
('mass_kg', models.FloatField(blank=True, null=True, verbose_name='Масса, кг')),
|
||||||
|
('cut_length_mm', models.FloatField(blank=True, null=True, verbose_name='Длина реза, мм')),
|
||||||
|
('pierce_count', models.PositiveIntegerField(blank=True, null=True, verbose_name='Кол-во врезок')),
|
||||||
|
('engraving', models.TextField(blank=True, default='', verbose_name='Гравировка')),
|
||||||
|
('technical_requirements', models.TextField(blank=True, default='', verbose_name='Технические требования')),
|
||||||
|
('entity', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='part_passport', to='manufacturing.productentity')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Паспорт детали',
|
||||||
|
'verbose_name_plural': 'Паспорта деталей',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PurchasedPassport',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('gost', models.CharField(blank=True, default='', max_length=255, verbose_name='ГОСТ/ТУ')),
|
||||||
|
('entity', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='purchased_passport', to='manufacturing.productentity')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Паспорт покупного',
|
||||||
|
'verbose_name_plural': 'Паспорта покупного',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -32,6 +32,9 @@ class ProductEntity(models.Model):
|
|||||||
('product', 'Готовое изделие'),
|
('product', 'Готовое изделие'),
|
||||||
('assembly', 'Сборочная единица'),
|
('assembly', 'Сборочная единица'),
|
||||||
('part', 'Деталь'),
|
('part', 'Деталь'),
|
||||||
|
('purchased', 'Покупное'),
|
||||||
|
('casting', 'Литьё'),
|
||||||
|
('outsourced', 'Аутсорс'),
|
||||||
]
|
]
|
||||||
|
|
||||||
name = models.CharField("Наименование", max_length=255)
|
name = models.CharField("Наименование", max_length=255)
|
||||||
@@ -60,6 +63,8 @@ class ProductEntity(models.Model):
|
|||||||
pdf_main = models.FileField("Чертёж (PDF)", upload_to="drawings_pdf/%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)
|
preview = models.ImageField("Превью", upload_to="previews/%Y/%m/", blank=True, null=True)
|
||||||
|
|
||||||
|
passport_filled = models.BooleanField('Паспорт заполнен', default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "КД (изделие/деталь)"
|
verbose_name = "КД (изделие/деталь)"
|
||||||
verbose_name_plural = "КД (изделия/детали)"
|
verbose_name_plural = "КД (изделия/детали)"
|
||||||
@@ -94,4 +99,98 @@ class BOM(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.parent} -> {self.child} x{self.quantity}"
|
return f"{self.parent} -> {self.child} x{self.quantity}"
|
||||||
|
|
||||||
# Create your models here.
|
|
||||||
|
class AssemblyPassport(models.Model):
|
||||||
|
entity = models.OneToOneField(ProductEntity, on_delete=models.CASCADE, related_name='assembly_passport')
|
||||||
|
|
||||||
|
weight_kg = models.FloatField('Масса, кг', null=True, blank=True)
|
||||||
|
coating = models.CharField('Покрытие', max_length=200, blank=True, default='')
|
||||||
|
coating_color = models.CharField('Цвет', max_length=100, blank=True, default='')
|
||||||
|
coating_area_m2 = models.FloatField('Площадь покрытия, м²', null=True, blank=True)
|
||||||
|
|
||||||
|
technical_requirements = models.TextField('Технические требования', blank=True, default='')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Паспорт сборки/изделия'
|
||||||
|
verbose_name_plural = 'Паспорта сборок/изделий'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.entity)
|
||||||
|
|
||||||
|
|
||||||
|
class WeldingSeam(models.Model):
|
||||||
|
passport = models.ForeignKey(AssemblyPassport, related_name='welding_seams', on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
name = models.CharField('Наименование', max_length=255)
|
||||||
|
leg_mm = models.FloatField('Катет, мм')
|
||||||
|
length_mm = models.FloatField('Длина, мм')
|
||||||
|
quantity = models.PositiveIntegerField('Кол-во', default=1)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Сварной шов'
|
||||||
|
verbose_name_plural = 'Сварные швы'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class PartPassport(models.Model):
|
||||||
|
entity = models.OneToOneField(ProductEntity, on_delete=models.CASCADE, related_name='part_passport')
|
||||||
|
|
||||||
|
thickness_mm = models.FloatField('Толщина, мм', null=True, blank=True)
|
||||||
|
length_mm = models.FloatField('Длина, мм', null=True, blank=True)
|
||||||
|
mass_kg = models.FloatField('Масса, кг', null=True, blank=True)
|
||||||
|
|
||||||
|
cut_length_mm = models.FloatField('Длина реза, мм', null=True, blank=True)
|
||||||
|
pierce_count = models.PositiveIntegerField('Кол-во врезок', null=True, blank=True)
|
||||||
|
engraving = models.TextField('Гравировка', blank=True, default='')
|
||||||
|
|
||||||
|
technical_requirements = models.TextField('Технические требования', blank=True, default='')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Паспорт детали'
|
||||||
|
verbose_name_plural = 'Паспорта деталей'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.entity)
|
||||||
|
|
||||||
|
|
||||||
|
class PurchasedPassport(models.Model):
|
||||||
|
entity = models.OneToOneField(ProductEntity, on_delete=models.CASCADE, related_name='purchased_passport')
|
||||||
|
|
||||||
|
gost = models.CharField('ГОСТ/ТУ', max_length=255, blank=True, default='')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Паспорт покупного'
|
||||||
|
verbose_name_plural = 'Паспорта покупного'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.entity)
|
||||||
|
|
||||||
|
|
||||||
|
class CastingPassport(models.Model):
|
||||||
|
entity = models.OneToOneField(ProductEntity, on_delete=models.CASCADE, related_name='casting_passport')
|
||||||
|
|
||||||
|
casting_material = models.CharField('Материал литья', max_length=200, blank=True, default='')
|
||||||
|
mass_kg = models.FloatField('Масса, кг', null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Паспорт литья'
|
||||||
|
verbose_name_plural = 'Паспорта литья'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.entity)
|
||||||
|
|
||||||
|
|
||||||
|
class OutsourcedPassport(models.Model):
|
||||||
|
entity = models.OneToOneField(ProductEntity, on_delete=models.CASCADE, related_name='outsourced_passport')
|
||||||
|
|
||||||
|
technical_requirements = models.TextField('Технические требования', blank=True, default='')
|
||||||
|
notes = models.TextField('Пояснения', blank=True, default='')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Паспорт аутсорса'
|
||||||
|
verbose_name_plural = 'Паспорта аутсорса'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.entity)
|
||||||
|
|||||||
283
shiftflow/templates/shiftflow/product_detail.html
Normal file
283
shiftflow/templates/shiftflow/product_detail.html
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card shadow border-secondary mb-3">
|
||||||
|
<div class="card-header border-secondary py-3 d-flex flex-wrap gap-2 justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-accent mb-0"><i class="bi bi-diagram-3 me-2"></i>{{ entity }}</h3>
|
||||||
|
<div class="small text-muted mt-1">{{ entity.get_entity_type_display }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||||
|
{% if can_edit %}
|
||||||
|
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#addComponentModal">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i>Добавить
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if parent_id %}
|
||||||
|
<a class="btn btn-outline-secondary btn-sm" href="{% url 'product_detail' parent_id %}">Назад</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-outline-secondary btn-sm" href="{% url 'products' %}">Назад</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0 align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr class="table-custom-header">
|
||||||
|
<th>Тип</th>
|
||||||
|
<th>Обозначение</th>
|
||||||
|
<th>Наименование</th>
|
||||||
|
<th>Заполнен</th>
|
||||||
|
<th>Кол-во</th>
|
||||||
|
<th data-sort="false"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for ln in lines %}
|
||||||
|
<tr class="product-row" role="button" data-info-url="{% url 'product_info' ln.child.id %}">
|
||||||
|
<td class="small text-muted">{{ ln.child.get_entity_type_display }}</td>
|
||||||
|
<td class="fw-bold">{{ ln.child.drawing_number|default:"—" }}</td>
|
||||||
|
<td>{{ ln.child.name }}</td>
|
||||||
|
<td>
|
||||||
|
{% if ln.child.passport_filled %}
|
||||||
|
<i class="bi bi-check-circle-fill text-success" title="Заполнено"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-circle text-muted" title="Не заполнено"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td style="max-width:220px;">
|
||||||
|
<form method="post" class="d-flex gap-2 align-items-center">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% if parent_id %}<input type="hidden" name="parent" value="{{ parent_id }}">{% endif %}
|
||||||
|
<input type="hidden" name="action" value="update_qty">
|
||||||
|
<input type="hidden" name="bom_id" value="{{ ln.id }}">
|
||||||
|
<input class="form-control form-control-sm bg-body text-body border-secondary" name="quantity" value="{{ ln.quantity }}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" type="submit" {% if not can_edit %}disabled{% endif %}>OK</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="d-flex gap-2 justify-content-end">
|
||||||
|
<a class="btn btn-outline-accent btn-sm" href="{% url 'product_detail' ln.child.id %}?parent={{ entity.id }}" onclick="event.stopPropagation();">Состав</a>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% if parent_id %}<input type="hidden" name="parent" value="{{ parent_id }}">{% endif %}
|
||||||
|
<input type="hidden" name="action" value="delete_line">
|
||||||
|
<input type="hidden" name="bom_id" value="{{ ln.id }}">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" type="submit" {% if not can_edit %}disabled{% endif %}>Удалить</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="5" class="text-center text-muted py-4">Пока нет компонентов</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="addComponentModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-xl">
|
||||||
|
<div class="modal-content border-secondary">
|
||||||
|
<div class="modal-header border-secondary">
|
||||||
|
<h5 class="modal-title">Добавить компонент</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="border border-secondary rounded p-3 mb-3">
|
||||||
|
<div class="fw-bold mb-2">Найти существующее (обозначение / наименование)</div>
|
||||||
|
|
||||||
|
<form method="get" class="row g-2 align-items-end">
|
||||||
|
<input type="hidden" name="open" value="1">
|
||||||
|
{% if parent_id %}<input type="hidden" name="parent" value="{{ parent_id }}">{% endif %}
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Обозначение</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="q_dn" value="{{ q_dn }}" placeholder="Напр. УРН.01">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label class="form-label">Наименование</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="q_name" value="{{ q_name }}" placeholder="Напр. Кубик декоративный">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 d-flex gap-2">
|
||||||
|
<button class="btn btn-outline-secondary w-100" type="submit"><i class="bi bi-search me-1"></i>Поиск</button>
|
||||||
|
<a class="btn btn-outline-secondary" href="{% url 'product_detail' entity.id %}?open=1{% if parent_id %}&parent={{ parent_id }}{% endif %}"><i class="bi bi-x-lg"></i></a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
{% if searched %}
|
||||||
|
{% if found %}
|
||||||
|
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center">
|
||||||
|
<div class="small text-muted">Найдено:</div>
|
||||||
|
<div class="fw-bold">{{ found.get_entity_type_display }} | {{ found.drawing_number }} {{ found.name }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" class="row g-2 align-items-end mt-2">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% if parent_id %}<input type="hidden" name="parent" value="{{ parent_id }}">{% endif %}
|
||||||
|
<input type="hidden" name="action" value="add_existing">
|
||||||
|
<input type="hidden" name="child_id" value="{{ found.id }}">
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Кол-во</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="quantity" value="1" required {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9 d-flex justify-content-end">
|
||||||
|
<button class="btn btn-outline-accent" type="submit" {% if not can_edit %}disabled{% endif %}>Добавить найденное</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<div class="small text-muted">Не найдено. Можно создать новое ниже.</div>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="small text-muted">Введи обозначение и/или наименование и нажми «Поиск».</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border border-secondary rounded p-3">
|
||||||
|
<div class="fw-bold mb-2">Создать новое и добавить</div>
|
||||||
|
|
||||||
|
<form method="post" id="createAndAddForm">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% if parent_id %}<input type="hidden" name="parent" value="{{ parent_id }}">{% endif %}
|
||||||
|
<input type="hidden" name="action" value="create_and_add">
|
||||||
|
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Тип</label>
|
||||||
|
<select class="form-select bg-body text-body border-secondary" name="child_type" id="childType" required {% if not can_edit %}disabled{% endif %}>
|
||||||
|
<option value="assembly">Сборочная единица</option>
|
||||||
|
<option value="part">Деталь</option>
|
||||||
|
<option value="purchased">Покупное</option>
|
||||||
|
<option value="casting">Литьё</option>
|
||||||
|
<option value="outsourced">Аутсорс</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Кол-во</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="quantity" value="1" required {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Обозначение</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="drawing_number" value="{% if found %}{{ found.drawing_number }}{% else %}{{ q_dn }}{% endif %}" placeholder="Опционально" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Наименование</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="name" value="{% if found %}{{ found.name }}{% else %}{{ q_name }}{% endif %}" required {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12" id="plannedMaterialBlock">
|
||||||
|
<label class="form-label">Материал (для детали)</label>
|
||||||
|
<select class="form-select bg-body text-body border-secondary" name="planned_material_id" id="plannedMaterialSelect" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
<option value="">— не указано —</option>
|
||||||
|
{% for m in materials %}
|
||||||
|
<option value="{{ m.id }}">{{ m.full_name|default:m.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 d-flex justify-content-end mt-2">
|
||||||
|
<button class="btn btn-outline-accent" type="submit" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
Создать и добавить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const typeSel = document.getElementById('childType');
|
||||||
|
const block = document.getElementById('plannedMaterialBlock');
|
||||||
|
const matSel = document.getElementById('plannedMaterialSelect');
|
||||||
|
|
||||||
|
function sync() {
|
||||||
|
const t = (typeSel && typeSel.value) || '';
|
||||||
|
const isPart = t === 'part';
|
||||||
|
if (block) block.style.display = isPart ? '' : 'none';
|
||||||
|
if (matSel) {
|
||||||
|
matSel.disabled = !isPart;
|
||||||
|
if (!isPart) matSel.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeSel) {
|
||||||
|
typeSel.addEventListener('change', sync);
|
||||||
|
sync();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer border-secondary">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="productInfoModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-xl">
|
||||||
|
<div class="modal-content border-secondary">
|
||||||
|
<div class="modal-header border-secondary">
|
||||||
|
<h5 class="modal-title">Информация о компоненте</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="productInfoBody">
|
||||||
|
<div class="text-muted">Загрузка...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if request.GET.open == '1' %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const el = document.getElementById('addComponentModal');
|
||||||
|
if (!el) return;
|
||||||
|
const modal = new bootstrap.Modal(el);
|
||||||
|
modal.show();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const modalEl = document.getElementById('productInfoModal');
|
||||||
|
const bodyEl = document.getElementById('productInfoBody');
|
||||||
|
if (!modalEl || !bodyEl) return;
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(modalEl);
|
||||||
|
|
||||||
|
async function openInfo(url) {
|
||||||
|
bodyEl.innerHTML = '<div class="text-muted">Загрузка...</div>';
|
||||||
|
modal.show();
|
||||||
|
try {
|
||||||
|
const nextUrl = encodeURIComponent(window.location.pathname + window.location.search);
|
||||||
|
const sep = url.includes('?') ? '&' : '?';
|
||||||
|
const res = await fetch(url + sep + 'next=' + nextUrl, { credentials: 'same-origin' });
|
||||||
|
bodyEl.innerHTML = await res.text();
|
||||||
|
} catch (e) {
|
||||||
|
bodyEl.innerHTML = '<div class="alert alert-danger">Не удалось загрузить информацию.</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('tr.product-row[data-info-url]').forEach(tr => {
|
||||||
|
tr.addEventListener('click', () => {
|
||||||
|
const url = tr.getAttribute('data-info-url');
|
||||||
|
if (!url) return;
|
||||||
|
openInfo(url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
160
shiftflow/templates/shiftflow/product_info_assembly.html
Normal file
160
shiftflow/templates/shiftflow/product_info_assembly.html
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<div class="container-fluid p-0">
|
||||||
|
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="save">
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Тип</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" value="{{ entity.get_entity_type_display }}" disabled>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Обозначение</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="drawing_number" value="{{ entity.drawing_number }}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Заполнен</label>
|
||||||
|
<div class="form-check mt-2">
|
||||||
|
<input class="form-check-input" type="checkbox" name="passport_filled" id="pf" {% if entity.passport_filled %}checked{% endif %} {% if not can_edit %}disabled{% endif %}>
|
||||||
|
<label class="form-check-label" for="pf">Заполнено</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Наименование</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Чертёж (PDF)</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
{% if entity.pdf_main %}
|
||||||
|
<div class="small mt-1"><a href="{{ entity.pdf_main.url }}" target="_blank">Открыть текущий</a></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Маршрут</label>
|
||||||
|
<select class="form-select bg-body text-body border-secondary" name="route_id" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
<option value="">— не указано —</option>
|
||||||
|
{% for r in routes %}
|
||||||
|
<option value="{{ r.id }}" {% if entity.route_id == r.id %}selected{% endif %}>{{ r.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Масса, кг</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="weight_kg" value="{% if passport and passport.weight_kg %}{{ passport.weight_kg }}{% endif %}" inputmode="decimal" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Покрытие</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="coating" value="{% if passport %}{{ passport.coating }}{% endif %}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Цвет</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="coating_color" value="{% if passport %}{{ passport.coating_color }}{% endif %}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Площадь покрытия, м²</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="coating_area_m2" value="{% if passport and passport.coating_area_m2 %}{{ passport.coating_area_m2 }}{% endif %}" inputmode="decimal" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Технические требования</label>
|
||||||
|
<textarea class="form-control bg-body text-body border-secondary" name="technical_requirements" rows="4" {% if not can_edit %}disabled{% endif %}>{% if passport %}{{ passport.technical_requirements }}{% endif %}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 d-flex justify-content-end mt-2">
|
||||||
|
{% if can_edit %}
|
||||||
|
<button class="btn btn-outline-accent" type="submit">Сохранить</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if can_edit %}
|
||||||
|
<form method="post" action="{% url 'product_info' entity.id %}" class="mt-3 d-flex gap-2 align-items-center">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="create_route">
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="route_name" placeholder="Новый маршрут">
|
||||||
|
<button class="btn btn-outline-secondary" type="submit">Добавить маршрут</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="fw-bold mb-2">Сварные швы</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0 align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr class="table-custom-header">
|
||||||
|
<th>Наименование</th>
|
||||||
|
<th>Катет, мм</th>
|
||||||
|
<th>Длина, мм</th>
|
||||||
|
<th>Кол-во</th>
|
||||||
|
<th data-sort="false"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for s in welding_seams %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ s.name }}</td>
|
||||||
|
<td>{{ s.leg_mm }}</td>
|
||||||
|
<td>{{ s.length_mm }}</td>
|
||||||
|
<td>{{ s.quantity }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
{% if can_edit %}
|
||||||
|
<form method="post" action="{% url 'product_info' entity.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="delete_weld_seam">
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
<input type="hidden" name="seam_id" value="{{ s.id }}">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" type="submit">Удалить</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="5" class="text-center text-muted py-3">Швы не добавлены</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if can_edit %}
|
||||||
|
<form method="post" action="{% url 'product_info' entity.id %}" class="row g-2 mt-2">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="add_weld_seam">
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label class="form-label">Наименование</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="seam_name" placeholder="Напр. Шов по периметру">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">Катет, мм</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="seam_leg_mm" inputmode="decimal" placeholder="4">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Длина, мм</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="seam_length_mm" inputmode="decimal" placeholder="120">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1">
|
||||||
|
<label class="form-label">Кол-во</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="seam_quantity" value="1">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1 d-flex align-items-end justify-content-end">
|
||||||
|
<button class="btn btn-outline-accent" type="submit">+</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
84
shiftflow/templates/shiftflow/product_info_casting.html
Normal file
84
shiftflow/templates/shiftflow/product_info_casting.html
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<div class="container-fluid p-0">
|
||||||
|
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="save">
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Тип</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" value="{{ entity.get_entity_type_display }}" disabled>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Обозначение</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="drawing_number" value="{{ entity.drawing_number }}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Заполнен</label>
|
||||||
|
<div class="form-check mt-2">
|
||||||
|
<input class="form-check-input" type="checkbox" name="passport_filled" id="pf" {% if entity.passport_filled %}checked{% endif %} {% if not can_edit %}disabled{% endif %}>
|
||||||
|
<label class="form-check-label" for="pf">Заполнено</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Наименование</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Материал литья</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="casting_material" value="{% if passport %}{{ passport.casting_material }}{% endif %}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Масса, кг</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="mass_kg" value="{% if passport and passport.mass_kg %}{{ passport.mass_kg }}{% endif %}" inputmode="decimal" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Маршрут</label>
|
||||||
|
<select class="form-select bg-body text-body border-secondary" name="route_id" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
<option value="">— не указано —</option>
|
||||||
|
{% for r in routes %}
|
||||||
|
<option value="{{ r.id }}" {% if entity.route_id == r.id %}selected{% endif %}>{{ r.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Чертёж (PDF)</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
{% if entity.pdf_main %}
|
||||||
|
<div class="small mt-1"><a href="{{ entity.pdf_main.url }}" target="_blank">Открыть текущий</a></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Картинка</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
{% if entity.preview %}
|
||||||
|
<div class="small mt-1"><a href="{{ entity.preview.url }}" target="_blank">Открыть текущую</a></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 d-flex justify-content-end mt-2">
|
||||||
|
{% if can_edit %}
|
||||||
|
<button class="btn btn-outline-accent" type="submit">Сохранить</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if can_edit %}
|
||||||
|
<form method="post" action="{% url 'product_info' entity.id %}" class="mt-3 d-flex gap-2 align-items-center">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="create_route">
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="route_name" placeholder="Новый маршрут">
|
||||||
|
<button class="btn btn-outline-secondary" type="submit">Добавить маршрут</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
36
shiftflow/templates/shiftflow/product_info_external.html
Normal file
36
shiftflow/templates/shiftflow/product_info_external.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<form method="post" action="{% url 'product_info' entity.id %}" class="container-fluid p-0">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="save">
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Тип</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" value="{{ entity.get_entity_type_display }}" disabled>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Обозначение</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="drawing_number" value="{{ entity.drawing_number }}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Заполнен</label>
|
||||||
|
<div class="form-check mt-2">
|
||||||
|
<input class="form-check-input" type="checkbox" name="passport_filled" id="pf" {% if entity.passport_filled %}checked{% endif %} {% if not can_edit %}disabled{% endif %}>
|
||||||
|
<label class="form-check-label" for="pf">Паспорт заполнен</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Наименование</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 d-flex align-items-end justify-content-end">
|
||||||
|
{% if can_edit %}
|
||||||
|
<button class="btn btn-outline-accent" type="submit">Сохранить</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
76
shiftflow/templates/shiftflow/product_info_outsourced.html
Normal file
76
shiftflow/templates/shiftflow/product_info_outsourced.html
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<div class="container-fluid p-0">
|
||||||
|
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="save">
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Тип</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" value="{{ entity.get_entity_type_display }}" disabled>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Обозначение</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="drawing_number" value="{{ entity.drawing_number }}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Заполнен</label>
|
||||||
|
<div class="form-check mt-2">
|
||||||
|
<input class="form-check-input" type="checkbox" name="passport_filled" id="pf" {% if entity.passport_filled %}checked{% endif %} {% if not can_edit %}disabled{% endif %}>
|
||||||
|
<label class="form-check-label" for="pf">Заполнено</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Наименование</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Маршрут</label>
|
||||||
|
<select class="form-select bg-body text-body border-secondary" name="route_id" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
<option value="">— не указано —</option>
|
||||||
|
{% for r in routes %}
|
||||||
|
<option value="{{ r.id }}" {% if entity.route_id == r.id %}selected{% endif %}>{{ r.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Чертёж/ТЗ (PDF)</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
{% if entity.pdf_main %}
|
||||||
|
<div class="small mt-1"><a href="{{ entity.pdf_main.url }}" target="_blank">Открыть текущий</a></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Технические требования</label>
|
||||||
|
<textarea class="form-control bg-body text-body border-secondary" name="technical_requirements" rows="4" {% if not can_edit %}disabled{% endif %}>{% if passport %}{{ passport.technical_requirements }}{% endif %}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Пояснения</label>
|
||||||
|
<textarea class="form-control bg-body text-body border-secondary" name="notes" rows="3" {% if not can_edit %}disabled{% endif %}>{% if passport %}{{ passport.notes }}{% endif %}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 d-flex justify-content-end mt-2">
|
||||||
|
{% if can_edit %}
|
||||||
|
<button class="btn btn-outline-accent" type="submit">Сохранить</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if can_edit %}
|
||||||
|
<form method="post" action="{% url 'product_info' entity.id %}" class="mt-3 d-flex gap-2 align-items-center">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="create_route">
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="route_name" placeholder="Новый маршрут">
|
||||||
|
<button class="btn btn-outline-secondary" type="submit">Добавить маршрут</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
127
shiftflow/templates/shiftflow/product_info_part.html
Normal file
127
shiftflow/templates/shiftflow/product_info_part.html
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<div class="container-fluid p-0">
|
||||||
|
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="save">
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Тип</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" value="{{ entity.get_entity_type_display }}" disabled>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Обозначение</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="drawing_number" value="{{ entity.drawing_number }}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Заполнен</label>
|
||||||
|
<div class="form-check mt-2">
|
||||||
|
<input class="form-check-input" type="checkbox" name="passport_filled" id="pf" {% if entity.passport_filled %}checked{% endif %} {% if not can_edit %}disabled{% endif %}>
|
||||||
|
<label class="form-check-label" for="pf">Заполнено</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Наименование</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Материал заготовки</label>
|
||||||
|
<select class="form-select bg-body text-body border-secondary" name="planned_material_id" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
<option value="">— не указано —</option>
|
||||||
|
{% for m in materials %}
|
||||||
|
<option value="{{ m.id }}" {% if entity.planned_material_id == m.id %}selected{% endif %}>{{ m.full_name|default:m.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Маршрут</label>
|
||||||
|
<select class="form-select bg-body text-body border-secondary" name="route_id" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
<option value="">— не указано —</option>
|
||||||
|
{% for r in routes %}
|
||||||
|
<option value="{{ r.id }}" {% if entity.route_id == r.id %}selected{% endif %}>{{ r.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Толщина, мм</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="thickness_mm" value="{% if passport and passport.thickness_mm %}{{ passport.thickness_mm }}{% endif %}" inputmode="decimal" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Длина, мм</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="length_mm" value="{% if passport and passport.length_mm %}{{ passport.length_mm }}{% endif %}" inputmode="decimal" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Масса, кг</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="mass_kg" value="{% if passport and passport.mass_kg %}{{ passport.mass_kg }}{% endif %}" inputmode="decimal" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Длина реза, мм</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="cut_length_mm" value="{% if passport and passport.cut_length_mm %}{{ passport.cut_length_mm }}{% endif %}" inputmode="decimal" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Кол-во врезок</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="pierce_count" value="{% if passport and passport.pierce_count %}{{ passport.pierce_count }}{% endif %}" inputmode="numeric" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Чертёж (PDF)</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
{% if entity.pdf_main %}
|
||||||
|
<div class="small mt-1"><a href="{{ entity.pdf_main.url }}" target="_blank">Открыть текущий</a></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">DXF/IGES/STEP</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="dxf_file" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
{% if entity.dxf_file %}
|
||||||
|
<div class="small mt-1"><a href="{{ entity.dxf_file.url }}" target="_blank">Открыть текущий</a></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Картинка</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
{% if entity.preview %}
|
||||||
|
<div class="small mt-1"><a href="{{ entity.preview.url }}" target="_blank">Открыть текущую</a></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Гравировка</label>
|
||||||
|
<textarea class="form-control bg-body text-body border-secondary" name="engraving" rows="2" {% if not can_edit %}disabled{% endif %}>{% if passport %}{{ passport.engraving }}{% endif %}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Технические требования</label>
|
||||||
|
<textarea class="form-control bg-body text-body border-secondary" name="technical_requirements" rows="4" {% if not can_edit %}disabled{% endif %}>{% if passport %}{{ passport.technical_requirements }}{% endif %}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 d-flex justify-content-end mt-2">
|
||||||
|
{% if can_edit %}
|
||||||
|
<button class="btn btn-outline-accent" type="submit">Сохранить</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if can_edit %}
|
||||||
|
<form method="post" action="{% url 'product_info' entity.id %}" class="mt-3 d-flex gap-2 align-items-center">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="create_route">
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="route_name" placeholder="Новый маршрут">
|
||||||
|
<button class="btn btn-outline-secondary" type="submit">Добавить маршрут</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
79
shiftflow/templates/shiftflow/product_info_purchased.html
Normal file
79
shiftflow/templates/shiftflow/product_info_purchased.html
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<div class="container-fluid p-0">
|
||||||
|
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="save">
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Тип</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" value="{{ entity.get_entity_type_display }}" disabled>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Обозначение</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="drawing_number" value="{{ entity.drawing_number }}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Заполнен</label>
|
||||||
|
<div class="form-check mt-2">
|
||||||
|
<input class="form-check-input" type="checkbox" name="passport_filled" id="pf" {% if entity.passport_filled %}checked{% endif %} {% if not can_edit %}disabled{% endif %}>
|
||||||
|
<label class="form-check-label" for="pf">Заполнено</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Наименование</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">ГОСТ/ТУ</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="gost" value="{% if passport %}{{ passport.gost }}{% endif %}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Маршрут</label>
|
||||||
|
<select class="form-select bg-body text-body border-secondary" name="route_id" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
<option value="">— не указано —</option>
|
||||||
|
{% for r in routes %}
|
||||||
|
<option value="{{ r.id }}" {% if entity.route_id == r.id %}selected{% endif %}>{{ r.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Чертёж/паспорт (PDF)</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
{% if entity.pdf_main %}
|
||||||
|
<div class="small mt-1"><a href="{{ entity.pdf_main.url }}" target="_blank">Открыть текущий</a></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Картинка</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
{% if entity.preview %}
|
||||||
|
<div class="small mt-1"><a href="{{ entity.preview.url }}" target="_blank">Открыть текущую</a></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 d-flex justify-content-end mt-2">
|
||||||
|
{% if can_edit %}
|
||||||
|
<button class="btn btn-outline-accent" type="submit">Сохранить</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if can_edit %}
|
||||||
|
<form method="post" action="{% url 'product_info' entity.id %}" class="mt-3 d-flex gap-2 align-items-center">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="create_route">
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="route_name" placeholder="Новый маршрут">
|
||||||
|
<button class="btn btn-outline-secondary" type="submit">Добавить маршрут</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
162
shiftflow/templates/shiftflow/products.html
Normal file
162
shiftflow/templates/shiftflow/products.html
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card shadow border-secondary mb-3">
|
||||||
|
<div class="card-header border-secondary py-3 d-flex flex-wrap gap-2 justify-content-between align-items-center">
|
||||||
|
<h3 class="text-accent mb-0"><i class="bi bi-diagram-3 me-2"></i>Изделия</h3>
|
||||||
|
|
||||||
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||||
|
{% if can_edit %}
|
||||||
|
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#createProductModal">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i>Создать
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form class="d-flex gap-2 align-items-center" method="get" action="{% url 'products' %}">
|
||||||
|
<select class="form-select form-select-sm bg-body text-body border-secondary" name="entity_type" style="min-width: 220px;">
|
||||||
|
<option value="" {% if not entity_type %}selected{% endif %}>Все типы</option>
|
||||||
|
<option value="product" {% if entity_type == 'product' %}selected{% endif %}>Готовое изделие</option>
|
||||||
|
<option value="assembly" {% if entity_type == 'assembly' %}selected{% endif %}>Сборочная единица</option>
|
||||||
|
<option value="part" {% if entity_type == 'part' %}selected{% endif %}>Деталь</option>
|
||||||
|
<option value="purchased" {% if entity_type == 'purchased' %}selected{% endif %}>Покупное</option>
|
||||||
|
<option value="casting" {% if entity_type == 'casting' %}selected{% endif %}>Литьё</option>
|
||||||
|
<option value="outsourced" {% if entity_type == 'outsourced' %}selected{% endif %}>Аутсорс</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input class="form-control form-control-sm bg-body text-body border-secondary" name="q" value="{{ q }}" placeholder="Поиск (обозначение/наименование)" style="min-width: 320px;">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" type="submit"><i class="bi bi-search me-1"></i>Найти</button>
|
||||||
|
<a class="btn btn-outline-secondary btn-sm" href="{% url 'products' %}"><i class="bi bi-arrow-counterclockwise me-1"></i>Сброс</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0 align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr class="table-custom-header">
|
||||||
|
<th>Тип</th>
|
||||||
|
<th>Обозначение</th>
|
||||||
|
<th>Наименование</th>
|
||||||
|
<th>Материал</th>
|
||||||
|
<th>Заполнен</th>
|
||||||
|
<th data-sort="false"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in products %}
|
||||||
|
<tr class="product-row" role="button" data-info-url="{% url 'product_info' p.id %}">
|
||||||
|
<td class="small text-muted">{{ p.get_entity_type_display }}</td>
|
||||||
|
<td class="fw-bold">{{ p.drawing_number|default:"—" }}</td>
|
||||||
|
<td>{{ p.name }}</td>
|
||||||
|
<td class="small text-muted">
|
||||||
|
{% if p.planned_material_id %}
|
||||||
|
{{ p.planned_material.full_name|default:p.planned_material.name }}
|
||||||
|
{% else %}
|
||||||
|
—
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if p.passport_filled %}
|
||||||
|
<i class="bi bi-check-circle-fill text-success" title="Заполнено"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-circle text-muted" title="Не заполнено"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a class="btn btn-outline-accent btn-sm" href="{% url 'product_detail' p.id %}" onclick="event.stopPropagation();">
|
||||||
|
{% if p.entity_type == 'product' or p.entity_type == 'assembly' %}Состав{% else %}Открыть{% endif %}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="5" class="text-center text-muted py-4">Пока ничего нет</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="createProductModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<form method="post" action="{% url 'products' %}" class="modal-content border-secondary">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="modal-header border-secondary">
|
||||||
|
<h5 class="modal-title">Создать изделие</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Тип</label>
|
||||||
|
<select class="form-select" name="entity_type" required>
|
||||||
|
<option value="product">Изделие</option>
|
||||||
|
<option value="assembly">Сборочная единица</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Обозначение</label>
|
||||||
|
<input class="form-control" name="drawing_number" placeholder="Напр. УРН.01">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Наименование</label>
|
||||||
|
<input class="form-control" name="name" placeholder="Напр. Урна уличная" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer border-secondary">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||||
|
<button type="submit" class="btn btn-outline-accent">Создать</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="productInfoModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-xl">
|
||||||
|
<div class="modal-content border-secondary">
|
||||||
|
<div class="modal-header border-secondary">
|
||||||
|
<h5 class="modal-title">Информация о компоненте</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="productInfoBody">
|
||||||
|
<div class="text-muted">Загрузка...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const modalEl = document.getElementById('productInfoModal');
|
||||||
|
const bodyEl = document.getElementById('productInfoBody');
|
||||||
|
if (!modalEl || !bodyEl) return;
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(modalEl);
|
||||||
|
|
||||||
|
async function openInfo(url) {
|
||||||
|
bodyEl.innerHTML = '<div class="text-muted">Загрузка...</div>';
|
||||||
|
modal.show();
|
||||||
|
try {
|
||||||
|
const nextUrl = encodeURIComponent(window.location.pathname + window.location.search);
|
||||||
|
const sep = url.includes('?') ? '&' : '?';
|
||||||
|
const res = await fetch(url + sep + 'next=' + nextUrl, { credentials: 'same-origin' });
|
||||||
|
bodyEl.innerHTML = await res.text();
|
||||||
|
} catch (e) {
|
||||||
|
bodyEl.innerHTML = '<div class="alert alert-danger">Не удалось загрузить информацию.</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('tr.product-row[data-info-url]').forEach(tr => {
|
||||||
|
tr.addEventListener('click', () => {
|
||||||
|
const url = tr.getAttribute('data-info-url');
|
||||||
|
if (!url) return;
|
||||||
|
openInfo(url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -21,6 +21,9 @@ from .views import (
|
|||||||
SteelGradeUpsertView,
|
SteelGradeUpsertView,
|
||||||
TaskItemsView,
|
TaskItemsView,
|
||||||
ClosingView,
|
ClosingView,
|
||||||
|
ProductDetailView,
|
||||||
|
ProductInfoView,
|
||||||
|
ProductsView,
|
||||||
WriteOffsView,
|
WriteOffsView,
|
||||||
WarehouseReceiptCreateView,
|
WarehouseReceiptCreateView,
|
||||||
WarehouseStocksView,
|
WarehouseStocksView,
|
||||||
@@ -60,4 +63,8 @@ urlpatterns = [
|
|||||||
|
|
||||||
path('closing/', ClosingView.as_view(), name='closing'),
|
path('closing/', ClosingView.as_view(), name='closing'),
|
||||||
path('writeoffs/', WriteOffsView.as_view(), name='writeoffs'),
|
path('writeoffs/', WriteOffsView.as_view(), name='writeoffs'),
|
||||||
|
|
||||||
|
path('products/', ProductsView.as_view(), name='products'),
|
||||||
|
path('products/<int:pk>/', ProductDetailView.as_view(), name='product_detail'),
|
||||||
|
path('products/<int:pk>/info/', ProductInfoView.as_view(), name='product_info'),
|
||||||
]
|
]
|
||||||
@@ -24,7 +24,17 @@ from django.views.generic import FormView, ListView, TemplateView, UpdateView
|
|||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from manufacturing.models import ProductEntity
|
from manufacturing.models import (
|
||||||
|
AssemblyPassport,
|
||||||
|
BOM,
|
||||||
|
CastingPassport,
|
||||||
|
OutsourcedPassport,
|
||||||
|
PartPassport,
|
||||||
|
ProductEntity,
|
||||||
|
PurchasedPassport,
|
||||||
|
RouteStub,
|
||||||
|
WeldingSeam,
|
||||||
|
)
|
||||||
|
|
||||||
from warehouse.models import Location, Material, MaterialCategory, SteelGrade, StockItem, TransferLine, TransferRecord
|
from warehouse.models import Location, Material, MaterialCategory, SteelGrade, StockItem, TransferLine, TransferRecord
|
||||||
from warehouse.services.transfers import receive_transfer
|
from warehouse.services.transfers import receive_transfer
|
||||||
@@ -1800,6 +1810,452 @@ class ClosingView(LoginRequiredMixin, TemplateView):
|
|||||||
return redirect(f"{reverse_lazy('closing')}?machine_id={machine_id}&material_id={material_id}")
|
return redirect(f"{reverse_lazy('closing')}?machine_id={machine_id}&material_id={material_id}")
|
||||||
|
|
||||||
|
|
||||||
|
class ProductsView(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = 'shiftflow/products.html'
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
profile = getattr(request.user, 'profile', None)
|
||||||
|
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||||
|
if role not in ['admin', 'technologist', 'observer']:
|
||||||
|
return redirect('registry')
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
profile = getattr(self.request.user, 'profile', None)
|
||||||
|
role = profile.role if profile else ('admin' if self.request.user.is_superuser else 'operator')
|
||||||
|
ctx['user_role'] = role
|
||||||
|
ctx['can_edit'] = role in ['admin', 'technologist']
|
||||||
|
|
||||||
|
q = (self.request.GET.get('q') or '').strip()
|
||||||
|
entity_type = (self.request.GET.get('entity_type') or '').strip()
|
||||||
|
|
||||||
|
qs = ProductEntity.objects.select_related('planned_material', 'route').all()
|
||||||
|
if entity_type:
|
||||||
|
qs = qs.filter(entity_type=entity_type)
|
||||||
|
|
||||||
|
if q:
|
||||||
|
qs = qs.filter(
|
||||||
|
Q(drawing_number__icontains=q)
|
||||||
|
| Q(name__icontains=q)
|
||||||
|
| Q(planned_material__name__icontains=q)
|
||||||
|
| Q(planned_material__full_name__icontains=q)
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx['q'] = q
|
||||||
|
ctx['entity_type'] = entity_type
|
||||||
|
ctx['products'] = qs.order_by('drawing_number', 'name')
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
profile = getattr(request.user, 'profile', None)
|
||||||
|
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||||
|
if role not in ['admin', 'technologist']:
|
||||||
|
return redirect('products')
|
||||||
|
|
||||||
|
entity_type = (request.POST.get('entity_type') or '').strip()
|
||||||
|
name = (request.POST.get('name') or '').strip()
|
||||||
|
drawing_number = (request.POST.get('drawing_number') or '').strip()
|
||||||
|
|
||||||
|
if entity_type not in ['product', 'assembly']:
|
||||||
|
messages.error(request, 'Выбери тип: изделие или сборочная единица.')
|
||||||
|
return redirect('products')
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
messages.error(request, 'Заполни наименование.')
|
||||||
|
return redirect('products')
|
||||||
|
|
||||||
|
obj = ProductEntity.objects.create(
|
||||||
|
entity_type=entity_type,
|
||||||
|
name=name[:255],
|
||||||
|
drawing_number=drawing_number[:100],
|
||||||
|
)
|
||||||
|
|
||||||
|
return redirect('product_detail', pk=obj.id)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductDetailView(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = 'shiftflow/product_detail.html'
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
profile = getattr(request.user, 'profile', None)
|
||||||
|
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||||
|
if role not in ['admin', 'technologist', 'observer']:
|
||||||
|
return redirect('registry')
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
profile = getattr(self.request.user, 'profile', None)
|
||||||
|
role = profile.role if profile else ('admin' if self.request.user.is_superuser else 'operator')
|
||||||
|
ctx['user_role'] = role
|
||||||
|
ctx['can_edit'] = role in ['admin', 'technologist']
|
||||||
|
|
||||||
|
entity = get_object_or_404(ProductEntity.objects.select_related('planned_material', 'route'), pk=int(self.kwargs['pk']))
|
||||||
|
ctx['entity'] = entity
|
||||||
|
|
||||||
|
lines = list(
|
||||||
|
BOM.objects.select_related('child', 'child__planned_material', 'child__route')
|
||||||
|
.filter(parent=entity)
|
||||||
|
.order_by('child__entity_type', 'child__drawing_number', 'child__name', 'id')
|
||||||
|
)
|
||||||
|
ctx['lines'] = lines
|
||||||
|
|
||||||
|
q = (self.request.GET.get('q') or '').strip()
|
||||||
|
ctx['q'] = q
|
||||||
|
|
||||||
|
candidates = ProductEntity.objects.select_related('planned_material').exclude(id=entity.id)
|
||||||
|
if q:
|
||||||
|
candidates = candidates.filter(Q(drawing_number__icontains=q) | Q(name__icontains=q))
|
||||||
|
|
||||||
|
ctx['candidates'] = list(candidates.order_by('drawing_number', 'name')[:200])
|
||||||
|
parent_id = (self.request.GET.get('parent') or '').strip()
|
||||||
|
ctx['parent_id'] = parent_id if parent_id.isdigit() else ''
|
||||||
|
|
||||||
|
q_dn = (self.request.GET.get('q_dn') or '').strip()
|
||||||
|
q_name = (self.request.GET.get('q_name') or '').strip()
|
||||||
|
ctx['q_dn'] = q_dn
|
||||||
|
ctx['q_name'] = q_name
|
||||||
|
|
||||||
|
found = None
|
||||||
|
searched = False
|
||||||
|
if q_dn or q_name:
|
||||||
|
searched = True
|
||||||
|
candidates_qs = ProductEntity.objects.exclude(id=entity.id)
|
||||||
|
if q_dn:
|
||||||
|
candidates_qs = candidates_qs.filter(drawing_number__icontains=q_dn)
|
||||||
|
if q_name:
|
||||||
|
candidates_qs = candidates_qs.filter(name__icontains=q_name)
|
||||||
|
found = candidates_qs.order_by('drawing_number', 'name', 'id').first()
|
||||||
|
|
||||||
|
ctx['searched'] = searched
|
||||||
|
ctx['found'] = found
|
||||||
|
|
||||||
|
ctx['materials'] = list(Material.objects.select_related('category').order_by('full_name'))
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
profile = getattr(request.user, 'profile', None)
|
||||||
|
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||||
|
if role not in ['admin', 'technologist']:
|
||||||
|
return redirect('product_detail', pk=self.kwargs['pk'])
|
||||||
|
|
||||||
|
entity = get_object_or_404(ProductEntity, pk=int(self.kwargs['pk']))
|
||||||
|
action = (request.POST.get('action') or '').strip()
|
||||||
|
parent_id = (request.POST.get('parent') or '').strip()
|
||||||
|
next_url = reverse_lazy('product_detail', kwargs={'pk': entity.id})
|
||||||
|
if parent_id.isdigit():
|
||||||
|
next_url = f"{next_url}?parent={parent_id}"
|
||||||
|
|
||||||
|
def parse_int(value, default=None):
|
||||||
|
s = (value or '').strip()
|
||||||
|
if not s.isdigit():
|
||||||
|
return default
|
||||||
|
return int(s)
|
||||||
|
|
||||||
|
def parse_qty(value):
|
||||||
|
v = parse_int(value, default=0)
|
||||||
|
return v if v and v > 0 else None
|
||||||
|
|
||||||
|
def would_cycle(parent_id: int, child_id: int) -> bool:
|
||||||
|
stack = [child_id]
|
||||||
|
seen = set()
|
||||||
|
while stack:
|
||||||
|
cur = stack.pop()
|
||||||
|
if cur == parent_id:
|
||||||
|
return True
|
||||||
|
if cur in seen:
|
||||||
|
continue
|
||||||
|
seen.add(cur)
|
||||||
|
stack.extend(list(BOM.objects.filter(parent_id=cur).values_list('child_id', flat=True)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
if action == 'delete_line':
|
||||||
|
bom_id = parse_int(request.POST.get('bom_id'))
|
||||||
|
if not bom_id:
|
||||||
|
messages.error(request, 'Не выбрана строка BOM.')
|
||||||
|
return redirect(next_url)
|
||||||
|
BOM.objects.filter(id=bom_id, parent_id=entity.id).delete()
|
||||||
|
messages.success(request, 'Строка удалена.')
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
if action == 'update_qty':
|
||||||
|
bom_id = parse_int(request.POST.get('bom_id'))
|
||||||
|
qty = parse_qty(request.POST.get('quantity'))
|
||||||
|
if not bom_id or not qty:
|
||||||
|
messages.error(request, 'Заполни количество.')
|
||||||
|
return redirect('product_detail', pk=entity.id)
|
||||||
|
BOM.objects.filter(id=bom_id, parent_id=entity.id).update(quantity=qty)
|
||||||
|
messages.success(request, 'Количество обновлено.')
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
if action == 'add_existing':
|
||||||
|
child_id = parse_int(request.POST.get('child_id'))
|
||||||
|
qty = parse_qty(request.POST.get('quantity'))
|
||||||
|
if not child_id or not qty:
|
||||||
|
messages.error(request, 'Выбери существующую сущность и количество.')
|
||||||
|
return redirect('product_detail', pk=entity.id)
|
||||||
|
|
||||||
|
if child_id == entity.id:
|
||||||
|
messages.error(request, 'Нельзя добавить узел сам в себя.')
|
||||||
|
return redirect('product_detail', pk=entity.id)
|
||||||
|
|
||||||
|
if would_cycle(entity.id, child_id):
|
||||||
|
messages.error(request, 'Нельзя добавить: получится цикл в структуре.')
|
||||||
|
return redirect('product_detail', pk=entity.id)
|
||||||
|
|
||||||
|
obj, _ = BOM.objects.get_or_create(parent_id=entity.id, child_id=child_id, defaults={'quantity': qty})
|
||||||
|
if obj.quantity != qty:
|
||||||
|
obj.quantity = qty
|
||||||
|
obj.save(update_fields=['quantity'])
|
||||||
|
|
||||||
|
messages.success(request, 'Компонент добавлен.')
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
if action == 'create_and_add':
|
||||||
|
child_type = (request.POST.get('child_type') or '').strip()
|
||||||
|
name = (request.POST.get('name') or '').strip()
|
||||||
|
drawing_number = (request.POST.get('drawing_number') or '').strip()
|
||||||
|
qty = parse_qty(request.POST.get('quantity'))
|
||||||
|
planned_material_id = parse_int(request.POST.get('planned_material_id'))
|
||||||
|
|
||||||
|
if child_type not in ['assembly', 'part', 'purchased', 'casting', 'outsourced']:
|
||||||
|
messages.error(request, 'Выбери корректный тип компонента.')
|
||||||
|
return redirect('product_detail', pk=entity.id)
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
messages.error(request, 'Заполни наименование компонента.')
|
||||||
|
return redirect('product_detail', pk=entity.id)
|
||||||
|
|
||||||
|
if not qty:
|
||||||
|
messages.error(request, 'Заполни количество.')
|
||||||
|
return redirect('product_detail', pk=entity.id)
|
||||||
|
|
||||||
|
child = ProductEntity.objects.create(
|
||||||
|
entity_type=child_type,
|
||||||
|
name=name[:255],
|
||||||
|
drawing_number=drawing_number[:100],
|
||||||
|
planned_material_id=(planned_material_id if child_type == 'part' and planned_material_id else None),
|
||||||
|
)
|
||||||
|
|
||||||
|
BOM.objects.create(parent_id=entity.id, child_id=child.id, quantity=qty)
|
||||||
|
messages.success(request, 'Компонент создан и добавлен.')
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
messages.error(request, 'Неизвестное действие.')
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductInfoView(LoginRequiredMixin, TemplateView):
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
profile = getattr(request.user, 'profile', None)
|
||||||
|
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||||
|
if role not in ['admin', 'technologist', 'observer']:
|
||||||
|
return redirect('registry')
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_template_names(self):
|
||||||
|
entity = get_object_or_404(ProductEntity, pk=int(self.kwargs['pk']))
|
||||||
|
et = entity.entity_type
|
||||||
|
if et == 'part':
|
||||||
|
return ['shiftflow/product_info_part.html']
|
||||||
|
if et in ['product', 'assembly']:
|
||||||
|
return ['shiftflow/product_info_assembly.html']
|
||||||
|
if et == 'purchased':
|
||||||
|
return ['shiftflow/product_info_purchased.html']
|
||||||
|
if et == 'casting':
|
||||||
|
return ['shiftflow/product_info_casting.html']
|
||||||
|
if et == 'outsourced':
|
||||||
|
return ['shiftflow/product_info_outsourced.html']
|
||||||
|
return ['shiftflow/product_info_external.html']
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
profile = getattr(self.request.user, 'profile', None)
|
||||||
|
role = profile.role if profile else ('admin' if self.request.user.is_superuser else 'operator')
|
||||||
|
ctx['user_role'] = role
|
||||||
|
ctx['can_edit'] = role in ['admin', 'technologist']
|
||||||
|
|
||||||
|
entity = get_object_or_404(ProductEntity.objects.select_related('planned_material', 'route'), pk=int(self.kwargs['pk']))
|
||||||
|
ctx['entity'] = entity
|
||||||
|
ctx['materials'] = list(Material.objects.select_related('category').order_by('full_name'))
|
||||||
|
ctx['routes'] = list(RouteStub.objects.all().order_by('name'))
|
||||||
|
|
||||||
|
passport = None
|
||||||
|
seams = []
|
||||||
|
|
||||||
|
if entity.entity_type in ['product', 'assembly']:
|
||||||
|
passport, _ = AssemblyPassport.objects.get_or_create(entity_id=entity.id)
|
||||||
|
seams = list(WeldingSeam.objects.filter(passport_id=passport.id).order_by('id'))
|
||||||
|
elif entity.entity_type == 'part':
|
||||||
|
passport, _ = PartPassport.objects.get_or_create(entity_id=entity.id)
|
||||||
|
elif entity.entity_type == 'purchased':
|
||||||
|
passport, _ = PurchasedPassport.objects.get_or_create(entity_id=entity.id)
|
||||||
|
elif entity.entity_type == 'casting':
|
||||||
|
passport, _ = CastingPassport.objects.get_or_create(entity_id=entity.id)
|
||||||
|
elif entity.entity_type == 'outsourced':
|
||||||
|
passport, _ = OutsourcedPassport.objects.get_or_create(entity_id=entity.id)
|
||||||
|
|
||||||
|
ctx['passport'] = passport
|
||||||
|
ctx['welding_seams'] = seams
|
||||||
|
|
||||||
|
next_url = (self.request.GET.get('next') or '').strip()
|
||||||
|
ctx['next'] = next_url if next_url.startswith('/') else reverse_lazy('products')
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
profile = getattr(request.user, 'profile', None)
|
||||||
|
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||||
|
if role not in ['admin', 'technologist']:
|
||||||
|
return redirect('products')
|
||||||
|
|
||||||
|
entity = get_object_or_404(ProductEntity, pk=int(self.kwargs['pk']))
|
||||||
|
action = (request.POST.get('action') or '').strip()
|
||||||
|
next_url = (request.POST.get('next') or '').strip()
|
||||||
|
if not next_url.startswith('/'):
|
||||||
|
next_url = reverse_lazy('products')
|
||||||
|
|
||||||
|
def parse_int(value, default=None):
|
||||||
|
s = (value or '').strip()
|
||||||
|
if not s.isdigit():
|
||||||
|
return default
|
||||||
|
return int(s)
|
||||||
|
|
||||||
|
def parse_float(value):
|
||||||
|
s = (value or '').strip().replace(',', '.')
|
||||||
|
if not s:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(s)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if action == 'create_route':
|
||||||
|
name = (request.POST.get('route_name') or '').strip()
|
||||||
|
if not name:
|
||||||
|
messages.error(request, 'Заполни название маршрута.')
|
||||||
|
return redirect(next_url)
|
||||||
|
RouteStub.objects.get_or_create(name=name[:200])
|
||||||
|
messages.success(request, 'Маршрут добавлен.')
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
if action == 'add_weld_seam':
|
||||||
|
passport, _ = AssemblyPassport.objects.get_or_create(entity_id=entity.id)
|
||||||
|
|
||||||
|
name = (request.POST.get('seam_name') or '').strip()
|
||||||
|
leg_mm = parse_float(request.POST.get('seam_leg_mm'))
|
||||||
|
length_mm = parse_float(request.POST.get('seam_length_mm'))
|
||||||
|
qty = parse_int(request.POST.get('seam_quantity'), default=1)
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
messages.error(request, 'Заполни наименование сварного шва.')
|
||||||
|
return redirect(next_url)
|
||||||
|
if leg_mm is None or leg_mm <= 0:
|
||||||
|
messages.error(request, 'Катет должен быть больше 0.')
|
||||||
|
return redirect(next_url)
|
||||||
|
if length_mm is None or length_mm <= 0:
|
||||||
|
messages.error(request, 'Длина должна быть больше 0.')
|
||||||
|
return redirect(next_url)
|
||||||
|
if not qty or qty <= 0:
|
||||||
|
messages.error(request, 'Количество должно быть больше 0.')
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
WeldingSeam.objects.create(
|
||||||
|
passport_id=passport.id,
|
||||||
|
name=name[:255],
|
||||||
|
leg_mm=float(leg_mm),
|
||||||
|
length_mm=float(length_mm),
|
||||||
|
quantity=int(qty),
|
||||||
|
)
|
||||||
|
messages.success(request, 'Сварной шов добавлен.')
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
if action == 'delete_weld_seam':
|
||||||
|
seam_id = parse_int(request.POST.get('seam_id'))
|
||||||
|
passport = AssemblyPassport.objects.filter(entity_id=entity.id).first()
|
||||||
|
if not seam_id or not passport:
|
||||||
|
messages.error(request, 'Не выбран сварной шов.')
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
WeldingSeam.objects.filter(id=seam_id, passport_id=passport.id).delete()
|
||||||
|
messages.success(request, 'Сварной шов удалён.')
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
if action != 'save':
|
||||||
|
messages.error(request, 'Неизвестное действие.')
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
entity.drawing_number = (request.POST.get('drawing_number') or '').strip()[:100]
|
||||||
|
entity.name = (request.POST.get('name') or '').strip()[:255]
|
||||||
|
|
||||||
|
if not entity.name:
|
||||||
|
messages.error(request, 'Наименование обязательно.')
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
entity.passport_filled = bool(request.POST.get('passport_filled'))
|
||||||
|
|
||||||
|
route_id = parse_int(request.POST.get('route_id'))
|
||||||
|
entity.route_id = route_id
|
||||||
|
|
||||||
|
if entity.entity_type == 'part':
|
||||||
|
pm_id = parse_int(request.POST.get('planned_material_id'))
|
||||||
|
entity.planned_material_id = pm_id
|
||||||
|
|
||||||
|
pdf = request.FILES.get('pdf_main')
|
||||||
|
if pdf:
|
||||||
|
entity.pdf_main = pdf
|
||||||
|
|
||||||
|
dxf = request.FILES.get('dxf_file')
|
||||||
|
if dxf:
|
||||||
|
entity.dxf_file = dxf
|
||||||
|
|
||||||
|
preview = request.FILES.get('preview')
|
||||||
|
if preview:
|
||||||
|
entity.preview = preview
|
||||||
|
|
||||||
|
entity.save()
|
||||||
|
|
||||||
|
if entity.entity_type in ['product', 'assembly']:
|
||||||
|
passport, _ = AssemblyPassport.objects.get_or_create(entity_id=entity.id)
|
||||||
|
passport.weight_kg = parse_float(request.POST.get('weight_kg'))
|
||||||
|
passport.coating = (request.POST.get('coating') or '').strip()[:200]
|
||||||
|
passport.coating_color = (request.POST.get('coating_color') or '').strip()[:100]
|
||||||
|
passport.coating_area_m2 = parse_float(request.POST.get('coating_area_m2'))
|
||||||
|
passport.technical_requirements = (request.POST.get('technical_requirements') or '').strip()
|
||||||
|
passport.save()
|
||||||
|
|
||||||
|
if entity.entity_type == 'part':
|
||||||
|
passport, _ = PartPassport.objects.get_or_create(entity_id=entity.id)
|
||||||
|
passport.thickness_mm = parse_float(request.POST.get('thickness_mm'))
|
||||||
|
passport.length_mm = parse_float(request.POST.get('length_mm'))
|
||||||
|
passport.mass_kg = parse_float(request.POST.get('mass_kg'))
|
||||||
|
passport.cut_length_mm = parse_float(request.POST.get('cut_length_mm'))
|
||||||
|
passport.pierce_count = parse_int(request.POST.get('pierce_count'))
|
||||||
|
passport.engraving = (request.POST.get('engraving') or '').strip()
|
||||||
|
passport.technical_requirements = (request.POST.get('technical_requirements') or '').strip()
|
||||||
|
passport.save()
|
||||||
|
|
||||||
|
if entity.entity_type == 'purchased':
|
||||||
|
passport, _ = PurchasedPassport.objects.get_or_create(entity_id=entity.id)
|
||||||
|
passport.gost = (request.POST.get('gost') or '').strip()[:255]
|
||||||
|
passport.save()
|
||||||
|
|
||||||
|
if entity.entity_type == 'casting':
|
||||||
|
passport, _ = CastingPassport.objects.get_or_create(entity_id=entity.id)
|
||||||
|
passport.casting_material = (request.POST.get('casting_material') or '').strip()[:200]
|
||||||
|
passport.mass_kg = parse_float(request.POST.get('mass_kg'))
|
||||||
|
passport.save()
|
||||||
|
|
||||||
|
if entity.entity_type == 'outsourced':
|
||||||
|
passport, _ = OutsourcedPassport.objects.get_or_create(entity_id=entity.id)
|
||||||
|
passport.technical_requirements = (request.POST.get('technical_requirements') or '').strip()
|
||||||
|
passport.notes = (request.POST.get('notes') or '').strip()
|
||||||
|
passport.save()
|
||||||
|
|
||||||
|
messages.success(request, 'Сохранено.')
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
|
||||||
class WriteOffsView(LoginRequiredMixin, TemplateView):
|
class WriteOffsView(LoginRequiredMixin, TemplateView):
|
||||||
template_name = 'shiftflow/writeoffs.html'
|
template_name = 'shiftflow/writeoffs.html'
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,12 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user_role in 'admin,technologist,observer' %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'products' %}active{% endif %}" href="{% url 'products' %}">Изделия</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if user_role in 'admin,master,operator,observer' %}
|
{% if user_role in 'admin,master,operator,observer' %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.resolver_match.url_name == 'closing' %}active{% endif %}" href="{% url 'closing' %}">Закрытие</a>
|
<a class="nav-link {% if request.resolver_match.url_name == 'closing' %}active{% endif %}" href="{% url 'closing' %}">Закрытие</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user