Фильтры сохраняются, мастер получил расширенные возможности
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' %}
|
||||
{% load l10n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
@@ -72,13 +73,29 @@
|
||||
</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 }}" 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>
|
||||
{% else %}
|
||||
<div class="alert alert-success">Статус: {{ item.get_status_display }}. Сделано: {{ item.quantity_fact }} шт.</div>
|
||||
<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 %}
|
||||
|
||||
@@ -124,7 +141,7 @@
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="small text-muted">Лом (кг)</label>
|
||||
<input type="number" step="0.01" name="scrap_weight" class="form-control border-secondary" value="{{ item.scrap_weight }}">
|
||||
<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>
|
||||
|
||||
|
||||
@@ -73,14 +73,63 @@
|
||||
</form>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
const form = document.getElementById('filter-form');
|
||||
const s = document.querySelector('input[name="start_date"]');
|
||||
const e = document.querySelector('input[name="end_date"]');
|
||||
const now = new Date();
|
||||
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(now.getDate()).padStart(2, '0');
|
||||
const today = `${now.getFullYear()}-${mm}-${dd}`;
|
||||
if (s && !s.value) s.value = today;
|
||||
|
||||
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;
|
||||
|
||||
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>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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">
|
||||
<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">
|
||||
<span class="small text-muted">Сделки:</span>
|
||||
<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 dealId = document.getElementById('dealId');
|
||||
const dealNumber = document.getElementById('dealNumber');
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<a class="btn btn-outline-secondary btn-sm" href="{% url 'planning' %}">
|
||||
<i class="bi bi-arrow-left me-1"></i>Назад
|
||||
</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>Добавить деталь
|
||||
</a>
|
||||
</div>
|
||||
@@ -43,7 +43,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% 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="small text-muted">{{ t.material.full_name|default:t.material.name }}</td>
|
||||
<td class="small">{{ t.size_value }}</td>
|
||||
@@ -88,12 +88,13 @@
|
||||
<div class="small text-muted mb-2" id="modalTaskTitle"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">Станок</label>
|
||||
<select class="form-select border-secondary" name="machine_id" required>
|
||||
<label class="form-label small text-muted d-block">Станок</label>
|
||||
<div class="d-flex flex-wrap gap-1" id="machineToggleGroup">
|
||||
{% 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 %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
@@ -113,10 +114,18 @@
|
||||
|
||||
<script>
|
||||
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');
|
||||
if (!modal) return;
|
||||
|
||||
modal.addEventListener('show.bs.modal', function (event) {
|
||||
modal.addEventListener('shown.bs.modal', function (event) {
|
||||
const btn = event.relatedTarget;
|
||||
const taskId = btn.getAttribute('data-task-id');
|
||||
const name = btn.getAttribute('data-task-name');
|
||||
@@ -128,8 +137,42 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
const qty = document.getElementById('modalQty');
|
||||
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>
|
||||
|
||||
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-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>
|
||||
{% if request.GET.next %}
|
||||
<a class="btn btn-outline-secondary btn-sm" href="{{ request.GET.next }}">
|
||||
<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>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="card-body p-4">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="next" value="{{ request.GET.next }}">
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<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 .views import (
|
||||
CompanyUpsertView,
|
||||
CustomerDealsView,
|
||||
CustomersView,
|
||||
DealDetailView,
|
||||
DealPlanningView,
|
||||
DealUpsertView,
|
||||
@@ -15,6 +17,7 @@ from .views import (
|
||||
RegistryPrintView,
|
||||
RegistryView,
|
||||
SteelGradeUpsertView,
|
||||
TaskItemsView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -23,9 +26,12 @@ urlpatterns = [
|
||||
|
||||
# Реестр
|
||||
path('registry/', RegistryView.as_view(), name='registry'),
|
||||
# Планирование
|
||||
# Сделки
|
||||
path('planning/', PlanningView.as_view(), name='planning'),
|
||||
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/task/add/', ProductionTaskCreateView.as_view(), name='task_add'),
|
||||
path('planning/deal/<int:pk>/json/', DealDetailView.as_view(), name='deal_json'),
|
||||
|
||||
@@ -286,6 +286,83 @@ class DealPlanningView(LoginRequiredMixin, TemplateView):
|
||||
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):
|
||||
def post(self, request, *args, **kwargs):
|
||||
profile = getattr(request.user, 'profile', None)
|
||||
@@ -366,7 +443,12 @@ class ProductionTaskCreateView(LoginRequiredMixin, FormView):
|
||||
task.extra_drawing = form.cleaned_data['extra_drawing']
|
||||
|
||||
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):
|
||||
@@ -608,14 +690,18 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
||||
return redirect('registry')
|
||||
|
||||
if role in ['operator', 'master']:
|
||||
if self.object.status != 'work':
|
||||
return redirect('registry')
|
||||
|
||||
material_taken = (request.POST.get('material_taken') or '').strip()
|
||||
usable_waste = (request.POST.get('usable_waste') or '').strip()
|
||||
scrap_weight_raw = (request.POST.get('scrap_weight') or '').strip()
|
||||
status = request.POST.get('status', self.object.status)
|
||||
|
||||
# Разрешаем мастеру редактировать операторские поля всегда,
|
||||
# оператору — только в процессе закрытия
|
||||
if role == 'operator' and self.object.status != 'work':
|
||||
return redirect('registry')
|
||||
|
||||
errors = []
|
||||
if role == 'operator' and self.object.status == 'work':
|
||||
if not material_taken:
|
||||
errors.append('Заполни поле "Взятый материал"')
|
||||
if not usable_waste:
|
||||
@@ -630,25 +716,26 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
||||
except ValueError:
|
||||
errors.append('Поле "Лом (кг)" должно быть числом')
|
||||
|
||||
status = request.POST.get('status', self.object.status)
|
||||
|
||||
if errors:
|
||||
context = self.get_context_data()
|
||||
context['errors'] = errors
|
||||
return self.render_to_response(context)
|
||||
|
||||
if material_taken:
|
||||
self.object.material_taken = material_taken
|
||||
if usable_waste:
|
||||
self.object.usable_waste = usable_waste
|
||||
if scrap_weight is not None:
|
||||
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.status = 'done'
|
||||
self.object.save()
|
||||
return redirect('registry')
|
||||
|
||||
if status == 'partial':
|
||||
if self.object.status == 'work' and status == 'partial':
|
||||
try:
|
||||
fact = int(request.POST.get('quantity_fact', '0'))
|
||||
except ValueError:
|
||||
@@ -677,6 +764,8 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
||||
|
||||
return redirect('registry')
|
||||
|
||||
# Если статус не менялся (или не 'work'), просто сохраняем поля
|
||||
self.object.save(update_fields=['material_taken', 'usable_waste', 'scrap_weight'])
|
||||
return redirect('registry')
|
||||
|
||||
if role == 'clerk':
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
|
||||
{% if user_role in 'admin,technologist' %}
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user