Доработали генерацию сменных заданий
All checks were successful
Deploy MES Core / deploy (push) Successful in 10s
All checks were successful
Deploy MES Core / deploy (push) Successful in 10s
This commit is contained in:
@@ -53,8 +53,11 @@ class ItemAdmin(admin.ModelAdmin):
|
|||||||
return obj.task.drawing_name if obj.task else "-"
|
return obj.task.drawing_name if obj.task else "-"
|
||||||
get_drawing.short_description = 'Деталь'
|
get_drawing.short_description = 'Деталь'
|
||||||
|
|
||||||
# Регистрация станков просто списком
|
@admin.register(Machine)
|
||||||
admin.site.register(Machine)
|
class MachineAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'machine_type')
|
||||||
|
list_filter = ('machine_type',)
|
||||||
|
search_fields = ('name',)
|
||||||
|
|
||||||
@admin.register(EmployeeProfile)
|
@admin.register(EmployeeProfile)
|
||||||
class EmployeeProfileAdmin(admin.ModelAdmin):
|
class EmployeeProfileAdmin(admin.ModelAdmin):
|
||||||
|
|||||||
18
shiftflow/migrations/0007_machine_machine_type.py
Normal file
18
shiftflow/migrations/0007_machine_machine_type.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-29 19:24
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('shiftflow', '0006_alter_item_options_alter_item_status'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='machine',
|
||||||
|
name='machine_type',
|
||||||
|
field=models.CharField(choices=[('linear', 'Линейный'), ('sheet', 'Листовой')], default='linear', max_length=10, verbose_name='Тип станка'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -20,9 +20,18 @@ class Machine(models.Model):
|
|||||||
Список производственных участков (станков).
|
Список производственных участков (станков).
|
||||||
Используется для фильтрации сменных заданий для конкретных операторов.
|
Используется для фильтрации сменных заданий для конкретных операторов.
|
||||||
"""
|
"""
|
||||||
name = models.CharField("Название станка", max_length=100)
|
|
||||||
|
|
||||||
def __str__(self): return self.name
|
MACHINE_TYPE_CHOICES = [
|
||||||
|
('linear', 'Линейный'),
|
||||||
|
('sheet', 'Листовой'),
|
||||||
|
]
|
||||||
|
|
||||||
|
name = models.CharField("Название станка", max_length=100)
|
||||||
|
machine_type = models.CharField("Тип станка", max_length=10, choices=MACHINE_TYPE_CHOICES, default='linear')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Станок"; verbose_name_plural = "Станки"
|
verbose_name = "Станок"; verbose_name_plural = "Станки"
|
||||||
|
|
||||||
|
|||||||
@@ -43,9 +43,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if user_role in 'admin,technologist,clerk' %}
|
{% if user_role in 'admin,technologist,clerk' %}
|
||||||
<div class="col-md-2">
|
<div class="col-md-auto">
|
||||||
<label class="small text-muted mb-1 fw-bold">Учёт 1С:</label>
|
<label class="small text-muted mb-1 fw-bold">Учёт 1С:</label>
|
||||||
<select name="is_synced" class="form-select form-select-sm bg-body text-body border-secondary" onchange="this.form.submit()">
|
<select name="is_synced" class="form-select form-select-sm bg-body text-body border-secondary registry-filter-1c" onchange="this.form.submit()">
|
||||||
<option value="" {% if not is_synced %}selected{% endif %}>Все</option>
|
<option value="" {% if not is_synced %}selected{% endif %}>Все</option>
|
||||||
<option value="1" {% if is_synced == '1' %}selected{% endif %}>Учтено</option>
|
<option value="1" {% if is_synced == '1' %}selected{% endif %}>Учтено</option>
|
||||||
<option value="0" {% if is_synced == '0' %}selected{% endif %}>Ожидает</option>
|
<option value="0" {% if is_synced == '0' %}selected{% endif %}>Ожидает</option>
|
||||||
@@ -53,13 +53,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="col-md-2">
|
<div class="col-md-auto">
|
||||||
<label class="small text-muted mb-1 fw-bold">С:</label>
|
<label class="small text-muted mb-1 fw-bold">С:</label>
|
||||||
<input type="date" name="start_date" class="form-control form-control-sm bg-body text-body border-secondary" value="{{ start_date }}" onchange="this.form.submit()">
|
<input type="date" name="start_date" class="form-control form-control-sm bg-body text-body border-secondary registry-filter-date" value="{{ start_date }}" onchange="this.form.submit()">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-auto">
|
||||||
<label class="small text-muted mb-1 fw-bold">По:</label>
|
<label class="small text-muted mb-1 fw-bold">По:</label>
|
||||||
<input type="date" name="end_date" class="form-control form-control-sm bg-body text-body border-secondary" value="{{ end_date }}" onchange="this.form.submit()">
|
<input type="date" name="end_date" class="form-control form-control-sm bg-body text-body border-secondary registry-filter-date" value="{{ end_date }}" onchange="this.form.submit()">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-1 text-end mt-auto">
|
<div class="col-md-1 text-end mt-auto">
|
||||||
|
|||||||
@@ -4,8 +4,13 @@
|
|||||||
{% include 'shiftflow/partials/_filter.html' %}
|
{% include 'shiftflow/partials/_filter.html' %}
|
||||||
|
|
||||||
<div class="card shadow border-secondary">
|
<div class="card shadow border-secondary">
|
||||||
<div class="card-header border-secondary py-3">
|
<div class="card-header border-secondary py-3 d-flex justify-content-between align-items-center">
|
||||||
<h3 class="text-accent mb-0"><i class="bi bi-list-task me-2"></i>Реестр заданий</h3>
|
<h3 class="text-accent mb-0"><i class="bi bi-list-task me-2"></i>Реестр заданий</h3>
|
||||||
|
{% if user_role in 'admin,technologist,master' %}
|
||||||
|
<a class="btn btn-outline-secondary btn-sm" target="_blank" href="{% url 'registry_print' %}?{{ request.GET.urlencode }}">
|
||||||
|
<i class="bi bi-printer me-1"></i>Печать
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
|||||||
148
shiftflow/templates/shiftflow/registry_print.html
Normal file
148
shiftflow/templates/shiftflow/registry_print.html
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Сменное задание</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/style.css' %}">
|
||||||
|
<style>
|
||||||
|
body { background: #fff; color: #000; }
|
||||||
|
.print-table { width: 100%; border-collapse: collapse; }
|
||||||
|
.print-table th, .print-table td { border: 1px solid #000; padding: 4px 6px; font-size: 12px; vertical-align: top; }
|
||||||
|
.print-header { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 8px; }
|
||||||
|
.print-title { font-size: 16px; font-weight: 700; margin: 0; }
|
||||||
|
.print-meta { font-size: 12px; }
|
||||||
|
.center { text-align: center; }
|
||||||
|
.blank { height: 18px; }
|
||||||
|
.sign-row { display: flex; justify-content: space-between; gap: 24px; margin-top: 18px; font-size: 12px; }
|
||||||
|
.sign-line { flex: 1; border-bottom: 1px solid #000; height: 16px; }
|
||||||
|
@media print {
|
||||||
|
.no-print { display: none !important; }
|
||||||
|
.page { page-break-after: always; }
|
||||||
|
.page:last-child { page-break-after: auto; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container-fluid my-3 no-print d-flex gap-2">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" onclick="window.print()">
|
||||||
|
Печать
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-sm btn-outline-secondary" href="{% url 'registry' %}?{{ request.GET.urlencode }}">
|
||||||
|
Назад
|
||||||
|
</a>
|
||||||
|
<div class="ms-auto small text-muted">
|
||||||
|
{{ printed_at|date:"d.m.Y H:i" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for machine, items in groups %}
|
||||||
|
<div class="container-fluid page my-3">
|
||||||
|
<div class="print-header">
|
||||||
|
<div>
|
||||||
|
<h1 class="print-title">Сменное задание</h1>
|
||||||
|
<div class="print-meta">Станок: <strong>{{ machine.name }}</strong></div>
|
||||||
|
</div>
|
||||||
|
<div class="print-meta text-end">
|
||||||
|
{% if print_date %}Дата: <strong>{{ print_date|date:"d.m.y" }}</strong>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if machine.machine_type == 'sheet' %}
|
||||||
|
<table class="print-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2">Материал</th>
|
||||||
|
<th colspan="5">Изделие</th>
|
||||||
|
<th colspan="2">Остаток</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Вид</th>
|
||||||
|
<th style="width: 170px;">Размеры</th>
|
||||||
|
|
||||||
|
<th style="width: 90px;">План, шт</th>
|
||||||
|
<th style="width: 90px;">Факт, шт</th>
|
||||||
|
<th style="width: 90px;">Толщина, мм</th>
|
||||||
|
<th>Наименование</th>
|
||||||
|
<th style="width: 90px;">Сделка</th>
|
||||||
|
|
||||||
|
<th style="width: 90px;">ДО</th>
|
||||||
|
<th style="width: 90px;">Отход, кг</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in items %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ item.task.material.full_name|default:item.task.material.name|default:"-" }}</td>
|
||||||
|
<td class="blank"></td>
|
||||||
|
|
||||||
|
<td class="center">{{ item.quantity_plan }}</td>
|
||||||
|
<td class="blank"></td>
|
||||||
|
<td class="center">{{ item.task.size_value|default:"-" }}</td>
|
||||||
|
<td>{{ item.task.drawing_name|default:"Б/ч" }}</td>
|
||||||
|
<td class="center">{{ item.task.deal.number|default:"-" }}</td>
|
||||||
|
|
||||||
|
<td class="blank"></td>
|
||||||
|
<td class="blank"></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<table class="print-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="3">Материал</th>
|
||||||
|
<th colspan="5">Изделие</th>
|
||||||
|
<th colspan="2">Остаток</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Вид</th>
|
||||||
|
<th style="width: 90px;">Длина, мм</th>
|
||||||
|
<th style="width: 90px;">Кол-во, шт</th>
|
||||||
|
|
||||||
|
<th style="width: 90px;">Длина, мм</th>
|
||||||
|
<th style="width: 90px;">План, шт</th>
|
||||||
|
<th style="width: 90px;">Факт, шт</th>
|
||||||
|
<th>Чертеж (если есть)</th>
|
||||||
|
<th style="width: 90px;">Сделка</th>
|
||||||
|
|
||||||
|
<th style="width: 90px;">ДО, мм</th>
|
||||||
|
<th style="width: 90px;">Отход, кг</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in items %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ item.task.material.full_name|default:item.task.material.name|default:"-" }}</td>
|
||||||
|
<td class="blank"></td>
|
||||||
|
<td class="blank"></td>
|
||||||
|
|
||||||
|
<td class="center">{{ item.task.size_value|default:"-" }}</td>
|
||||||
|
<td class="center">{{ item.quantity_plan }}</td>
|
||||||
|
<td class="blank"></td>
|
||||||
|
<td>{{ item.task.drawing_name|default:"Б/ч" }}</td>
|
||||||
|
<td class="center">{{ item.task.deal.number|default:"-" }}</td>
|
||||||
|
|
||||||
|
<td class="blank"></td>
|
||||||
|
<td class="blank"></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="sign-row">
|
||||||
|
<div style="display:flex; gap:8px; align-items:flex-end;">
|
||||||
|
<div>Оператор</div><div class="sign-line"></div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; gap:8px; align-items:flex-end;">
|
||||||
|
<div>Выдал</div><div class="sign-line"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from .views import IndexView, ItemUpdateView, RegistryView
|
from .views import (
|
||||||
|
IndexView,
|
||||||
|
ItemUpdateView,
|
||||||
|
RegistryView,
|
||||||
|
RegistryPrintView,
|
||||||
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Главная страница (путь пустой)
|
# Главная страница (путь пустой)
|
||||||
@@ -7,5 +12,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Реестр
|
# Реестр
|
||||||
path('registry/', RegistryView.as_view(), name='registry'),
|
path('registry/', RegistryView.as_view(), name='registry'),
|
||||||
|
# Печать сменного листа
|
||||||
|
path('registry/print/', RegistryPrintView.as_view(), name='registry_print'),
|
||||||
path('item/<int:pk>/', ItemUpdateView.as_view(), name='item_detail'),
|
path('item/<int:pk>/', ItemUpdateView.as_view(), name='item_detail'),
|
||||||
]
|
]
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.views.generic import TemplateView, ListView, UpdateView
|
from django.views.generic import TemplateView, ListView, UpdateView
|
||||||
@@ -103,6 +105,97 @@ class RegistryView(LoginRequiredMixin, ListView):
|
|||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class RegistryPrintView(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = 'shiftflow/registry_print.html'
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
profile = getattr(request.user, 'profile', None)
|
||||||
|
role = profile.role if profile else 'operator'
|
||||||
|
if role not in ['admin', 'technologist', 'master']:
|
||||||
|
return redirect('registry')
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
profile = getattr(self.request.user, 'profile', None)
|
||||||
|
role = profile.role if profile else 'operator'
|
||||||
|
context['user_role'] = role
|
||||||
|
|
||||||
|
queryset = Item.objects.select_related('task', 'task__deal', 'task__material', 'task__material__category', 'machine')
|
||||||
|
filtered = self.request.GET.get('filtered')
|
||||||
|
|
||||||
|
m_ids = self.request.GET.getlist('m_ids')
|
||||||
|
if filtered and not m_ids:
|
||||||
|
queryset = queryset.none()
|
||||||
|
if m_ids:
|
||||||
|
queryset = queryset.filter(machine_id__in=m_ids)
|
||||||
|
|
||||||
|
statuses = self.request.GET.getlist('statuses')
|
||||||
|
if filtered and not statuses:
|
||||||
|
queryset = queryset.none()
|
||||||
|
if statuses:
|
||||||
|
expanded = []
|
||||||
|
for s in statuses:
|
||||||
|
if s == 'closed':
|
||||||
|
expanded += ['done', 'partial']
|
||||||
|
else:
|
||||||
|
expanded.append(s)
|
||||||
|
queryset = queryset.filter(status__in=expanded)
|
||||||
|
|
||||||
|
start_date = self.request.GET.get('start_date')
|
||||||
|
end_date = self.request.GET.get('end_date')
|
||||||
|
if not filtered:
|
||||||
|
today = timezone.now().date()
|
||||||
|
queryset = queryset.filter(date=today, status__in=['work', 'leftover'])
|
||||||
|
start_date = today.strftime('%Y-%m-%d')
|
||||||
|
end_date = start_date
|
||||||
|
else:
|
||||||
|
if start_date:
|
||||||
|
queryset = queryset.filter(date__gte=start_date)
|
||||||
|
if end_date:
|
||||||
|
queryset = queryset.filter(date__lte=end_date)
|
||||||
|
|
||||||
|
is_synced = self.request.GET.get('is_synced')
|
||||||
|
if is_synced in ['0', '1']:
|
||||||
|
queryset = queryset.filter(is_synced_1c=bool(int(is_synced)))
|
||||||
|
|
||||||
|
if role == 'master' and not filtered:
|
||||||
|
queryset = queryset.filter(status='work')
|
||||||
|
|
||||||
|
items = list(queryset.order_by('machine__name', 'date', 'task__deal__number', 'id'))
|
||||||
|
groups = {}
|
||||||
|
for item in items:
|
||||||
|
groups.setdefault(item.machine, []).append(item)
|
||||||
|
|
||||||
|
context['groups'] = list(groups.items())
|
||||||
|
context['printed_at'] = timezone.now()
|
||||||
|
context['end_date'] = end_date or ''
|
||||||
|
|
||||||
|
print_date_raw = end_date or start_date
|
||||||
|
print_date = None
|
||||||
|
if isinstance(print_date_raw, str) and print_date_raw:
|
||||||
|
try:
|
||||||
|
print_date = datetime.strptime(print_date_raw, '%Y-%m-%d').date()
|
||||||
|
except ValueError:
|
||||||
|
print_date = None
|
||||||
|
context['print_date'] = print_date
|
||||||
|
|
||||||
|
if start_date and end_date and start_date == end_date:
|
||||||
|
context['date_label'] = start_date
|
||||||
|
elif start_date and end_date:
|
||||||
|
context['date_label'] = f"{start_date} — {end_date}"
|
||||||
|
elif start_date:
|
||||||
|
context['date_label'] = f"c {start_date}"
|
||||||
|
elif end_date:
|
||||||
|
context['date_label'] = f"по {end_date}"
|
||||||
|
else:
|
||||||
|
context['date_label'] = ''
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
# Вьюха детального вида и редактирования
|
# Вьюха детального вида и редактирования
|
||||||
class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
model = Item
|
model = Item
|
||||||
|
|||||||
@@ -117,6 +117,14 @@ body {
|
|||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.registry-filter-date {
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-filter-1c {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Специальный класс для центрирования окна логина (вернем его только там) */
|
/* Специальный класс для центрирования окна логина (вернем его только там) */
|
||||||
.flex-center-center {
|
.flex-center-center {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -30,17 +30,19 @@ class Material(models.Model):
|
|||||||
category = models.ForeignKey(MaterialCategory, on_delete=models.PROTECT, verbose_name="Категория")
|
category = models.ForeignKey(MaterialCategory, on_delete=models.PROTECT, verbose_name="Категория")
|
||||||
steel_grade = models.ForeignKey(SteelGrade, on_delete=models.PROTECT, verbose_name="Марка стали", null=True, blank=True)
|
steel_grade = models.ForeignKey(SteelGrade, on_delete=models.PROTECT, verbose_name="Марка стали", null=True, blank=True)
|
||||||
name = models.CharField("Наименование (размер/характеристики)", max_length=255)
|
name = models.CharField("Наименование (размер/характеристики)", max_length=255)
|
||||||
full_name = models.CharField("Полное наименование", max_length=500, blank=True, help_text="Генерируется автоматически, если пусто")
|
full_name = models.CharField("Полное наименование", max_length=500, blank=True, help_text="Генерируется автоматически")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Материал (номенклатура)"
|
verbose_name = "Материал (номенклатура)"
|
||||||
verbose_name_plural = "Материалы"
|
verbose_name_plural = "Материалы"
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.full_name:
|
category_part = (self.category.name or '').strip() if self.category_id else ''
|
||||||
grade_str = f" {self.steel_grade.name}" if self.steel_grade else ""
|
name_part = (self.name or '').strip()
|
||||||
self.full_name = f"{self.category.name} {self.name}{grade_str}"
|
grade_part = (self.steel_grade.name or '').strip() if self.steel_grade_id else ''
|
||||||
|
|
||||||
|
self.full_name = ' '.join([p for p in [category_part, name_part, grade_part] if p])
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.full_name or f"{self.category.name} {self.name}"
|
return self.full_name or ' '.join([p for p in [(self.category.name if self.category_id else ''), self.name, (self.steel_grade.name if self.steel_grade_id else '')] if p]).strip()
|
||||||
|
|||||||
Reference in New Issue
Block a user