Теперь сделки на странице планирования
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:
@@ -11,9 +11,9 @@ class CompanyAdmin(admin.ModelAdmin):
|
|||||||
# --- Настройка отображения Сделок ---
|
# --- Настройка отображения Сделок ---
|
||||||
@admin.register(Deal)
|
@admin.register(Deal)
|
||||||
class DealAdmin(admin.ModelAdmin):
|
class DealAdmin(admin.ModelAdmin):
|
||||||
list_display = ('number', 'company')
|
list_display = ('number', 'status', 'company')
|
||||||
search_fields = ('number', 'company__name')
|
search_fields = ('number', 'company__name')
|
||||||
list_filter = ('company',)
|
list_filter = ('status', 'company')
|
||||||
|
|
||||||
# --- Задания на производство (База) ---
|
# --- Задания на производство (База) ---
|
||||||
@admin.register(ProductionTask)
|
@admin.register(ProductionTask)
|
||||||
|
|||||||
18
shiftflow/migrations/0009_deal_status.py
Normal file
18
shiftflow/migrations/0009_deal_status.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-31 05:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('shiftflow', '0008_alter_item_date'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='deal',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('lead', 'Зашла'), ('work', 'В работе'), ('done', 'Завершена')], default='work', max_length=10, verbose_name='Статус'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -40,11 +40,21 @@ class Deal(models.Model):
|
|||||||
Заказ или проект. Номер парсится из пути к файлам.
|
Заказ или проект. Номер парсится из пути к файлам.
|
||||||
Служит контейнером для группы деталей (позиций).
|
Служит контейнером для группы деталей (позиций).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('lead', 'Зашла'),
|
||||||
|
('work', 'В работе'),
|
||||||
|
('done', 'Завершена'),
|
||||||
|
]
|
||||||
|
|
||||||
number = models.CharField("№ Сделки", max_length=100, unique=True)
|
number = models.CharField("№ Сделки", max_length=100, unique=True)
|
||||||
|
status = models.CharField("Статус", max_length=10, choices=STATUS_CHOICES, default='work')
|
||||||
company = models.ForeignKey(Company, on_delete=models.PROTECT, verbose_name="Заказчик", null=True, blank=True)
|
company = models.ForeignKey(Company, on_delete=models.PROTECT, verbose_name="Заказчик", null=True, blank=True)
|
||||||
description = models.TextField("Описание сделки", blank=True, help_text="Общая информация по заказу")
|
description = models.TextField("Описание сделки", blank=True, help_text="Общая информация по заказу")
|
||||||
|
|
||||||
def __str__(self): return f"Сделка №{self.number} ({self.company})"
|
def __str__(self):
|
||||||
|
return f"Сделка №{self.number} ({self.company})"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Сделка"; verbose_name_plural = "Сделки"
|
verbose_name = "Сделка"; verbose_name_plural = "Сделки"
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,25 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<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">
|
||||||
<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>
|
||||||
<a class="btn btn-outline-accent btn-sm" href="{% url 'task_add' %}">
|
<form method="get" class="d-flex align-items-center gap-2">
|
||||||
<i class="bi bi-plus-lg me-1"></i>Добавить
|
<span class="small text-muted">Сделки:</span>
|
||||||
</a>
|
<div class="d-flex flex-wrap gap-1">
|
||||||
|
<input type="radio" class="btn-check" name="status" id="deal_s_work" value="work" {% if selected_status == 'work' %}checked{% endif %} onchange="this.form.submit()">
|
||||||
|
<label class="btn btn-outline-primary btn-sm" for="deal_s_work">В работе</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="status" id="deal_s_lead" value="lead" {% if selected_status == 'lead' %}checked{% endif %} onchange="this.form.submit()">
|
||||||
|
<label class="btn btn-outline-secondary btn-sm" for="deal_s_lead">Зашла</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="status" id="deal_s_done" value="done" {% if selected_status == 'done' %}checked{% endif %} onchange="this.form.submit()">
|
||||||
|
<label class="btn btn-outline-success btn-sm" for="deal_s_done">Завершена</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#dealModal" data-mode="create">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i>Добавить сделку
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
@@ -15,108 +30,232 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr class="table-custom-header">
|
<tr class="table-custom-header">
|
||||||
<th>Сделка</th>
|
<th>Сделка</th>
|
||||||
<th>Деталь</th>
|
<th>Заказчик</th>
|
||||||
<th>Материал</th>
|
<th>Описание</th>
|
||||||
<th>Размер</th>
|
<th style="width: 140px;">Статус</th>
|
||||||
<th class="text-center">Надо</th>
|
|
||||||
<th class="text-center">Сделано</th>
|
|
||||||
<th class="text-center">В плане</th>
|
|
||||||
<th class="text-center">Осталось</th>
|
|
||||||
<th class="text-end">Действия</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for t in tasks %}
|
{% for d in deals %}
|
||||||
<tr>
|
<tr class="planning-row" style="cursor:pointer" data-href="{% url 'planning_deal' d.id %}">
|
||||||
<td><span class="text-accent fw-bold">{{ t.deal.number }}</span></td>
|
<td><span class="text-accent fw-bold">{{ d.number }}</span></td>
|
||||||
<td class="fw-bold">{{ t.drawing_name|default:"Б/ч" }}</td>
|
<td class="small">{{ d.company.name|default:"-" }}</td>
|
||||||
<td class="small text-muted">{{ t.material.full_name|default:t.material.name }}</td>
|
<td class="small text-muted">{{ d.description|default:"" }}</td>
|
||||||
<td class="small">{{ t.size_value }}</td>
|
<td>
|
||||||
<td class="text-center">{{ t.quantity_ordered }}</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 class="text-center">{{ t.done_qty }}</td>
|
|
||||||
<td class="text-center">{{ t.planned_qty }}</td>
|
|
||||||
<td class="text-center">{{ t.remaining_qty }}</td>
|
|
||||||
<td class="text-end">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-accent btn-sm"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#addToPlanModal"
|
|
||||||
data-task-id="{{ t.id }}"
|
|
||||||
data-task-name="{{ t.drawing_name|default:'Б/ч' }}"
|
|
||||||
data-task-deal="{{ t.deal.number }}"
|
|
||||||
data-task-rem="{{ t.remaining_qty }}"
|
|
||||||
>
|
|
||||||
<i class="bi bi-plus-lg me-1"></i>В план
|
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr><td colspan="9" class="text-center p-5 text-muted">Заданий не найдено</td></tr>
|
<tr><td colspan="4" class="text-center p-5 text-muted">Сделок не найдено</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal fade" id="dealModal" tabindex="-1" aria-hidden="true">
|
||||||
<div class="modal fade" id="addToPlanModal" tabindex="-1" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<form method="post" action="{% url 'planning_add' %}" class="modal-content border-secondary">
|
<div class="modal-content border-secondary">
|
||||||
{% csrf_token %}
|
|
||||||
<div class="modal-header border-secondary">
|
<div class="modal-header border-secondary">
|
||||||
<h5 class="modal-title">Добавить в план</h5>
|
<h5 class="modal-title">Сделка</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<input type="hidden" name="task_id" id="modalTaskId">
|
<input type="hidden" id="dealId">
|
||||||
|
|
||||||
<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">№ Сделки</label>
|
||||||
<select class="form-select border-secondary" name="machine_id" required>
|
<input type="text" class="form-control border-secondary" id="dealNumber">
|
||||||
{% for m in machines %}
|
</div>
|
||||||
<option value="{{ m.id }}">{{ m.name }}</option>
|
<div class="mb-3">
|
||||||
{% endfor %}
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
<div class="mb-2">
|
<label class="form-label small text-muted">Компания</label>
|
||||||
<label class="form-label small text-muted">Сколько в план (шт)</label>
|
<div class="d-flex gap-2">
|
||||||
<input type="number" min="1" class="form-control border-secondary" name="quantity_plan" id="modalQty" required>
|
<select class="form-select border-secondary" id="dealCompany">
|
||||||
|
<option value="">— не выбрано —</option>
|
||||||
|
{% for c in companies %}
|
||||||
|
<option value="{{ c.id }}">{{ c.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<button type="button" class="btn btn-outline-accent btn-sm" id="openCompanyModalBtn">Создать</button>
|
||||||
|
</div>
|
||||||
|
</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="small text-muted" id="modalHint"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer border-secondary">
|
<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-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||||
<button type="submit" class="btn btn-outline-accent">Добавить</button>
|
<button type="button" class="btn btn-outline-accent" id="dealSaveBtn">Сохранить</button>
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
<input type="hidden" id="companyId">
|
||||||
|
<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>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const modal = document.getElementById('addToPlanModal');
|
document.querySelectorAll('tr.planning-row[data-href]').forEach(function (row) {
|
||||||
if (!modal) return;
|
row.addEventListener('click', function () {
|
||||||
|
const href = row.getAttribute('data-href');
|
||||||
modal.addEventListener('show.bs.modal', function (event) {
|
if (href) window.location.href = href;
|
||||||
const btn = event.relatedTarget;
|
|
||||||
const taskId = btn.getAttribute('data-task-id');
|
|
||||||
const name = btn.getAttribute('data-task-name');
|
|
||||||
const deal = btn.getAttribute('data-task-deal');
|
|
||||||
const rem = btn.getAttribute('data-task-rem');
|
|
||||||
|
|
||||||
document.getElementById('modalTaskId').value = taskId;
|
|
||||||
document.getElementById('modalTaskTitle').textContent = `Сделка ${deal} · ${name}`;
|
|
||||||
document.getElementById('modalHint').textContent = rem !== null ? `Осталось: ${rem} шт` : '';
|
|
||||||
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');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dealModal = document.getElementById('dealModal');
|
||||||
|
const dealId = document.getElementById('dealId');
|
||||||
|
const dealNumber = document.getElementById('dealNumber');
|
||||||
|
const dealStatus = document.getElementById('dealStatus');
|
||||||
|
const dealCompany = document.getElementById('dealCompany');
|
||||||
|
const dealDescription = document.getElementById('dealDescription');
|
||||||
|
const dealSaveBtn = document.getElementById('dealSaveBtn');
|
||||||
|
const openCompanyModalBtn = document.getElementById('openCompanyModalBtn');
|
||||||
|
|
||||||
|
const companyModal = document.getElementById('companyModal');
|
||||||
|
const companyId = document.getElementById('companyId');
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertSelectOption(select, id, label) {
|
||||||
|
if (!select) return;
|
||||||
|
let opt = select.querySelector(`option[value="${id}"]`);
|
||||||
|
if (!opt) {
|
||||||
|
opt = document.createElement('option');
|
||||||
|
opt.value = String(id);
|
||||||
|
select.appendChild(opt);
|
||||||
|
}
|
||||||
|
opt.textContent = label;
|
||||||
|
select.value = String(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openChildModal(parentEl, childEl) {
|
||||||
|
if (!parentEl || !childEl) return;
|
||||||
|
childEl.dataset.returnTo = parentEl.id;
|
||||||
|
const parent = bootstrap.Modal.getOrCreateInstance(parentEl);
|
||||||
|
const child = bootstrap.Modal.getOrCreateInstance(childEl);
|
||||||
|
parentEl.addEventListener('hidden.bs.modal', function () {
|
||||||
|
child.show();
|
||||||
|
}, { once: true });
|
||||||
|
parent.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function returnToParent(childEl) {
|
||||||
|
const returnId = childEl ? childEl.dataset.returnTo : '';
|
||||||
|
if (!returnId) return;
|
||||||
|
delete childEl.dataset.returnTo;
|
||||||
|
const parentEl = document.getElementById(returnId);
|
||||||
|
if (parentEl) bootstrap.Modal.getOrCreateInstance(parentEl).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (companyModal) companyModal.addEventListener('hidden.bs.modal', function () { returnToParent(companyModal); });
|
||||||
|
|
||||||
|
if (openCompanyModalBtn) {
|
||||||
|
openCompanyModalBtn.addEventListener('click', function () {
|
||||||
|
openChildModal(dealModal, companyModal);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dealModal) {
|
||||||
|
dealModal.addEventListener('show.bs.modal', function (event) {
|
||||||
|
if (!event.relatedTarget) return;
|
||||||
|
const mode = event.relatedTarget.getAttribute('data-mode') || 'create';
|
||||||
|
if (mode !== 'create') return;
|
||||||
|
dealId.value = '';
|
||||||
|
dealNumber.value = '';
|
||||||
|
dealDescription.value = '';
|
||||||
|
if (dealStatus) dealStatus.value = 'work';
|
||||||
|
if (dealCompany) dealCompany.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (companyModal) {
|
||||||
|
companyModal.addEventListener('show.bs.modal', function () {
|
||||||
|
companyId.value = '';
|
||||||
|
companyName.value = '';
|
||||||
|
companyDescription.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (companySaveBtn) {
|
||||||
|
companySaveBtn.addEventListener('click', async function () {
|
||||||
|
const payload = {
|
||||||
|
id: companyId.value,
|
||||||
|
name: companyName.value,
|
||||||
|
description: companyDescription.value,
|
||||||
|
};
|
||||||
|
const data = await postForm('{% url "company_upsert" %}', payload);
|
||||||
|
upsertSelectOption(dealCompany, data.id, data.label);
|
||||||
|
bootstrap.Modal.getInstance(companyModal).hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dealSaveBtn) {
|
||||||
|
dealSaveBtn.addEventListener('click', async function () {
|
||||||
|
const payload = {
|
||||||
|
id: dealId.value,
|
||||||
|
number: dealNumber.value,
|
||||||
|
status: dealStatus ? dealStatus.value : 'work',
|
||||||
|
company_id: dealCompany.value,
|
||||||
|
description: dealDescription.value,
|
||||||
|
};
|
||||||
|
await postForm('{% url "deal_upsert" %}', payload);
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
136
shiftflow/templates/shiftflow/planning_deal.html
Normal file
136
shiftflow/templates/shiftflow/planning_deal.html
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
{% 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-briefcase me-2"></i>Сделка {{ deal.number }}
|
||||||
|
</h3>
|
||||||
|
<div class="small text-muted">
|
||||||
|
{% if deal.company %}{{ deal.company.name }}{% else %}—{% endif %}
|
||||||
|
{% if deal.description %} · {{ deal.description }}{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<span class="badge {% if deal.status == 'work' %}bg-primary{% elif deal.status == 'done' %}bg-success{% else %}bg-secondary{% endif %} align-self-center">
|
||||||
|
{{ deal.get_status_display }}
|
||||||
|
</span>
|
||||||
|
<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 }}">
|
||||||
|
<i class="bi bi-plus-lg 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>Размер</th>
|
||||||
|
<th class="text-center">Надо</th>
|
||||||
|
<th class="text-center">Сделано</th>
|
||||||
|
<th class="text-center">В плане</th>
|
||||||
|
<th class="text-center">Осталось</th>
|
||||||
|
<th class="text-end">Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for t in tasks %}
|
||||||
|
<tr>
|
||||||
|
<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>
|
||||||
|
<td class="text-center">{{ t.quantity_ordered }}</td>
|
||||||
|
<td class="text-center">{{ t.done_qty }}</td>
|
||||||
|
<td class="text-center">{{ t.planned_qty }}</td>
|
||||||
|
<td class="text-center">{{ t.remaining_qty }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-accent btn-sm"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#addToPlanModal"
|
||||||
|
data-task-id="{{ t.id }}"
|
||||||
|
data-task-name="{{ t.drawing_name|default:'Б/ч' }}"
|
||||||
|
data-task-rem="{{ t.remaining_qty }}"
|
||||||
|
>
|
||||||
|
<i class="bi bi-plus-lg me-1"></i>В план
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="8" class="text-center p-5 text-muted">Деталей не найдено</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="addToPlanModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<form method="post" action="{% url 'planning_add' %}" class="modal-content border-secondary">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="next" value="{{ request.get_full_path }}">
|
||||||
|
<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">
|
||||||
|
<input type="hidden" name="task_id" id="modalTaskId">
|
||||||
|
<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>
|
||||||
|
{% for m in machines %}
|
||||||
|
<option value="{{ m.id }}">{{ m.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label small text-muted">Сколько в план (шт)</label>
|
||||||
|
<input type="number" min="1" class="form-control border-secondary" name="quantity_plan" id="modalQty" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="small text-muted" id="modalHint"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-secondary">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||||
|
<button type="submit" class="btn btn-outline-accent">Добавить</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const modal = document.getElementById('addToPlanModal');
|
||||||
|
if (!modal) return;
|
||||||
|
|
||||||
|
modal.addEventListener('show.bs.modal', function (event) {
|
||||||
|
const btn = event.relatedTarget;
|
||||||
|
const taskId = btn.getAttribute('data-task-id');
|
||||||
|
const name = btn.getAttribute('data-task-name');
|
||||||
|
const rem = btn.getAttribute('data-task-rem');
|
||||||
|
|
||||||
|
document.getElementById('modalTaskId').value = taskId;
|
||||||
|
document.getElementById('modalTaskTitle').textContent = name;
|
||||||
|
document.getElementById('modalHint').textContent = rem !== null ? `Осталось: ${rem} шт` : '';
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -135,6 +135,14 @@
|
|||||||
<label class="form-label small text-muted">№ Сделки</label>
|
<label class="form-label small text-muted">№ Сделки</label>
|
||||||
<input type="text" class="form-control border-secondary" id="dealNumber">
|
<input type="text" class="form-control border-secondary" id="dealNumber">
|
||||||
</div>
|
</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-3">
|
<div class="mb-3">
|
||||||
<label class="form-label small text-muted">Компания</label>
|
<label class="form-label small text-muted">Компания</label>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
@@ -311,6 +319,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
const dealNumber = document.getElementById('dealNumber');
|
const dealNumber = document.getElementById('dealNumber');
|
||||||
const dealCompany = document.getElementById('dealCompany');
|
const dealCompany = document.getElementById('dealCompany');
|
||||||
const dealDescription = document.getElementById('dealDescription');
|
const dealDescription = document.getElementById('dealDescription');
|
||||||
|
const dealStatus = document.getElementById('dealStatus');
|
||||||
const dealSaveBtn = document.getElementById('dealSaveBtn');
|
const dealSaveBtn = document.getElementById('dealSaveBtn');
|
||||||
|
|
||||||
const companyModal = document.getElementById('companyModal');
|
const companyModal = document.getElementById('companyModal');
|
||||||
@@ -362,7 +371,13 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
if (fillFromPdf) fillFromPdf.disabled = !hasPdf;
|
if (fillFromPdf) fillFromPdf.disabled = !hasPdf;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (drawingFile) drawingFile.addEventListener('change', updateFileButtons);
|
if (drawingFile) drawingFile.addEventListener('change', function () {
|
||||||
|
updateFileButtons();
|
||||||
|
if (!drawingName) return;
|
||||||
|
if (drawingName.value && drawingName.value.trim() !== '') return;
|
||||||
|
if (!drawingFile.files || drawingFile.files.length === 0) return;
|
||||||
|
drawingName.value = filenameBase(drawingFile.files[0].name);
|
||||||
|
});
|
||||||
if (extraDrawing) extraDrawing.addEventListener('change', updateFileButtons);
|
if (extraDrawing) extraDrawing.addEventListener('change', updateFileButtons);
|
||||||
updateFileButtons();
|
updateFileButtons();
|
||||||
|
|
||||||
@@ -459,6 +474,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
dealNumber.value = '';
|
dealNumber.value = '';
|
||||||
dealCompany.value = '';
|
dealCompany.value = '';
|
||||||
dealDescription.value = '';
|
dealDescription.value = '';
|
||||||
|
if (dealStatus) dealStatus.value = 'work';
|
||||||
|
|
||||||
if (mode === 'edit' && dealSelect && dealSelect.value) {
|
if (mode === 'edit' && dealSelect && dealSelect.value) {
|
||||||
const data = await getJson(`/planning/deal/${dealSelect.value}/json/`);
|
const data = await getJson(`/planning/deal/${dealSelect.value}/json/`);
|
||||||
@@ -466,6 +482,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
dealNumber.value = data.number || '';
|
dealNumber.value = data.number || '';
|
||||||
dealCompany.value = data.company_id ? String(data.company_id) : '';
|
dealCompany.value = data.company_id ? String(data.company_id) : '';
|
||||||
dealDescription.value = data.description || '';
|
dealDescription.value = data.description || '';
|
||||||
|
if (dealStatus) dealStatus.value = data.status || 'work';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -476,6 +493,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
id: dealId.value,
|
id: dealId.value,
|
||||||
number: dealNumber.value,
|
number: dealNumber.value,
|
||||||
company_id: dealCompany.value,
|
company_id: dealCompany.value,
|
||||||
|
status: dealStatus ? dealStatus.value : 'work',
|
||||||
description: dealDescription.value,
|
description: dealDescription.value,
|
||||||
};
|
};
|
||||||
const data = await postForm('{% url "deal_upsert" %}', payload);
|
const data = await postForm('{% url "deal_upsert" %}', payload);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from django.urls import path
|
|||||||
from .views import (
|
from .views import (
|
||||||
CompanyUpsertView,
|
CompanyUpsertView,
|
||||||
DealDetailView,
|
DealDetailView,
|
||||||
|
DealPlanningView,
|
||||||
DealUpsertView,
|
DealUpsertView,
|
||||||
IndexView,
|
IndexView,
|
||||||
ItemUpdateView,
|
ItemUpdateView,
|
||||||
@@ -24,6 +25,7 @@ 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/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'),
|
||||||
|
|||||||
@@ -232,7 +232,37 @@ class PlanningView(LoginRequiredMixin, TemplateView):
|
|||||||
role = profile.role if profile else ('admin' if self.request.user.is_superuser else 'operator')
|
role = profile.role if profile else ('admin' if self.request.user.is_superuser else 'operator')
|
||||||
context['user_role'] = role
|
context['user_role'] = role
|
||||||
|
|
||||||
tasks = ProductionTask.objects.select_related('deal', 'material').annotate(
|
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(status=status).order_by('-id')
|
||||||
|
context['companies'] = Company.objects.all().order_by('name')
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class DealPlanningView(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = 'shiftflow/planning_deal.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
|
||||||
|
|
||||||
|
deal = get_object_or_404(Deal.objects.select_related('company'), pk=self.kwargs['pk'])
|
||||||
|
context['deal'] = deal
|
||||||
|
|
||||||
|
tasks = ProductionTask.objects.filter(deal=deal).select_related('material').annotate(
|
||||||
done_qty=Coalesce(Sum('items__quantity_fact'), 0),
|
done_qty=Coalesce(Sum('items__quantity_fact'), 0),
|
||||||
planned_qty=Coalesce(
|
planned_qty=Coalesce(
|
||||||
Sum(
|
Sum(
|
||||||
@@ -249,7 +279,7 @@ class PlanningView(LoginRequiredMixin, TemplateView):
|
|||||||
F('quantity_ordered') - F('done_qty') - F('planned_qty'),
|
F('quantity_ordered') - F('done_qty') - F('planned_qty'),
|
||||||
output_field=IntegerField(),
|
output_field=IntegerField(),
|
||||||
)
|
)
|
||||||
)
|
).order_by('-id')
|
||||||
|
|
||||||
context['tasks'] = tasks
|
context['tasks'] = tasks
|
||||||
context['machines'] = Machine.objects.all()
|
context['machines'] = Machine.objects.all()
|
||||||
@@ -284,6 +314,10 @@ class PlanningAddView(LoginRequiredMixin, View):
|
|||||||
is_synced_1c=False,
|
is_synced_1c=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
next_url = request.POST.get('next') or ''
|
||||||
|
if next_url.startswith('/planning/deal/'):
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
return redirect('planning')
|
return redirect('planning')
|
||||||
|
|
||||||
|
|
||||||
@@ -292,6 +326,13 @@ class ProductionTaskCreateView(LoginRequiredMixin, FormView):
|
|||||||
form_class = ProductionTaskCreateForm
|
form_class = ProductionTaskCreateForm
|
||||||
success_url = reverse_lazy('planning')
|
success_url = reverse_lazy('planning')
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
initial = super().get_initial()
|
||||||
|
deal_id = self.request.GET.get('deal')
|
||||||
|
if deal_id and str(deal_id).isdigit():
|
||||||
|
initial['deal'] = int(deal_id)
|
||||||
|
return initial
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
profile = getattr(request.user, 'profile', None)
|
profile = getattr(request.user, 'profile', None)
|
||||||
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||||
@@ -339,6 +380,7 @@ class DealDetailView(LoginRequiredMixin, View):
|
|||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'id': deal.id,
|
'id': deal.id,
|
||||||
'number': deal.number,
|
'number': deal.number,
|
||||||
|
'status': deal.status,
|
||||||
'company_id': deal.company_id,
|
'company_id': deal.company_id,
|
||||||
'description': deal.description or '',
|
'description': deal.description or '',
|
||||||
})
|
})
|
||||||
@@ -355,6 +397,7 @@ class DealUpsertView(LoginRequiredMixin, View):
|
|||||||
number = (request.POST.get('number') or '').strip()
|
number = (request.POST.get('number') or '').strip()
|
||||||
description = (request.POST.get('description') or '').strip()
|
description = (request.POST.get('description') or '').strip()
|
||||||
company_id = request.POST.get('company_id')
|
company_id = request.POST.get('company_id')
|
||||||
|
status = (request.POST.get('status') or 'work').strip()
|
||||||
|
|
||||||
if not number:
|
if not number:
|
||||||
return JsonResponse({'error': 'number_required'}, status=400)
|
return JsonResponse({'error': 'number_required'}, status=400)
|
||||||
@@ -365,6 +408,11 @@ class DealUpsertView(LoginRequiredMixin, View):
|
|||||||
else:
|
else:
|
||||||
deal, _ = Deal.objects.get_or_create(number=number)
|
deal, _ = Deal.objects.get_or_create(number=number)
|
||||||
|
|
||||||
|
allowed = {k for k, _ in Deal.STATUS_CHOICES}
|
||||||
|
if status not in allowed:
|
||||||
|
status = 'work'
|
||||||
|
|
||||||
|
deal.status = status
|
||||||
deal.description = description
|
deal.description = description
|
||||||
if company_id and str(company_id).isdigit():
|
if company_id and str(company_id).isdigit():
|
||||||
deal.company_id = int(company_id)
|
deal.company_id = int(company_id)
|
||||||
|
|||||||
Reference in New Issue
Block a user