Доработали апдейт пунктов заданий
All checks were successful
Deploy MES Core / deploy (push) Successful in 9s

This commit is contained in:
2026-03-30 00:18:00 +03:00
parent ff0b791a24
commit 78d4a1a04f
7 changed files with 226 additions and 39 deletions

View File

@@ -0,0 +1,19 @@
# Generated by Django 6.0.3 on 2026-03-29 21:15
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shiftflow', '0007_machine_machine_type'),
]
operations = [
migrations.AlterField(
model_name='item',
name='date',
field=models.DateField(default=django.utils.timezone.localdate, verbose_name='Дата смены'),
),
]

View File

@@ -90,7 +90,7 @@ class Item(models.Model):
task = models.ForeignKey(ProductionTask, on_delete=models.CASCADE, related_name='items', verbose_name="Задание", null=True, blank=True) task = models.ForeignKey(ProductionTask, on_delete=models.CASCADE, related_name='items', verbose_name="Задание", null=True, blank=True)
# --- Смена (заполняет мастер) --- # --- Смена (заполняет мастер) ---
date = models.DateField("Дата смены", default=timezone.now) date = models.DateField("Дата смены", default=timezone.localdate)
machine = models.ForeignKey(Machine, on_delete=models.PROTECT, verbose_name="Станок") machine = models.ForeignKey(Machine, on_delete=models.PROTECT, verbose_name="Станок")
quantity_plan = models.PositiveIntegerField("План на смену, шт") quantity_plan = models.PositiveIntegerField("План на смену, шт")

View File

