Фильтры сохраняются, мастер получил расширенные возможности
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:
171
shiftflow/popup_views.py
Normal file
171
shiftflow/popup_views.py
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.views.generic import CreateView, UpdateView
|
||||||
|
|
||||||
|
from warehouse.models import Material, MaterialCategory, SteelGrade
|
||||||
|
|
||||||
|
from .models import Company, Deal
|
||||||
|
|
||||||
|
|
||||||
|
class _PopupRoleMixin(LoginRequiredMixin):
|
||||||
|
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"]:
|
||||||
|
return redirect("registry")
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_target(self):
|
||||||
|
return (self.request.GET.get("target") or self.request.POST.get("target") or "").strip()
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
self.object = form.save()
|
||||||
|
return TemplateResponse(
|
||||||
|
self.request,
|
||||||
|
"shiftflow/popup_done.html",
|
||||||
|
{"target": self.get_target(), "value": self.object.pk, "label": self.get_popup_label()},
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx["target"] = self.get_target()
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def get_popup_label(self):
|
||||||
|
return str(self.object)
|
||||||
|
|
||||||
|
|
||||||
|
class _BootstrapModelForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for name, field in self.fields.items():
|
||||||
|
widget = field.widget
|
||||||
|
if isinstance(widget, (forms.Select, forms.SelectMultiple)):
|
||||||
|
cls = "form-select border-secondary"
|
||||||
|
elif isinstance(widget, forms.CheckboxInput):
|
||||||
|
cls = "form-check-input"
|
||||||
|
else:
|
||||||
|
cls = "form-control border-secondary"
|
||||||
|
widget.attrs["class"] = cls
|
||||||
|
|
||||||
|
|
||||||
|
class DealForm(_BootstrapModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Deal
|
||||||
|
fields = ["number", "status", "company", "description"]
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyForm(_BootstrapModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Company
|
||||||
|
fields = ["name", "description"]
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialForm(_BootstrapModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Material
|
||||||
|
fields = ["category", "steel_grade", "name"]
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialCategoryForm(_BootstrapModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = MaterialCategory
|
||||||
|
fields = ["name", "gost_standard"]
|
||||||
|
|
||||||
|
|
||||||
|
class SteelGradeForm(_BootstrapModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = SteelGrade
|
||||||
|
fields = ["name", "gost_standard"]
|
||||||
|
|
||||||
|
|
||||||
|
class DealPopupCreateView(_PopupRoleMixin, CreateView):
|
||||||
|
template_name = "shiftflow/popup_form.html"
|
||||||
|
model = Deal
|
||||||
|
form_class = DealForm
|
||||||
|
|
||||||
|
def get_popup_label(self):
|
||||||
|
return self.object.number
|
||||||
|
|
||||||
|
|
||||||
|
class DealPopupUpdateView(_PopupRoleMixin, UpdateView):
|
||||||
|
template_name = "shiftflow/popup_form.html"
|
||||||
|
model = Deal
|
||||||
|
form_class = DealForm
|
||||||
|
|
||||||
|
def get_popup_label(self):
|
||||||
|
return self.object.number
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyPopupCreateView(_PopupRoleMixin, CreateView):
|
||||||
|
template_name = "shiftflow/popup_form.html"
|
||||||
|
model = Company
|
||||||
|
form_class = CompanyForm
|
||||||
|
|
||||||
|
def get_popup_label(self):
|
||||||
|
return self.object.name
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyPopupUpdateView(_PopupRoleMixin, UpdateView):
|
||||||
|
template_name = "shiftflow/popup_form.html"
|
||||||
|
model = Company
|
||||||
|
form_class = CompanyForm
|
||||||
|
|
||||||
|
def get_popup_label(self):
|
||||||
|
return self.object.name
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialPopupCreateView(_PopupRoleMixin, CreateView):
|
||||||
|
template_name = "shiftflow/popup_form.html"
|
||||||
|
model = Material
|
||||||
|
form_class = MaterialForm
|
||||||
|
|
||||||
|
def get_popup_label(self):
|
||||||
|
return self.object.full_name
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialPopupUpdateView(_PopupRoleMixin, UpdateView):
|
||||||
|
template_name = "shiftflow/popup_form.html"
|
||||||
|
model = Material
|
||||||
|
form_class = MaterialForm
|
||||||
|
|
||||||
|
def get_popup_label(self):
|
||||||
|
return self.object.full_name
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialCategoryPopupCreateView(_PopupRoleMixin, CreateView):
|
||||||
|
template_name = "shiftflow/popup_form.html"
|
||||||
|
model = MaterialCategory
|
||||||
|
form_class = MaterialCategoryForm
|
||||||
|
|
||||||
|
def get_popup_label(self):
|
||||||
|
return self.object.name
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialCategoryPopupUpdateView(_PopupRoleMixin, UpdateView):
|
||||||
|
template_name = "shiftflow/popup_form.html"
|
||||||
|
model = MaterialCategory
|
||||||
|
form_class = MaterialCategoryForm
|
||||||
|
|
||||||
|
def get_popup_label(self):
|
||||||
|
return self.object.name
|
||||||
|
|
||||||
|
|
||||||
|
class SteelGradePopupCreateView(_PopupRoleMixin, CreateView):
|
||||||
|
template_name = "shiftflow/popup_form.html"
|
||||||
|
model = SteelGrade
|
||||||
|
form_class = SteelGradeForm
|
||||||
|
|
||||||
|
def get_popup_label(self):
|
||||||
|
return self.object.name
|
||||||
|
|
||||||
|
|
||||||
|
class SteelGradePopupUpdateView(_PopupRoleMixin, UpdateView):
|
||||||
|
template_name = "shiftflow/popup_form.html"
|
||||||
|
model = SteelGrade
|
||||||
|
form_class = SteelGradeForm
|
||||||
|
|
||||||
|
def get_popup_label(self):
|
||||||
|
return self.object.name
|
||||||
158
shiftflow/templates/shiftflow/customer_deals.html
Normal file
158
shiftflow/templates/shiftflow/customer_deals.html
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card shadow border-secondary">
|
||||||
|
<div class="card-header border-secondary py-3 d-flex justify-content-between align-items-center">
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-accent mb-0"><i class="bi bi-briefcase me-2"></i>Сделки</h3>
|
||||||
|
<div class="small text-muted">{{ company.name }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="get" class="d-flex align-items-center gap-2">
|
||||||
|
<span class="small text-muted">Статус:</span>
|
||||||
|
<div class="d-flex flex-wrap gap-1">
|
||||||
|
<input type="radio" class="btn-check" name="status" id="cust_s_work" value="work" {% if selected_status == 'work' %}checked{% endif %} onchange="this.form.submit()">
|
||||||
|
<label class="btn btn-outline-primary btn-sm" for="cust_s_work">В работе</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="status" id="cust_s_lead" value="lead" {% if selected_status == 'lead' %}checked{% endif %} onchange="this.form.submit()">
|
||||||
|
<label class="btn btn-outline-secondary btn-sm" for="cust_s_lead">Зашла</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="status" id="cust_s_done" value="done" {% if selected_status == 'done' %}checked{% endif %} onchange="this.form.submit()">
|
||||||
|
<label class="btn btn-outline-success btn-sm" for="cust_s_done">Завершена</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<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>Создать сделку
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-outline-secondary btn-sm" href="{% url 'customers' %}">
|
||||||
|
<i class="bi bi-arrow-left me-1"></i>К заказчикам
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 style="width: 140px;">Статус</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for d in deals %}
|
||||||
|
<tr class="planning-row" style="cursor:pointer" data-href="{% url 'planning_deal' d.id %}">
|
||||||
|
<td><span class="text-accent fw-bold">{{ d.number }}</span></td>
|
||||||
|
<td class="small text-muted">{{ d.description|default:"" }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if d.status == 'work' %}bg-primary{% elif d.status == 'done' %}bg-success{% else %}bg-secondary{% endif %}">
|
||||||
|
{{ d.get_status_display }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="3" class="text-center p-5 text-muted">Сделок не найдено</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="dealModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<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="mb-3">
|
||||||
|
<label class="form-label small text-muted">№ Сделки</label>
|
||||||
|
<input type="text" class="form-control border-secondary" id="dealNumber">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small text-muted">Статус</label>
|
||||||
|
<select class="form-select border-secondary" id="dealStatus">
|
||||||
|
<option value="lead">Зашла</option>
|
||||||
|
<option value="work" selected>В работе</option>
|
||||||
|
<option value="done">Завершена</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-0">
|
||||||
|
<label class="form-label small text-muted">Описание</label>
|
||||||
|
<textarea class="form-control border-secondary" rows="3" id="dealDescription"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-secondary">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||||
|
<button type="button" class="btn btn-outline-accent" id="dealSaveBtn">Сохранить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
document.querySelectorAll('tr.planning-row[data-href]').forEach(function (row) {
|
||||||
|
row.addEventListener('click', function () {
|
||||||
|
const href = row.getAttribute('data-href');
|
||||||
|
if (href) window.location.href = href;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const dealModal = document.getElementById('dealModal');
|
||||||
|
const dealNumber = document.getElementById('dealNumber');
|
||||||
|
const dealStatus = document.getElementById('dealStatus');
|
||||||
|
const dealDescription = document.getElementById('dealDescription');
|
||||||
|
const dealSaveBtn = document.getElementById('dealSaveBtn');
|
||||||
|
|
||||||
|
function getCookie(name) {
|
||||||
|
const value = `; ${document.cookie}`;
|
||||||
|
const parts = value.split(`; ${name}=`);
|
||||||
|
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postForm(url, data) {
|
||||||
|
const csrftoken = getCookie('csrftoken');
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||||
|
'X-CSRFToken': csrftoken,
|
||||||
|
},
|
||||||
|
body: new URLSearchParams(data).toString(),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('request_failed');
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dealModal) {
|
||||||
|
dealModal.addEventListener('show.bs.modal', function () {
|
||||||
|
if (dealNumber) dealNumber.value = '';
|
||||||
|
if (dealDescription) dealDescription.value = '';
|
||||||
|
if (dealStatus) dealStatus.value = 'work';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dealSaveBtn) {
|
||||||
|
dealSaveBtn.addEventListener('click', async function () {
|
||||||
|
const payload = {
|
||||||
|
number: (dealNumber ? dealNumber.value : ''),
|
||||||
|
status: dealStatus ? dealStatus.value : 'work',
|
||||||
|
company_id: '{{ company.id }}',
|
||||||
|
description: (dealDescription ? dealDescription.value : ''),
|
||||||
|
};
|
||||||
|
await postForm('{% url "deal_upsert" %}', payload);
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
115
shiftflow/templates/shiftflow/customers.html
Normal file
115
shiftflow/templates/shiftflow/customers.html
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card shadow border-secondary">
|
||||||
|
<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>
|
||||||
|
<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>Добавить заказчика
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for c in companies %}
|
||||||
|
<tr class="customer-row" style="cursor:pointer" data-href="{% url 'customer_deals' c.id %}">
|
||||||
|
<td class="fw-bold">{{ c.name }}</td>
|
||||||
|
<td class="small text-muted">{{ c.description|default:"" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="2" class="text-center p-5 text-muted">Заказчиков не найдено</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="companyModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<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="mb-3">
|
||||||
|
<label class="form-label small text-muted">Название</label>
|
||||||
|
<input type="text" class="form-control border-secondary" id="companyName">
|
||||||
|
</div>
|
||||||
|
<div class="mb-0">
|
||||||
|
<label class="form-label small text-muted">Примечание</label>
|
||||||
|
<textarea class="form-control border-secondary" rows="3" id="companyDescription"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-secondary">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||||
|
<button type="button" class="btn btn-outline-accent" id="companySaveBtn">Сохранить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
document.querySelectorAll('tr.customer-row[data-href]').forEach(function (row) {
|
||||||
|
row.addEventListener('click', function () {
|
||||||
|
const href = row.getAttribute('data-href');
|
||||||
|
if (href) window.location.href = href;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const companyModal = document.getElementById('companyModal');
|
||||||
|
const companyName = document.getElementById('companyName');
|
||||||
|
const companyDescription = document.getElementById('companyDescription');
|
||||||
|
const companySaveBtn = document.getElementById('companySaveBtn');
|
||||||
|
|
||||||
|
function getCookie(name) {
|
||||||
|
const value = `; ${document.cookie}`;
|
||||||
|
const parts = value.split(`; ${name}=`);
|
||||||
|
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postForm(url, data) {
|
||||||
|
const csrftoken = getCookie('csrftoken');
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||||
|
'X-CSRFToken': csrftoken,
|
||||||
|
},
|
||||||
|
body: new URLSearchParams(data).toString(),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('request_failed');
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (companyModal) {
|
||||||
|
companyModal.addEventListener('show.bs.modal', function () {
|
||||||
|
if (companyName) companyName.value = '';
|
||||||
|
if (companyDescription) companyDescription.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (companySaveBtn) {
|
||||||
|
companySaveBtn.addEventListener('click', async function () {
|
||||||
|
const payload = {
|
||||||
|
name: (companyName ? companyName.value : ''),
|
||||||
|
description: (companyDescription ? companyDescription.value : ''),
|
||||||
|
};
|
||||||
|
await postForm('{% url "company_upsert" %}', payload);
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% load l10n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
@@ -72,13 +73,29 @@
|
|||||||
</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" min="0" name="scrap_weight" class="form-control border-secondary" value="{{ item.scrap_weight }}" required>
|
<input type="number" step="0.01" min="0" name="scrap_weight" class="form-control border-secondary" value="{{ item.scrap_weight|default_if_none:'0'|unlocalize }}" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-success">Статус: {{ item.get_status_display }}. Сделано: {{ item.quantity_fact }} шт.</div>
|
<div class="alert alert-success">Статус: {{ item.get_status_display }}. Сделано: {{ item.quantity_fact }} шт.</div>
|
||||||
<input type="hidden" name="quantity_fact" value="{{ item.quantity_fact }}">
|
<input type="hidden" name="quantity_fact" value="{{ item.quantity_fact }}">
|
||||||
|
{% if user_role == 'master' %}
|
||||||
|
<div class="row g-3 mt-3 text-start">
|
||||||
|
<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="Напр: 0.8м / 12кг">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="small text-muted">Лом (кг)</label>
|
||||||
|
<input type="number" step="0.01" min="0" name="scrap_weight" class="form-control border-secondary" value="{{ item.scrap_weight|default_if_none:'0'|unlocalize }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -124,7 +141,7 @@
|
|||||||
</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" name="scrap_weight" class="form-control border-secondary" value="{{ item.scrap_weight|default_if_none:'0'|unlocalize }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -73,14 +73,63 @@
|
|||||||
</form>
|
</form>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function(){
|
document.addEventListener('DOMContentLoaded', function(){
|
||||||
|
const form = document.getElementById('filter-form');
|
||||||
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 now = new Date();
|
const now = new Date();
|
||||||
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
||||||
const dd = String(now.getDate()).padStart(2, '0');
|
const dd = String(now.getDate()).padStart(2, '0');
|
||||||
const today = `${now.getFullYear()}-${mm}-${dd}`;
|
const today = `${now.getFullYear()}-${mm}-${dd}`;
|
||||||
if (s && !s.value) s.value = today;
|
|
||||||
|
const weekAgoDate = new Date(now);
|
||||||
|
weekAgoDate.setDate(weekAgoDate.getDate() - 7);
|
||||||
|
const mm2 = String(weekAgoDate.getMonth() + 1).padStart(2, '0');
|
||||||
|
const dd2 = String(weekAgoDate.getDate()).padStart(2, '0');
|
||||||
|
const weekAgo = `${weekAgoDate.getFullYear()}-${mm2}-${dd2}`;
|
||||||
|
|
||||||
|
if (s && !s.value) s.value = weekAgo;
|
||||||
if (e && !e.value) e.value = today;
|
if (e && !e.value) e.value = today;
|
||||||
|
|
||||||
|
function saveFilters(){
|
||||||
|
if (!form) return;
|
||||||
|
const data = {
|
||||||
|
statuses: Array.from(form.querySelectorAll('input[name="statuses"]:checked')).map(i=>i.value),
|
||||||
|
m_ids: Array.from(form.querySelectorAll('input[name="m_ids"]:checked')).map(i=>i.value),
|
||||||
|
start_date: s ? s.value : '',
|
||||||
|
is_synced: (form.querySelector('select[name="is_synced"]')||{}).value || ''
|
||||||
|
};
|
||||||
|
try { localStorage.setItem('registry_filters', JSON.stringify(data)); } catch(_){}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreFilters(){
|
||||||
|
if (!form) return false;
|
||||||
|
const qs = new URLSearchParams(window.location.search);
|
||||||
|
if (qs.get('filtered') === '1') return false;
|
||||||
|
let raw = null; try { raw = localStorage.getItem('registry_filters'); } catch(_){}
|
||||||
|
if (!raw) return false;
|
||||||
|
let data = null; try { data = JSON.parse(raw); } catch(_){}
|
||||||
|
if (!data) return false;
|
||||||
|
|
||||||
|
if (Array.isArray(data.statuses)){
|
||||||
|
form.querySelectorAll('input[name="statuses"]').forEach(i=>{ i.checked = data.statuses.includes(i.value); });
|
||||||
|
}
|
||||||
|
if (Array.isArray(data.m_ids)){
|
||||||
|
form.querySelectorAll('input[name="m_ids"]').forEach(i=>{ i.checked = data.m_ids.includes(i.value); });
|
||||||
|
}
|
||||||
|
if (s) s.value = data.start_date || weekAgo;
|
||||||
|
if (e) e.value = today;
|
||||||
|
const sel = form.querySelector('select[name="is_synced"]');
|
||||||
|
if (sel && data.is_synced !== undefined) sel.value = data.is_synced;
|
||||||
|
const filtered = form.querySelector('input[name="filtered"]');
|
||||||
|
if (filtered) filtered.value = '1';
|
||||||
|
form.submit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form){
|
||||||
|
form.addEventListener('change', saveFilters, true);
|
||||||
|
restoreFilters();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<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">
|
||||||
<div class="d-flex align-items-center gap-3">
|
<div class="d-flex align-items-center gap-3">
|
||||||
<h3 class="text-accent mb-0"><i class="bi bi-kanban me-2"></i>Планирование</h3>
|
<h3 class="text-accent mb-0"><i class="bi bi-kanban me-2"></i>Сделки</h3>
|
||||||
<form method="get" class="d-flex align-items-center gap-2">
|
<form method="get" class="d-flex align-items-center gap-2">
|
||||||
<span class="small text-muted">Сделки:</span>
|
<span class="small text-muted">Сделки:</span>
|
||||||
<div class="d-flex flex-wrap gap-1">
|
<div class="d-flex flex-wrap gap-1">
|
||||||
@@ -134,6 +134,21 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const statusRadios = document.querySelectorAll('input[name="status"]');
|
||||||
|
const qs = new URLSearchParams(window.location.search);
|
||||||
|
if (!qs.get('status')){
|
||||||
|
try{
|
||||||
|
const saved = localStorage.getItem('planning_status');
|
||||||
|
if (saved){
|
||||||
|
const r = Array.from(statusRadios).find(x=>x.value===saved);
|
||||||
|
if (r && !r.checked){ r.checked = true; r.form.submit(); }
|
||||||
|
}
|
||||||
|
}catch(_){ }
|
||||||
|
}
|
||||||
|
statusRadios.forEach(r=> r.addEventListener('change', ()=>{
|
||||||
|
try{ localStorage.setItem('planning_status', r.value); }catch(_){ }
|
||||||
|
}));
|
||||||
|
|
||||||
const dealModal = document.getElementById('dealModal');
|
const dealModal = document.getElementById('dealModal');
|
||||||
const dealId = document.getElementById('dealId');
|
const dealId = document.getElementById('dealId');
|
||||||
const dealNumber = document.getElementById('dealNumber');
|
const dealNumber = document.getElementById('dealNumber');
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<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>
|
||||||
<a class="btn btn-outline-accent btn-sm" href="{% url 'task_add' %}?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>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for t in tasks %}
|
{% for t in tasks %}
|
||||||
<tr>
|
<tr class="task-row" style="cursor:pointer" data-href="{% url 'task_items' t.id %}">
|
||||||
<td class="fw-bold">{{ t.drawing_name|default:"Б/ч" }}</td>
|
<td class="fw-bold">{{ t.drawing_name|default:"Б/ч" }}</td>
|
||||||
<td class="small text-muted">{{ t.material.full_name|default:t.material.name }}</td>
|
<td class="small text-muted">{{ t.material.full_name|default:t.material.name }}</td>
|
||||||
<td class="small">{{ t.size_value }}</td>
|
<td class="small">{{ t.size_value }}</td>
|
||||||
@@ -88,12 +88,13 @@
|
|||||||
<div class="small text-muted mb-2" id="modalTaskTitle"></div>
|
<div class="small text-muted mb-2" id="modalTaskTitle"></div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label small text-muted">Станок</label>
|
<label class="form-label small text-muted d-block">Станок</label>
|
||||||
<select class="form-select border-secondary" name="machine_id" required>
|
<div class="d-flex flex-wrap gap-1" id="machineToggleGroup">
|
||||||
{% for m in machines %}
|
{% for m in machines %}
|
||||||
<option value="{{ m.id }}">{{ m.name }}</option>
|
<input type="radio" class="btn-check" name="machine_id" id="m_{{ m.id }}" value="{{ m.id }}" required>
|
||||||
|
<label class="btn btn-outline-accent btn-sm" for="m_{{ m.id }}">{{ m.name }}</label>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
@@ -113,10 +114,18 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
document.querySelectorAll('tr.task-row[data-href]').forEach(function (row) {
|
||||||
|
row.addEventListener('click', function (e) {
|
||||||
|
if (e.target && e.target.closest('button')) return;
|
||||||
|
const href = row.getAttribute('data-href');
|
||||||
|
if (href) window.location.href = href;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const modal = document.getElementById('addToPlanModal');
|
const modal = document.getElementById('addToPlanModal');
|
||||||
if (!modal) return;
|
if (!modal) return;
|
||||||
|
|
||||||
modal.addEventListener('show.bs.modal', function (event) {
|
modal.addEventListener('shown.bs.modal', function (event) {
|
||||||
const btn = event.relatedTarget;
|
const btn = event.relatedTarget;
|
||||||
const taskId = btn.getAttribute('data-task-id');
|
const taskId = btn.getAttribute('data-task-id');
|
||||||
const name = btn.getAttribute('data-task-name');
|
const name = btn.getAttribute('data-task-name');
|
||||||
@@ -128,8 +137,42 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
const qty = document.getElementById('modalQty');
|
const qty = document.getElementById('modalQty');
|
||||||
qty.value = '';
|
qty.value = '';
|
||||||
if (rem && !isNaN(parseInt(rem, 10))) qty.max = Math.max(1, parseInt(rem, 10));
|
|
||||||
else qty.removeAttribute('max');
|
let remInt = null;
|
||||||
|
if (rem && !isNaN(parseInt(rem, 10))) {
|
||||||
|
remInt = Math.max(1, parseInt(rem, 10));
|
||||||
|
qty.max = remInt;
|
||||||
|
qty.value = String(remInt);
|
||||||
|
} else {
|
||||||
|
qty.removeAttribute('max');
|
||||||
|
}
|
||||||
|
|
||||||
|
qty.focus({ preventScroll: true });
|
||||||
|
qty.select();
|
||||||
|
|
||||||
|
qty.onkeydown = function (e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = document.querySelector('#addToPlanModal form');
|
||||||
|
if (form) form.requestSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const radios = Array.from(document.querySelectorAll('input[name="machine_id"]'));
|
||||||
|
const savedMachine = (() => { try { return localStorage.getItem('planning_machine_id'); } catch (_) { return null; } })();
|
||||||
|
|
||||||
|
let selected = null;
|
||||||
|
if (savedMachine) {
|
||||||
|
selected = radios.find(r => r.value === savedMachine);
|
||||||
|
}
|
||||||
|
if (!selected && radios.length) selected = radios[0];
|
||||||
|
if (selected) selected.checked = true;
|
||||||
|
|
||||||
|
radios.forEach(r => {
|
||||||
|
r.onchange = function () {
|
||||||
|
try { localStorage.setItem('planning_machine_id', r.value); } catch (_) {}
|
||||||
|
};
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
20
shiftflow/templates/shiftflow/popup_done.html
Normal file
20
shiftflow/templates/shiftflow/popup_done.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Сохранено</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
if (window.opener && typeof window.opener.shiftflowReceivePopup === 'function') {
|
||||||
|
window.opener.shiftflowReceivePopup('{{ target|escapejs }}', '{{ value }}', '{{ label|escapejs }}');
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
window.close();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
46
shiftflow/templates/shiftflow/popup_form.html
Normal file
46
shiftflow/templates/shiftflow/popup_form.html
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ view.model._meta.verbose_name }}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="p-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h5 class="mb-0">{{ view.model._meta.verbose_name }}</h5>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="window.close()">Закрыть</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="target" value="{{ target }}">
|
||||||
|
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{% for e in form.non_field_errors %}<div>{{ e }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small text-muted">{{ field.label }}</label>
|
||||||
|
{% if field.field.widget.input_type == "checkbox" %}
|
||||||
|
<div class="form-check">
|
||||||
|
{{ field }}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{{ field }}
|
||||||
|
{% endif %}
|
||||||
|
{% if field.help_text %}<div class="form-text">{{ field.help_text }}</div>{% endif %}
|
||||||
|
{% for e in field.errors %}<div class="text-danger small">{{ e }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 justify-content-end">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" onclick="window.close()">Отмена</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -6,13 +6,20 @@
|
|||||||
<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-plus-circle me-2"></i>Новое задание</h3>
|
<h3 class="text-accent mb-0"><i class="bi bi-plus-circle me-2"></i>Новое задание</h3>
|
||||||
<a class="btn btn-outline-secondary btn-sm" href="{% url 'planning' %}">
|
{% if request.GET.next %}
|
||||||
<i class="bi bi-arrow-left me-1"></i>Назад
|
<a class="btn btn-outline-secondary btn-sm" href="{{ request.GET.next }}">
|
||||||
</a>
|
<i class="bi bi-arrow-left me-1"></i>Назад
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-outline-secondary btn-sm" href="{% url 'planning' %}">
|
||||||
|
<i class="bi bi-arrow-left me-1"></i>Назад
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" enctype="multipart/form-data" class="card-body p-4">
|
<form method="post" enctype="multipart/form-data" class="card-body p-4">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="next" value="{{ request.GET.next }}">
|
||||||
|
|
||||||
{% if form.non_field_errors %}
|
{% if form.non_field_errors %}
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
|
|||||||
92
shiftflow/templates/shiftflow/task_items.html
Normal file
92
shiftflow/templates/shiftflow/task_items.html
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card shadow border-secondary mb-3">
|
||||||
|
<div class="card-header border-secondary py-3 d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-accent mb-1">
|
||||||
|
<i class="bi bi-list-task me-2"></i>{{ task.drawing_name|default:"Б/ч" }}
|
||||||
|
</h3>
|
||||||
|
<div class="small text-muted">
|
||||||
|
Сделка {{ task.deal.number }}{% if task.deal.company %} · {{ task.deal.company.name }}{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="btn btn-outline-secondary btn-sm" href="{% url 'planning_deal' task.deal.id %}">
|
||||||
|
<i class="bi bi-arrow-left me-1"></i>Назад к сделке
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>План / Факт</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>
|
||||||
|
|
||||||
|
<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 %}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from .views import (
|
from .views import (
|
||||||
CompanyUpsertView,
|
CompanyUpsertView,
|
||||||
|
CustomerDealsView,
|
||||||
|
CustomersView,
|
||||||
DealDetailView,
|
DealDetailView,
|
||||||
DealPlanningView,
|
DealPlanningView,
|
||||||
DealUpsertView,
|
DealUpsertView,
|
||||||
@@ -15,6 +17,7 @@ from .views import (
|
|||||||
RegistryPrintView,
|
RegistryPrintView,
|
||||||
RegistryView,
|
RegistryView,
|
||||||
SteelGradeUpsertView,
|
SteelGradeUpsertView,
|
||||||
|
TaskItemsView,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -23,9 +26,12 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Реестр
|
# Реестр
|
||||||
path('registry/', RegistryView.as_view(), name='registry'),
|
path('registry/', RegistryView.as_view(), name='registry'),
|
||||||
# Планирование
|
# Сделки
|
||||||
path('planning/', PlanningView.as_view(), name='planning'),
|
path('planning/', PlanningView.as_view(), name='planning'),
|
||||||
path('planning/deal/<int:pk>/', DealPlanningView.as_view(), name='planning_deal'),
|
path('planning/deal/<int:pk>/', DealPlanningView.as_view(), name='planning_deal'),
|
||||||
|
path('planning/task/<int:pk>/items/', TaskItemsView.as_view(), name='task_items'),
|
||||||
|
path('customers/', CustomersView.as_view(), name='customers'),
|
||||||
|
path('customers/<int:pk>/', CustomerDealsView.as_view(), name='customer_deals'),
|
||||||
path('planning/add/', PlanningAddView.as_view(), name='planning_add'),
|
path('planning/add/', PlanningAddView.as_view(), name='planning_add'),
|
||||||
path('planning/task/add/', ProductionTaskCreateView.as_view(), name='task_add'),
|
path('planning/task/add/', ProductionTaskCreateView.as_view(), name='task_add'),
|
||||||
path('planning/deal/<int:pk>/json/', DealDetailView.as_view(), name='deal_json'),
|
path('planning/deal/<int:pk>/json/', DealDetailView.as_view(), name='deal_json'),
|
||||||
|
|||||||
@@ -286,6 +286,83 @@ class DealPlanningView(LoginRequiredMixin, TemplateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class TaskItemsView(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = 'shiftflow/task_items.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']:
|
||||||
|
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 ('admin' if self.request.user.is_superuser else 'operator')
|
||||||
|
context['user_role'] = role
|
||||||
|
|
||||||
|
task = get_object_or_404(
|
||||||
|
ProductionTask.objects.select_related('deal', 'deal__company', 'material'),
|
||||||
|
pk=self.kwargs['pk'],
|
||||||
|
)
|
||||||
|
context['task'] = task
|
||||||
|
|
||||||
|
items = Item.objects.filter(task=task).select_related('machine').order_by('-date', 'machine__name', '-id')
|
||||||
|
context['items'] = items
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class CustomersView(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = 'shiftflow/customers.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']:
|
||||||
|
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 ('admin' if self.request.user.is_superuser else 'operator')
|
||||||
|
context['user_role'] = role
|
||||||
|
|
||||||
|
companies = Company.objects.all().order_by('name')
|
||||||
|
context['companies'] = companies
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerDealsView(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = 'shiftflow/customer_deals.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']:
|
||||||
|
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 ('admin' if self.request.user.is_superuser else 'operator')
|
||||||
|
context['user_role'] = role
|
||||||
|
|
||||||
|
company = get_object_or_404(Company, pk=self.kwargs['pk'])
|
||||||
|
context['company'] = company
|
||||||
|
|
||||||
|
status = (self.request.GET.get('status') or 'work').strip()
|
||||||
|
allowed = {k for k, _ in Deal.STATUS_CHOICES}
|
||||||
|
if status not in allowed:
|
||||||
|
status = 'work'
|
||||||
|
|
||||||
|
context['selected_status'] = status
|
||||||
|
context['deals'] = Deal.objects.select_related('company').filter(company=company, status=status).order_by('-id')
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class PlanningAddView(LoginRequiredMixin, View):
|
class PlanningAddView(LoginRequiredMixin, View):
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
profile = getattr(request.user, 'profile', None)
|
profile = getattr(request.user, 'profile', None)
|
||||||
@@ -366,7 +443,12 @@ class ProductionTaskCreateView(LoginRequiredMixin, FormView):
|
|||||||
task.extra_drawing = form.cleaned_data['extra_drawing']
|
task.extra_drawing = form.cleaned_data['extra_drawing']
|
||||||
|
|
||||||
task.save()
|
task.save()
|
||||||
return super().form_valid(form)
|
|
||||||
|
next_url = (self.request.POST.get('next') or '').strip()
|
||||||
|
if next_url.startswith('/'):
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
return redirect('planning_deal', pk=task.deal_id)
|
||||||
|
|
||||||
|
|
||||||
class DealDetailView(LoginRequiredMixin, View):
|
class DealDetailView(LoginRequiredMixin, View):
|
||||||
@@ -608,20 +690,24 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
return redirect('registry')
|
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()
|
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)
|
||||||
|
|
||||||
|
# Разрешаем мастеру редактировать операторские поля всегда,
|
||||||
|
# оператору — только в процессе закрытия
|
||||||
|
if role == 'operator' and self.object.status != 'work':
|
||||||
|
return redirect('registry')
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
if not material_taken:
|
if role == 'operator' and self.object.status == 'work':
|
||||||
errors.append('Заполни поле "Взятый материал"')
|
if not material_taken:
|
||||||
if not usable_waste:
|
errors.append('Заполни поле "Взятый материал"')
|
||||||
errors.append('Заполни поле "Остаток ДО"')
|
if not usable_waste:
|
||||||
if scrap_weight_raw == '':
|
errors.append('Заполни поле "Остаток ДО"')
|
||||||
errors.append('Заполни поле "Лом (кг)" (можно 0)')
|
if scrap_weight_raw == '':
|
||||||
|
errors.append('Заполни поле "Лом (кг)" (можно 0)')
|
||||||
|
|
||||||
scrap_weight = None
|
scrap_weight = None
|
||||||
if scrap_weight_raw != '':
|
if scrap_weight_raw != '':
|
||||||
@@ -630,25 +716,26 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
errors.append('Поле "Лом (кг)" должно быть числом')
|
errors.append('Поле "Лом (кг)" должно быть числом')
|
||||||
|
|
||||||
status = request.POST.get('status', self.object.status)
|
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
context = self.get_context_data()
|
context = self.get_context_data()
|
||||||
context['errors'] = errors
|
context['errors'] = errors
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
self.object.material_taken = material_taken
|
if material_taken:
|
||||||
self.object.usable_waste = usable_waste
|
self.object.material_taken = material_taken
|
||||||
|
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
|
||||||
|
|
||||||
if status == 'done':
|
# Логика закрытия доступна и мастеру, и оператору, но только из 'work'
|
||||||
|
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('registry')
|
||||||
|
|
||||||
if status == 'partial':
|
if self.object.status == 'work' and status == 'partial':
|
||||||
try:
|
try:
|
||||||
fact = int(request.POST.get('quantity_fact', '0'))
|
fact = int(request.POST.get('quantity_fact', '0'))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -677,6 +764,8 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
|
|
||||||
return redirect('registry')
|
return redirect('registry')
|
||||||
|
|
||||||
|
# Если статус не менялся (или не 'work'), просто сохраняем поля
|
||||||
|
self.object.save(update_fields=['material_taken', 'usable_waste', 'scrap_weight'])
|
||||||
return redirect('registry')
|
return redirect('registry')
|
||||||
|
|
||||||
if role == 'clerk':
|
if role == 'clerk':
|
||||||
|
|||||||
@@ -17,7 +17,10 @@
|
|||||||
|
|
||||||
{% if user_role in 'admin,technologist' %}
|
{% if user_role in 'admin,technologist' %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.resolver_match.url_name == 'planning' %}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 class="nav-item">
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'customers' or request.resolver_match.url_name == 'customer_deals' %}active{% endif %}" href="{% url 'customers' %}">Заказчик</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user