Files
MES_Core/shiftflow/templates/shiftflow/planning_deal.html
ackFromRedmi 6da7b775c7
All checks were successful
Deploy MES Core / deploy (push) Successful in 4m16s
Ввел логику сделок через партии
2026-04-22 08:35:07 +03:00

1054 lines
58 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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>
{% if deal.status == 'lead' and user_role in 'admin,prod_head,technologist,master,clerk' %}
<form method="post" class="d-inline">
{% csrf_token %}
<input type="hidden" name="action" value="set_work">
<button type="submit" class="btn btn-outline-accent btn-sm">
<i class="bi bi-arrow-right-circle me-1"></i>В работу
</button>
</form>
{% endif %}
<a class="btn btn-outline-secondary btn-sm" href="{% url 'planning' %}">
<i class="bi bi-arrow-left me-1"></i>Назад
</a>
{% if user_role in 'admin,prod_head,technologist,master' %}
<form method="post" class="d-inline">
{% csrf_token %}
<input type="hidden" name="action" value="explode_deal">
<button type="submit" class="btn btn-outline-warning btn-sm" title="Пересчитать потребности снабжения">
<i class="bi bi-lightning me-1"></i>Вскрыть BOM
</button>
</form>
{% endif %}
{% if user_role in 'admin,clerk,manager,prod_head,technologist' %}
<a class="btn btn-outline-secondary btn-sm" href="{% url 'shipping' %}?deal_id={{ deal.id }}">
<i class="bi bi-truck me-1"></i>Отгрузка
</a>
{% endif %}
{% if user_role in 'admin,technologist,manager,prod_head' %}
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#dealItemModal">
<i class="bi bi-plus-lg me-1"></i>Добавить задание
</button>
{% endif %}
</div>
</div>
<div class="card-body p-0">
<div class="p-3">
<div class="card border-secondary mb-3">
<div class="card-header border-secondary py-2 d-flex justify-content-between align-items-center">
<strong>Позиции сделки</strong>
<div class="small text-muted">Изделие / СБ / Деталь</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle" data-sortable="1">
<thead>
<tr class="table-custom-header">
<th>Позиция</th>
<th data-sort="false" style="width: 160px;">Прогресс</th>
<th class="text-center">Заказано / Сделано / В плане</th>
<th class="text-center">Осталось</th>
<th data-sort="false" class="text-end">Действия</th>
</tr>
</thead>
<tbody>
{% for it in deal_items %}
<tr class="deal-entity-row" role="button" data-href="{% url 'product_info' it.entity.id %}?next={{ request.get_full_path|urlencode }}">
<td>
<div class="fw-bold">{{ it.entity.drawing_number|default:"—" }} {{ it.entity.name }}</div>
<div class="small text-muted">{{ it.entity.get_entity_type_display }}</div>
</td>
<td>
<div class="progress bg-secondary-subtle border border-secondary sf-progress" style="height: 10px;" data-done-width="{{ it.done_width }}" data-plan-width="{{ it.plan_width }}" title="Сделано: {{ it.done_qty }} · В плане: {{ it.planned_qty }}">
<div class="progress-bar bg-success sf-progress-done"></div>
<div class="progress-bar bg-warning sf-progress-plan"></div>
</div>
</td>
<td class="text-center">
<span class="text-info fw-bold">{{ it.quantity }}</span> /
<span class="text-success">{{ it.done_qty }}</span> /
<span class="text-warning">{{ it.planned_qty }}</span>
</td>
<td class="text-center">{{ it.remaining_qty }}</td>
<td class="text-end" onclick="event.stopPropagation();">
<div class="d-flex justify-content-end gap-1 flex-wrap" onclick="event.stopPropagation();">
{% if user_role in 'admin,technologist,manager,prod_head' %}
<button
type="button"
class="btn btn-outline-accent btn-sm"
data-bs-toggle="modal"
data-bs-target="#startProductionModal"
data-entity-id="{{ it.entity.id }}"
data-entity-label="{{ it.entity.drawing_number|default:'—' }} {{ it.entity.name }}"
>
<i class="bi bi-play-fill me-1"></i>В производство
</button>
<form method="post" action="{% url 'deal_item_upsert' %}" class="d-inline-flex gap-1 align-items-center" onclick="event.stopPropagation();">
{% csrf_token %}
<input type="hidden" name="action" value="set_qty">
<input type="hidden" name="deal_id" value="{{ deal.id }}">
<input type="hidden" name="entity_id" value="{{ it.entity.id }}">
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<input class="form-control form-control-sm bg-body text-body border-secondary" style="width:90px;" type="number" min="1" name="quantity" value="{{ it.quantity }}" title="Кол-во по сделке" required>
<button class="btn btn-outline-secondary btn-sm" type="submit" title="Обновить количество">OK</button>
</form>
<button
type="button"
class="btn btn-outline-danger btn-sm"
data-bs-toggle="modal"
data-bs-target="#dealItemDeleteModal"
data-deal-id="{{ deal.id }}"
data-entity-id="{{ it.entity.id }}"
data-next="{{ request.get_full_path }}"
data-entity-label="{{ it.entity.drawing_number|default:'—' }} {{ it.entity.name }}"
title="Удалить из сделки"
>
<i class="bi bi-trash"></i>
</button>
{% else %}
<button type="button" class="btn btn-outline-secondary btn-sm" disabled>В производство</button>
{% endif %}
</div>
</td>
</tr>
{% empty %}
<tr><td colspan="5" class="text-center text-muted py-4">Пока нет позиций</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="p-3 pt-0">
<div class="card border-secondary mb-3">
<div class="card-header border-secondary py-2 d-flex justify-content-between align-items-center">
<strong>Партии поставки</strong>
{% if user_role in 'admin,technologist' %}
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#dealBatchModal">
<i class="bi bi-plus-lg me-1"></i>Добавить партию
</button>
{% endif %}
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th style="width:160px;">Отгрузка</th>
<th>Партия</th>
<th style="width:220px;">Запущено</th>
<th>Состав партии</th>
<th data-sort="false" class="text-end"></th>
</tr>
</thead>
<tbody>
{% for b in delivery_batches %}
<tr>
<td class="fw-bold">{{ b.due_date|date:"d.m.Y" }}</td>
<td>
{{ b.name|default:"—" }}
{% if b.is_default %}
<span class="badge bg-secondary ms-2">по умолчанию</span>
{% endif %}
</td>
<td>
<div class="small text-muted mb-1">{{ b.total_started }} / {{ b.total_qty }} (осталось {{ b.total_remaining }})</div>
<div class="progress bg-secondary-subtle border border-secondary" style="height: 10px;">
<div class="progress-bar bg-warning" style="width: {{ b.started_pct }}%"></div>
</div>
</td>
<td>
{% if b.items_list %}
<div class="table-responsive">
<table class="table table-sm mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th style="width:110px;">Тип</th>
<th style="width:160px;">Обозначение</th>
<th>Наименование</th>
<th class="text-center" style="width:80px;">Кол-во</th>
<th class="text-center" style="width:90px;">Запущено</th>
<th class="text-center" style="width:90px;">Осталось</th>
<th style="width:160px;">Прогресс</th>
<th data-sort="false" class="text-end" style="width:160px;"></th>
</tr>
</thead>
<tbody>
{% for bi in b.items_list %}
<tr>
<td class="small text-muted">{{ bi.entity.get_entity_type_display }}</td>
<td class="fw-bold">{{ bi.entity.drawing_number|default:"—" }}</td>
<td>{{ bi.entity.name }}</td>
<td class="text-center">{{ bi.quantity }}</td>
<td class="text-center">{{ bi.started_qty }}</td>
<td class="text-center">{{ bi.remaining_to_start }}</td>
<td>
<div class="progress bg-secondary-subtle border border-secondary" style="height: 10px;">
<div class="progress-bar bg-warning" style="width: {{ bi.started_pct }}%"></div>
</div>
</td>
<td class="text-end">
{% if user_role in 'admin,technologist' %}
{% if bi.started_qty and bi.started_qty > 0 %}
<button
type="button"
class="btn btn-outline-warning btn-sm"
data-bs-toggle="modal"
data-bs-target="#rollbackProductionModal{{ bi.id }}"
title="Откатить запуск в производство"
>
<i class="bi bi-arrow-counterclockwise"></i>
</button>
{% endif %}
{% endif %}
{% if user_role in 'admin,technologist' and not b.is_default %}
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#dealBatchItemModal" data-batch-id="{{ b.id }}">Добавить</button>
<form method="post" action="{% url 'deal_batch_action' %}" class="d-inline">
{% csrf_token %}
<input type="hidden" name="action" value="delete_batch_item">
<input type="hidden" name="deal_id" value="{{ deal.id }}">
<input type="hidden" name="item_id" value="{{ bi.id }}">
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<button class="btn btn-outline-secondary btn-sm" type="submit">Удалить</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-muted">Пусто</div>
{% endif %}
{% for bi in b.items_list %}
{% if user_role in 'admin,technologist' and bi.started_qty and bi.started_qty > 0 %}
<div class="modal fade" id="rollbackProductionModal{{ bi.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<form method="post" action="{% url 'deal_batch_action' %}" class="modal-content border-secondary">
{% csrf_token %}
<input type="hidden" name="action" value="rollback_batch_item_production">
<input type="hidden" name="deal_id" value="{{ deal.id }}">
<input type="hidden" name="item_id" value="{{ bi.id }}">
<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">
<div class="small text-muted mb-2">{{ bi.entity.drawing_number|default:"—" }} {{ bi.entity.name }}</div>
<div class="mb-3">
<label class="form-label">Сколько откатить, шт</label>
<input class="form-control bg-body text-body border-secondary" type="number" min="1" max="{{ bi.started_qty }}" name="quantity" value="{{ bi.started_qty }}" required>
<div class="form-text">Запущено в партии: {{ bi.started_qty }} шт</div>
</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-warning">Откатить</button>
</div>
</form>
</div>
</div>
{% endif %}
{% endfor %}
</td>
<td class="text-end">
{% if user_role in 'admin,technologist' and not b.is_default %}
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#dealBatchItemModal" data-batch-id="{{ b.id }}">Добавить</button>
<form method="post" action="{% url 'deal_batch_action' %}" class="d-inline">
{% csrf_token %}
<input type="hidden" name="action" value="delete_batch">
<input type="hidden" name="deal_id" value="{{ deal.id }}">
<input type="hidden" name="batch_id" value="{{ b.id }}">
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<button class="btn btn-outline-secondary btn-sm" type="submit">Удалить</button>
</form>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="5" class="text-center text-muted py-3">Партий пока нет</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% for g in workshop_task_groups %}
<div class="card border-secondary mb-3">
<div class="card-header border-secondary py-2 d-flex justify-content-between align-items-center">
<strong>{{ g.name }}</strong>
<div class="small text-muted">{{ g.tasks|length }} задач</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle" data-sortable="1">
<thead>
<tr class="table-custom-header">
<th>Позиция</th>
<th>Операция</th>
<th data-sort="false" style="width: 160px;">Прогресс</th>
<th class="text-center">Заказано / Сделано / В смене</th>
<th class="text-center">Осталось</th>
<th data-sort="false" class="text-center">Файлы</th>
<th data-sort="false" class="text-end">Действия</th>
</tr>
</thead>
<tbody>
{% for t in g.tasks %}
<tr class="task-row" style="cursor:pointer" {% if t.entity_id %}data-href="{% url 'product_info' t.entity_id %}?next={{ request.get_full_path|urlencode }}"{% endif %}>
<td>
<div class="fw-bold">
{% if t.entity %}
{{ t.entity.drawing_number|default:"—" }} {{ t.entity.name }}
{% else %}
{{ t.drawing_name|default:"Б/ч" }}
{% endif %}
</div>
<div class="small text-muted">
{% if t.material %}{{ t.material.full_name|default:t.material.name }}{% else %}—{% endif %}
</div>
</td>
<td class="small">{{ t.current_operation_name|default:"—" }}</td>
<td>
<div class="progress bg-secondary-subtle border border-secondary sf-progress" style="height: 10px;" data-done-width="{{ t.done_width }}" data-plan-width="{{ t.plan_width }}" title="Сделано: {{ t.done_pct }}% · В смене: {{ t.plan_pct }}%">
<div class="progress-bar bg-success sf-progress-done"></div>
<div class="progress-bar bg-warning sf-progress-plan"></div>
</div>
</td>
<td class="text-center">
<span class="text-info fw-bold">{{ t.quantity_ordered }}</span> /
<span class="text-success">{{ t.done_qty }}</span> /
<span class="text-warning">{{ t.planned_qty }}</span>
</td>
<td class="text-center">{{ t.remaining_qty }}</td>
<td class="text-center">
{% if t.drawing_file %}
<a href="{{ t.drawing_file.url }}" target="_blank" class="btn btn-sm btn-outline-info p-1 stop-prop" title="DXF/IGES">
<i class="bi bi-file-earmark-code"></i>
</a>
{% endif %}
{% if t.extra_drawing %}
<a href="{{ t.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-end">
{% if user_role in 'admin,technologist' %}
{% if t.current_operation_id and t.entity_id %}
<button
type="button"
class="btn btn-outline-accent btn-sm"
data-bs-toggle="modal"
data-bs-target="#workItemModal"
data-entity-id="{{ t.entity_id }}"
data-batch-id="{{ t.delivery_batch_id|default:'' }}"
data-operation-id="{{ t.current_operation_id }}"
data-workshop-id="{{ t.current_workshop_id|default:'' }}"
data-workshop-name="{{ t.current_workshop_name|default:'' }}"
data-task-name="{% if t.entity %}{{ t.entity.drawing_number|default:'—' }} {{ t.entity.name }}{% else %}{{ t.drawing_name|default:'Б/ч' }}{% endif %}"
data-operation-name="{{ t.current_operation_name|default:'' }}"
data-task-rem="{{ t.remaining_qty }}"
>
<i class="bi bi-plus-lg me-1"></i>В смену
</button>
{% else %}
<button type="button" class="btn btn-outline-secondary btn-sm" disabled>В смену</button>
{% endif %}
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="7" class="text-center p-4 text-muted">Задач нет</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% empty %}
<div class="text-center p-5 text-muted">Задач нет</div>
{% endfor %}
</div>
</div>
<div class="modal fade" id="startProductionModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<form method="post" action="{% url 'deal_batch_action' %}" class="modal-content border-secondary">
{% csrf_token %}
<input type="hidden" name="action" value="start_batch_item_production">
<input type="hidden" name="deal_id" value="{{ deal.id }}">
<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">
<div class="small text-muted mb-2" id="spTitle"></div>
<div class="mb-3">
<label class="form-label">Партия</label>
<select class="form-select bg-body text-body border-secondary" name="item_id" id="spBatchItem" required>
{% for b in delivery_batches %}
{% for bi in b.items_list %}
{% if bi.remaining_to_start > 0 %}
<option value="{{ bi.id }}" data-entity-id="{{ bi.entity_id }}" data-rem="{{ bi.remaining_to_start }}">
{{ b.due_date|date:"d.m.Y" }}{% if b.name %} · {{ b.name }}{% endif %} — осталось {{ bi.remaining_to_start }} шт
</option>
{% endif %}
{% endfor %}
{% endfor %}
</select>
<div class="form-text">Если списка нет — сначала создай партию и добавь туда позицию сделки.</div>
</div>
<div class="mb-3">
<label class="form-label">Количество, шт</label>
<input class="form-control bg-body text-body border-secondary" type="number" min="1" name="quantity" id="spQty" required>
</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" id="spSubmit">Запустить</button>
</div>
</form>
</div>
</div>
<div class="modal fade" id="workItemModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<form method="post" action="{% url 'workitem_add' %}" class="modal-content border-secondary">
{% csrf_token %}
<input type="hidden" name="deal_id" value="{{ deal.id }}">
<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="entity_id" id="wiEntityId">
<input type="hidden" name="delivery_batch_id" id="wiBatchId">
<input type="hidden" name="operation_id" id="wiOperationId">
<div class="small text-muted mb-2" id="wiTitle"></div>
<div class="small text-muted mb-3" id="wiOp"></div>
<input type="hidden" name="workshop_id" id="wiWorkshopId">
<div class="small text-muted mb-3" id="wiWorkshopLabel"></div>
<div class="mb-3">
<label class="form-label small text-muted d-block">Пост (опционально)</label>
<div class="d-flex flex-wrap gap-1" id="machineToggleGroup">
<input type="radio" class="btn-check" name="machine_id" id="m_none" value="">
<label class="btn btn-outline-secondary btn-sm" for="m_none">Без станка</label>
{% for m in machines %}
<input type="radio" class="btn-check" name="machine_id" id="m_{{ m.id }}" value="{{ m.id }}" data-workshop-id="{{ m.workshop_id|default:'' }}">
<label class="btn btn-outline-accent btn-sm" for="m_{{ m.id }}">{{ m.name }}</label>
{% endfor %}
</div>
</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="wiQty" required>
</div>
<div class="form-check mb-3">
<input class="form-check-input border-secondary" type="checkbox" name="recursive_bom" id="recursiveBomCheck" checked>
<label class="form-check-label small text-muted" for="recursiveBomCheck">
Включить в смену все дочерние компоненты (по всем операциям, строго по БОМу)
</label>
</div>
<div class="small text-muted" id="wiHint"></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" id="wiSubmit">Добавить</button>
</div>
</form>
</div>
</div>
<!-- productInfoModal удалён: паспорт компонента открывается отдельной страницей -->
<div class="d-none" id="productInfoModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl">
<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" id="productInfoBody">
<div class="text-muted">Загрузка...</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="dealBatchModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<form method="post" action="{% url 'deal_batch_action' %}" class="modal-content border-secondary">
{% csrf_token %}
<input type="hidden" name="action" value="create_batch">
<input type="hidden" name="deal_id" value="{{ deal.id }}">
<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">
<div class="row g-2">
<div class="col-md-6">
<label class="form-label">Отгрузка</label>
<input class="form-control bg-body text-body border-secondary" type="date" name="due_date" required>
</div>
<div class="col-md-6">
<label class="form-label">Название (опц.)</label>
<input class="form-control bg-body text-body border-secondary" name="name" placeholder="Напр. Партия 1">
</div>
</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>
<div class="modal fade" id="dealBatchItemModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<form method="post" action="{% url 'deal_batch_action' %}" class="modal-content border-secondary">
{% csrf_token %}
<input type="hidden" name="action" value="add_batch_item">
<input type="hidden" name="deal_id" value="{{ deal.id }}">
<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">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label">Партия</label>
<select class="form-select bg-body text-body border-secondary" name="batch_id" id="batchSelect" required>
{% for b in delivery_batches %}
{% if not b.is_default %}
<option value="{{ b.id }}">{{ b.due_date|date:"d.m.Y" }}{% if b.name %} · {{ b.name }}{% endif %}</option>
{% endif %}
{% empty %}
<option value="">Сначала создай партию</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Позиция сделки</label>
<select class="form-select bg-body text-body border-secondary" name="entity_id" id="biEntitySelect" required {% if not deal_items %}disabled{% endif %}>
{% if deal_items %}
{% for it in deal_items %}
<option value="{{ it.entity.id }}" data-rem="{{ it.remaining_to_allocate|default:0 }}">{{ it.entity.drawing_number|default:"—" }} {{ it.entity.name }}</option>
{% endfor %}
{% else %}
<option value="">Сначала добавь позиции сделки</option>
{% endif %}
</select>
</div>
<div class="col-md-2">
<label class="form-label">Кол-во, шт</label>
<input class="form-control bg-body text-body border-secondary" name="quantity" id="biQty" value="1" min="1" required>
<div class="form-text" id="biQtyHint"></div>
</div>
</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" {% if not delivery_batches or not deal_items %}disabled{% endif %}>Добавить</button>
</div>
</form>
</div>
</div>
<div class="modal fade" id="dealItemModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<form method="post" action="{% url 'deal_item_upsert' %}" class="modal-content border-secondary" id="dealItemForm">
{% csrf_token %}
<input type="hidden" name="action" value="add">
<input type="hidden" name="deal_id" value="{{ deal.id }}">
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<input type="hidden" name="quantity" value="1">
<input type="hidden" name="entity_id" id="diEntityId" required>
<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="row g-2 align-items-end">
<div class="col-md-3">
<label class="form-label">Тип</label>
<select class="form-select bg-body text-body border-secondary" id="diType">
<option value="product">Изделие</option>
<option value="assembly">Сборочная единица</option>
<option value="part" selected>Деталь</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Обозначение</label>
<input class="form-control bg-body text-body border-secondary" id="diDn" autocomplete="off">
</div>
<div class="col-md-4">
<label class="form-label">Наименование</label>
<input class="form-control bg-body text-body border-secondary" id="diName" autocomplete="off">
</div>
<div class="col-md-2 d-grid">
<button type="button" class="btn btn-outline-secondary" id="diSearchBtn">Поиск</button>
</div>
</div>
<div class="small text-muted mt-2" id="diSearchStatus"></div>
<div class="mt-2">
<label class="form-label">Результаты</label>
<select class="form-select bg-body text-body border-secondary" id="diFound" size="8"></select>
<div class="form-text">Выбери строку и нажми «Добавить».</div>
</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>
<div class="modal fade" id="dealItemDeleteModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<form method="post" action="{% url 'deal_item_upsert' %}" class="modal-content border-secondary">
{% csrf_token %}
<input type="hidden" name="action" value="delete">
<input type="hidden" name="deal_id" id="diDelDealId" value="">
<input type="hidden" name="entity_id" id="diDelEntityId" value="">
<input type="hidden" name="next" id="diDelNext" value="">
<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-2">Вы уверены?</div>
<div class="small text-muted" id="diDelLabel"></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-danger">Удалить</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('tr.deal-entity-row[data-href]').forEach(tr => {
tr.addEventListener('click', (e) => {
if (e.target && (e.target.closest('button') || e.target.closest('a') || e.target.closest('form') || e.target.closest('input'))) return;
const url = tr.getAttribute('data-href');
if (url) window.location.href = url;
});
});
const spModal = document.getElementById('startProductionModal');
const spTitle = document.getElementById('spTitle');
const spSelect = document.getElementById('spBatchItem');
const spQty = document.getElementById('spQty');
const spSubmit = document.getElementById('spSubmit');
const delModal = document.getElementById('dealItemDeleteModal');
const delDealId = document.getElementById('diDelDealId');
const delEntityId = document.getElementById('diDelEntityId');
const delNext = document.getElementById('diDelNext');
const delLabel = document.getElementById('diDelLabel');
if (delModal) {
delModal.addEventListener('shown.bs.modal', (event) => {
const btn = event.relatedTarget;
const dealId = btn ? (btn.getAttribute('data-deal-id') || '') : '';
const entityId = btn ? (btn.getAttribute('data-entity-id') || '') : '';
const nextUrl = btn ? (btn.getAttribute('data-next') || '') : '';
const label = btn ? (btn.getAttribute('data-entity-label') || '') : '';
if (delDealId) delDealId.value = dealId;
if (delEntityId) delEntityId.value = entityId;
if (delNext) delNext.value = nextUrl;
if (delLabel) delLabel.textContent = label;
});
}
function spApplyFilter(entityId) {
if (!spSelect) return;
let firstVisible = null;
Array.from(spSelect.options).forEach(opt => {
const eid = opt.getAttribute('data-entity-id');
const visible = eid && String(eid) === String(entityId);
opt.hidden = !visible;
opt.disabled = !visible;
if (visible && !firstVisible) firstVisible = opt;
});
if (firstVisible) {
spSelect.value = firstVisible.value;
const rem = parseInt(firstVisible.getAttribute('data-rem') || '0', 10) || 0;
if (spQty) {
spQty.max = rem > 0 ? String(rem) : '';
spQty.value = rem > 0 ? String(rem) : '';
}
if (spSubmit) spSubmit.disabled = false;
} else {
if (spQty) {
spQty.value = '';
spQty.removeAttribute('max');
}
if (spSubmit) spSubmit.disabled = true;
}
}
if (spSelect) {
spSelect.addEventListener('change', () => {
const opt = spSelect.options[spSelect.selectedIndex];
const rem = opt ? (parseInt(opt.getAttribute('data-rem') || '0', 10) || 0) : 0;
if (spQty) {
spQty.max = rem > 0 ? String(rem) : '';
spQty.value = rem > 0 ? String(rem) : '';
}
});
}
if (spModal) {
spModal.addEventListener('shown.bs.modal', (event) => {
const btn = event.relatedTarget;
const eid = btn ? btn.getAttribute('data-entity-id') : '';
const label = btn ? btn.getAttribute('data-entity-label') : '';
if (spTitle) spTitle.textContent = label || '';
spApplyFilter(eid);
if (spQty) spQty.focus({ preventScroll: true });
});
}
});
document.addEventListener('DOMContentLoaded', () => {
const biModal = document.getElementById('dealBatchItemModal');
const batchSelect = document.getElementById('batchSelect');
const entitySelect = document.getElementById('biEntitySelect');
const qtyEl = document.getElementById('biQty');
const hintEl = document.getElementById('biQtyHint');
function syncRemainingHint() {
if (!entitySelect || !qtyEl) return;
const opt = entitySelect.options[entitySelect.selectedIndex];
const rem = opt ? (parseInt(opt.getAttribute('data-rem') || '0', 10) || 0) : 0;
qtyEl.max = rem > 0 ? String(rem) : '';
if (rem > 0 && (parseInt(qtyEl.value || '0', 10) || 0) > rem) {
qtyEl.value = String(rem);
}
if (hintEl) {
hintEl.textContent = rem > 0 ? `Доступно к распределению: ${rem} шт` : 'Доступно к распределению: 0 шт';
}
}
if (entitySelect) {
entitySelect.addEventListener('change', syncRemainingHint);
}
document.querySelectorAll('[data-bs-target="#dealBatchItemModal"][data-batch-id]').forEach(btn => {
btn.addEventListener('click', () => {
const bid = btn.getAttribute('data-batch-id');
if (batchSelect && bid) batchSelect.value = String(bid);
});
});
if (biModal) {
biModal.addEventListener('shown.bs.modal', () => {
syncRemainingHint();
if (entitySelect) entitySelect.focus({ preventScroll: true });
});
}
});
document.addEventListener('DOMContentLoaded', () => {
const modalEl = document.getElementById('dealItemModal');
const formEl = document.getElementById('dealItemForm');
const typeEl = document.getElementById('diType');
const dnEl = document.getElementById('diDn');
const nameEl = document.getElementById('diName');
const foundEl = document.getElementById('diFound');
const idEl = document.getElementById('diEntityId');
const btn = document.getElementById('diSearchBtn');
const statusEl = document.getElementById('diSearchStatus');
if (!modalEl || !formEl || !typeEl || !dnEl || !nameEl || !foundEl || !idEl || !btn || !statusEl) return;
function setStatus(text) {
statusEl.textContent = text || '';
}
function setSelectedFromFound() {
idEl.value = foundEl.value || '';
}
async function runSearch() {
const params = new URLSearchParams({
entity_type: (typeEl.value || ''),
q_dn: (dnEl.value || ''),
q_name: (nameEl.value || ''),
});
setStatus('Поиск...');
foundEl.innerHTML = '';
idEl.value = '';
let res;
try {
res = await fetch('{% url "entities_search" %}?' + params.toString(), { credentials: 'same-origin' });
} catch (_) {
setStatus('Ошибка сети при поиске.');
return;
}
if (!res.ok) {
setStatus(`Ошибка поиска: ${res.status}`);
return;
}
let data;
try {
data = await res.json();
} catch (_) {
setStatus('Ошибка: сервер вернул не JSON.');
return;
}
if (data && data.error) {
setStatus(`Ошибка поиска: ${data.error}`);
return;
}
const items = (data && data.results) || [];
items.forEach(it => {
const opt = document.createElement('option');
opt.value = String(it.id);
opt.textContent = `${it.type} | ${it.drawing_number || '—'} ${it.name || ''}`;
foundEl.appendChild(opt);
});
const count = (data && typeof data.count === 'number') ? data.count : items.length;
if (items.length) {
foundEl.value = String(items[0].id);
setSelectedFromFound();
setStatus(`Найдено: ${count}`);
foundEl.focus({ preventScroll: true });
} else {
setStatus(`Ничего не найдено (0).`);
}
}
btn.addEventListener('click', () => runSearch());
foundEl.addEventListener('change', () => setSelectedFromFound());
const onEnterSearch = (e) => {
if (e.key !== 'Enter') return;
e.preventDefault();
runSearch();
};
dnEl.addEventListener('keydown', onEnterSearch);
nameEl.addEventListener('keydown', onEnterSearch);
formEl.addEventListener('submit', (e) => {
setSelectedFromFound();
if (!idEl.value) {
e.preventDefault();
setStatus('Выбери позицию из результатов поиска.');
}
});
modalEl.addEventListener('shown.bs.modal', () => {
setStatus('');
dnEl.focus({ preventScroll: true });
dnEl.select();
});
});
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') || e.target.closest('.stop-prop'))) return;
const href = row.getAttribute('data-href');
if (href) window.location.href = href;
});
});
document.querySelectorAll('.sf-progress').forEach(function (el) {
const done = parseInt(el.getAttribute('data-done-width') || '0', 10) || 0;
const plan = parseInt(el.getAttribute('data-plan-width') || '0', 10) || 0;
const doneEl = el.querySelector('.sf-progress-done');
const planEl = el.querySelector('.sf-progress-plan');
if (doneEl) doneEl.style.width = `${done}%`;
if (planEl) planEl.style.width = `${plan}%`;
});
const modal = document.getElementById('workItemModal');
if (!modal) return;
modal.addEventListener('shown.bs.modal', function (event) {
const btn = event.relatedTarget;
const entityId = btn.getAttribute('data-entity-id') || '';
const batchId = btn.getAttribute('data-batch-id') || '';
const opId = btn.getAttribute('data-operation-id') || '';
const name = btn.getAttribute('data-task-name') || '';
const opName = btn.getAttribute('data-operation-name') || '';
const rem = btn.getAttribute('data-task-rem');
document.getElementById('wiEntityId').value = entityId;
document.getElementById('wiBatchId').value = batchId;
document.getElementById('wiOperationId').value = opId;
document.getElementById('wiTitle').textContent = name;
document.getElementById('wiOp').textContent = opName ? `Операция: ${opName}` : 'Операция: —';
const hint = document.getElementById('wiHint');
if (hint) hint.textContent = rem !== null ? `Осталось: ${rem} шт` : '';
const qty = document.getElementById('wiQty');
qty.value = '';
if (!opId) {
const submit = document.getElementById('wiSubmit');
if (submit) submit.disabled = true;
if (hint) hint.textContent = 'У этой позиции не задан техпроцесс (операции). Добавь операции в паспорте.';
return;
}
const submit = document.getElementById('wiSubmit');
if (submit) submit.disabled = false;
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('#workItemModal form');
if (form) form.requestSubmit();
}
};
const radios = Array.from(document.querySelectorAll('input[name="machine_id"]'));
const noneRadio = document.getElementById('m_none');
const wsIdEl = document.getElementById('wiWorkshopId');
const wsLabelEl = document.getElementById('wiWorkshopLabel');
const savedMachine = (() => { try { return localStorage.getItem('planning_machine_id'); } catch (_) { return null; } })();
const wsId = btn.getAttribute('data-workshop-id') || '';
const wsName = btn.getAttribute('data-workshop-name') || '';
if (wsIdEl) wsIdEl.value = wsId;
if (wsLabelEl) {
wsLabelEl.textContent = wsId ? `Цех: ${wsName || '—'}` : 'Цех: —';
}
function setMachineVisible(radio, visible) {
const lbl = document.querySelector(`label[for="${radio.id}"]`);
if (lbl) lbl.style.display = visible ? '' : 'none';
radio.style.display = visible ? '' : 'none';
radio.disabled = !visible;
if (!visible && radio.checked) radio.checked = false;
}
radios.forEach(r => {
if (r.id === 'm_none') {
setMachineVisible(r, true);
return;
}
const mWs = r.getAttribute('data-workshop-id') || '';
setMachineVisible(r, !wsId || (mWs && String(mWs) === String(wsId)));
});
let selected = null;
if (savedMachine) {
selected = radios.find(r => r.value === savedMachine && !r.disabled);
}
if (selected) {
selected.checked = true;
} else if (noneRadio) {
noneRadio.checked = true;
}
radios.forEach(r => {
r.onchange = function () {
if (!r.checked) return;
if (r.value) {
try { localStorage.setItem('planning_machine_id', r.value); } catch (_) {}
} else {
try { localStorage.removeItem('planning_machine_id'); } catch (_) {}
}
};
});
});
});
</script>
{% endblock %}