Открыл мастеру возможность просмотра сделок и потребность в деталях, подправил окно редактирования позиции сделки, подправил работу фильтра
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:
@@ -25,9 +25,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
|
{% if user_role in 'admin,technologist' %}
|
||||||
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#dealModal">
|
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#dealModal">
|
||||||
<i class="bi bi-plus-lg me-1"></i>Создать сделку
|
<i class="bi bi-plus-lg me-1"></i>Создать сделку
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
<a class="btn btn-outline-secondary btn-sm" href="{% url 'customers' %}">
|
<a class="btn btn-outline-secondary btn-sm" href="{% url 'customers' %}">
|
||||||
<i class="bi bi-arrow-left me-1"></i>К заказчикам
|
<i class="bi bi-arrow-left me-1"></i>К заказчикам
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
<div class="card shadow border-secondary">
|
<div class="card shadow border-secondary">
|
||||||
<div class="card-header border-secondary py-3 d-flex justify-content-between align-items-center">
|
<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-building me-2"></i>Заказчики</h3>
|
<h3 class="text-accent mb-0"><i class="bi bi-building me-2"></i>Заказчики</h3>
|
||||||
|
{% if user_role in 'admin,technologist' %}
|
||||||
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#companyModal">
|
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#companyModal">
|
||||||
<i class="bi bi-plus-lg me-1"></i>Добавить заказчика
|
<i class="bi bi-plus-lg me-1"></i>Добавить заказчика
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
|||||||
@@ -7,12 +7,27 @@
|
|||||||
<div class="card shadow-sm border-secondary mb-4">
|
<div class="card shadow-sm border-secondary mb-4">
|
||||||
|
|
||||||
<div class="card-header border-secondary d-flex justify-content-between align-items-center py-3">
|
<div class="card-header border-secondary d-flex justify-content-between align-items-center py-3">
|
||||||
<h3 class="text-accent mb-0"><i class="bi bi-info-circle me-2"></i>{{ item.task.drawing_name|default:"Без названия" }}</h3>
|
<h3 class="text-accent mb-0">
|
||||||
<span class="badge bg-secondary">Сделка № {{ item.task.deal.number }}</span>
|
{% if user_role == 'operator' %}
|
||||||
|
<i class="bi bi-info-circle me-2"></i>{{ item.task.drawing_name|default:"Без названия" }}
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'task_items' item.task.id %}" class="text-decoration-none text-reset">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>{{ item.task.drawing_name|default:"Без названия" }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</h3>
|
||||||
|
{% if user_role == 'operator' %}
|
||||||
|
<span class="badge bg-secondary">Сделка № {{ item.task.deal.number }}</span>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'planning_deal' item.task.deal.id %}" class="text-decoration-none">
|
||||||
|
<span class="badge bg-secondary">Сделка № {{ item.task.deal.number }}</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" id="mainForm" class="card-body p-4">
|
<form method="post" id="mainForm" class="card-body p-4">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="next" value="{{ back_url }}">
|
||||||
|
|
||||||
{% if errors %}
|
{% if errors %}
|
||||||
<div class="alert alert-danger mb-4">
|
<div class="alert alert-danger mb-4">
|
||||||
@@ -55,22 +70,12 @@
|
|||||||
{% if user_role in 'operator,master' %}
|
{% if user_role in 'operator,master' %}
|
||||||
{% if item.status == 'work' %}
|
{% if item.status == 'work' %}
|
||||||
<div class="bg-body-tertiary p-3 rounded border mb-4 text-center">
|
<div class="bg-body-tertiary p-3 rounded border mb-4 text-center">
|
||||||
<h5 class="mb-3">Закрыть задание:</h5>
|
<div class="row g-3 text-start">
|
||||||
<div class="btn-group btn-group-lg w-100">
|
<div class="col-md-4">
|
||||||
<button type="button" class="btn btn-success" onclick="closeTask('done')">
|
<label class="small text-muted">Факт (шт)</label>
|
||||||
<i class="bi bi-check-all"></i> Выполнено
|
<input type="number" name="quantity_fact" id="id_quantity_fact" class="form-control border-secondary" value="{{ item.quantity_fact }}" max="{{ item.quantity_plan }}">
|
||||||
</button>
|
</div>
|
||||||
<button type="button" class="btn btn-outline-warning" onclick="showPartial()">
|
|
||||||
Частично
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="partialInput" class="mt-3 d-none">
|
|
||||||
<label class="small text-muted">Сколько сделано?</label>
|
|
||||||
<input type="number" name="quantity_fact" id="id_quantity_fact" class="form-control form-control-lg text-center mx-auto" style="max-width: 200px;" value="{{ item.quantity_fact }}" max="{{ item.quantity_plan }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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м" required>
|
<input type="text" name="material_taken" class="form-control border-secondary" value="{{ item.material_taken }}" placeholder="Напр: 3 трубы по 12м" required>
|
||||||
@@ -187,38 +192,25 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="d-flex justify-content-between mt-4">
|
<div class="d-flex justify-content-between mt-4">
|
||||||
<a href="{% url 'registry' %}" class="btn btn-outline-secondary">Назад</a>
|
<a href="{{ back_url }}" class="btn btn-outline-secondary">Назад</a>
|
||||||
<button type="submit" class="btn btn-outline-accent px-5 fw-bold">
|
<div class="d-flex gap-2">
|
||||||
<i class="bi bi-save me-2"></i>
|
<input type="hidden" name="action" id="actionField" value="save">
|
||||||
{% if user_role in 'operator,master' %}Закрыть задание{% else %}Сохранить{% endif %}
|
{% if item.status == 'work' %}
|
||||||
</button>
|
<button type="submit" class="btn btn-success px-4" onclick="document.getElementById('actionField').value='close_done'">
|
||||||
|
<i class="bi bi-check-all me-2"></i>Выполнено
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-outline-warning px-4" onclick="document.getElementById('actionField').value='close_partial'">
|
||||||
|
Частично
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<button type="submit" class="btn btn-outline-accent px-4 fw-bold" onclick="document.getElementById('actionField').value='save'">
|
||||||
|
<i class="bi bi-save me-2"></i>Сохранить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
function closeTask(status) {
|
|
||||||
document.getElementById('id_status').value = status;
|
|
||||||
// Если "Выполнено", автоматом ставим факт = плану
|
|
||||||
if(status === 'done') {
|
|
||||||
const factInput = document.getElementById('id_quantity_fact');
|
|
||||||
if(factInput) factInput.value = "{{ item.quantity_plan }}";
|
|
||||||
else {
|
|
||||||
let hiddenFact = document.createElement('input');
|
|
||||||
hiddenFact.type = 'hidden';
|
|
||||||
hiddenFact.name = 'quantity_fact';
|
|
||||||
hiddenFact.value = "{{ item.quantity_plan }}";
|
|
||||||
document.getElementById('mainForm').appendChild(hiddenFact);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.getElementById('mainForm').submit();
|
|
||||||
}
|
|
||||||
|
|
||||||
function showPartial() {
|
|
||||||
document.getElementById('partialInput').classList.remove('d-none');
|
|
||||||
document.getElementById('id_status').value = 'partial';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-1 text-end mt-auto">
|
<div class="col-md-1 text-end mt-auto">
|
||||||
<a href="{% url 'registry' %}" class="btn btn-outline-secondary btn-sm w-100" title="Сброс">
|
<a href="{% url 'registry' %}?reset=1" class="btn btn-outline-secondary btn-sm w-100" id="registryResetBtn" title="Сброс">
|
||||||
<i class="bi bi-arrow-counterclockwise me-1"></i>Сброс
|
<i class="bi bi-arrow-counterclockwise me-1"></i>Сброс
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,6 +104,10 @@
|
|||||||
function restoreFilters(){
|
function restoreFilters(){
|
||||||
if (!form) return false;
|
if (!form) return false;
|
||||||
const qs = new URLSearchParams(window.location.search);
|
const qs = new URLSearchParams(window.location.search);
|
||||||
|
if (qs.get('reset') === '1') {
|
||||||
|
try { localStorage.removeItem('registry_filters'); } catch(_) {}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (qs.get('filtered') === '1') return false;
|
if (qs.get('filtered') === '1') return false;
|
||||||
let raw = null; try { raw = localStorage.getItem('registry_filters'); } catch(_){}
|
let raw = null; try { raw = localStorage.getItem('registry_filters'); } catch(_){}
|
||||||
if (!raw) return false;
|
if (!raw) return false;
|
||||||
@@ -128,6 +132,14 @@
|
|||||||
|
|
||||||
if (form){
|
if (form){
|
||||||
form.addEventListener('change', saveFilters, true);
|
form.addEventListener('change', saveFilters, true);
|
||||||
|
|
||||||
|
const resetBtn = document.getElementById('registryResetBtn');
|
||||||
|
if (resetBtn) {
|
||||||
|
resetBtn.addEventListener('click', function () {
|
||||||
|
try { localStorage.removeItem('registry_filters'); } catch(_) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
restoreFilters();
|
restoreFilters();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
94
shiftflow/templates/shiftflow/partials/_items_table.html
Normal file
94
shiftflow/templates/shiftflow/partials/_items_table.html
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<div class="card-body p-0">
|
||||||
|
<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 style="width: 160px;">Прогресс</th>
|
||||||
|
<th>План / Факт</th>
|
||||||
|
<th>Материал</th>
|
||||||
|
<th class="text-center">Файлы</th>
|
||||||
|
<th class="text-center">1С</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in items %}
|
||||||
|
<tr class="clickable-row" data-href="{% url 'item_detail' item.pk %}">
|
||||||
|
<td class="small">{{ item.date|date:"d.m.y" }}</td>
|
||||||
|
<td><span class="text-accent fw-bold">{{ item.task.deal.number|default:"-" }}</span></td>
|
||||||
|
<td><span class="badge bg-dark border border-secondary">{{ item.machine.name }}</span></td>
|
||||||
|
<td class="fw-bold">{{ item.task.drawing_name|default:"Б/ч" }}</td>
|
||||||
|
<td class="small">{{ item.task.size_value|default:"-" }}</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div class="progress bg-secondary-subtle border border-secondary sf-item-progress"
|
||||||
|
style="height: 10px;"
|
||||||
|
data-fact-width="{{ item.fact_width|default:0 }}"
|
||||||
|
title="Факт: {{ item.fact_pct|default:0 }}%">
|
||||||
|
<div class="progress-bar {{ item.fact_bar_class|default:'bg-warning' }} sf-item-progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<span class="text-info fw-bold">{{ item.quantity_plan }}</span> /
|
||||||
|
<span class="text-success">{{ item.quantity_fact }}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="small text-muted">{{ item.task.material.full_name|default:item.task.material.name|default:"-" }}</td>
|
||||||
|
|
||||||
|
<td class="text-center">
|
||||||
|
{% if item.task.drawing_file %}
|
||||||
|
<a href="{{ item.task.drawing_file.url }}" target="_blank" class="btn btn-sm btn-outline-info p-1 stop-prop" title="DXF/STEP">
|
||||||
|
<i class="bi bi-file-earmark-code"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if item.task.extra_drawing %}
|
||||||
|
<a href="{{ item.task.extra_drawing.url }}" target="_blank" class="btn btn-sm btn-outline-danger p-1 stop-prop" title="Чертеж PDF">
|
||||||
|
<i class="bi bi-file-pdf"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="text-center">
|
||||||
|
{% if item.is_synced_1c %}
|
||||||
|
<i class="bi bi-check-circle-fill text-success" title="Учтено"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-clock-history text-muted" title="Ожидает"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<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 }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="11" class="text-center p-5 text-muted">Заданий не найдено</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
document.querySelectorAll(".clickable-row").forEach(row => {
|
||||||
|
row.addEventListener("click", function(e) {
|
||||||
|
if (e.target.closest('.stop-prop')) return;
|
||||||
|
window.location.href = this.dataset.href;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.sf-item-progress').forEach(function (el) {
|
||||||
|
const w = parseInt(el.getAttribute('data-fact-width') || '0', 10) || 0;
|
||||||
|
const bar = el.querySelector('.sf-item-progress-bar');
|
||||||
|
if (bar) bar.style.width = `${w}%`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -19,9 +19,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
{% if user_role in 'admin,technologist' %}
|
||||||
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#dealModal" data-mode="create">
|
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#dealModal" data-mode="create">
|
||||||
<i class="bi bi-plus-lg me-1"></i>Добавить сделку
|
<i class="bi bi-plus-lg me-1"></i>Добавить сделку
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
|||||||
@@ -20,9 +20,11 @@
|
|||||||
<a class="btn btn-outline-secondary btn-sm" href="{% url 'planning' %}">
|
<a class="btn btn-outline-secondary btn-sm" href="{% url 'planning' %}">
|
||||||
<i class="bi bi-arrow-left me-1"></i>Назад
|
<i class="bi bi-arrow-left me-1"></i>Назад
|
||||||
</a>
|
</a>
|
||||||
|
{% if user_role in 'admin,technologist' %}
|
||||||
<a class="btn btn-outline-accent btn-sm" href="{% url 'task_add' %}?deal={{ deal.id }}&next={% url 'planning_deal' deal.id %}">
|
<a class="btn btn-outline-accent btn-sm" href="{% url 'task_add' %}?deal={{ deal.id }}&next={% url 'planning_deal' deal.id %}">
|
||||||
<i class="bi bi-plus-lg me-1"></i>Добавить деталь
|
<i class="bi bi-plus-lg me-1"></i>Добавить деталь
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -72,6 +74,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
|
{% if user_role in 'admin,technologist' %}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-accent btn-sm"
|
class="btn btn-outline-accent btn-sm"
|
||||||
@@ -83,6 +86,7 @@
|
|||||||
>
|
>
|
||||||
<i class="bi bi-plus-lg me-1"></i>В план
|
<i class="bi bi-plus-lg me-1"></i>В план
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
|
|||||||
@@ -13,81 +13,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body p-0">
|
{% include 'shiftflow/partials/_items_table.html' with items=items %}
|
||||||
<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>План / Факт</th>
|
|
||||||
<th>Материал</th>
|
|
||||||
<th class="text-center">Файлы</th>
|
|
||||||
<th class="text-center">1С</th>
|
|
||||||
<th>Статус</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for item in items %}
|
|
||||||
<tr class="clickable-row" data-href="{% url 'item_detail' item.pk %}">
|
|
||||||
<td class="small">{{ item.date|date:"d.m.y" }}</td>
|
|
||||||
<td><span class="text-accent fw-bold">{{ item.task.deal.number|default:"-" }}</span></td>
|
|
||||||
<td><span class="badge bg-dark border border-secondary">{{ item.machine.name }}</span></td>
|
|
||||||
<td class="fw-bold">{{ item.task.drawing_name|default:"Б/ч" }}</td>
|
|
||||||
<td class="small">{{ item.task.size_value|default:"-" }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="text-info fw-bold">{{ item.quantity_plan }}</span> /
|
|
||||||
<span class="text-success">{{ item.quantity_fact }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="small text-muted">{{ item.task.material.full_name|default:item.task.material.name|default:"-" }}</td>
|
|
||||||
<td class="text-center">
|
|
||||||
{% if item.task.drawing_file %}
|
|
||||||
<a href="{{ item.task.drawing_file.url }}" target="_blank" class="btn btn-sm btn-outline-info p-1 stop-prop" title="DXF/STEP">
|
|
||||||
<i class="bi bi-file-earmark-code"></i>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if item.task.extra_drawing %}
|
|
||||||
<a href="{{ item.task.extra_drawing.url }}" target="_blank" class="btn btn-sm btn-outline-danger p-1 stop-prop" title="Чертеж PDF">
|
|
||||||
<i class="bi bi-file-pdf"></i>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
{% if item.is_synced_1c %}
|
|
||||||
<i class="bi bi-check-circle-fill text-success" title="Учтено"></i>
|
|
||||||
{% else %}
|
|
||||||
<i class="bi bi-clock-history text-muted" title="Ожидает"></i>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<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 }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr><td colspan="10" class="text-center p-5 text-muted">Заданий не найдено</td></tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
const rows = document.querySelectorAll(".clickable-row");
|
|
||||||
rows.forEach(row => {
|
|
||||||
row.addEventListener("click", function(e) {
|
|
||||||
// Если нажали на ссылку файла (класс stop-prop), не переходим на страницу деталей
|
|
||||||
if (e.target.closest('.stop-prop')) return;
|
|
||||||
// Иначе переходим по ссылке из data-href
|
|
||||||
window.location.href = this.dataset.href;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -16,77 +16,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body p-0">
|
{% include 'shiftflow/partials/_items_table.html' with items=items %}
|
||||||
<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>План / Факт</th>
|
|
||||||
<th>Материал</th>
|
|
||||||
<th class="text-center">Файлы</th>
|
|
||||||
<th class="text-center">1С</th>
|
|
||||||
<th>Статус</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for it in items %}
|
|
||||||
<tr class="clickable-row" data-href="{% url 'item_detail' it.pk %}">
|
|
||||||
<td class="small">{{ it.date|date:"d.m.y" }}</td>
|
|
||||||
<td><span class="text-accent fw-bold">{{ it.task.deal.number|default:"-" }}</span></td>
|
|
||||||
<td><span class="badge bg-dark border border-secondary">{{ it.machine.name }}</span></td>
|
|
||||||
<td class="fw-bold">{{ it.task.drawing_name|default:"Б/ч" }}</td>
|
|
||||||
<td class="small">{{ it.task.size_value|default:"-" }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="text-info fw-bold">{{ it.quantity_plan }}</span> /
|
|
||||||
<span class="text-success">{{ it.quantity_fact }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="small text-muted">{{ it.task.material.full_name|default:it.task.material.name|default:"-" }}</td>
|
|
||||||
<td class="text-center">
|
|
||||||
{% if it.task.drawing_file %}
|
|
||||||
<a href="{{ it.task.drawing_file.url }}" target="_blank" class="btn btn-sm btn-outline-info p-1 stop-prop" title="DXF/STEP">
|
|
||||||
<i class="bi bi-file-earmark-code"></i>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if it.task.extra_drawing %}
|
|
||||||
<a href="{{ it.task.extra_drawing.url }}" target="_blank" class="btn btn-sm btn-outline-danger p-1 stop-prop" title="Чертеж PDF">
|
|
||||||
<i class="bi bi-file-pdf"></i>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
{% if it.is_synced_1c %}
|
|
||||||
<i class="bi bi-check-circle-fill text-success" title="Учтено"></i>
|
|
||||||
{% else %}
|
|
||||||
<i class="bi bi-clock-history text-muted" title="Ожидает"></i>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge {% if it.status == 'work' %}bg-primary{% elif it.status == 'done' %}bg-success{% elif it.status == 'partial' %}bg-success-subtle text-success-emphasis border border-success-subtle{% else %}bg-secondary{% endif %}">{{ it.get_status_display }}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr><td colspan="10" class="text-center p-5 text-muted">Пунктов сменки не найдено</td></tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
const rows = document.querySelectorAll(".clickable-row");
|
|
||||||
rows.forEach(row => {
|
|
||||||
row.addEventListener("click", function(e) {
|
|
||||||
if (e.target.closest('.stop-prop')) return;
|
|
||||||
window.location.href = this.dataset.href;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
from django.db.models import Case, ExpressionWrapper, F, IntegerField, Sum, Value, When
|
from django.db.models import Case, ExpressionWrapper, F, IntegerField, Sum, Value, When
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
@@ -37,7 +38,10 @@ class RegistryView(LoginRequiredMixin, ListView):
|
|||||||
user = self.request.user
|
user = self.request.user
|
||||||
profile = getattr(user, 'profile', None)
|
profile = getattr(user, 'profile', None)
|
||||||
role = profile.role if profile else 'operator'
|
role = profile.role if profile else 'operator'
|
||||||
|
# Флаг, что фильтрация была применена через форму. Если нет — используем дефолты
|
||||||
filtered = self.request.GET.get('filtered')
|
filtered = self.request.GET.get('filtered')
|
||||||
|
# Принудительный сброс фильтров (?reset=1) — ведёт себя как первый заход на страницу
|
||||||
|
reset = self.request.GET.get('reset')
|
||||||
|
|
||||||
# Станки
|
# Станки
|
||||||
m_ids = self.request.GET.getlist('m_ids')
|
m_ids = self.request.GET.getlist('m_ids')
|
||||||
@@ -59,18 +63,18 @@ class RegistryView(LoginRequiredMixin, ListView):
|
|||||||
expanded.append(s)
|
expanded.append(s)
|
||||||
queryset = queryset.filter(status__in=expanded)
|
queryset = queryset.filter(status__in=expanded)
|
||||||
|
|
||||||
# Даты
|
# Диапазон дат, задаваемый пользователем. Если фильтры не активны или явно указан reset=1 — используем дефолты
|
||||||
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:
|
# Дефолтный режим: последние 7 дней и только статус "В работе"
|
||||||
|
is_default = (not filtered) or bool(reset)
|
||||||
|
|
||||||
|
if is_default:
|
||||||
today = timezone.localdate()
|
today = timezone.localdate()
|
||||||
if role == 'clerk':
|
week_ago = today - timezone.timedelta(days=7)
|
||||||
queryset = queryset.filter(date=today, status__in=['done', 'partial'])
|
queryset = queryset.filter(date__gte=week_ago, date__lte=today, status__in=['work'])
|
||||||
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)
|
||||||
if end_date:
|
if end_date:
|
||||||
@@ -103,16 +107,15 @@ class RegistryView(LoginRequiredMixin, ListView):
|
|||||||
context['machines'] = machines
|
context['machines'] = machines
|
||||||
filtered = self.request.GET.get('filtered')
|
filtered = self.request.GET.get('filtered')
|
||||||
|
|
||||||
if not filtered:
|
reset = self.request.GET.get('reset')
|
||||||
today_str = timezone.localdate().strftime('%Y-%m-%d')
|
# Дефолтное состояние формы фильтра: все станки включены, статус "В работе",
|
||||||
context['start_date'] = today_str
|
# период от сегодня−7 до сегодня. Совпадает с серверной выборкой выше
|
||||||
context['end_date'] = today_str
|
if (not filtered) or reset:
|
||||||
if role == 'clerk':
|
today = timezone.localdate()
|
||||||
context['selected_statuses'] = ['closed']
|
week_ago = today - timezone.timedelta(days=7)
|
||||||
elif role in ['operator', 'master']:
|
context['start_date'] = week_ago.strftime('%Y-%m-%d')
|
||||||
context['selected_statuses'] = ['work']
|
context['end_date'] = today.strftime('%Y-%m-%d')
|
||||||
else:
|
context['selected_statuses'] = ['work']
|
||||||
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:
|
||||||
@@ -123,6 +126,19 @@ class RegistryView(LoginRequiredMixin, ListView):
|
|||||||
context['is_synced'] = self.request.GET.get('is_synced', '')
|
context['is_synced'] = self.request.GET.get('is_synced', '')
|
||||||
context['all_selected_machines'] = False
|
context['all_selected_machines'] = False
|
||||||
|
|
||||||
|
items = list(context.get('items') or [])
|
||||||
|
for it in items:
|
||||||
|
plan = int(it.quantity_plan or 0)
|
||||||
|
fact = int(it.quantity_fact or 0)
|
||||||
|
if plan > 0:
|
||||||
|
fact_pct = int(round(fact * 100 / plan))
|
||||||
|
else:
|
||||||
|
fact_pct = 0
|
||||||
|
it.fact_pct = fact_pct
|
||||||
|
it.fact_width = max(0, min(100, fact_pct))
|
||||||
|
it.fact_bar_class = 'bg-success' if it.status in ['done', 'partial'] else 'bg-warning'
|
||||||
|
context['items'] = items
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@@ -222,7 +238,7 @@ class PlanningView(LoginRequiredMixin, TemplateView):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
profile = getattr(request.user, 'profile', None)
|
profile = getattr(request.user, 'profile', None)
|
||||||
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||||
if role not in ['admin', 'technologist']:
|
if role not in ['admin', 'technologist', 'master', 'clerk']:
|
||||||
return redirect('registry')
|
return redirect('registry')
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
@@ -249,7 +265,7 @@ class DealPlanningView(LoginRequiredMixin, TemplateView):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
profile = getattr(request.user, 'profile', None)
|
profile = getattr(request.user, 'profile', None)
|
||||||
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||||
if role not in ['admin', 'technologist']:
|
if role not in ['admin', 'technologist', 'master', 'clerk']:
|
||||||
return redirect('registry')
|
return redirect('registry')
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
@@ -282,6 +298,8 @@ class DealPlanningView(LoginRequiredMixin, TemplateView):
|
|||||||
).order_by('-id')
|
).order_by('-id')
|
||||||
|
|
||||||
tasks = list(tasks_qs)
|
tasks = list(tasks_qs)
|
||||||
|
# Рассчитываем показатели прогресса для визуализации:
|
||||||
|
# done_pct/plan_pct — проценты от "Надо"; done_width/plan_width — ширины сегментов бары, ограниченные 0..100
|
||||||
for t in tasks:
|
for t in tasks:
|
||||||
need = int(t.quantity_ordered or 0)
|
need = int(t.quantity_ordered or 0)
|
||||||
done_qty = int(t.done_qty or 0)
|
done_qty = int(t.done_qty or 0)
|
||||||
@@ -313,7 +331,7 @@ class TaskItemsView(LoginRequiredMixin, TemplateView):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
profile = getattr(request.user, 'profile', None)
|
profile = getattr(request.user, 'profile', None)
|
||||||
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||||
if role not in ['admin', 'technologist']:
|
if role not in ['admin', 'technologist', 'master', 'clerk']:
|
||||||
return redirect('registry')
|
return redirect('registry')
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
@@ -329,7 +347,17 @@ class TaskItemsView(LoginRequiredMixin, TemplateView):
|
|||||||
)
|
)
|
||||||
context['task'] = task
|
context['task'] = task
|
||||||
|
|
||||||
items = Item.objects.filter(task=task).select_related('machine').order_by('-date', 'machine__name', '-id')
|
items = list(Item.objects.filter(task=task).select_related('machine').order_by('-date', 'machine__name', '-id'))
|
||||||
|
for it in items:
|
||||||
|
plan = int(it.quantity_plan or 0)
|
||||||
|
fact = int(it.quantity_fact or 0)
|
||||||
|
if plan > 0:
|
||||||
|
fact_pct = int(round(fact * 100 / plan))
|
||||||
|
else:
|
||||||
|
fact_pct = 0
|
||||||
|
it.fact_pct = fact_pct
|
||||||
|
it.fact_width = max(0, min(100, fact_pct))
|
||||||
|
it.fact_bar_class = 'bg-success' if it.status in ['done', 'partial'] else 'bg-warning'
|
||||||
context['items'] = items
|
context['items'] = items
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@@ -340,7 +368,7 @@ class CustomersView(LoginRequiredMixin, TemplateView):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
profile = getattr(request.user, 'profile', None)
|
profile = getattr(request.user, 'profile', None)
|
||||||
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||||
if role not in ['admin', 'technologist']:
|
if role not in ['admin', 'technologist', 'master', 'clerk']:
|
||||||
return redirect('registry')
|
return redirect('registry')
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
@@ -361,7 +389,7 @@ class CustomerDealsView(LoginRequiredMixin, TemplateView):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
profile = getattr(request.user, 'profile', None)
|
profile = getattr(request.user, 'profile', None)
|
||||||
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||||
if role not in ['admin', 'technologist']:
|
if role not in ['admin', 'technologist', 'master', 'clerk']:
|
||||||
return redirect('registry')
|
return redirect('registry')
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
@@ -667,6 +695,22 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
role = profile.role if profile else ('admin' if self.request.user.is_superuser else 'operator')
|
role = profile.role if profile else ('admin' if self.request.user.is_superuser else 'operator')
|
||||||
context['user_role'] = role
|
context['user_role'] = role
|
||||||
context['machines'] = Machine.objects.all()
|
context['machines'] = Machine.objects.all()
|
||||||
|
|
||||||
|
# Вычисляем URL "Назад": приоритетно берём ?next=..., иначе пробуем Referer
|
||||||
|
# Используем только ссылки на текущий хост, чтобы избежать внешних редиректов
|
||||||
|
next_url = (self.request.GET.get('next') or '').strip()
|
||||||
|
back_url = ''
|
||||||
|
if next_url.startswith('/'):
|
||||||
|
back_url = next_url
|
||||||
|
else:
|
||||||
|
ref = (self.request.META.get('HTTP_REFERER') or '').strip()
|
||||||
|
if ref:
|
||||||
|
parts = urlsplit(ref)
|
||||||
|
if parts.netloc == self.request.get_host():
|
||||||
|
back_url = parts.path + (('?' + parts.query) if parts.query else '')
|
||||||
|
if not back_url:
|
||||||
|
back_url = str(reverse_lazy('registry'))
|
||||||
|
context['back_url'] = back_url
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
@@ -674,7 +718,24 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
profile = getattr(request.user, 'profile', None)
|
profile = getattr(request.user, 'profile', None)
|
||||||
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||||
|
|
||||||
|
# Поддерживаем "умный" возврат после действия: ?next=... или Referer
|
||||||
|
next_url = (request.POST.get('next') or '').strip()
|
||||||
|
if not next_url:
|
||||||
|
ref = (request.META.get('HTTP_REFERER') or '').strip()
|
||||||
|
if ref:
|
||||||
|
parts = urlsplit(ref)
|
||||||
|
if parts.netloc == request.get_host():
|
||||||
|
next_url = parts.path + (('?' + parts.query) if parts.query else '')
|
||||||
|
|
||||||
|
def redirect_back():
|
||||||
|
# Возвращаемся туда, откуда пришли, иначе в реестр
|
||||||
|
if next_url.startswith('/'):
|
||||||
|
return redirect(next_url)
|
||||||
|
return redirect('registry')
|
||||||
|
|
||||||
if role in ['admin', 'technologist']:
|
if role in ['admin', 'technologist']:
|
||||||
|
action = request.POST.get('action', 'save')
|
||||||
|
|
||||||
machine_id = request.POST.get('machine')
|
machine_id = request.POST.get('machine')
|
||||||
if machine_id and machine_id.isdigit():
|
if machine_id and machine_id.isdigit():
|
||||||
self.object.machine_id = int(machine_id)
|
self.object.machine_id = int(machine_id)
|
||||||
@@ -691,11 +752,6 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
if quantity_fact and quantity_fact.isdigit():
|
if quantity_fact and quantity_fact.isdigit():
|
||||||
self.object.quantity_fact = int(quantity_fact)
|
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.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.material_taken = request.POST.get('material_taken', self.object.material_taken)
|
||||||
self.object.usable_waste = request.POST.get('usable_waste', self.object.usable_waste)
|
self.object.usable_waste = request.POST.get('usable_waste', self.object.usable_waste)
|
||||||
@@ -707,35 +763,70 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Действия закрытия для админа/технолога
|
||||||
|
if action == 'close_done' and self.object.status == 'work':
|
||||||
|
self.object.quantity_fact = self.object.quantity_plan
|
||||||
|
self.object.status = 'done'
|
||||||
|
self.object.save()
|
||||||
|
return redirect_back()
|
||||||
|
|
||||||
|
if action == 'close_partial' and self.object.status == 'work':
|
||||||
|
try:
|
||||||
|
fact = int(request.POST.get('quantity_fact', '0'))
|
||||||
|
except ValueError:
|
||||||
|
fact = 0
|
||||||
|
fact = max(0, min(fact, self.object.quantity_plan))
|
||||||
|
residual = self.object.quantity_plan - fact
|
||||||
|
self.object.quantity_fact = fact
|
||||||
|
self.object.status = 'partial'
|
||||||
|
self.object.save()
|
||||||
|
if residual > 0:
|
||||||
|
Item.objects.create(
|
||||||
|
task=self.object.task,
|
||||||
|
date=self.object.date,
|
||||||
|
machine=self.object.machine,
|
||||||
|
quantity_plan=residual,
|
||||||
|
quantity_fact=0,
|
||||||
|
status='leftover',
|
||||||
|
is_synced_1c=False,
|
||||||
|
)
|
||||||
|
return redirect_back()
|
||||||
|
|
||||||
self.object.save()
|
self.object.save()
|
||||||
return redirect('registry')
|
return redirect_back()
|
||||||
|
|
||||||
if role in ['operator', 'master']:
|
if role in ['operator', 'master']:
|
||||||
|
action = request.POST.get('action', 'save')
|
||||||
material_taken = (request.POST.get('material_taken') or '').strip()
|
material_taken = (request.POST.get('material_taken') or '').strip()
|
||||||
usable_waste = (request.POST.get('usable_waste') or '').strip()
|
usable_waste = (request.POST.get('usable_waste') or '').strip()
|
||||||
scrap_weight_raw = (request.POST.get('scrap_weight') or '').strip()
|
scrap_weight_raw = (request.POST.get('scrap_weight') or '').strip()
|
||||||
status = request.POST.get('status', self.object.status)
|
|
||||||
|
|
||||||
machine_changed = False
|
if action == 'save':
|
||||||
if role == 'master':
|
qf = request.POST.get('quantity_fact')
|
||||||
machine_id = request.POST.get('machine')
|
if qf and qf.isdigit():
|
||||||
if machine_id and machine_id.isdigit():
|
self.object.quantity_fact = int(qf)
|
||||||
self.object.machine_id = int(machine_id)
|
machine_changed = False
|
||||||
machine_changed = True
|
if role == 'master':
|
||||||
|
machine_id = request.POST.get('machine')
|
||||||
|
if machine_id and machine_id.isdigit():
|
||||||
|
self.object.machine_id = int(machine_id)
|
||||||
|
machine_changed = True
|
||||||
|
fields = ['quantity_fact']
|
||||||
|
if machine_changed:
|
||||||
|
fields.append('machine')
|
||||||
|
self.object.save(update_fields=fields)
|
||||||
|
return redirect_back()
|
||||||
|
|
||||||
# Разрешаем мастеру редактировать операторские поля всегда,
|
if self.object.status != 'work':
|
||||||
# оператору — только в процессе закрытия
|
return redirect_back()
|
||||||
if role == 'operator' and self.object.status != 'work':
|
|
||||||
return redirect('registry')
|
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
if role == 'operator' and self.object.status == 'work':
|
if not material_taken:
|
||||||
if not material_taken:
|
errors.append('Заполни поле "Взятый материал"')
|
||||||
errors.append('Заполни поле "Взятый материал"')
|
if not usable_waste:
|
||||||
if not usable_waste:
|
errors.append('Заполни поле "Остаток ДО"')
|
||||||
errors.append('Заполни поле "Остаток ДО"')
|
if scrap_weight_raw == '':
|
||||||
if scrap_weight_raw == '':
|
errors.append('Заполни поле "Лом (кг)" (можно 0)')
|
||||||
errors.append('Заполни поле "Лом (кг)" (можно 0)')
|
|
||||||
|
|
||||||
scrap_weight = None
|
scrap_weight = None
|
||||||
if scrap_weight_raw != '':
|
if scrap_weight_raw != '':
|
||||||
@@ -749,21 +840,18 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
context['errors'] = errors
|
context['errors'] = errors
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
if material_taken:
|
self.object.material_taken = material_taken
|
||||||
self.object.material_taken = material_taken
|
self.object.usable_waste = usable_waste
|
||||||
if usable_waste:
|
|
||||||
self.object.usable_waste = usable_waste
|
|
||||||
if scrap_weight is not None:
|
if scrap_weight is not None:
|
||||||
self.object.scrap_weight = scrap_weight
|
self.object.scrap_weight = scrap_weight
|
||||||
|
|
||||||
# Логика закрытия доступна и мастеру, и оператору, но только из 'work'
|
if action == 'close_done':
|
||||||
if self.object.status == 'work' and 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()
|
||||||
return redirect('registry')
|
return redirect_back()
|
||||||
|
|
||||||
if self.object.status == 'work' and status == 'partial':
|
if action == 'close_partial':
|
||||||
try:
|
try:
|
||||||
fact = int(request.POST.get('quantity_fact', '0'))
|
fact = int(request.POST.get('quantity_fact', '0'))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -789,24 +877,18 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
status='leftover',
|
status='leftover',
|
||||||
is_synced_1c=False,
|
is_synced_1c=False,
|
||||||
)
|
)
|
||||||
|
return redirect_back()
|
||||||
|
|
||||||
return redirect('registry')
|
return redirect_back()
|
||||||
|
|
||||||
# Если статус не менялся (или не 'work'), просто сохраняем поля
|
|
||||||
update_fields = ['material_taken', 'usable_waste', 'scrap_weight']
|
|
||||||
if machine_changed:
|
|
||||||
update_fields.append('machine')
|
|
||||||
self.object.save(update_fields=update_fields)
|
|
||||||
return redirect('registry')
|
|
||||||
|
|
||||||
if role == 'clerk':
|
if role == 'clerk':
|
||||||
if self.object.status not in ['done', 'partial']:
|
if self.object.status not in ['done', 'partial']:
|
||||||
return redirect('registry')
|
return redirect_back()
|
||||||
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(update_fields=['is_synced_1c'])
|
self.object.save(update_fields=['is_synced_1c'])
|
||||||
return redirect('registry')
|
return redirect_back()
|
||||||
|
|
||||||
return redirect('registry')
|
return redirect_back()
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy('registry')
|
return reverse_lazy('registry')
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<a class="nav-link {% if request.resolver_match.url_name == 'registry' %}active{% endif %}" href="{% url 'registry' %}">Реестр</a>
|
<a class="nav-link {% if request.resolver_match.url_name == 'registry' %}active{% endif %}" href="{% url 'registry' %}">Реестр</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{% if user_role in 'admin,technologist' %}
|
{% if user_role in 'admin,technologist,master,clerk' %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.resolver_match.url_name == 'planning' or request.resolver_match.url_name == 'planning_deal' %}active{% endif %}" href="{% url 'planning' %}">Сделки</a>
|
<a class="nav-link {% if request.resolver_match.url_name == 'planning' or request.resolver_match.url_name == 'planning_deal' %}active{% endif %}" href="{% url 'planning' %}">Сделки</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
Reference in New Issue
Block a user