Починили закрытие сварки, доработали интерфейс
All checks were successful
Deploy MES Core / deploy (push) Successful in 13s
All checks were successful
Deploy MES Core / deploy (push) Successful in 13s
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@@ -9,13 +9,19 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
-
|
- Журнал отгрузки: список документов перемещения на «Склад отгруженных позиций».
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- Отгрузка: можно добавлять несколько сделок в одну сессию отгрузки, выбирать позиции и подтверждать общий список.
|
||||||
|
- Журнал отгрузки: добавлены фильтр по периоду (по умолчанию 2 недели) и поиск по сделкам (номер/описание/заказчик), убран столбец «Куда».
|
||||||
|
- Списание / Производство: в блоках «Списано» и «Остаток ДО» выводится масса материалов (по размерам и «Масса на ед. учёта»); если масса не задана — показывается прочерк.
|
||||||
|
- Паспорта изделий/компонентов: ссылки на PDF/DXF/картинки отображаются иконками и открываются в новой вкладке.
|
||||||
|
- Паспорта изделий/сборок: блок «Состав» перенесён в верхнюю часть страницы, в таблицу состава добавлена колонка «Файлы».
|
||||||
- Производственные задачи и прогресс техпроцесса ведутся в разрезе партий поставки (серий) для одной сделки.
|
- Производственные задачи и прогресс техпроцесса ведутся в разрезе партий поставки (серий) для одной сделки.
|
||||||
- Улучшено сообщение о блокировке запуска «В производство» при отсутствии техпроцесса или материала: показывается модалка (для техпроцесса также есть отдельная страница) со списком проблемных позиций.
|
- Улучшено сообщение о блокировке запуска «В производство» при отсутствии техпроцесса или материала: показывается модалка и отдельная страница со списком проблемных позиций.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- Починено закрытие сборок/изделий на странице «Закрыть сборку»: выбор поста доступен и сохраняется, списание/выпуск выполняются.
|
||||||
- Запуск «В производство» блокируется, если в BOM есть узлы без техпроцесса (EntityOperation seq=1), чтобы компоненты не попадали в «без техпроцесса».
|
- Запуск «В производство» блокируется, если в BOM есть узлы без техпроцесса (EntityOperation seq=1), чтобы компоненты не попадали в «без техпроцесса».
|
||||||
- Повторный запуск в производство по новой серии не увеличивает объём в уже закрытых задачах прошлых серий.
|
- Повторный запуск в производство по новой серии не увеличивает объём в уже закрытых задачах прошлых серий.
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ if os.path.exists(env_file):
|
|||||||
|
|
||||||
# читаем переменную окружения
|
# читаем переменную окружения
|
||||||
ENV_TYPE = os.getenv('ENV_TYPE', 'local')
|
ENV_TYPE = os.getenv('ENV_TYPE', 'local')
|
||||||
APP_VERSION = '0.8.0'
|
APP_VERSION = '0.8.9'
|
||||||
|
|
||||||
# Настройки безопасности
|
# Настройки безопасности
|
||||||
# DEBUG будет True везде, кроме сервера
|
# DEBUG будет True везде, кроме сервера
|
||||||
|
|||||||
@@ -77,22 +77,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" action="">
|
<form method="post" action="{% url 'assembly_closing' workitem.id %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="close">
|
<input type="hidden" name="action" value="close">
|
||||||
|
|
||||||
<div class="row align-items-end g-2">
|
<div class="row align-items-end g-2">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label text-muted small mb-1">Фактически собрано (шт.)</label>
|
<label class="form-label text-muted small mb-1">Фактически собрано (шт.)</label>
|
||||||
<input type="number" class="form-control border-secondary" name="fact_qty" min="1" max="{{ max_possible }}" value="{{ max_possible }}" {% if max_possible == 0 %}disabled{% endif %}>
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control border-secondary"
|
||||||
|
name="fact_qty"
|
||||||
|
min="1"
|
||||||
|
{% if max_possible and max_possible > 0 %}max="{{ max_possible }}"{% endif %}
|
||||||
|
value="{% if max_possible and max_possible > 0 %}{{ max_possible }}{% else %}1{% endif %}"
|
||||||
|
required
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{% if workitem.machine_id %}
|
{% if workitem.machine_id %}
|
||||||
<button type="submit" class="btn btn-warning w-100" {% if max_possible == 0 %}disabled{% endif %}>
|
<button type="submit" class="btn btn-warning w-100">
|
||||||
Списать компоненты и закрыть сборку
|
Списать компоненты и закрыть сборку
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="button" class="btn btn-warning w-100" data-bs-toggle="modal" data-bs-target="#selectMachineModal" {% if max_possible == 0 %}disabled{% endif %}>
|
<button type="button" class="btn btn-warning w-100" data-bs-toggle="modal" data-bs-target="#selectMachineModal">
|
||||||
Выбрать пост и закрыть
|
Выбрать пост и закрыть
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -113,10 +121,10 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
{% if workshop_machines %}
|
{% if workshop_machines %}
|
||||||
<label class="form-label small text-muted mb-1">Пост</label>
|
<label class="form-label small text-muted mb-1">Пост</label>
|
||||||
<select class="form-select border-secondary" name="machine_id" required>
|
<select class="form-select border-secondary" name="machine_id" {% if not workitem.machine_id %}required{% else %}disabled{% endif %}>
|
||||||
<option value="">— выбрать —</option>
|
<option value="">— выбрать —</option>
|
||||||
{% for m in workshop_machines %}
|
{% for m in workshop_machines %}
|
||||||
<option value="{{ m.id }}">{{ m.name }}</option>
|
<option value="{{ m.id }}">{% if m.workshop %}{{ m.workshop.name }} · {% endif %}{{ m.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<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>
|
<div>
|
||||||
<h3 class="text-accent mb-1">
|
<h3 class="text-accent mb-1">
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>Позиции без техпроцесса
|
<i class="bi bi-exclamation-triangle me-2"></i>{{ page_title|default:"Проблемные позиции" }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="small text-muted">
|
<div class="small text-muted">
|
||||||
Сделка {{ deal.number }}
|
Сделка {{ deal.number }}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if items %}
|
{% if items %}
|
||||||
<div class="text-muted mb-2">Для запуска в производство нужен техпроцесс (операция seq=1).</div>
|
<div class="text-muted mb-2">{{ page_hint|default:"" }}</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0" data-sortable="1">
|
<table class="table table-hover align-middle mb-0" data-sortable="1">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -601,6 +601,9 @@
|
|||||||
</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>
|
||||||
|
{% if missing_material_details_url %}
|
||||||
|
<a class="btn btn-outline-accent" href="{{ missing_material_details_url }}" target="_blank">Подробнее</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,6 +25,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="container-fluid p-0">
|
<div class="container-fluid p-0">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-lg-5">
|
||||||
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data" id="product-info-form">
|
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data" id="product-info-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="save">
|
<input type="hidden" name="action" value="save">
|
||||||
@@ -32,22 +34,12 @@
|
|||||||
<input type="hidden" name="trail" value="{{ request.GET.trail|default:'' }}">
|
<input type="hidden" name="trail" value="{{ request.GET.trail|default:'' }}">
|
||||||
|
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-md-2">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Тип</label>
|
<label class="form-label">Тип</label>
|
||||||
<div class="mt-1"><span class="badge bg-secondary">{{ entity.get_entity_type_display }}</span></div>
|
<div class="mt-1"><span class="badge bg-secondary">{{ entity.get_entity_type_display }}</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Обозначение</label>
|
|
||||||
<input class="form-control bg-body text-body border-secondary" name="drawing_number" value="{{ entity.drawing_number }}" {% if not can_edit %}disabled{% endif %}>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-5">
|
|
||||||
<label class="form-label">Наименование</label>
|
|
||||||
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Заполнен</label>
|
<label class="form-label">Заполнен</label>
|
||||||
<div class="form-check mt-2">
|
<div class="form-check mt-2">
|
||||||
<input class="form-check-input" type="checkbox" name="passport_filled" id="pf" {% if entity.passport_filled %}checked{% endif %} {% if not can_edit %}disabled{% endif %}>
|
<input class="form-check-input" type="checkbox" name="passport_filled" id="pf" {% if entity.passport_filled %}checked{% endif %} {% if not can_edit %}disabled{% endif %}>
|
||||||
@@ -55,48 +47,70 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Обозначение</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="drawing_number" value="{{ entity.drawing_number }}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Наименование</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
<label class="form-label">Масса, кг</label>
|
<label class="form-label">Масса, кг</label>
|
||||||
<input class="form-control bg-body text-body border-secondary" name="weight_kg" value="{% if passport and passport.weight_kg %}{{ passport.weight_kg }}{% endif %}" inputmode="decimal" {% if not can_edit %}disabled{% endif %}>
|
<input class="form-control bg-body text-body border-secondary" name="weight_kg" value="{% if passport and passport.weight_kg %}{{ passport.weight_kg }}{% endif %}" inputmode="decimal" {% if not can_edit %}disabled{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Покрытие</label>
|
|
||||||
<input class="form-control bg-body text-body border-secondary" name="coating" value="{% if passport %}{{ passport.coating }}{% endif %}" {% if not can_edit %}disabled{% endif %}>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Цвет</label>
|
|
||||||
<input class="form-control bg-body text-body border-secondary" name="coating_color" value="{% if passport %}{{ passport.coating_color }}{% endif %}" {% if not can_edit %}disabled{% endif %}>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Площадь покрытия, м²</label>
|
<label class="form-label">Площадь покрытия, м²</label>
|
||||||
<input class="form-control bg-body text-body border-secondary" name="coating_area_m2" value="{% if passport and passport.coating_area_m2 %}{{ passport.coating_area_m2 }}{% endif %}" inputmode="decimal" {% if not can_edit %}disabled{% endif %}>
|
<input class="form-control bg-body text-body border-secondary" name="coating_area_m2" value="{% if passport and passport.coating_area_m2 %}{{ passport.coating_area_m2 }}{% endif %}" inputmode="decimal" {% if not can_edit %}disabled{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Покрытие</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="coating" value="{% if passport %}{{ passport.coating }}{% endif %}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Цвет</label>
|
||||||
|
<input class="form-control bg-body text-body border-secondary" name="coating_color" value="{% if passport %}{{ passport.coating_color }}{% endif %}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
<label class="form-label">Чертёж (PDF)</label>
|
<label class="form-label">Чертёж (PDF)</label>
|
||||||
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
|
<div class="d-flex gap-2 align-items-center">
|
||||||
{% if entity.pdf_main %}
|
{% if entity.pdf_main %}
|
||||||
<div class="small mt-1"><a href="{{ entity.pdf_main.url }}" target="_blank">Открыть текущий</a></div>
|
<a href="{{ entity.pdf_main.url }}" target="_blank" rel="noopener" class="btn btn-sm btn-outline-danger p-1" title="Чертёж PDF">
|
||||||
|
<i class="bi bi-file-pdf"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-12">
|
||||||
<label class="form-label">DXF/IGES/STEP</label>
|
<label class="form-label">DXF/IGES/STEP</label>
|
||||||
<input class="form-control bg-body text-body border-secondary" type="file" name="dxf_file" accept=".dxf,.iges,.igs,.step,.stp" {% if not can_edit %}disabled{% endif %}>
|
<div class="d-flex gap-2 align-items-center">
|
||||||
{% if entity.dxf_file %}
|
{% if entity.dxf_file %}
|
||||||
<div class="small mt-1"><a href="{{ entity.dxf_file.url }}" target="_blank">Открыть текущий</a></div>
|
<a href="{{ entity.dxf_file.url }}" target="_blank" rel="noopener" class="btn btn-sm btn-outline-info p-1" title="DXF/IGES/STEP">
|
||||||
|
<i class="bi bi-file-earmark-code"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="dxf_file" accept=".dxf,.iges,.igs,.step,.stp" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-12">
|
||||||
<label class="form-label">Картинка</label>
|
<label class="form-label">Картинка</label>
|
||||||
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
|
<div class="d-flex gap-2 align-items-center">
|
||||||
{% if entity.preview %}
|
{% if entity.preview %}
|
||||||
<div class="small mt-1"><a href="{{ entity.preview.url }}" target="_blank">Открыть текущую</a></div>
|
<a href="{{ entity.preview.url }}" target="_blank" rel="noopener" class="btn btn-sm btn-outline-success p-1" title="Картинка">
|
||||||
|
<i class="bi bi-file-earmark-image"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if not can_edit %}
|
{% if not can_edit %}
|
||||||
@@ -287,6 +301,92 @@
|
|||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<div class="border border-secondary rounded p-2">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<div class="fw-bold">Состав</div>
|
||||||
|
{% if can_edit %}
|
||||||
|
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#bomAddModal">Добавить компонент</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 data-sort="false" class="text-center" style="width:110px;">Файлы</th>
|
||||||
|
<th class="text-center" style="width:120px;">Заполнено</th>
|
||||||
|
<th class="text-center">Кол-во</th>
|
||||||
|
<th data-sort="false" class="text-end"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for ln in bom_lines %}
|
||||||
|
<tr role="button" style="cursor:pointer" onclick="window.location.href='{% url 'product_info' ln.child.id %}?next={{ request.get_full_path|urlencode }}&trail={{ trail_child|urlencode }}';">
|
||||||
|
<td class="small text-muted">{{ ln.child.get_entity_type_display }}</td>
|
||||||
|
<td class="fw-bold">{{ ln.child.drawing_number|default:"—" }}</td>
|
||||||
|
<td>{{ ln.child.name }}</td>
|
||||||
|
<td class="text-center" onclick="event.stopPropagation();">
|
||||||
|
{% if ln.child.dxf_file %}
|
||||||
|
<a href="{{ ln.child.dxf_file.url }}" target="_blank" rel="noopener" class="btn btn-sm btn-outline-info p-1 stop-prop" title="DXF/IGES/STEP">
|
||||||
|
<i class="bi bi-file-earmark-code"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if ln.child.pdf_main %}
|
||||||
|
<a href="{{ ln.child.pdf_main.url }}" target="_blank" rel="noopener" class="btn btn-sm btn-outline-danger p-1 stop-prop" title="Чертёж PDF">
|
||||||
|
<i class="bi bi-file-pdf"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if ln.child.preview %}
|
||||||
|
<a href="{{ ln.child.preview.url }}" target="_blank" rel="noopener" class="btn btn-sm btn-outline-success p-1 stop-prop" title="Картинка">
|
||||||
|
<i class="bi bi-file-earmark-image"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% if ln.child.passport_filled %}
|
||||||
|
<span class="badge bg-success">Да</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Нет</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center" style="max-width:220px;" onclick="event.stopPropagation();">
|
||||||
|
<form method="post" action="{% url 'product_info' entity.id %}" class="d-flex gap-2 align-items-center justify-content-center" onclick="event.stopPropagation();">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="bom_update_qty">
|
||||||
|
<input type="hidden" name="bom_id" value="{{ ln.id }}">
|
||||||
|
<input type="hidden" name="trail" value="{{ request.GET.trail|default:'' }}">
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
<input class="form-control form-control-sm bg-body text-body border-secondary" name="quantity" value="{{ ln.quantity }}" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" type="submit" {% if not can_edit %}disabled{% endif %}>OK</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
{% if can_edit %}
|
||||||
|
<form method="post" action="{% url 'product_info' entity.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="bom_delete_line">
|
||||||
|
<input type="hidden" name="bom_id" value="{{ ln.id }}">
|
||||||
|
<input type="hidden" name="trail" value="{{ request.GET.trail|default:'' }}">
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" type="submit">Удалить</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="7" class="text-center text-muted py-4">Пока нет компонентов</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
@@ -358,72 +458,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-secondary my-4">
|
|
||||||
|
|
||||||
<div class="mt-4">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
||||||
<div class="fw-bold">Состав</div>
|
|
||||||
{% if can_edit %}
|
|
||||||
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#bomAddModal">Добавить компонент</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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" style="width:120px;">Заполнено</th>
|
|
||||||
<th class="text-center">Кол-во</th>
|
|
||||||
<th data-sort="false" class="text-end"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for ln in bom_lines %}
|
|
||||||
<tr role="button" style="cursor:pointer" onclick="window.location.href='{% url 'product_info' ln.child.id %}?next={{ request.get_full_path|urlencode }}&trail={{ trail_child|urlencode }}';">
|
|
||||||
<td class="small text-muted">{{ ln.child.get_entity_type_display }}</td>
|
|
||||||
<td class="fw-bold">{{ ln.child.drawing_number|default:"—" }}</td>
|
|
||||||
<td>{{ ln.child.name }}</td>
|
|
||||||
<td class="text-center">
|
|
||||||
{% if ln.child.passport_filled %}
|
|
||||||
<span class="badge bg-success">Да</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-secondary">Нет</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="text-center" style="max-width:220px;" onclick="event.stopPropagation();">
|
|
||||||
<form method="post" action="{% url 'product_info' entity.id %}" class="d-flex gap-2 align-items-center justify-content-center" onclick="event.stopPropagation();">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="bom_update_qty">
|
|
||||||
<input type="hidden" name="bom_id" value="{{ ln.id }}">
|
|
||||||
<input type="hidden" name="trail" value="{{ request.GET.trail|default:'' }}">
|
|
||||||
<input type="hidden" name="next" value="{{ next }}">
|
|
||||||
<input class="form-control form-control-sm bg-body text-body border-secondary" name="quantity" value="{{ ln.quantity }}" {% if not can_edit %}disabled{% endif %}>
|
|
||||||
<button class="btn btn-outline-secondary btn-sm" type="submit" {% if not can_edit %}disabled{% endif %}>OK</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
<td class="text-end">
|
|
||||||
{% if can_edit %}
|
|
||||||
<form method="post" action="{% url 'product_info' entity.id %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="bom_delete_line">
|
|
||||||
<input type="hidden" name="bom_id" value="{{ ln.id }}">
|
|
||||||
<input type="hidden" name="trail" value="{{ request.GET.trail|default:'' }}">
|
|
||||||
<input type="hidden" name="next" value="{{ next }}">
|
|
||||||
<button class="btn btn-outline-secondary btn-sm" type="submit">Удалить</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr><td colspan="6" class="text-center text-muted py-4">Пока нет компонентов</td></tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
<div class="modal fade" id="bomAddModal" tabindex="-1" aria-hidden="true">
|
<div class="modal fade" id="bomAddModal" tabindex="-1" aria-hidden="true">
|
||||||
|
|||||||
@@ -82,26 +82,38 @@
|
|||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Чертёж (PDF)</label>
|
<label class="form-label">Чертёж (PDF)</label>
|
||||||
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
|
<div class="input-group">
|
||||||
{% if entity.pdf_main %}
|
{% if entity.pdf_main %}
|
||||||
<div class="small mt-1"><a href="{{ entity.pdf_main.url }}" target="_blank">Открыть текущий</a></div>
|
<a href="{{ entity.pdf_main.url }}" target="_blank" rel="noopener" class="btn btn-outline-danger" title="Чертёж PDF">
|
||||||
|
<i class="bi bi-file-pdf"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">DXF/IGES/STEP</label>
|
<label class="form-label">DXF/IGES/STEP</label>
|
||||||
<input class="form-control bg-body text-body border-secondary" type="file" name="dxf_file" accept=".dxf,.iges,.igs,.step,.stp" {% if not can_edit %}disabled{% endif %}>
|
<div class="input-group">
|
||||||
{% if entity.dxf_file %}
|
{% if entity.dxf_file %}
|
||||||
<div class="small mt-1"><a href="{{ entity.dxf_file.url }}" target="_blank">Открыть текущий</a></div>
|
<a href="{{ entity.dxf_file.url }}" target="_blank" rel="noopener" class="btn btn-outline-info" title="DXF/IGES/STEP">
|
||||||
|
<i class="bi bi-file-earmark-code"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="dxf_file" accept=".dxf,.iges,.igs,.step,.stp" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Картинка</label>
|
<label class="form-label">Картинка</label>
|
||||||
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
|
<div class="input-group">
|
||||||
{% if entity.preview %}
|
{% if entity.preview %}
|
||||||
<div class="small mt-1"><a href="{{ entity.preview.url }}" target="_blank">Открыть текущую</a></div>
|
<a href="{{ entity.preview.url }}" target="_blank" rel="noopener" class="btn btn-outline-success" title="Картинка">
|
||||||
|
<i class="bi bi-file-earmark-image"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 d-flex justify-content-end mt-2">
|
<div class="col-12 d-flex justify-content-end mt-2">
|
||||||
|
|||||||
@@ -57,26 +57,38 @@
|
|||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Чертёж/ТЗ (PDF)</label>
|
<label class="form-label">Чертёж/ТЗ (PDF)</label>
|
||||||
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
|
<div class="input-group">
|
||||||
{% if entity.pdf_main %}
|
{% if entity.pdf_main %}
|
||||||
<div class="small mt-1"><a href="{{ entity.pdf_main.url }}" target="_blank">Открыть текущий</a></div>
|
<a href="{{ entity.pdf_main.url }}" target="_blank" rel="noopener" class="btn btn-outline-danger" title="Чертёж/ТЗ PDF">
|
||||||
|
<i class="bi bi-file-pdf"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">DXF/IGES/STEP</label>
|
<label class="form-label">DXF/IGES/STEP</label>
|
||||||
<input class="form-control bg-body text-body border-secondary" type="file" name="dxf_file" accept=".dxf,.iges,.igs,.step,.stp" {% if not can_edit %}disabled{% endif %}>
|
<div class="input-group">
|
||||||
{% if entity.dxf_file %}
|
{% if entity.dxf_file %}
|
||||||
<div class="small mt-1"><a href="{{ entity.dxf_file.url }}" target="_blank">Открыть текущий</a></div>
|
<a href="{{ entity.dxf_file.url }}" target="_blank" rel="noopener" class="btn btn-outline-info" title="DXF/IGES/STEP">
|
||||||
|
<i class="bi bi-file-earmark-code"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="dxf_file" accept=".dxf,.iges,.igs,.step,.stp" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Картинка</label>
|
<label class="form-label">Картинка</label>
|
||||||
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
|
<div class="input-group">
|
||||||
{% if entity.preview %}
|
{% if entity.preview %}
|
||||||
<div class="small mt-1"><a href="{{ entity.preview.url }}" target="_blank">Открыть текущую</a></div>
|
<a href="{{ entity.preview.url }}" target="_blank" rel="noopener" class="btn btn-outline-success" title="Картинка">
|
||||||
|
<i class="bi bi-file-earmark-image"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if not can_edit %}
|
{% if not can_edit %}
|
||||||
|
|||||||
@@ -107,26 +107,38 @@
|
|||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Чертёж (PDF)</label>
|
<label class="form-label">Чертёж (PDF)</label>
|
||||||
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
|
<div class="d-flex gap-2 align-items-center">
|
||||||
{% if entity.pdf_main %}
|
{% if entity.pdf_main %}
|
||||||
<div class="small mt-1"><a href="{{ entity.pdf_main.url }}" target="_blank">Открыть текущий</a></div>
|
<a href="{{ entity.pdf_main.url }}" target="_blank" class="btn btn-sm btn-outline-danger p-1" title="PDF">
|
||||||
|
<i class="bi bi-file-pdf"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">DXF/IGES/STEP</label>
|
<label class="form-label">DXF/IGES/STEP</label>
|
||||||
<input class="form-control bg-body text-body border-secondary" type="file" name="dxf_file" accept=".dxf,.iges,.igs,.step,.stp" {% if not can_edit %}disabled{% endif %}>
|
<div class="d-flex gap-2 align-items-center">
|
||||||
{% if entity.dxf_file %}
|
{% if entity.dxf_file %}
|
||||||
<div class="small mt-1"><a href="{{ entity.dxf_file.url }}" target="_blank">Открыть текущий</a></div>
|
<a href="{{ entity.dxf_file.url }}" target="_blank" class="btn btn-sm btn-outline-info p-1" title="DXF/IGES/STEP">
|
||||||
|
<i class="bi bi-file-earmark-code"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="dxf_file" accept=".dxf,.iges,.igs,.step,.stp" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Картинка</label>
|
<label class="form-label">Картинка</label>
|
||||||
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
|
<div class="d-flex gap-2 align-items-center">
|
||||||
{% if entity.preview %}
|
{% if entity.preview %}
|
||||||
<div class="small mt-1"><a href="{{ entity.preview.url }}" target="_blank">Открыть текущую</a></div>
|
<a href="{{ entity.preview.url }}" target="_blank" class="btn btn-sm btn-outline-success p-1" title="Картинка">
|
||||||
|
<i class="bi bi-file-earmark-image"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|||||||
@@ -77,27 +77,40 @@
|
|||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Чертёж/паспорт (PDF)</label>
|
<label class="form-label">Чертёж/паспорт (PDF)</label>
|
||||||
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
|
<div class="d-flex gap-2 align-items-center">
|
||||||
{% if entity.pdf_main %}
|
{% if entity.pdf_main %}
|
||||||
<div class="small mt-1"><a href="{{ entity.pdf_main.url }}" target="_blank">Открыть текущий</a></div>
|
<a href="{{ entity.pdf_main.url }}" target="_blank" class="btn btn-sm btn-outline-danger p-1" title="PDF">
|
||||||
|
<i class="bi bi-file-pdf"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">DXF/IGES/STEP</label>
|
<label class="form-label">DXF/IGES/STEP</label>
|
||||||
<input class="form-control bg-body text-body border-secondary" type="file" name="dxf_file" accept=".dxf,.iges,.igs,.step,.stp" {% if not can_edit %}disabled{% endif %}>
|
<div class="d-flex gap-2 align-items-center">
|
||||||
{% if entity.dxf_file %}
|
{% if entity.dxf_file %}
|
||||||
<div class="small mt-1"><a href="{{ entity.dxf_file.url }}" target="_blank">Открыть текущий</a></div>
|
<a href="{{ entity.dxf_file.url }}" target="_blank" class="btn btn-sm btn-outline-info p-1" title="DXF/IGES/STEP">
|
||||||
|
<i class="bi bi-file-earmark-code"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="dxf_file" accept=".dxf,.iges,.igs,.step,.stp" {% if not can_edit %}disabled{% endif %}>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Картинка</label>
|
<label class="form-label">Картинка</label>
|
||||||
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
|
<div class="d-flex gap-2 align-items-center">
|
||||||
{% if entity.preview %}
|
{% if entity.preview %}
|
||||||
<div class="small mt-1"><a href="{{ entity.preview.url }}" target="_blank">Открыть текущую</a></div>
|
<a href="{{ entity.preview.url }}" target="_blank" class="btn btn-sm btn-outline-success p-1" title="Картинка">
|
||||||
|
<i class="bi bi-file-earmark-image"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="col-12 d-flex justify-content-end mt-2">
|
<div class="col-12 d-flex justify-content-end mt-2">
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
|
|||||||
177
shiftflow/templates/shiftflow/shipping_cart.html
Normal file
177
shiftflow/templates/shiftflow/shipping_cart.html
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
{% 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">
|
||||||
|
<h3 class="text-accent mb-0"><i class="bi bi-truck me-2"></i>Отгрузка</h3>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a class="btn btn-outline-secondary btn-sm" href="{{ journal_url }}" target="_blank">
|
||||||
|
<i class="bi bi-journal-text me-1"></i>Журнал отгрузки
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#addDealModal">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i>Добавить к отгрузке сделку
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" id="shippingCartForm">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" id="shippingAction" value="">
|
||||||
|
<input type="hidden" name="remove_deal_id" id="removeDealId" value="">
|
||||||
|
|
||||||
|
{% if not cart %}
|
||||||
|
<div class="text-muted">Добавь сделку к отгрузке, чтобы выбрать готовые позиции.</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for b in cart %}
|
||||||
|
<div class="card border-secondary mb-3">
|
||||||
|
<div class="card-header border-secondary py-2 d-flex justify-content-between align-items-center">
|
||||||
|
<div class="fw-bold">Сделка №{{ b.deal.number }}{% if b.deal.company %} · {{ b.deal.company.name }}{% endif %}</div>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm js-remove-deal" data-deal-id="{{ b.deal.id }}">Убрать</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr class="table-custom-header">
|
||||||
|
<th data-sort="false" style="width:44px;"></th>
|
||||||
|
<th>Позиция</th>
|
||||||
|
<th class="text-center" style="width:120px;">Доступно</th>
|
||||||
|
<th class="text-center" style="width:180px;">К отгрузке</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for r in b.entity_rows %}
|
||||||
|
<tr>
|
||||||
|
<td class="text-center"><input class="form-check-input border-secondary js-pick" type="checkbox" data-target="#qty_d{{ b.deal.id }}_ent_{{ r.entity.id }}"></td>
|
||||||
|
<td><div class="fw-bold">{{ r.entity.drawing_number|default:"—" }} {{ r.entity.name }}</div><div class="small text-muted">{{ r.entity.get_entity_type_display }}</div></td>
|
||||||
|
<td class="text-center fw-bold">{{ r.remaining_ready }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<input id="qty_d{{ b.deal.id }}_ent_{{ r.entity.id }}" class="form-control bg-body text-body border-secondary ship-qty" type="number" min="0" step="1" max="{{ r.remaining_ready }}" name="d{{ b.deal.id }}_ent_{{ r.entity.id }}" value="0" data-deal="№{{ b.deal.number }}" data-label="{{ r.entity.drawing_number|default:'—' }} {{ r.entity.name }}" disabled>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="4" class="text-center text-muted py-4">Нет готовых позиций к отгрузке</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end mt-3">
|
||||||
|
{% if can_edit %}
|
||||||
|
<button type="button" class="btn btn-outline-accent" data-bs-toggle="modal" data-bs-target="#shipConfirmModal" id="shipOpenConfirm">Отгрузить</button>
|
||||||
|
{% else %}
|
||||||
|
<button type="button" class="btn btn-outline-secondary" disabled>Отгрузить</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="addDealModal" 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">
|
||||||
|
<label class="form-label">Сделка (статус: В работе)</label>
|
||||||
|
<select class="form-select bg-body text-body border-secondary" name="add_deal_id">
|
||||||
|
<option value="">— выбери —</option>
|
||||||
|
{% for d in available_deals %}
|
||||||
|
<option value="{{ d.id }}">№{{ d.number }}{% if d.company %} · {{ d.company.name }}{% endif %}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</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 js-action" data-action="add_deal">Добавить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="shipConfirmModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<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="small text-muted mb-2">Проверь итоговый список к отгрузке:</div>
|
||||||
|
<div id="shipSummary" class="border border-secondary rounded p-2"></div>
|
||||||
|
<div id="shipSummaryEmpty" class="text-muted d-none">Нечего отгружать (везде 0).</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 js-action" data-action="ship" id="shipConfirmBtn">Принять отгрузку</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const form = document.getElementById('shippingCartForm');
|
||||||
|
const actionEl = document.getElementById('shippingAction');
|
||||||
|
const removeDealId = document.getElementById('removeDealId');
|
||||||
|
const summary = document.getElementById('shipSummary');
|
||||||
|
const empty = document.getElementById('shipSummaryEmpty');
|
||||||
|
const confirmBtn = document.getElementById('shipConfirmBtn');
|
||||||
|
|
||||||
|
function setAction(a){ if(actionEl) actionEl.value = a || ''; }
|
||||||
|
|
||||||
|
document.querySelectorAll('.js-action').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => setAction(btn.getAttribute('data-action') || ''));
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.js-remove-deal').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const id = btn.getAttribute('data-deal-id') || '';
|
||||||
|
if (!id) return;
|
||||||
|
if (removeDealId) removeDealId.value = id;
|
||||||
|
setAction('remove_deal');
|
||||||
|
form?.requestSubmit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.js-pick').forEach(chk => {
|
||||||
|
chk.addEventListener('change', () => {
|
||||||
|
const target = chk.getAttribute('data-target');
|
||||||
|
const inp = target ? document.querySelector(target) : null;
|
||||||
|
if (!inp) return;
|
||||||
|
if (chk.checked) {
|
||||||
|
inp.disabled = false;
|
||||||
|
const max = inp.getAttribute('max');
|
||||||
|
const v = (max && parseInt(max, 10) > 0) ? String(parseInt(max, 10)) : '1';
|
||||||
|
if (!inp.value || inp.value === '0') inp.value = v;
|
||||||
|
} else {
|
||||||
|
inp.value = '0';
|
||||||
|
inp.disabled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('shipOpenConfirm')?.addEventListener('click', () => {
|
||||||
|
const inputs = Array.from(document.querySelectorAll('#shippingCartForm .ship-qty'));
|
||||||
|
const rows = [];
|
||||||
|
inputs.forEach(inp => {
|
||||||
|
if (inp.disabled) return;
|
||||||
|
const raw = (inp.value || '').toString().trim();
|
||||||
|
const val = parseFloat(raw.replace(',', '.'));
|
||||||
|
if (!val || val <= 0) return;
|
||||||
|
rows.push({ deal: inp.getAttribute('data-deal') || '', label: inp.getAttribute('data-label') || '', val });
|
||||||
|
});
|
||||||
|
if (!rows.length) {
|
||||||
|
summary.innerHTML = '';
|
||||||
|
empty.classList.remove('d-none');
|
||||||
|
if (confirmBtn) confirmBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
empty.classList.add('d-none');
|
||||||
|
if (confirmBtn) confirmBtn.disabled = false;
|
||||||
|
summary.innerHTML = rows.map(r => `<div class="d-flex justify-content-between gap-2"><div>${r.deal} · ${r.label}</div><div class="fw-bold">${r.val}</div></div>`).join('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
90
shiftflow/templates/shiftflow/shipping_journal.html
Normal file
90
shiftflow/templates/shiftflow/shipping_journal.html
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{% 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-journal-text me-2"></i>Журнал отгрузки</h3>
|
||||||
|
<div class="small text-muted">Склад отгрузки: {{ shipping_location.name }}</div>
|
||||||
|
</div>
|
||||||
|
<a class="btn btn-outline-secondary btn-sm" href="{% url 'shipping' %}">
|
||||||
|
<i class="bi bi-arrow-left me-1"></i>Назад
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="get" class="row g-2 align-items-end mb-3">
|
||||||
|
<input type="hidden" name="filtered" value="1">
|
||||||
|
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label class="small text-muted mb-1 fw-bold">Поиск (сделка):</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="q"
|
||||||
|
value="{{ q }}"
|
||||||
|
class="form-control form-control-sm bg-body text-body border-secondary"
|
||||||
|
placeholder="№ сделки, описание, заказчик"
|
||||||
|
onchange="this.form.submit()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-auto ms-md-auto">
|
||||||
|
<label class="small text-muted mb-1 fw-bold">Период (с):</label>
|
||||||
|
<input type="date" name="start_date" class="form-control form-control-sm bg-body text-body border-secondary" value="{{ start_date }}" onchange="this.form.submit()">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-auto">
|
||||||
|
<label class="small text-muted mb-1 fw-bold">Период (по):</label>
|
||||||
|
<input type="date" name="end_date" class="form-control form-control-sm bg-body text-body border-secondary" value="{{ end_date }}" onchange="this.form.submit()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-auto">
|
||||||
|
<a href="{% url 'shipping_journal' %}?reset=1" class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="bi bi-arrow-counterclockwise me-1"></i>Сброс
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0" data-sortable="1">
|
||||||
|
<thead>
|
||||||
|
<tr class="table-custom-header">
|
||||||
|
<th style="width:170px;">Дата</th>
|
||||||
|
<th style="width:120px;">Документ</th>
|
||||||
|
<th>Сделки</th>
|
||||||
|
<th>Откуда</th>
|
||||||
|
<th style="width:160px;">Кто</th>
|
||||||
|
<th data-sort="false" class="text-end" style="width:140px;"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for r in rows %}
|
||||||
|
<tr>
|
||||||
|
<td class="small">{{ r.transfer.occurred_at|date:"d.m.Y H:i" }}</td>
|
||||||
|
<td class="fw-bold">№{{ r.transfer.id }}</td>
|
||||||
|
<td class="small">
|
||||||
|
{% if r.deals %}
|
||||||
|
{% for d in r.deals %}
|
||||||
|
<div>
|
||||||
|
<span class="fw-bold">№{{ d.number }}</span>
|
||||||
|
{% if d.description %} — {{ d.description }}{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="small">{{ r.transfer.from_location.name }}</td>
|
||||||
|
<td class="small">{{ r.transfer.sender.username|default:"—" }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a class="btn btn-outline-secondary btn-sm" href="{{ r.admin_url }}" target="_blank">Открыть</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="6" class="text-center text-muted py-4">Пока нет отгрузок</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -76,15 +76,15 @@
|
|||||||
<div class="row g-3 mt-1">
|
<div class="row g-3 mt-1">
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<div class="small text-muted fw-bold mb-1">Списано</div>
|
<div class="small text-muted fw-bold mb-1">Списано</div>
|
||||||
{% if card.report.consumptions.all %}
|
{% if card.consumption_rows %}
|
||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
{% for c in card.report.consumptions.all %}
|
{% for c in card.consumption_rows %}
|
||||||
{% if c.stock_item_id and c.stock_item.material_id %}
|
{% if c.stock_item_id and c.stock_item.material_id %}
|
||||||
<li>
|
<li>
|
||||||
{{ c.stock_item.material.full_name|default:c.stock_item.material.name }}
|
{{ c.stock_item.material.full_name|default:c.stock_item.material.name }}
|
||||||
({% if c.stock_item.current_length and c.stock_item.current_width %}{{ c.stock_item.current_length|floatformat:"-g" }}×{{ c.stock_item.current_width|floatformat:"-g" }}{% elif c.stock_item.current_length %}{{ c.stock_item.current_length|floatformat:"-g" }}{% else %}—{% endif %})
|
({% if c.stock_item.current_length and c.stock_item.current_width %}{{ c.stock_item.current_length|floatformat:"-g" }}×{{ c.stock_item.current_width|floatformat:"-g" }}{% elif c.stock_item.current_length %}{{ c.stock_item.current_length|floatformat:"-g" }}{% else %}—{% endif %})
|
||||||
{% if c.stock_item.deal_id %}<span class="text-muted">(сделка № {{ c.stock_item.deal.number }})</span>{% endif %}
|
{% if c.stock_item.deal_id %}<span class="text-muted">(сделка № {{ c.stock_item.deal.number }})</span>{% endif %}
|
||||||
{{ c.quantity|floatformat:"-g" }} шт
|
{{ c.quantity|floatformat:"-g" }} шт — масса {% if c.mass_kg or c.mass_kg == 0 %}{{ c.mass_kg|floatformat:1 }}{% else %}—{% endif %} кг
|
||||||
</li>
|
</li>
|
||||||
{% elif c.stock_item_id and c.stock_item.entity_id %}
|
{% elif c.stock_item_id and c.stock_item.entity_id %}
|
||||||
<li>
|
<li>
|
||||||
@@ -119,13 +119,13 @@
|
|||||||
|
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<div class="small text-muted fw-bold mb-1">Остаток ДО</div>
|
<div class="small text-muted fw-bold mb-1">Остаток ДО</div>
|
||||||
{% if card.report.remnants.all %}
|
{% if card.remnant_rows %}
|
||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
{% for r in card.report.remnants.all %}
|
{% for r in card.remnant_rows %}
|
||||||
<li>
|
<li>
|
||||||
{{ r.material.full_name|default:r.material.name|default:r.material }}
|
{{ r.material.full_name|default:r.material.name|default:r.material }}
|
||||||
({% if r.current_length and r.current_width %}{{ r.current_length|floatformat:"-g" }}×{{ r.current_width|floatformat:"-g" }}{% elif r.current_length %}{{ r.current_length|floatformat:"-g" }}{% else %}—{% endif %})
|
({% if r.current_length and r.current_width %}{{ r.current_length|floatformat:"-g" }}×{{ r.current_width|floatformat:"-g" }}{% elif r.current_length %}{{ r.current_length|floatformat:"-g" }}{% else %}—{% endif %})
|
||||||
{{ r.quantity|floatformat:"-g" }} шт
|
{{ r.quantity|floatformat:"-g" }} шт — масса {% if r.mass_kg or r.mass_kg == 0 %}{{ r.mass_kg|floatformat:1 }}{% else %}—{% endif %} кг
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from .views import (
|
|||||||
DealDetailView,
|
DealDetailView,
|
||||||
DealPlanningView,
|
DealPlanningView,
|
||||||
DealMissingTechProcessView,
|
DealMissingTechProcessView,
|
||||||
|
DealMissingMaterialView,
|
||||||
DealUpsertView,
|
DealUpsertView,
|
||||||
DealBatchActionView,
|
DealBatchActionView,
|
||||||
DealItemUpsertView,
|
DealItemUpsertView,
|
||||||
@@ -58,6 +59,7 @@ from .views import (
|
|||||||
WarehouseStocksView,
|
WarehouseStocksView,
|
||||||
WarehouseTransferCreateView,
|
WarehouseTransferCreateView,
|
||||||
ProcurementDashboardView,
|
ProcurementDashboardView,
|
||||||
|
ShippingJournalView,
|
||||||
ShippingView,
|
ShippingView,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,6 +74,7 @@ urlpatterns = [
|
|||||||
path('planning/', PlanningView.as_view(), name='planning'),
|
path('planning/', PlanningView.as_view(), name='planning'),
|
||||||
path('planning/deal/<int:pk>/', DealPlanningView.as_view(), name='planning_deal'),
|
path('planning/deal/<int:pk>/', DealPlanningView.as_view(), name='planning_deal'),
|
||||||
path('planning/deal/<int:pk>/missing-tech/', DealMissingTechProcessView.as_view(), name='deal_missing_tech_process'),
|
path('planning/deal/<int:pk>/missing-tech/', DealMissingTechProcessView.as_view(), name='deal_missing_tech_process'),
|
||||||
|
path('planning/deal/<int:pk>/missing-material/', DealMissingMaterialView.as_view(), name='deal_missing_material'),
|
||||||
path('planning/task/<int:pk>/items/', TaskItemsView.as_view(), name='task_items'),
|
path('planning/task/<int:pk>/items/', TaskItemsView.as_view(), name='task_items'),
|
||||||
path('customers/', CustomersView.as_view(), name='customers'),
|
path('customers/', CustomersView.as_view(), name='customers'),
|
||||||
path('customers/<int:pk>/', CustomerDealsView.as_view(), name='customer_deals'),
|
path('customers/<int:pk>/', CustomerDealsView.as_view(), name='customer_deals'),
|
||||||
@@ -124,6 +127,7 @@ urlpatterns = [
|
|||||||
path('writeoffs/', WriteOffsView.as_view(), name='writeoffs'),
|
path('writeoffs/', WriteOffsView.as_view(), name='writeoffs'),
|
||||||
path('procurement/', ProcurementDashboardView.as_view(), name='procurement'),
|
path('procurement/', ProcurementDashboardView.as_view(), name='procurement'),
|
||||||
path('shipping/', ShippingView.as_view(), name='shipping'),
|
path('shipping/', ShippingView.as_view(), name='shipping'),
|
||||||
|
path('shipping/journal/', ShippingJournalView.as_view(), name='shipping_journal'),
|
||||||
path('legacy/closing/', LegacyClosingView.as_view(), name='legacy_closing'),
|
path('legacy/closing/', LegacyClosingView.as_view(), name='legacy_closing'),
|
||||||
path('legacy/writeoffs/', LegacyWriteOffsView.as_view(), name='legacy_writeoffs'),
|
path('legacy/writeoffs/', LegacyWriteOffsView.as_view(), name='legacy_writeoffs'),
|
||||||
|
|
||||||
|
|||||||
@@ -2051,6 +2051,7 @@ class DealPlanningView(LoginRequiredMixin, TemplateView):
|
|||||||
self.request.session['sf_missing_tech_process'] = raw_missing
|
self.request.session['sf_missing_tech_process'] = raw_missing
|
||||||
|
|
||||||
context['missing_material_rows'] = []
|
context['missing_material_rows'] = []
|
||||||
|
context['missing_material_details_url'] = ''
|
||||||
context['missing_material_autoshow'] = False
|
context['missing_material_autoshow'] = False
|
||||||
|
|
||||||
raw_mat = self.request.session.get('sf_missing_material')
|
raw_mat = self.request.session.get('sf_missing_material')
|
||||||
@@ -2070,6 +2071,7 @@ class DealPlanningView(LoginRequiredMixin, TemplateView):
|
|||||||
})
|
})
|
||||||
|
|
||||||
context['missing_material_rows'] = rows
|
context['missing_material_rows'] = rows
|
||||||
|
context['missing_material_details_url'] = str(reverse_lazy('deal_missing_material', kwargs={'pk': int(deal.id)}))
|
||||||
context['missing_material_autoshow'] = not bool(raw_mat.get('shown'))
|
context['missing_material_autoshow'] = not bool(raw_mat.get('shown'))
|
||||||
|
|
||||||
if not bool(raw_mat.get('shown')):
|
if not bool(raw_mat.get('shown')):
|
||||||
@@ -2338,6 +2340,47 @@ class DealMissingTechProcessView(LoginRequiredMixin, TemplateView):
|
|||||||
'url': f"{reverse_lazy('product_info', kwargs={'pk': int(e.id)})}?next={next_url}",
|
'url': f"{reverse_lazy('product_info', kwargs={'pk': int(e.id)})}?next={next_url}",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ctx['page_title'] = 'Позиции без техпроцесса'
|
||||||
|
ctx['page_hint'] = 'Для запуска в производство нужен техпроцесс (операция seq=1).'
|
||||||
|
ctx['items'] = rows
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class DealMissingMaterialView(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = 'shiftflow/missing_techprocess.html'
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
roles = get_user_roles(request.user)
|
||||||
|
if not has_any_role(roles, ['admin', 'technologist', 'prod_head']):
|
||||||
|
return redirect('registry')
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
roles = get_user_roles(self.request.user)
|
||||||
|
ctx['user_roles'] = sorted(roles)
|
||||||
|
ctx['user_role'] = primary_role(roles)
|
||||||
|
|
||||||
|
deal = get_object_or_404(Deal.objects.select_related('company'), pk=int(self.kwargs['pk']))
|
||||||
|
ctx['deal'] = deal
|
||||||
|
|
||||||
|
raw = self.request.session.get('sf_missing_material')
|
||||||
|
entity_ids = []
|
||||||
|
if raw and int(raw.get('deal_id') or 0) == int(deal.id):
|
||||||
|
entity_ids = [int(x) for x in (raw.get('entity_ids') or []) if isinstance(x, int) or str(x).isdigit()]
|
||||||
|
|
||||||
|
qs = ProductEntity.objects.filter(id__in=entity_ids).order_by('entity_type', 'drawing_number', 'name', 'id')
|
||||||
|
next_url = str(reverse_lazy('planning_deal', kwargs={'pk': int(deal.id)}))
|
||||||
|
next_qs = urlencode({'next': next_url})
|
||||||
|
rows = []
|
||||||
|
for e in qs:
|
||||||
|
rows.append({
|
||||||
|
'entity': e,
|
||||||
|
'url': f"{reverse_lazy('product_info', kwargs={'pk': int(e.id)})}?{next_qs}",
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx['page_title'] = 'Позиции без материала'
|
||||||
|
ctx['page_hint'] = 'Для запуска в производство у детали должен быть заполнен material в паспорте.'
|
||||||
ctx['items'] = rows
|
ctx['items'] = rows
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@@ -3551,7 +3594,7 @@ class DealBatchActionView(LoginRequiredMixin, View):
|
|||||||
'entity_ids': missing_ids,
|
'entity_ids': missing_ids,
|
||||||
'shown': False,
|
'shown': False,
|
||||||
}
|
}
|
||||||
messages.error(request, 'В спецификации есть детали без материала. Добавь material в паспорт(ы) и повтори запуск.')
|
messages.error(request, 'В спецификации есть детали без материала. Добавь material в паспорт(ы) и повтори запуск. Список доступен по ссылке в карточке сделки.')
|
||||||
return redirect(next_url)
|
return redirect(next_url)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception('start_batch_item_production: failed deal_id=%s item_id=%s qty=%s', deal_id, item_id, qty)
|
logger.exception('start_batch_item_production: failed deal_id=%s item_id=%s qty=%s', deal_id, item_id, qty)
|
||||||
@@ -4578,7 +4621,77 @@ class WarehouseReceiptCreateView(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
|
|
||||||
class ShippingView(LoginRequiredMixin, TemplateView):
|
class ShippingView(LoginRequiredMixin, TemplateView):
|
||||||
template_name = 'shiftflow/shipping.html'
|
template_name = 'shiftflow/shipping_cart.html'
|
||||||
|
|
||||||
|
SESSION_DEALS_KEY = 'shipping_cart_deal_ids'
|
||||||
|
SESSION_QTY_KEY = 'shipping_cart_qty'
|
||||||
|
|
||||||
|
def _get_cart_deal_ids(self) -> list[int]:
|
||||||
|
raw = self.request.session.get(self.SESSION_DEALS_KEY) or []
|
||||||
|
out = []
|
||||||
|
for x in raw:
|
||||||
|
try:
|
||||||
|
v = int(x)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if v > 0:
|
||||||
|
out.append(v)
|
||||||
|
return sorted(set(out))
|
||||||
|
|
||||||
|
def _set_cart_deal_ids(self, ids: list[int]) -> None:
|
||||||
|
cleaned = sorted({int(x) for x in (ids or []) if int(x) > 0})
|
||||||
|
self.request.session[self.SESSION_DEALS_KEY] = cleaned
|
||||||
|
self.request.session.modified = True
|
||||||
|
|
||||||
|
def _get_cart_qty(self) -> dict:
|
||||||
|
raw = self.request.session.get(self.SESSION_QTY_KEY)
|
||||||
|
return raw if isinstance(raw, dict) else {}
|
||||||
|
|
||||||
|
def _set_cart_qty(self, data: dict) -> None:
|
||||||
|
self.request.session[self.SESSION_QTY_KEY] = data if isinstance(data, dict) else {}
|
||||||
|
self.request.session.modified = True
|
||||||
|
|
||||||
|
def _parse_post_qty(self) -> tuple[dict[int, dict[int, int]], dict[int, dict[int, float]]]:
|
||||||
|
ent_by_deal: dict[int, dict[int, int]] = {}
|
||||||
|
mat_by_deal: dict[int, dict[int, float]] = {}
|
||||||
|
|
||||||
|
for k, v in (self.request.POST or {}).items():
|
||||||
|
if not k:
|
||||||
|
continue
|
||||||
|
s = (str(v) if v is not None else '').strip().replace(',', '.')
|
||||||
|
|
||||||
|
if k.startswith('d') and '_ent_' in k:
|
||||||
|
try:
|
||||||
|
left, ent_id_raw = k.split('_ent_', 1)
|
||||||
|
deal_id_raw = left[1:]
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if not deal_id_raw.isdigit() or not ent_id_raw.isdigit():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
qty = int(float(s)) if s else 0
|
||||||
|
except Exception:
|
||||||
|
qty = 0
|
||||||
|
if qty > 0:
|
||||||
|
ent_by_deal.setdefault(int(deal_id_raw), {})[int(ent_id_raw)] = int(qty)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if k.startswith('d') and '_mat_' in k:
|
||||||
|
try:
|
||||||
|
left, mat_id_raw = k.split('_mat_', 1)
|
||||||
|
deal_id_raw = left[1:]
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if not deal_id_raw.isdigit() or not mat_id_raw.isdigit():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
qty_f = float(s) if s else 0.0
|
||||||
|
except Exception:
|
||||||
|
qty_f = 0.0
|
||||||
|
if qty_f > 0:
|
||||||
|
mat_by_deal.setdefault(int(deal_id_raw), {})[int(mat_id_raw)] = float(qty_f)
|
||||||
|
|
||||||
|
return ent_by_deal, mat_by_deal
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
profile = getattr(request.user, 'profile', None)
|
profile = getattr(request.user, 'profile', None)
|
||||||
@@ -4600,32 +4713,70 @@ class ShippingView(LoginRequiredMixin, TemplateView):
|
|||||||
ctx['can_edit'] = bool(self.can_edit)
|
ctx['can_edit'] = bool(self.can_edit)
|
||||||
ctx['is_readonly'] = bool(self.is_readonly)
|
ctx['is_readonly'] = bool(self.is_readonly)
|
||||||
|
|
||||||
deal_id_raw = (self.request.GET.get('deal_id') or '').strip()
|
|
||||||
deal_id = int(deal_id_raw) if deal_id_raw.isdigit() else None
|
|
||||||
|
|
||||||
shipping_loc, _ = Location.objects.get_or_create(
|
shipping_loc, _ = Location.objects.get_or_create(
|
||||||
name='Склад отгруженных позиций',
|
name='Склад отгруженных позиций',
|
||||||
defaults={'is_production_area': False},
|
defaults={'is_production_area': False},
|
||||||
)
|
)
|
||||||
ctx['shipping_location'] = shipping_loc
|
ctx['shipping_location'] = shipping_loc
|
||||||
|
|
||||||
ctx['deals'] = list(Deal.objects.select_related('company').order_by('-id')[:300])
|
add_deal_id_raw = (self.request.GET.get('deal_id') or '').strip()
|
||||||
|
if add_deal_id_raw.isdigit():
|
||||||
|
cart_ids = self._get_cart_deal_ids()
|
||||||
|
cart_ids.append(int(add_deal_id_raw))
|
||||||
|
self._set_cart_deal_ids(cart_ids)
|
||||||
|
|
||||||
ctx['selected_deal_id'] = deal_id
|
ctx['available_deals'] = list(
|
||||||
|
Deal.objects.select_related('company').filter(status='work').order_by('-id')[:500]
|
||||||
|
)
|
||||||
|
|
||||||
ctx['entity_rows'] = []
|
cart_ids = self._get_cart_deal_ids()
|
||||||
ctx['material_rows'] = []
|
deals_by_id = {
|
||||||
|
int(d.id): d
|
||||||
|
for d in Deal.objects.select_related('company').filter(id__in=cart_ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
qty_data = self._get_cart_qty() or {}
|
||||||
|
ent_qty_data = qty_data.get('ent') if isinstance(qty_data.get('ent'), dict) else {}
|
||||||
|
mat_qty_data = qty_data.get('mat') if isinstance(qty_data.get('mat'), dict) else {}
|
||||||
|
|
||||||
if deal_id:
|
|
||||||
from shiftflow.services.shipping import build_shipment_rows
|
from shiftflow.services.shipping import build_shipment_rows
|
||||||
|
|
||||||
|
cart = []
|
||||||
|
for deal_id in cart_ids:
|
||||||
|
d = deals_by_id.get(int(deal_id))
|
||||||
|
if not d:
|
||||||
|
continue
|
||||||
|
|
||||||
entity_rows, material_rows = build_shipment_rows(
|
entity_rows, material_rows = build_shipment_rows(
|
||||||
deal_id=int(deal_id),
|
deal_id=int(deal_id),
|
||||||
shipping_location_id=int(shipping_loc.id),
|
shipping_location_id=int(shipping_loc.id),
|
||||||
)
|
)
|
||||||
ctx['entity_rows'] = entity_rows
|
|
||||||
ctx['material_rows'] = material_rows
|
|
||||||
|
|
||||||
|
ent_sel = ent_qty_data.get(str(deal_id)) if isinstance(ent_qty_data, dict) else None
|
||||||
|
mat_sel = mat_qty_data.get(str(deal_id)) if isinstance(mat_qty_data, dict) else None
|
||||||
|
ent_sel = ent_sel if isinstance(ent_sel, dict) else {}
|
||||||
|
mat_sel = mat_sel if isinstance(mat_sel, dict) else {}
|
||||||
|
|
||||||
|
for r in entity_rows:
|
||||||
|
try:
|
||||||
|
r['selected_qty'] = int(ent_sel.get(str(int(r['entity'].id)), 0))
|
||||||
|
except Exception:
|
||||||
|
r['selected_qty'] = 0
|
||||||
|
|
||||||
|
for r in material_rows:
|
||||||
|
try:
|
||||||
|
r['selected_qty'] = float(mat_sel.get(str(int(r['material'].id)), 0.0))
|
||||||
|
except Exception:
|
||||||
|
r['selected_qty'] = 0.0
|
||||||
|
|
||||||
|
cart.append({
|
||||||
|
'deal': d,
|
||||||
|
'entity_rows': entity_rows,
|
||||||
|
'material_rows': material_rows,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx['cart'] = cart
|
||||||
|
ctx['journal_url'] = str(reverse_lazy('shipping_journal'))
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
@@ -4633,70 +4784,198 @@ class ShippingView(LoginRequiredMixin, TemplateView):
|
|||||||
messages.error(request, 'Доступ только для просмотра.')
|
messages.error(request, 'Доступ только для просмотра.')
|
||||||
return redirect('shipping')
|
return redirect('shipping')
|
||||||
|
|
||||||
deal_id_raw = (request.POST.get('deal_id') or '').strip()
|
action = (request.POST.get('action') or '').strip()
|
||||||
|
|
||||||
if not deal_id_raw.isdigit():
|
|
||||||
messages.error(request, 'Выбери сделку.')
|
|
||||||
return redirect('shipping')
|
|
||||||
|
|
||||||
deal_id = int(deal_id_raw)
|
|
||||||
|
|
||||||
shipping_loc, _ = Location.objects.get_or_create(
|
shipping_loc, _ = Location.objects.get_or_create(
|
||||||
name='Склад отгруженных позиций',
|
name='Склад отгруженных позиций',
|
||||||
defaults={'is_production_area': False},
|
defaults={'is_production_area': False},
|
||||||
)
|
)
|
||||||
|
|
||||||
entity_qty: dict[int, int] = {}
|
ent_by_deal, mat_by_deal = self._parse_post_qty()
|
||||||
material_qty: dict[int, float] = {}
|
qty_data = {
|
||||||
|
'ent': {str(k): {str(kk): int(vv) for kk, vv in (v or {}).items()} for k, v in (ent_by_deal or {}).items()},
|
||||||
|
'mat': {str(k): {str(kk): float(vv) for kk, vv in (v or {}).items()} for k, v in (mat_by_deal or {}).items()},
|
||||||
|
}
|
||||||
|
self._set_cart_qty(qty_data)
|
||||||
|
|
||||||
for k, v in request.POST.items():
|
cart_ids = self._get_cart_deal_ids()
|
||||||
if not k or v is None:
|
|
||||||
continue
|
|
||||||
s = (str(v) or '').strip().replace(',', '.')
|
|
||||||
|
|
||||||
if k.startswith('ent_'):
|
if action == 'add_deal':
|
||||||
ent_id_raw = k.replace('ent_', '').strip()
|
add_id_raw = (request.POST.get('add_deal_id') or '').strip()
|
||||||
if not ent_id_raw.isdigit():
|
if not add_id_raw.isdigit():
|
||||||
continue
|
messages.error(request, 'Выбери сделку.')
|
||||||
try:
|
return redirect('shipping')
|
||||||
qty = int(float(s)) if s else 0
|
|
||||||
except ValueError:
|
|
||||||
qty = 0
|
|
||||||
if qty > 0:
|
|
||||||
entity_qty[int(ent_id_raw)] = int(qty)
|
|
||||||
|
|
||||||
if k.startswith('mat_'):
|
add_id = int(add_id_raw)
|
||||||
mat_id_raw = k.replace('mat_', '').strip()
|
ok = Deal.objects.filter(id=add_id, status='work').exists()
|
||||||
if not mat_id_raw.isdigit():
|
if not ok:
|
||||||
continue
|
messages.error(request, 'Сделка не найдена или не в статусе «В работе».')
|
||||||
try:
|
return redirect('shipping')
|
||||||
qty_f = float(s) if s else 0.0
|
|
||||||
except ValueError:
|
|
||||||
qty_f = 0.0
|
|
||||||
if qty_f > 0:
|
|
||||||
material_qty[int(mat_id_raw)] = float(qty_f)
|
|
||||||
|
|
||||||
if not entity_qty and not material_qty:
|
cart_ids.append(int(add_id))
|
||||||
messages.error(request, 'Укажи количество к отгрузке хотя бы по одной позиции.')
|
self._set_cart_deal_ids(cart_ids)
|
||||||
return redirect(f"{reverse_lazy('shipping')}?deal_id={deal_id}&from_location_id={from_location_id}")
|
return redirect('shipping')
|
||||||
|
|
||||||
|
if action == 'remove_deal':
|
||||||
|
rem_id_raw = (request.POST.get('remove_deal_id') or '').strip()
|
||||||
|
if rem_id_raw.isdigit():
|
||||||
|
rem_id = int(rem_id_raw)
|
||||||
|
cart_ids = [x for x in cart_ids if int(x) != int(rem_id)]
|
||||||
|
self._set_cart_deal_ids(cart_ids)
|
||||||
|
return redirect('shipping')
|
||||||
|
|
||||||
|
if action != 'ship':
|
||||||
|
return redirect('shipping')
|
||||||
|
|
||||||
|
if not ent_by_deal and not mat_by_deal:
|
||||||
|
messages.error(request, 'Нечего отгружать: выбери позиции и количество.')
|
||||||
|
return redirect('shipping')
|
||||||
|
|
||||||
from shiftflow.services.shipping import create_shipment_transfers
|
from shiftflow.services.shipping import create_shipment_transfers
|
||||||
|
|
||||||
|
all_transfer_ids: list[int] = []
|
||||||
try:
|
try:
|
||||||
|
for deal_id, ent_map in (ent_by_deal or {}).items():
|
||||||
|
mat_map = (mat_by_deal or {}).get(int(deal_id), {})
|
||||||
|
if not ent_map and not mat_map:
|
||||||
|
continue
|
||||||
|
|
||||||
ids = create_shipment_transfers(
|
ids = create_shipment_transfers(
|
||||||
deal_id=int(deal_id),
|
deal_id=int(deal_id),
|
||||||
shipping_location_id=int(shipping_loc.id),
|
shipping_location_id=int(shipping_loc.id),
|
||||||
entity_qty=entity_qty,
|
entity_qty=ent_map,
|
||||||
material_qty=material_qty,
|
material_qty=mat_map,
|
||||||
user_id=int(request.user.id),
|
user_id=int(request.user.id),
|
||||||
)
|
)
|
||||||
msg = ', '.join([str(i) for i in ids])
|
all_transfer_ids += [int(x) for x in (ids or [])]
|
||||||
|
|
||||||
|
all_transfer_ids = sorted(set(all_transfer_ids))
|
||||||
|
msg = ', '.join([str(i) for i in all_transfer_ids])
|
||||||
messages.success(request, f'Отгрузка оформлена. Документы перемещения: {msg}.')
|
messages.success(request, f'Отгрузка оформлена. Документы перемещения: {msg}.')
|
||||||
|
|
||||||
|
self._set_cart_deal_ids([])
|
||||||
|
self._set_cart_qty({})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception('shipping:error deal_id=%s', deal_id)
|
logger.exception('shipping:error')
|
||||||
messages.error(request, f'Ошибка отгрузки: {e}')
|
messages.error(request, f'Ошибка отгрузки: {e}')
|
||||||
|
|
||||||
return redirect(f"{reverse_lazy('shipping')}?deal_id={deal_id}")
|
return redirect('shipping')
|
||||||
|
|
||||||
|
|
||||||
|
class ShippingJournalView(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = 'shiftflow/shipping_journal.html'
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
roles = get_user_roles(request.user)
|
||||||
|
if not has_any_role(roles, ['admin', 'clerk', 'manager', 'prod_head', 'director', 'technologist', 'observer']):
|
||||||
|
return redirect('registry')
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
roles = get_user_roles(self.request.user)
|
||||||
|
ctx['user_roles'] = sorted(roles)
|
||||||
|
ctx['user_role'] = primary_role(roles)
|
||||||
|
|
||||||
|
shipping_loc, _ = Location.objects.get_or_create(
|
||||||
|
name='Склад отгруженных позиций',
|
||||||
|
defaults={'is_production_area': False},
|
||||||
|
)
|
||||||
|
ctx['shipping_location'] = shipping_loc
|
||||||
|
|
||||||
|
q = (self.request.GET.get('q') or '').strip()
|
||||||
|
start_date = (self.request.GET.get('start_date') or '').strip()
|
||||||
|
end_date = (self.request.GET.get('end_date') or '').strip()
|
||||||
|
filtered = self.request.GET.get('filtered')
|
||||||
|
reset = self.request.GET.get('reset')
|
||||||
|
is_default = (not filtered) or bool(reset)
|
||||||
|
|
||||||
|
if is_default:
|
||||||
|
today = timezone.localdate()
|
||||||
|
start = today - timezone.timedelta(days=14)
|
||||||
|
ctx['start_date'] = start.strftime('%Y-%m-%d')
|
||||||
|
ctx['end_date'] = today.strftime('%Y-%m-%d')
|
||||||
|
else:
|
||||||
|
ctx['start_date'] = start_date
|
||||||
|
ctx['end_date'] = end_date
|
||||||
|
|
||||||
|
ctx['q'] = q
|
||||||
|
|
||||||
|
from warehouse.models import TransferRecord, TransferLine
|
||||||
|
|
||||||
|
trs_qs = (
|
||||||
|
TransferRecord.objects.select_related('from_location', 'to_location', 'sender', 'receiver')
|
||||||
|
.filter(to_location_id=int(shipping_loc.id))
|
||||||
|
)
|
||||||
|
|
||||||
|
start_val = (ctx.get('start_date') or '').strip()
|
||||||
|
end_val = (ctx.get('end_date') or '').strip()
|
||||||
|
if start_val:
|
||||||
|
trs_qs = trs_qs.filter(occurred_at__date__gte=start_val)
|
||||||
|
if end_val:
|
||||||
|
trs_qs = trs_qs.filter(occurred_at__date__lte=end_val)
|
||||||
|
|
||||||
|
if q:
|
||||||
|
matched_tr_ids = (
|
||||||
|
TransferLine.objects.select_related('stock_item', 'stock_item__deal', 'stock_item__deal__company')
|
||||||
|
.filter(transfer__in=trs_qs)
|
||||||
|
.filter(stock_item__deal__isnull=False)
|
||||||
|
.filter(
|
||||||
|
Q(stock_item__deal__number__icontains=q)
|
||||||
|
| Q(stock_item__deal__description__icontains=q)
|
||||||
|
| Q(stock_item__deal__company__name__icontains=q)
|
||||||
|
)
|
||||||
|
.values_list('transfer_id', flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
trs_qs = trs_qs.filter(id__in=matched_tr_ids)
|
||||||
|
|
||||||
|
trs = list(trs_qs.order_by('-occurred_at', '-id')[:200])
|
||||||
|
|
||||||
|
tr_ids = [int(x.id) for x in trs]
|
||||||
|
lines = list(
|
||||||
|
TransferLine.objects.select_related(
|
||||||
|
'stock_item',
|
||||||
|
'stock_item__deal',
|
||||||
|
'stock_item__deal__company',
|
||||||
|
'stock_item__entity',
|
||||||
|
'stock_item__material',
|
||||||
|
)
|
||||||
|
.filter(transfer_id__in=tr_ids)
|
||||||
|
.order_by('transfer_id', 'id')
|
||||||
|
)
|
||||||
|
|
||||||
|
by_tr: dict[int, list] = {}
|
||||||
|
for ln in lines:
|
||||||
|
by_tr.setdefault(int(ln.transfer_id), []).append(ln)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for tr in trs:
|
||||||
|
lns = by_tr.get(int(tr.id), [])
|
||||||
|
|
||||||
|
deals_by_id: dict[int, dict] = {}
|
||||||
|
for ln in lns:
|
||||||
|
d = getattr(getattr(ln, 'stock_item', None), 'deal', None)
|
||||||
|
if not d or not getattr(d, 'id', None):
|
||||||
|
continue
|
||||||
|
deals_by_id[int(d.id)] = {
|
||||||
|
'number': getattr(d, 'number', '') or '',
|
||||||
|
'description': getattr(d, 'description', '') or '',
|
||||||
|
'company': getattr(getattr(d, 'company', None), 'name', '') or '',
|
||||||
|
}
|
||||||
|
|
||||||
|
deals = list(deals_by_id.values())
|
||||||
|
deals = sorted(deals, key=lambda x: (str(x.get('number') or ''), str(x.get('description') or '')))
|
||||||
|
|
||||||
|
rows.append({
|
||||||
|
'transfer': tr,
|
||||||
|
'deals': deals,
|
||||||
|
'lines': lns,
|
||||||
|
'admin_url': f"/admin/warehouse/transferrecord/{int(tr.id)}/change/",
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx['rows'] = rows
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
from shiftflow.services.assembly_closing import get_assembly_closing_info, apply_assembly_closing
|
from shiftflow.services.assembly_closing import get_assembly_closing_info, apply_assembly_closing
|
||||||
@@ -4731,7 +5010,14 @@ class AssemblyClosingView(LoginRequiredMixin, TemplateView):
|
|||||||
ctx.update(info)
|
ctx.update(info)
|
||||||
|
|
||||||
ws_id = getattr(self.workitem, 'workshop_id', None)
|
ws_id = getattr(self.workitem, 'workshop_id', None)
|
||||||
ctx['workshop_machines'] = list(Machine.objects.filter(workshop_id=ws_id).order_by('name')) if ws_id else []
|
if ws_id:
|
||||||
|
ctx['workshop_machines'] = list(
|
||||||
|
Machine.objects.select_related('workshop').filter(workshop_id=ws_id, machine_type='post').order_by('name')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ctx['workshop_machines'] = list(
|
||||||
|
Machine.objects.select_related('workshop').filter(machine_type='post').order_by('workshop__name', 'name')
|
||||||
|
)
|
||||||
|
|
||||||
if info.get('error'):
|
if info.get('error'):
|
||||||
messages.warning(self.request, info['error'])
|
messages.warning(self.request, info['error'])
|
||||||
@@ -4758,17 +5044,24 @@ class AssemblyClosingView(LoginRequiredMixin, TemplateView):
|
|||||||
return redirect('assembly_closing', pk=self.workitem.id)
|
return redirect('assembly_closing', pk=self.workitem.id)
|
||||||
|
|
||||||
mid = int(mid_raw)
|
mid = int(mid_raw)
|
||||||
|
machine = Machine.objects.select_related('workshop').filter(id=mid).first()
|
||||||
|
if not machine or (machine.machine_type or '') != 'post':
|
||||||
|
messages.error(request, 'Выбери пост (тип: Пост).')
|
||||||
|
return redirect('assembly_closing', pk=self.workitem.id)
|
||||||
|
|
||||||
ws_id = getattr(self.workitem, 'workshop_id', None)
|
ws_id = getattr(self.workitem, 'workshop_id', None)
|
||||||
if ws_id:
|
if ws_id and int(machine.workshop_id or 0) != int(ws_id):
|
||||||
ok = Machine.objects.filter(id=mid, workshop_id=int(ws_id)).exists()
|
|
||||||
else:
|
|
||||||
ok = Machine.objects.filter(id=mid).exists()
|
|
||||||
if not ok:
|
|
||||||
messages.error(request, 'Выбранный пост не относится к цеху задания.')
|
messages.error(request, 'Выбранный пост не относится к цеху задания.')
|
||||||
return redirect('assembly_closing', pk=self.workitem.id)
|
return redirect('assembly_closing', pk=self.workitem.id)
|
||||||
|
|
||||||
WorkItem.objects.filter(id=int(self.workitem.id), machine_id__isnull=True).update(machine_id=mid)
|
upd = {'machine_id': int(machine.id)}
|
||||||
self.workitem.machine_id = mid
|
if not getattr(self.workitem, 'workshop_id', None) and getattr(machine, 'workshop_id', None):
|
||||||
|
upd['workshop_id'] = int(machine.workshop_id)
|
||||||
|
|
||||||
|
WorkItem.objects.filter(id=int(self.workitem.id), machine_id__isnull=True).update(**upd)
|
||||||
|
self.workitem.machine_id = int(machine.id)
|
||||||
|
if 'workshop_id' in upd:
|
||||||
|
self.workitem.workshop_id = int(upd['workshop_id'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
apply_assembly_closing(self.workitem.id, qty, request.user.id)
|
apply_assembly_closing(self.workitem.id, qty, request.user.id)
|
||||||
@@ -5399,10 +5692,72 @@ class LegacyWriteOffsView(LoginRequiredMixin, TemplateView):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _calc_mass_kg(material, length_mm, width_mm, qty):
|
||||||
|
if not material or getattr(material, 'mass_per_unit', None) in (None, ''):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
mpu = float(material.mass_per_unit)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
q = float(qty or 0)
|
||||||
|
except Exception:
|
||||||
|
q = 0.0
|
||||||
|
if q <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
l = float(length_mm) if length_mm not in (None, '') else None
|
||||||
|
except Exception:
|
||||||
|
l = None
|
||||||
|
try:
|
||||||
|
w = float(width_mm) if width_mm not in (None, '') else None
|
||||||
|
except Exception:
|
||||||
|
w = None
|
||||||
|
|
||||||
|
if l is not None and w is not None:
|
||||||
|
factor = (l * w) / 1_000_000.0
|
||||||
|
elif l is not None:
|
||||||
|
factor = l / 1000.0
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return factor * mpu * q
|
||||||
|
|
||||||
|
cons_by_report_id = {}
|
||||||
|
rem_by_report_id = {}
|
||||||
|
|
||||||
|
for r in reports:
|
||||||
|
cons = list(getattr(r, 'consumptions', []).all() if hasattr(getattr(r, 'consumptions', None), 'all') else [])
|
||||||
|
for c in cons:
|
||||||
|
mat = None
|
||||||
|
if getattr(c, 'material_id', None):
|
||||||
|
mat = c.material
|
||||||
|
elif getattr(c, 'stock_item_id', None) and getattr(c.stock_item, 'material_id', None):
|
||||||
|
mat = c.stock_item.material
|
||||||
|
|
||||||
|
length_mm = getattr(getattr(c, 'stock_item', None), 'current_length', None)
|
||||||
|
width_mm = getattr(getattr(c, 'stock_item', None), 'current_width', None)
|
||||||
|
c.mass_kg = _calc_mass_kg(mat, length_mm, width_mm, getattr(c, 'quantity', None))
|
||||||
|
|
||||||
|
rems = list(getattr(r, 'remnants', []).all() if hasattr(getattr(r, 'remnants', None), 'all') else [])
|
||||||
|
for rm in rems:
|
||||||
|
mat = getattr(rm, 'material', None)
|
||||||
|
rm.mass_kg = _calc_mass_kg(
|
||||||
|
mat,
|
||||||
|
getattr(rm, 'current_length', None),
|
||||||
|
getattr(rm, 'current_width', None),
|
||||||
|
getattr(rm, 'quantity', None),
|
||||||
|
)
|
||||||
|
|
||||||
|
cons_by_report_id[int(r.id)] = cons
|
||||||
|
rem_by_report_id[int(r.id)] = rems
|
||||||
|
|
||||||
report_cards = []
|
report_cards = []
|
||||||
for r in reports:
|
for r in reports:
|
||||||
consumed = {}
|
consumed = {}
|
||||||
for c in list(getattr(r, 'consumptions', []).all() if hasattr(getattr(r, 'consumptions', None), 'all') else []):
|
cons = cons_by_report_id.get(int(r.id), [])
|
||||||
|
for c in cons:
|
||||||
mat = None
|
mat = None
|
||||||
if getattr(c, 'material_id', None):
|
if getattr(c, 'material_id', None):
|
||||||
mat = c.material
|
mat = c.material
|
||||||
@@ -5431,6 +5786,8 @@ class LegacyWriteOffsView(LoginRequiredMixin, TemplateView):
|
|||||||
'consumed': consumed,
|
'consumed': consumed,
|
||||||
'produced': produced,
|
'produced': produced,
|
||||||
'remnants': remnants,
|
'remnants': remnants,
|
||||||
|
'consumption_rows': cons_by_report_id.get(int(r.id), []),
|
||||||
|
'remnant_rows': rem_by_report_id.get(int(r.id), []),
|
||||||
'tasks': list(getattr(r, 'tasks', []).all() if hasattr(getattr(r, 'tasks', None), 'all') else []),
|
'tasks': list(getattr(r, 'tasks', []).all() if hasattr(getattr(r, 'tasks', None), 'all') else []),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -6246,12 +6603,27 @@ class ProductInfoView(LoginRequiredMixin, TemplateView):
|
|||||||
if swap_with < 0 or swap_with >= len(ops):
|
if swap_with < 0 or swap_with >= len(ops):
|
||||||
return redirect(stay_url)
|
return redirect(stay_url)
|
||||||
|
|
||||||
a = ops[idx]
|
with transaction.atomic():
|
||||||
b = ops[swap_with]
|
qs = EntityOperation.objects.select_for_update().filter(entity_id=entity.id)
|
||||||
a_seq, b_seq = int(a.seq), int(b.seq)
|
ops = list(qs.order_by('seq', 'id'))
|
||||||
EntityOperation.objects.filter(pk=a.id).update(seq=0)
|
idx = next((i for i, x in enumerate(ops) if int(x.id) == int(eo.id)), None)
|
||||||
EntityOperation.objects.filter(pk=b.id).update(seq=a_seq)
|
if idx is None:
|
||||||
EntityOperation.objects.filter(pk=a.id).update(seq=b_seq)
|
return redirect(stay_url)
|
||||||
|
|
||||||
|
swap_with = idx - 1 if direction == 'up' else idx + 1
|
||||||
|
if swap_with < 0 or swap_with >= len(ops):
|
||||||
|
return redirect(stay_url)
|
||||||
|
|
||||||
|
ops[idx], ops[swap_with] = ops[swap_with], ops[idx]
|
||||||
|
|
||||||
|
max_seq = qs.aggregate(m=Max('seq'))['m']
|
||||||
|
tmp_base = int(max_seq or 0) + 1000
|
||||||
|
|
||||||
|
for j, x in enumerate(ops, start=1):
|
||||||
|
EntityOperation.objects.filter(pk=x.id).update(seq=tmp_base + j)
|
||||||
|
|
||||||
|
for j, x in enumerate(ops, start=1):
|
||||||
|
EntityOperation.objects.filter(pk=x.id).update(seq=j)
|
||||||
|
|
||||||
messages.success(request, 'Порядок обновлён.')
|
messages.success(request, 'Порядок обновлён.')
|
||||||
return redirect(stay_url)
|
return redirect(stay_url)
|
||||||
@@ -6333,21 +6705,35 @@ class ProductInfoView(LoginRequiredMixin, TemplateView):
|
|||||||
valid = set(Operation.objects.filter(id__in=op_ids).values_list('id', flat=True))
|
valid = set(Operation.objects.filter(id__in=op_ids).values_list('id', flat=True))
|
||||||
op_ids = [int(x) for x in op_ids if int(x) in valid]
|
op_ids = [int(x) for x in op_ids if int(x) in valid]
|
||||||
|
|
||||||
EntityOperation.objects.filter(entity_id=entity.id).exclude(operation_id__in=op_ids).delete()
|
with transaction.atomic():
|
||||||
|
qs = EntityOperation.objects.select_for_update().filter(entity_id=entity.id)
|
||||||
|
qs.exclude(operation_id__in=op_ids).delete()
|
||||||
|
|
||||||
existing = list(EntityOperation.objects.filter(entity_id=entity.id, operation_id__in=op_ids).order_by('id'))
|
if op_ids:
|
||||||
|
existing = list(qs.filter(operation_id__in=op_ids).order_by('id'))
|
||||||
by_op = {int(eo.operation_id): eo for eo in existing}
|
by_op = {int(eo.operation_id): eo for eo in existing}
|
||||||
|
|
||||||
if existing:
|
max_seq = qs.aggregate(m=Max('seq'))['m']
|
||||||
EntityOperation.objects.filter(id__in=[eo.id for eo in existing]).update(seq=0)
|
tmp_base = int(max_seq or 0) + 1000
|
||||||
|
next_tmp = tmp_base + 1
|
||||||
|
|
||||||
|
for op_id in op_ids:
|
||||||
|
if int(op_id) in by_op:
|
||||||
|
continue
|
||||||
|
eo = EntityOperation.objects.create(entity_id=entity.id, operation_id=int(op_id), seq=next_tmp)
|
||||||
|
by_op[int(op_id)] = eo
|
||||||
|
next_tmp += 1
|
||||||
|
|
||||||
|
all_ids = [int(eo.id) for eo in by_op.values() if getattr(eo, 'id', None)]
|
||||||
|
all_ids = sorted(set(all_ids))
|
||||||
|
|
||||||
|
for j, eo_id in enumerate(all_ids, start=1):
|
||||||
|
EntityOperation.objects.filter(pk=eo_id).update(seq=tmp_base + j)
|
||||||
|
|
||||||
for i, op_id in enumerate(op_ids, start=1):
|
for i, op_id in enumerate(op_ids, start=1):
|
||||||
eo = by_op.get(int(op_id))
|
eo = by_op.get(int(op_id))
|
||||||
if eo:
|
if eo:
|
||||||
if int(eo.seq or 0) != i:
|
EntityOperation.objects.filter(pk=eo.id).update(seq=i)
|
||||||
EntityOperation.objects.filter(id=eo.id).update(seq=i)
|
|
||||||
else:
|
|
||||||
EntityOperation.objects.create(entity_id=entity.id, operation_id=int(op_id), seq=i)
|
|
||||||
|
|
||||||
messages.success(request, 'Сохранено.')
|
messages.success(request, 'Сохранено.')
|
||||||
return redirect(stay_url)
|
return redirect(stay_url)
|
||||||
@@ -6400,19 +6786,81 @@ class WriteOffsView(LoginRequiredMixin, TemplateView):
|
|||||||
'tasks__task__deal',
|
'tasks__task__deal',
|
||||||
'tasks__task__material',
|
'tasks__task__material',
|
||||||
'consumptions__material',
|
'consumptions__material',
|
||||||
|
'consumptions__material__category',
|
||||||
'consumptions__stock_item__material',
|
'consumptions__stock_item__material',
|
||||||
|
'consumptions__stock_item__material__category',
|
||||||
'consumptions__stock_item__deal',
|
'consumptions__stock_item__deal',
|
||||||
'results__stock_item__material',
|
'results__stock_item__material',
|
||||||
'results__stock_item__entity',
|
'results__stock_item__entity',
|
||||||
'results__stock_item__deal',
|
'results__stock_item__deal',
|
||||||
'remnants__material',
|
'remnants__material',
|
||||||
|
'remnants__material__category',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _calc_mass_kg(material, length_mm, width_mm, qty):
|
||||||
|
if not material or getattr(material, 'mass_per_unit', None) in (None, ''):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
mpu = float(material.mass_per_unit)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
q = float(qty or 0)
|
||||||
|
except Exception:
|
||||||
|
q = 0.0
|
||||||
|
if q <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
l = float(length_mm) if length_mm not in (None, '') else None
|
||||||
|
except Exception:
|
||||||
|
l = None
|
||||||
|
try:
|
||||||
|
w = float(width_mm) if width_mm not in (None, '') else None
|
||||||
|
except Exception:
|
||||||
|
w = None
|
||||||
|
|
||||||
|
ff = getattr(getattr(material, 'category', None), 'form_factor', '') or ''
|
||||||
|
if ff == 'sheet':
|
||||||
|
if l is None or w is None:
|
||||||
|
return None
|
||||||
|
factor = (l * w) / 1_000_000.0
|
||||||
|
elif ff == 'bar':
|
||||||
|
if l is None:
|
||||||
|
return None
|
||||||
|
factor = l / 1000.0
|
||||||
|
else:
|
||||||
|
if l is not None and w is not None:
|
||||||
|
factor = (l * w) / 1_000_000.0
|
||||||
|
elif l is not None:
|
||||||
|
factor = l / 1000.0
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return factor * mpu * q
|
||||||
|
|
||||||
report_cards = []
|
report_cards = []
|
||||||
for r in reports:
|
for r in reports:
|
||||||
|
cons_rows = list(getattr(r, 'consumptions', []).all() if hasattr(getattr(r, 'consumptions', None), 'all') else [])
|
||||||
|
for c in cons_rows:
|
||||||
|
mat = None
|
||||||
|
if getattr(c, 'material_id', None):
|
||||||
|
mat = c.material
|
||||||
|
elif getattr(c, 'stock_item_id', None) and getattr(c.stock_item, 'material_id', None):
|
||||||
|
mat = c.stock_item.material
|
||||||
|
|
||||||
|
length_mm = getattr(getattr(c, 'stock_item', None), 'current_length', None)
|
||||||
|
width_mm = getattr(getattr(c, 'stock_item', None), 'current_width', None)
|
||||||
|
c.mass_kg = _calc_mass_kg(mat, length_mm, width_mm, getattr(c, 'quantity', None))
|
||||||
|
|
||||||
|
rem_rows = list(getattr(r, 'remnants', []).all() if hasattr(getattr(r, 'remnants', None), 'all') else [])
|
||||||
|
for rm in rem_rows:
|
||||||
|
mat = getattr(rm, 'material', None)
|
||||||
|
rm.mass_kg = _calc_mass_kg(mat, getattr(rm, 'current_length', None), getattr(rm, 'current_width', None), getattr(rm, 'quantity', None))
|
||||||
|
|
||||||
consumed = {}
|
consumed = {}
|
||||||
for c in list(getattr(r, 'consumptions', []).all() if hasattr(getattr(r, 'consumptions', None), 'all') else []):
|
for c in cons_rows:
|
||||||
mat = None
|
mat = None
|
||||||
if getattr(c, 'material_id', None):
|
if getattr(c, 'material_id', None):
|
||||||
mat = c.material
|
mat = c.material
|
||||||
@@ -6441,6 +6889,8 @@ class WriteOffsView(LoginRequiredMixin, TemplateView):
|
|||||||
'consumed': consumed,
|
'consumed': consumed,
|
||||||
'produced': produced,
|
'produced': produced,
|
||||||
'remnants': remnants,
|
'remnants': remnants,
|
||||||
|
'consumption_rows': cons_rows,
|
||||||
|
'remnant_rows': rem_rows,
|
||||||
'tasks': list(getattr(r, 'tasks', []).all() if hasattr(getattr(r, 'tasks', None), 'all') else []),
|
'tasks': list(getattr(r, 'tasks', []).all() if hasattr(getattr(r, 'tasks', None), 'all') else []),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
{% if user_role in 'admin,clerk,manager,prod_head,director,observer,technologist' %}
|
{% if user_role in 'admin,clerk,manager,prod_head,director,observer,technologist' %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.resolver_match.url_name == 'shipping' %}active{% endif %}" href="{% url 'shipping' %}">Отгрузка</a>
|
<a class="nav-link {% if request.resolver_match.url_name == 'shipping' or request.resolver_match.url_name == 'shipping_journal' %}active{% endif %}" href="{% url 'shipping' %}">Отгрузка</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user