@@ -12,6 +12,14 @@
<form method="post" id="mainForm" class="card-body p-4"> <form method="post" id="mainForm" class="card-body p-4">
{% csrf_token %} {% csrf_token %}
{% if errors %}
<div class="alert alert-danger mb-4">
{% for e in errors %}
<div>{{ e }}</div>
{% endfor %}
</div>
{% endif %}
<div class="row g-3 mb-4 border-bottom border-secondary pb-3 text-body"> <div class="row g-3 mb-4 border-bottom border-secondary pb-3 text-body">
<div class="col-md-4"> <div class="col-md-4">
@@ -56,15 +64,15 @@
<div class="row g-3 mt-3 text-start"> <div class="row g-3 mt-3 text-start">
<div class="col-md-4"> <div class="col-md-4">
<label class="small text-muted">Взятый материал</label> <label class="small text-muted">Взятый материал</label>
<input type="text" name="material_taken" class="form-control border-secondary" value="{{ item.material_taken }}" placeholder="Напр: 3 трубы по 12м"> <input type="text" name="material_taken" class="form-control border-secondary" value="{{ item.material_taken }}" placeholder="Напр: 3 трубы по 12м" required>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="small text-muted">Деловой отход</label> <label class="small text-muted">Остаток ДО</label>
<input type="text" name="usable_waste" class="form-control border-secondary" value="{{ item.usable_waste }}" placeholder="Напр: кусок 1500мм"> <input type="text" name="usable_waste" class="form-control border-secondary" value="{{ item.usable_waste }}" placeholder="Напр: 0.8м / 12кг" required>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="small text-muted">Лом (кг)</label> <label class="small text-muted">Лом (кг)</label>
<input type="number" step="0.01" name="scrap_weight" class="form-control border-secondary" value="{{ item.scrap_weight }}"> <input type="number" step="0.01" min="0" name="scrap_weight" class="form-control border-secondary" value="{{ item.scrap_weight }}" required>
</div> </div>
</div> </div>
</div> </div>
@@ -74,10 +82,27 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if user_role == 'admin' %} {% if user_role in 'admin,technologist' %}
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
<div class="col-md-4">
<label class="small text-muted">Дата смены</label>
<input type="date" name="date" class="form-control border-secondary" value="{{ item.date|date:'Y-m-d' }}">
</div>
<div class="col-md-4">
<label class="small text-muted">Станок</label>
<select name="machine" class="form-select border-secondary">
{% for m in machines %}
<option value="{{ m.id }}" {% if item.machine.id == m.id %}selected{% endif %}>{{ m.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="small text-muted">План на смену (шт)</label>
<input type="number" name="quantity_plan" class="form-control border-secondary" value="{{ item.quantity_plan }}">
</div>
<div class="col-md-6"> <div class="col-md-6">
<label class="small text-muted">Статус задания (Админ)</label> <label class="small text-muted">Статус задания</label>
<select name="status" class="form-select border-secondary"> <select name="status" class="form-select border-secondary">
{% for val, name in form.fields.status.choices %} {% for val, name in form.fields.status.choices %}
<option value="{{ val }}" {% if item.status == val %}selected{% endif %}>{{ name }}</option> <option value="{{ val }}" {% if item.status == val %}selected{% endif %}>{{ name }}</option>
@@ -88,19 +113,52 @@
<label class="small text-muted">Факт (шт)</label> <label class="small text-muted">Факт (шт)</label>
<input type="number" name="quantity_fact" class="form-control border-secondary" value="{{ item.quantity_fact }}"> <input type="number" name="quantity_fact" class="form-control border-secondary" value="{{ item.quantity_fact }}">
</div> </div>
<div class="col-md-4">
<label class="small text-muted">Взятый материал</label>
<input type="text" name="material_taken" class="form-control border-secondary" value="{{ item.material_taken }}" placeholder="Напр: 3 трубы по 12м">
</div>
<div class="col-md-4">
<label class="small text-muted">Деловой отход</label>
<input type="text" name="usable_waste" class="form-control border-secondary" value="{{ item.usable_waste }}" placeholder="Напр: кусок 1500мм">
</div>
<div class="col-md-4">
<label class="small text-muted">Лом (кг)</label>
<input type="number" step="0.01" name="scrap_weight" class="form-control border-secondary" value="{{ item.scrap_weight }}">
</div>
</div>
<div class="form-check form-switch p-3 rounded border border-warning mb-4 bg-body-tertiary d-flex justify-content-between align-items-center">
<label class="form-check-label fw-bold ms-2" for="sync1c">Списано в 1С</label>
<input class="form-check-input ms-0" style="width: 3em; height: 1.5em;" type="checkbox" name="is_synced_1c" id="sync1c" {% if item.is_synced_1c %}checked{% endif %}>
</div> </div>
{% endif %} {% endif %}
{% if user_role in 'admin,clerk' %} {% if user_role == 'clerk' %}
{% if item.status == 'done' or item.quantity_fact > 0 %} <div class="row g-3 mb-4">
<div class="col-md-4">
<small class="text-muted d-block">Взятый материал</small>
<strong>{{ item.material_taken|default:"-" }}</strong>
</div>
<div class="col-md-4">
<small class="text-muted d-block">Остаток ДО</small>
<strong>{{ item.usable_waste|default:"-" }}</strong>
</div>
<div class="col-md-4">
<small class="text-muted d-block">Лом (кг)</small>
<strong>{{ item.scrap_weight }}</strong>
</div>
</div>
{% if item.status == 'done' or item.status == 'partial' %}
<div class="form-check form-switch p-3 rounded border border-warning mb-4 bg-body-tertiary d-flex justify-content-between align-items-center"> <div class="form-check form-switch p-3 rounded border border-warning mb-4 bg-body-tertiary d-flex justify-content-between align-items-center">
<label class="form-check-label fw-bold ms-2" for="sync1c">Списано в 1С</label> <label class="form-check-label fw-bold ms-2" for="sync1c">Списано в 1С</label>
<input class="form-check-input ms-0" style="width: 3em; height: 1.5em;" type="checkbox" name="is_synced_1c" id="sync1c" {% if item.is_synced_1c %}checked{% endif %}> <input class="form-check-input ms-0" style="width: 3em; height: 1.5em;" type="checkbox" name="is_synced_1c" id="sync1c" {% if item.is_synced_1c %}checked{% endif %}>
</div> </div>
{% else %} {% else %}
<div class="text-muted small mb-4"><i class="bi bi-info-circle me-1"></i>Списание будет доступно после выполнения.</div> <div class="text-muted small mb-4"><i class="bi bi-info-circle me-1"></i>Списание будет доступно после закрытия (Выполнено/Частично).</div>
{% endif %} {% endif %}
{% if user_role == 'clerk' %}<input type="hidden" name="quantity_fact" value="{{ item.quantity_fact }}">{% endif %} <input type="hidden" name="quantity_fact" value="{{ item.quantity_fact }}">
{% endif %} {% endif %}
<div class="d-flex justify-content-between mt-4"> <div class="d-flex justify-content-between mt-4">

View File

@@ -22,8 +22,11 @@
<div class="small text-muted mb-1 fw-bold">Статус:</div> <div class="small text-muted mb-1 fw-bold">Статус:</div>
<div class="d-flex flex-wrap gap-1"> <div class="d-flex flex-wrap gap-1">
{% if user_role == 'operator' %} {% if user_role == 'operator' %}
<input type="hidden" name="statuses" value="work"> <input type="checkbox" class="btn-check" name="statuses" id="s_work" value="work" {% if 'work' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
<span class="badge bg-primary">В работе</span> <label class="btn btn-outline-primary btn-sm" for="s_work">В работе</label>
<input type="checkbox" class="btn-check" name="statuses" id="s_closed" value="closed" {% if 'closed' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-success btn-sm" for="s_closed">Завершено</label>
{% else %} {% else %}
<input type="checkbox" class="btn-check" name="statuses" id="s_work" value="work" {% if 'work' in selected_statuses %}checked{% endif %} onchange="this.form.submit()"> <input type="checkbox" class="btn-check" name="statuses" id="s_work" value="work" {% if 'work' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-primary btn-sm" for="s_work">В работе</label> <label class="btn btn-outline-primary btn-sm" for="s_work">В работе</label>
@@ -72,7 +75,10 @@
document.addEventListener('DOMContentLoaded', function(){ document.addEventListener('DOMContentLoaded', function(){
const s = document.querySelector('input[name="start_date"]'); const s = document.querySelector('input[name="start_date"]');
const e = document.querySelector('input[name="end_date"]'); const e = document.querySelector('input[name="end_date"]');
const today = new Date().toISOString().slice(0,10); const now = new Date();
const mm = String(now.getMonth() + 1).padStart(2, '0');
const dd = String(now.getDate()).padStart(2, '0');
const today = `${now.getFullYear()}-${mm}-${dd}`;
if (s && !s.value) s.value = today; if (s && !s.value) s.value = today;
if (e && !e.value) e.value = today; if (e && !e.value) e.value = today;
}); });

View File

@@ -63,7 +63,7 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
<span class="badge {% if item.status == 'work' %}bg-primary{% elif item.status == 'done' %}bg-success{% else %}bg-secondary{% endif %}"> <span class="badge {% if item.status == 'work' %}bg-primary{% elif item.status == 'done' %}bg-success{% elif item.status == 'partial' %}bg-success-subtle text-success-emphasis border border-success-subtle{% else %}bg-secondary{% endif %}">
{{ item.get_status_display }} {{ item.get_status_display }}
</span> </span>
</td> </td>

View File

@@ -55,8 +55,13 @@ class RegistryView(LoginRequiredMixin, ListView):
start_date = self.request.GET.get('start_date') start_date = self.request.GET.get('start_date')
end_date = self.request.GET.get('end_date') end_date = self.request.GET.get('end_date')
if not filtered: if not filtered:
today = timezone.now().date() today = timezone.localdate()
queryset = queryset.filter(date=today, status__in=['work', 'leftover']) if role == 'clerk':
queryset = queryset.filter(date=today, status__in=['done', 'partial'])
elif role in ['operator', 'master']:
queryset = queryset.filter(date=today, status__in=['work'])
else:
queryset = queryset.filter(date=today, status__in=['work', 'leftover'])
else: else:
if start_date: if start_date:
queryset = queryset.filter(date__gte=start_date) queryset = queryset.filter(date__gte=start_date)
@@ -71,7 +76,9 @@ class RegistryView(LoginRequiredMixin, ListView):
# Ограничения по ролям # Ограничения по ролям
if role == 'operator': if role == 'operator':
user_machines = profile.machines.all() if profile else Machine.objects.none() user_machines = profile.machines.all() if profile else Machine.objects.none()
queryset = queryset.filter(machine__in=user_machines, status='work') queryset = queryset.filter(machine__in=user_machines)
if not filtered:
queryset = queryset.filter(status='work')
elif role == 'master' and not filtered: elif role == 'master' and not filtered:
queryset = queryset.filter(status='work') queryset = queryset.filter(status='work')
@@ -89,10 +96,15 @@ class RegistryView(LoginRequiredMixin, ListView):
filtered = self.request.GET.get('filtered') filtered = self.request.GET.get('filtered')
if not filtered: if not filtered:
today_str = timezone.now().date().strftime('%Y-%m-%d') today_str = timezone.localdate().strftime('%Y-%m-%d')
context['start_date'] = today_str context['start_date'] = today_str
context['end_date'] = today_str context['end_date'] = today_str
context['selected_statuses'] = ['work', 'leftover'] if role == 'clerk':
context['selected_statuses'] = ['closed']
elif role in ['operator', 'master']:
context['selected_statuses'] = ['work']
else:
context['selected_statuses'] = ['work', 'leftover']
context['selected_machines'] = [m.id for m in machines] context['selected_machines'] = [m.id for m in machines]
context['all_selected_machines'] = True context['all_selected_machines'] = True
else: else:
@@ -147,7 +159,7 @@ class RegistryPrintView(LoginRequiredMixin, TemplateView):
start_date = self.request.GET.get('start_date') start_date = self.request.GET.get('start_date')
end_date = self.request.GET.get('end_date') end_date = self.request.GET.get('end_date')
if not filtered: if not filtered:
today = timezone.now().date() today = timezone.localdate()
queryset = queryset.filter(date=today, status__in=['work', 'leftover']) queryset = queryset.filter(date=today, status__in=['work', 'leftover'])
start_date = today.strftime('%Y-%m-%d') start_date = today.strftime('%Y-%m-%d')
end_date = start_date end_date = start_date
@@ -210,38 +222,110 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
# Обязательно добавляем роль в контекст этого шаблона! profile = getattr(self.request.user, 'profile', None)
if hasattr(self.request.user, 'profile'): role = profile.role if profile else ('admin' if self.request.user.is_superuser else 'operator')
context['user_role'] = self.request.user.profile.role context['user_role'] = role
context['machines'] = Machine.objects.all()
return context return context
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
profile = getattr(request.user, 'profile', None) profile = getattr(request.user, 'profile', None)
role = profile.role if profile else 'operator' role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
# Общие поля if role in ['admin', 'technologist']:
self.object.material_taken = request.POST.get('material_taken', self.object.material_taken) machine_id = request.POST.get('machine')
self.object.usable_waste = request.POST.get('usable_waste', self.object.usable_waste) if machine_id and machine_id.isdigit():
self.object.scrap_weight = request.POST.get('scrap_weight', self.object.scrap_weight or 0) self.object.machine_id = int(machine_id)
status = request.POST.get('status', self.object.status) date_value = request.POST.get('date')
if date_value:
self.object.date = date_value
quantity_plan = request.POST.get('quantity_plan')
if quantity_plan and quantity_plan.isdigit():
self.object.quantity_plan = int(quantity_plan)
quantity_fact = request.POST.get('quantity_fact')
if quantity_fact and quantity_fact.isdigit():
self.object.quantity_fact = int(quantity_fact)
status = request.POST.get('status')
allowed_statuses = {k for k, _ in self.object.STATUS_CHOICES}
if status in allowed_statuses:
self.object.status = status
self.object.is_synced_1c = bool(request.POST.get('is_synced_1c'))
self.object.material_taken = request.POST.get('material_taken', self.object.material_taken)
self.object.usable_waste = request.POST.get('usable_waste', self.object.usable_waste)
scrap_weight = request.POST.get('scrap_weight')
if scrap_weight is not None and scrap_weight != '':
try:
self.object.scrap_weight = float(scrap_weight)
except ValueError:
pass
self.object.save()
return redirect('registry')
if role in ['operator', 'master']: if role in ['operator', 'master']:
if self.object.status != 'work':
return redirect('registry')
material_taken = (request.POST.get('material_taken') or '').strip()
usable_waste = (request.POST.get('usable_waste') or '').strip()
scrap_weight_raw = (request.POST.get('scrap_weight') or '').strip()
errors = []
if not material_taken:
errors.append('Заполни поле "Взятый материал"')
if not usable_waste:
errors.append('Заполни поле "Остаток ДО"')
if scrap_weight_raw == '':
errors.append('Заполни поле "Лом (кг)" (можно 0)')
scrap_weight = None
if scrap_weight_raw != '':
try:
scrap_weight = float(scrap_weight_raw)
except ValueError:
errors.append('Поле "Лом (кг)" должно быть числом')
status = request.POST.get('status', self.object.status)
if errors:
context = self.get_context_data()
context['errors'] = errors
return self.render_to_response(context)
self.object.material_taken = material_taken
self.object.usable_waste = usable_waste
if scrap_weight is not None:
self.object.scrap_weight = scrap_weight
if status == 'done': if status == 'done':
self.object.quantity_fact = self.object.quantity_plan self.object.quantity_fact = self.object.quantity_plan
self.object.status = 'done' self.object.status = 'done'
self.object.save() self.object.save()
elif status == 'partial': return redirect('registry')
if status == 'partial':
try: try:
fact = int(request.POST.get('quantity_fact', '0')) fact = int(request.POST.get('quantity_fact', '0'))
except ValueError: except ValueError:
fact = 0 fact = 0
if fact <= 0:
context = self.get_context_data()
context['errors'] = ['При частичном закрытии укажи, сколько сделано (больше 0)']
return self.render_to_response(context)
fact = max(0, min(fact, self.object.quantity_plan)) fact = max(0, min(fact, self.object.quantity_plan))
residual = self.object.quantity_plan - fact residual = self.object.quantity_plan - fact
self.object.quantity_fact = fact self.object.quantity_fact = fact
self.object.status = 'partial' self.object.status = 'partial'
self.object.save() self.object.save()
if residual > 0: if residual > 0:
Item.objects.create( Item.objects.create(
task=self.object.task, task=self.object.task,
@@ -252,15 +336,17 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
status='leftover', status='leftover',
is_synced_1c=False, is_synced_1c=False,
) )
else:
# Просто сохранить без спец-логики return redirect('registry')
return super().post(request, *args, **kwargs)
elif role == 'clerk': return redirect('registry')
# Учетчик может отмечать списание 1С
if role == 'clerk':
if self.object.status not in ['done', 'partial']:
return redirect('registry')
self.object.is_synced_1c = bool(request.POST.get('is_synced_1c')) self.object.is_synced_1c = bool(request.POST.get('is_synced_1c'))
self.object.save() self.object.save(update_fields=['is_synced_1c'])
else: return redirect('registry')
return super().post(request, *args, **kwargs)
return redirect('registry') return redirect('registry')

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.3 on 2026-03-29 21:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('warehouse', '0002_materialcategory_gost_standard'),
]
operations = [
migrations.AlterField(
model_name='material',
name='full_name',
field=models.CharField(blank=True, help_text='Генерируется автоматически', max_length=500, verbose_name='Полное наименование'),
),
]