feat: Реализовать систему поступления товаров с партиями (IncomingBatch)
Основные изменения: - Создана модель IncomingBatch для группировки товаров по документам - Каждое поступление (Incoming) связано с одной батчем поступления - Автоматическое создание StockBatch для каждого товара в приходе - Реализована система нумерации партий (IN-XXXX-XXXX) с поиском максимума в БД - Обновлены все представления (views) для работы с новой архитектурой - Добавлены детальные страницы просмотра партий поступлений - Обновлены шаблоны для отображения информации о партиях и их товарах - Исправлена логика сигналов для создания StockBatch при приходе товара - Обновлены формы для работы с новой структурой IncomingBatch Архитектура FIFO: - IncomingBatch: одна партия поступления (номер IN-XXXX-XXXX) - Incoming: товар в партии поступления - StockBatch: одна партия товара на складе (создается для каждого товара) Это позволяет системе правильно применять FIFO при продаже товаров. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}Распределение продаж{% endblock %}
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Распределение продаж по партиям (FIFO)</h4></div><div class="card-body">{% if allocations %}<table class="table table-hover table-sm"><thead><tr><th>Продажа</th><th>Товар</th><th>Партия</th><th>Кол-во</th><th>Цена</th><th>Дата</th></tr></thead><tbody>{% for a in allocations %}<tr><td>#{{ a.sale.id }}</td><td>{{ a.sale.product.name }}</td><td>#{{ a.batch.id }}</td><td>{{ a.quantity }}</td><td>{{ a.cost_price }}</td><td>{{ a.sale.date|date:"d.m.Y" }}</td></tr>{% endfor %}</tbody></table>{% else %}<div class="alert alert-info">Распределений не найдено.</div>{% endif %}</div></div>
|
||||
{% endblock %}
|
||||
96
myproject/inventory/templates/inventory/base_inventory.html
Normal file
96
myproject/inventory/templates/inventory/base_inventory.html
Normal file
@@ -0,0 +1,96 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{% block inventory_title %}Склад{% endblock %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row">
|
||||
<!-- Боковая панель навигации -->
|
||||
<div class="col-md-3 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">Управление складом</h5>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a href="{% url 'inventory:inventory-home' %}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-house-door"></i> Главная
|
||||
</a>
|
||||
<a href="{% url 'inventory:warehouse-list' %}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-building"></i> Склады
|
||||
</a>
|
||||
<a href="{% url 'inventory:incoming-list' %}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-arrow-down-square"></i> Приходы
|
||||
</a>
|
||||
<a href="{% url 'inventory:incoming-create' %}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-file-earmark-plus"></i> Поступление товара
|
||||
</a>
|
||||
<a href="{% url 'inventory:incoming-batch-list' %}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-diagram-3-fill"></i> Партии поступлений
|
||||
</a>
|
||||
<a href="{% url 'inventory:sale-list' %}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-arrow-up-square"></i> Продажи
|
||||
</a>
|
||||
<a href="{% url 'inventory:inventory-list' %}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-clipboard-check"></i> Инвентаризация
|
||||
</a>
|
||||
<a href="{% url 'inventory:writeoff-list' %}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-x-circle"></i> Списания
|
||||
</a>
|
||||
<a href="{% url 'inventory:transfer-list' %}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-arrow-left-right"></i> Перемещения
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Справочная информация -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h5 class="mb-0">Справочная информация</h5>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a href="{% url 'inventory:stock-list' %}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-box-seam"></i> Остатки
|
||||
</a>
|
||||
<a href="{% url 'inventory:batch-list' %}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-diagram-3"></i> Партии
|
||||
</a>
|
||||
<a href="{% url 'inventory:movement-list' %}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-journal-check"></i> Журнал
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Основной контент -->
|
||||
<div class="col-md-9">
|
||||
{% block inventory_content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.list-group-item {
|
||||
border-left: 4px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.list-group-item:hover {
|
||||
border-left-color: #007bff;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.list-group-item.active {
|
||||
border-left-color: #007bff;
|
||||
background-color: #e7f1ff;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}Партия товара{% endblock %}
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Партия #{{ batch.id }}: {{ batch.product.name }}</h4></div><div class="card-body"><table class="table table-borderless"><tr><th>Товар:</th><td>{{ batch.product.name }}</td></tr><tr><th>Склад:</th><td>{{ batch.warehouse.name }}</td></tr><tr><th>Количество:</th><td><strong>{{ batch.quantity }} шт</strong></td></tr><tr><th>Цена закупки:</th><td>{{ batch.cost_price }} ₽</td></tr><tr><th>Создана:</th><td>{{ batch.created_at|date:"d.m.Y H:i" }}</td></tr><tr><th>Статус:</th><td>{% if batch.is_active %}<span class="badge bg-success">Активна</span>{% else %}<span class="badge bg-secondary">Неактивна</span>{% endif %}</td></tr></table><h5 class="mt-4">История операций</h5><div class="alert alert-info">История продаж и списаний этой партии.</div><a href="{% url 'inventory:batch-list' %}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Вернуться</a></div></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,49 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}Партии товаров{% endblock %}
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">Все партии товаров на складе</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if batches %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID Партии</th>
|
||||
<th>Товар</th>
|
||||
<th>Склад</th>
|
||||
<th>Кол-во</th>
|
||||
<th>Цена</th>
|
||||
<th>Создана</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for batch in batches %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>#{{ batch.pk }}</strong>
|
||||
</td>
|
||||
<td>{{ batch.product.name }}</td>
|
||||
<td>{{ batch.warehouse.name }}</td>
|
||||
<td>{{ batch.quantity }}</td>
|
||||
<td>{{ batch.cost_price }} ₽</td>
|
||||
<td>{{ batch.created_at|date:"d.m.Y" }}</td>
|
||||
<td>
|
||||
<a href="{% url 'inventory:batch-detail' batch.pk %}" class="btn btn-sm btn-outline-info" title="Просмотр партии">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">Партий не найдено.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
149
myproject/inventory/templates/inventory/home.html
Normal file
149
myproject/inventory/templates/inventory/home.html
Normal file
@@ -0,0 +1,149 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Склад{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="display-5">Управление складом</h1>
|
||||
<p class="lead text-muted">Здесь будут инструменты для управления инвентаризацией и складским учетом</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Основные операции -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-building"></i> Управление складами
|
||||
</h5>
|
||||
<p class="card-text text-muted">Создание и управление физическими складами</p>
|
||||
<a href="{% url 'inventory:warehouse-list' %}" class="btn btn-outline-primary">Перейти</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-arrow-down-square"></i> Приход товара
|
||||
</h5>
|
||||
<p class="card-text text-muted">Регистрация поступления товаров на склад</p>
|
||||
<a href="{% url 'inventory:incoming-list' %}" class="btn btn-outline-primary">Перейти</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-arrow-up-square"></i> Реализация товара
|
||||
</h5>
|
||||
<p class="card-text text-muted">Учет проданных товаров с применением FIFO</p>
|
||||
<a href="{% url 'inventory:sale-list' %}" class="btn btn-outline-primary">Перейти</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-clipboard-check"></i> Инвентаризация
|
||||
</h5>
|
||||
<p class="card-text text-muted">Проверка фактических остатков и корректировка</p>
|
||||
<a href="{% url 'inventory:inventory-list' %}" class="btn btn-outline-primary">Перейти</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Дополнительные операции -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-x-circle"></i> Списание товара
|
||||
</h5>
|
||||
<p class="card-text text-muted">Списание брака, порчи, недостач</p>
|
||||
<a href="{% url 'inventory:writeoff-list' %}" class="btn btn-outline-secondary">Перейти</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-arrow-left-right"></i> Перемещение товара
|
||||
</h5>
|
||||
<p class="card-text text-muted">Перемещение между складами с сохранением партийности</p>
|
||||
<a href="{% url 'inventory:transfer-list' %}" class="btn btn-outline-secondary">Перейти</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Справочная информация -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-box-seam"></i> Остатки товаров
|
||||
</h5>
|
||||
<p class="card-text text-muted">Просмотр текущих остатков по складам и товарам</p>
|
||||
<a href="{% url 'inventory:stock-list' %}" class="btn btn-outline-info">Перейти</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-diagram-3"></i> Партии товаров
|
||||
</h5>
|
||||
<p class="card-text text-muted">История партий и их распределение</p>
|
||||
<a href="{% url 'inventory:batch-list' %}" class="btn btn-outline-info">Перейти</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-journal-check"></i> Журнал операций
|
||||
</h5>
|
||||
<p class="card-text text-muted">Полный журнал всех складских движений</p>
|
||||
<a href="{% url 'inventory:movement-list' %}" class="btn btn-outline-info">Перейти</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,475 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}Массовое поступление товара{% endblock %}
|
||||
{% block inventory_content %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">Поступление товара от поставщика</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Ошибки общей формы -->
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<strong>❌ Ошибка:</strong>
|
||||
{% for error in form.non_field_errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" novalidate id="bulkIncomingForm">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- ============== HEADER ИНФОРМАЦИЯ ============== -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.warehouse.label }} <span class="text-danger">*</span></label>
|
||||
{{ form.warehouse }}
|
||||
{% if form.warehouse.errors %}
|
||||
<div class="text-danger small">{{ form.warehouse.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.document_number.label }}</label>
|
||||
{{ form.document_number }}
|
||||
{% if form.document_number.errors %}
|
||||
<div class="text-danger small">{{ form.document_number.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<small class="text-muted d-block mt-1">
|
||||
Оставьте пустым для автогенерации свободного номера (формат: IN-XXXX-XXXX). Номера, начинающиеся с IN-, зарезервированы для системы.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.supplier_name.label }}</label>
|
||||
{{ form.supplier_name }}
|
||||
{% if form.supplier_name.errors %}
|
||||
<div class="text-danger small">{{ form.supplier_name.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.notes.label }}</label>
|
||||
{{ form.notes }}
|
||||
{% if form.notes.errors %}
|
||||
<div class="text-danger small">{{ form.notes.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- ============== ТАБЛИЦА ТОВАРОВ ============== -->
|
||||
<div class="mb-3">
|
||||
<h5>Товары в поступлении</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered" id="productsTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 45%;">Товар</th>
|
||||
<th style="width: 15%;">Кол-во (шт)</th>
|
||||
<th style="width: 15%;">Цена закупки</th>
|
||||
<th style="width: 15%;">Сумма</th>
|
||||
<th style="width: 10%;">Действие</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="productsBody">
|
||||
<!-- Строки будут добавлены через JavaScript -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addRowBtn">
|
||||
<i class="bi bi-plus-circle"></i> Добавить товар
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ============== ИТОГО ============== -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6"></div>
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<div class="row mb-2">
|
||||
<div class="col-6"><strong>Кол-во позиций:</strong></div>
|
||||
<div class="col-6 text-end"><span id="totalItems">0</span></div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-6"><strong>Общее количество:</strong></div>
|
||||
<div class="col-6 text-end"><span id="totalQuantity">0</span> шт</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6"><strong>Сумма поступления:</strong></div>
|
||||
<div class="col-6 text-end text-primary"><strong><span id="totalSum">0.00</span> руб</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden input для JSON данных товаров -->
|
||||
<input type="hidden" id="productsJson" name="products_json" value="[]">
|
||||
|
||||
<!-- ============== КНОПКИ ============== -->
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<i class="bi bi-check-circle"></i> Создать поступление
|
||||
</button>
|
||||
<a href="{% url 'inventory:incoming-list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Отмена
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
select, textarea, input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
border-radius: 4px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.btn-remove-row {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
input[readonly] {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.row-error {
|
||||
background-color: #fff5f5;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #dc3545;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('bulkIncomingForm');
|
||||
const productsBody = document.getElementById('productsBody');
|
||||
const addRowBtn = document.getElementById('addRowBtn');
|
||||
const productsJsonInput = document.getElementById('productsJson');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
|
||||
// Список всех доступных товаров (преобразуем QuerySet в JSON)
|
||||
const products = [
|
||||
{% for product in products %}
|
||||
{ id: {{ product.id }}, name: "{{ product.name }}" },
|
||||
{% endfor %}
|
||||
];
|
||||
const productOptions = products.map(p => `<option value="${p.id}">${p.name}</option>`).join('');
|
||||
|
||||
let rowCounter = 0;
|
||||
|
||||
// Добавление новой строки товара
|
||||
addRowBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
addProductRow();
|
||||
});
|
||||
|
||||
function addProductRow() {
|
||||
rowCounter++;
|
||||
const rowId = `row-${rowCounter}`;
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.id = rowId;
|
||||
row.innerHTML = `
|
||||
<td>
|
||||
<select class="form-control form-control-sm product-select" data-row-id="${rowId}">
|
||||
<option value="">Выберите товар...</option>
|
||||
${productOptions}
|
||||
</select>
|
||||
<div class="error-message" style="display:none;"></div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm quantity-input"
|
||||
data-row-id="${rowId}" step="0.001" placeholder="0" min="0">
|
||||
<div class="error-message" style="display:none;"></div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm price-input"
|
||||
data-row-id="${rowId}" step="0.01" placeholder="0.00" min="0">
|
||||
<div class="error-message" style="display:none;"></div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm sum-display"
|
||||
data-row-id="${rowId}" readonly style="text-align:right;">
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-danger btn-remove-row" data-row-id="${rowId}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
productsBody.appendChild(row);
|
||||
|
||||
// Добавляем event listeners для новой строки
|
||||
const quantityInput = row.querySelector('.quantity-input');
|
||||
const priceInput = row.querySelector('.price-input');
|
||||
const removeBtn = row.querySelector('.btn-remove-row');
|
||||
|
||||
quantityInput.addEventListener('input', updateTotals);
|
||||
priceInput.addEventListener('input', updateTotals);
|
||||
removeBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
row.remove();
|
||||
updateTotals();
|
||||
});
|
||||
|
||||
updateTotals();
|
||||
}
|
||||
|
||||
function updateTotals() {
|
||||
let totalItems = 0;
|
||||
let totalQuantity = 0;
|
||||
let totalSum = 0;
|
||||
const productsData = [];
|
||||
|
||||
productsBody.querySelectorAll('tr').forEach(row => {
|
||||
const productSelect = row.querySelector('.product-select');
|
||||
const quantityInput = row.querySelector('.quantity-input');
|
||||
const priceInput = row.querySelector('.price-input');
|
||||
const sumDisplay = row.querySelector('.sum-display');
|
||||
|
||||
const productId = productSelect.value;
|
||||
const quantity = parseFloat(quantityInput.value) || 0;
|
||||
const price = parseFloat(priceInput.value) || 0;
|
||||
const sum = quantity * price;
|
||||
|
||||
// Обновляем дисплей суммы
|
||||
sumDisplay.value = sum.toFixed(2);
|
||||
|
||||
// Только считаем если данные заполнены
|
||||
if (productId && quantity > 0 && price >= 0) {
|
||||
totalItems++;
|
||||
totalQuantity += quantity;
|
||||
totalSum += sum;
|
||||
|
||||
productsData.push({
|
||||
product_id: parseInt(productId),
|
||||
quantity: quantity,
|
||||
cost_price: price
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Обновляем итоги
|
||||
document.getElementById('totalItems').textContent = totalItems;
|
||||
document.getElementById('totalQuantity').textContent = totalQuantity.toFixed(3);
|
||||
document.getElementById('totalSum').textContent = totalSum.toFixed(2);
|
||||
|
||||
// Обновляем JSON данные
|
||||
productsJsonInput.value = JSON.stringify(productsData);
|
||||
|
||||
// Отключаем кнопку отправки если нет товаров
|
||||
submitBtn.disabled = totalItems === 0;
|
||||
}
|
||||
|
||||
// Добавляем первую пустую строку
|
||||
addProductRow();
|
||||
|
||||
// Восстанавливаем товары из JSON если была ошибка (сохранение данных при ошибке)
|
||||
const savedProductsJson = '{{ products_json|escapejs }}';
|
||||
if (savedProductsJson && savedProductsJson.trim() !== '[]' && savedProductsJson.trim() !== '') {
|
||||
try {
|
||||
const savedProducts = JSON.parse(savedProductsJson);
|
||||
if (savedProducts && savedProducts.length > 0) {
|
||||
// Удаляем пустую первую строку
|
||||
productsBody.innerHTML = '';
|
||||
rowCounter = 0;
|
||||
|
||||
// Добавляем восстановленные товары
|
||||
savedProducts.forEach(item => {
|
||||
const product = products.find(p => p.id === item.product_id);
|
||||
if (product) {
|
||||
addProductRow();
|
||||
const lastRow = productsBody.querySelector('tr:last-child');
|
||||
|
||||
lastRow.querySelector('.product-select').value = item.product_id;
|
||||
lastRow.querySelector('.quantity-input').value = item.quantity;
|
||||
lastRow.querySelector('.price-input').value = item.cost_price;
|
||||
}
|
||||
});
|
||||
|
||||
// Обновляем итоги
|
||||
updateTotals();
|
||||
|
||||
// Очищаем поле номера документа для автогенерации
|
||||
const documentNumberInput = document.querySelector('[name="document_number"]');
|
||||
if (documentNumberInput) {
|
||||
documentNumberInput.value = '';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Ошибка восстановления товаров:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем элемент поля номера документа
|
||||
const documentNumberInput = document.querySelector('[name="document_number"]');
|
||||
|
||||
// Валидация номера документа (запретить номера, начинающиеся с "IN-" только для заполненного поля)
|
||||
documentNumberInput.addEventListener('change', function() {
|
||||
const value = this.value.trim().toUpperCase();
|
||||
const container = this.closest('.mb-3');
|
||||
let errorDiv = container.querySelector('.document-number-error');
|
||||
|
||||
// Проверяем IN-* ТОЛЬКО если поле НЕ пусто
|
||||
if (value && value.startsWith('IN-')) {
|
||||
// Показать ошибку
|
||||
this.classList.add('is-invalid');
|
||||
if (!errorDiv) {
|
||||
errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'document-number-error text-danger small mt-2';
|
||||
container.appendChild(errorDiv);
|
||||
}
|
||||
errorDiv.textContent = 'Номера, начинающиеся с "IN-", зарезервированы для системы. Если хотите автогенерацию, оставьте поле пустым.';
|
||||
} else {
|
||||
// Очистить ошибку (пусто или другой формат - это ОК)
|
||||
this.classList.remove('is-invalid');
|
||||
if (errorDiv) {
|
||||
errorDiv.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Валидация перед отправкой
|
||||
form.addEventListener('submit', function(e) {
|
||||
// Проверка номера документа - запретить IN-* только если поле ЗАПОЛНЕНО
|
||||
const docNumberValue = documentNumberInput.value.trim().toUpperCase();
|
||||
const docNumberContainer = documentNumberInput.closest('.mb-3');
|
||||
|
||||
if (docNumberValue && docNumberValue.startsWith('IN-')) {
|
||||
e.preventDefault();
|
||||
|
||||
documentNumberInput.classList.add('is-invalid');
|
||||
let errorDiv = docNumberContainer.querySelector('.document-number-error');
|
||||
if (!errorDiv) {
|
||||
errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'document-number-error text-danger small mt-2';
|
||||
docNumberContainer.appendChild(errorDiv);
|
||||
}
|
||||
errorDiv.textContent = 'Номера, начинающиеся с "IN-", зарезервированы для системы. Оставьте пустым для автогенерации.';
|
||||
|
||||
alert('Номера, начинающиеся с "IN-", зарезервированы для системы. Оставьте пустым для автогенерации.');
|
||||
documentNumberInput.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверка склада
|
||||
const warehouseSelect = document.querySelector('[name="warehouse"]');
|
||||
const warehouseContainer = warehouseSelect.closest('.mb-3');
|
||||
|
||||
if (!warehouseSelect.value) {
|
||||
e.preventDefault();
|
||||
|
||||
// Добавляем класс ошибки если его нет
|
||||
if (!warehouseSelect.classList.contains('is-invalid')) {
|
||||
warehouseSelect.classList.add('is-invalid');
|
||||
warehouseContainer.classList.add('has-validation');
|
||||
}
|
||||
|
||||
// Создаём или обновляем сообщение об ошибке
|
||||
let errorDiv = warehouseContainer.querySelector('.warehouse-error');
|
||||
if (!errorDiv) {
|
||||
errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'warehouse-error text-danger small mt-2';
|
||||
warehouseContainer.appendChild(errorDiv);
|
||||
}
|
||||
errorDiv.textContent = 'Пожалуйста, выберите склад перед отправкой.';
|
||||
|
||||
alert('Пожалуйста, выберите склад перед отправкой.');
|
||||
warehouseSelect.focus();
|
||||
return false;
|
||||
} else {
|
||||
// Очищаем ошибку если склад выбран
|
||||
warehouseSelect.classList.remove('is-invalid');
|
||||
const errorDiv = warehouseContainer.querySelector('.warehouse-error');
|
||||
if (errorDiv) {
|
||||
errorDiv.remove();
|
||||
}
|
||||
}
|
||||
|
||||
const productsData = JSON.parse(productsJsonInput.value);
|
||||
|
||||
if (productsData.length === 0) {
|
||||
e.preventDefault();
|
||||
alert('Пожалуйста, добавьте хотя бы один товар.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем что все товары корректно заполнены
|
||||
let hasErrors = false;
|
||||
productsBody.querySelectorAll('tr').forEach(row => {
|
||||
const productSelect = row.querySelector('.product-select');
|
||||
const quantityInput = row.querySelector('.quantity-input');
|
||||
const priceInput = row.querySelector('.price-input');
|
||||
|
||||
const productError = row.querySelector('td:nth-child(1) .error-message');
|
||||
const quantityError = row.querySelector('td:nth-child(2) .error-message');
|
||||
const priceError = row.querySelector('td:nth-child(3) .error-message');
|
||||
|
||||
let rowHasError = false;
|
||||
|
||||
if (!productSelect.value) {
|
||||
productError.textContent = 'Выберите товар';
|
||||
productError.style.display = 'block';
|
||||
hasErrors = true;
|
||||
rowHasError = true;
|
||||
} else {
|
||||
productError.style.display = 'none';
|
||||
}
|
||||
|
||||
const quantity = parseFloat(quantityInput.value) || 0;
|
||||
if (quantity <= 0) {
|
||||
quantityError.textContent = 'Количество должно быть > 0';
|
||||
quantityError.style.display = 'block';
|
||||
hasErrors = true;
|
||||
rowHasError = true;
|
||||
} else {
|
||||
quantityError.style.display = 'none';
|
||||
}
|
||||
|
||||
const price = parseFloat(priceInput.value) || 0;
|
||||
if (price < 0) {
|
||||
priceError.textContent = 'Цена не может быть отрицательной';
|
||||
priceError.style.display = 'block';
|
||||
hasErrors = true;
|
||||
rowHasError = true;
|
||||
} else {
|
||||
priceError.style.display = 'none';
|
||||
}
|
||||
|
||||
if (rowHasError) {
|
||||
row.classList.add('row-error');
|
||||
} else {
|
||||
row.classList.remove('row-error');
|
||||
}
|
||||
});
|
||||
|
||||
if (hasErrors) {
|
||||
e.preventDefault();
|
||||
alert('Пожалуйста, исправьте ошибки в форме.');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,48 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
|
||||
{% block inventory_title %}Отмена приходу товара{% endblock %}
|
||||
|
||||
{% block inventory_content %}
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h4 class="mb-0">Подтверждение отмены</h4>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<strong>Внимание!</strong> Вы собираетесь отменить приход товара.
|
||||
</div>
|
||||
|
||||
<p class="text-muted">
|
||||
Это действие удалит запись о приходе товара и может повлиять на остатки на складе.
|
||||
</p>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<h5>Информация о приходе:</h5>
|
||||
<ul class="mb-0">
|
||||
<li><strong>Товар:</strong> {{ incoming.product.name }}</li>
|
||||
<li><strong>Склад:</strong> {{ incoming.warehouse.name }}</li>
|
||||
<li><strong>Количество:</strong> {{ incoming.quantity }} шт</li>
|
||||
<li><strong>Цена закупки:</strong> {{ incoming.cost_price }} ₽</li>
|
||||
{% if incoming.document_number %}
|
||||
<li><strong>Номер документа:</strong> {{ incoming.document_number }}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form method="post" class="mt-4">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i> Подтвердить отмену
|
||||
</button>
|
||||
<a href="{% url 'inventory:incoming-list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Вернуться
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,125 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
|
||||
{% block inventory_title %}
|
||||
{% if form.instance.pk %}
|
||||
Редактирование приходу товара
|
||||
{% else %}
|
||||
Новый приход товара
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">
|
||||
{% if form.instance.pk %}
|
||||
Редактирование приходу
|
||||
{% else %}
|
||||
Регистрация нового поступления
|
||||
{% endif %}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form method="post" class="form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.product.id_for_label }}" class="form-label">
|
||||
{{ form.product.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.product }}
|
||||
{% if form.product.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.product.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.warehouse.id_for_label }}" class="form-label">
|
||||
{{ form.warehouse.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.warehouse }}
|
||||
{% if form.warehouse.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.warehouse.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.quantity.id_for_label }}" class="form-label">
|
||||
{{ form.quantity.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.quantity }}
|
||||
{% if form.quantity.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.quantity.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.cost_price.id_for_label }}" class="form-label">
|
||||
{{ form.cost_price.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.cost_price }}
|
||||
{% if form.cost_price.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.cost_price.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.document_number.id_for_label }}" class="form-label">
|
||||
{{ form.document_number.label }}
|
||||
</label>
|
||||
{{ form.document_number }}
|
||||
{% if form.document_number.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.document_number.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.notes.id_for_label }}" class="form-label">
|
||||
{{ form.notes.label }}
|
||||
</label>
|
||||
{{ form.notes }}
|
||||
{% if form.notes.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.notes.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i>
|
||||
{% if form.instance.pk %}
|
||||
Сохранить
|
||||
{% else %}
|
||||
Создать
|
||||
{% endif %}
|
||||
</button>
|
||||
<a href="{% url 'inventory:incoming-list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Отменить
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
select, textarea, input {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,112 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
|
||||
{% block inventory_title %}История приходов товара{% endblock %}
|
||||
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Приходы товара</h4>
|
||||
<a href="{% url 'inventory:incoming-create' %}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-circle"></i> Новый приход
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
{% if incomings %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Товар</th>
|
||||
<th>Склад</th>
|
||||
<th>Количество</th>
|
||||
<th>Цена закупки</th>
|
||||
<th>Номер документа</th>
|
||||
<th>Партия</th>
|
||||
<th>Дата</th>
|
||||
<th class="text-end">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for incoming in incomings %}
|
||||
<tr>
|
||||
<td><strong>{{ incoming.product.name }}</strong></td>
|
||||
<td>{{ incoming.batch.warehouse.name }}</td>
|
||||
<td>{{ incoming.quantity }} шт</td>
|
||||
<td>{{ incoming.cost_price }} ₽</td>
|
||||
<td>
|
||||
{% if incoming.batch.document_number %}
|
||||
<code>{{ incoming.batch.document_number }}</code>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if incoming.stock_batch %}
|
||||
<a href="{% url 'inventory:batch-detail' incoming.stock_batch.pk %}" title="Перейти к партии на складе">
|
||||
<strong>#{{ incoming.stock_batch.pk }}</strong>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="badge bg-warning">Не назначена</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ incoming.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td class="text-end">
|
||||
<a href="{% url 'inventory:incoming-update' incoming.pk %}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<a href="{% url 'inventory:incoming-delete' incoming.pk %}" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Пагинация -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1">Первая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Предыдущая</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ num }}</span>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Следующая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Последняя</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> Приходов не найдено.
|
||||
<a href="{% url 'inventory:incoming-create' %}" class="alert-link">Зарегистрировать новый приход</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,110 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}Партия {{ batch.document_number }}{% endblock %}
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Партия: <strong>{{ batch.document_number }}</strong></h4>
|
||||
<a href="{% url 'inventory:incoming-batch-list' %}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Назад
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<h5>Основная информация</h5>
|
||||
<table class="table table-sm table-borderless">
|
||||
<tr>
|
||||
<th>Номер:</th>
|
||||
<td><strong>{{ batch.document_number }}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Склад:</th>
|
||||
<td>{{ batch.warehouse.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Поставщик:</th>
|
||||
<td>{{ batch.supplier_name|default:"—" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Создана:</th>
|
||||
<td>{{ batch.created_at|date:"d.m.Y H:i" }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h5>Статистика</h5>
|
||||
<table class="table table-sm table-borderless">
|
||||
<tr>
|
||||
<th>Товаров:</th>
|
||||
<td><span class="badge bg-info">{{ items.count }}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Общее количество:</th>
|
||||
<td>
|
||||
{% with total=items.all|length %}
|
||||
<strong>{{ total }} шт</strong>
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mt-4 mb-3">Товары в партии</h5>
|
||||
{% if items %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Товар</th>
|
||||
<th>Количество</th>
|
||||
<th>Цена</th>
|
||||
<th>Сумма</th>
|
||||
<th>StockBatch ID</th>
|
||||
<th>Дата</th>
|
||||
<th>Действие</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>{{ item.product.name }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td>{{ item.cost_price }} ₽</td>
|
||||
<td>
|
||||
{% widthratio item.quantity 1 item.cost_price as total_price %}
|
||||
<strong>{{ total_price|floatformat:2 }} ₽</strong>
|
||||
</td>
|
||||
<td>
|
||||
{% if item.stock_batch %}
|
||||
<strong>#{{ item.stock_batch.pk }}</strong>
|
||||
{% else %}
|
||||
<span class="badge bg-warning">Не назначена</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.created_at|date:"d.m.Y" }}</td>
|
||||
<td>
|
||||
{% if item.stock_batch %}
|
||||
<a href="{% url 'inventory:batch-detail' item.stock_batch.pk %}" class="btn btn-sm btn-outline-info" title="Просмотр партии на складе">
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">В этой партии нет товаров.</div>
|
||||
{% endif %}
|
||||
|
||||
{% if batch.notes %}
|
||||
<h5 class="mt-4 mb-3">Примечания</h5>
|
||||
<p>{{ batch.notes }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,60 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}Партии поступлений{% endblock %}
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Партии поступлений товара</h4>
|
||||
<a href="{% url 'inventory:incoming-create' %}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-circle"></i> Новое поступление
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if batches %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Номер документа</th>
|
||||
<th>Склад</th>
|
||||
<th>Поставщик</th>
|
||||
<th>Товары</th>
|
||||
<th>Кол-во</th>
|
||||
<th>Дата создания</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for batch in batches %}
|
||||
<tr>
|
||||
<td><strong>{{ batch.document_number }}</strong></td>
|
||||
<td>{{ batch.warehouse.name }}</td>
|
||||
<td>{{ batch.supplier_name|default:"—" }}</td>
|
||||
<td>
|
||||
<small>
|
||||
{% for item in batch.items.all %}
|
||||
{{ item.product.name }}{% if not forloop.last %}<br>{% endif %}
|
||||
{% endfor %}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info">{{ batch.items_count }}</span>
|
||||
</td>
|
||||
<td>{{ batch.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>
|
||||
<a href="{% url 'inventory:incoming-batch-detail' batch.pk %}" class="btn btn-sm btn-outline-info" title="Просмотр партии">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">Партий поступлений не найдено.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,111 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
|
||||
{% block inventory_title %}Детали инвентаризации{% endblock %}
|
||||
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Инвентаризация: {{ inventory.warehouse.name }}</h4>
|
||||
<a href="{% url 'inventory:inventory-list' %}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Вернуться
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<h5>Информация</h5>
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<th>Склад:</th>
|
||||
<td><strong>{{ inventory.warehouse.name }}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Статус:</th>
|
||||
<td>
|
||||
{% if inventory.status == 'draft' %}
|
||||
<span class="badge bg-secondary">Черновик</span>
|
||||
{% elif inventory.status == 'processing' %}
|
||||
<span class="badge bg-warning">В обработке</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success">Завершена</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Дата:</th>
|
||||
<td>{{ inventory.date|date:"d.m.Y H:i" }}</td>
|
||||
</tr>
|
||||
{% if inventory.conducted_by %}
|
||||
<tr>
|
||||
<th>Провёл:</th>
|
||||
<td>{{ inventory.conducted_by }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h5>Строки инвентаризации</h5>
|
||||
|
||||
{% if lines %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Товар</th>
|
||||
<th>В системе</th>
|
||||
<th>По факту</th>
|
||||
<th>Разница</th>
|
||||
<th>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for line in lines %}
|
||||
<tr>
|
||||
<td>{{ line.product.name }}</td>
|
||||
<td>{{ line.quantity_system }}</td>
|
||||
<td>{{ line.quantity_fact }}</td>
|
||||
<td>
|
||||
{% if line.difference > 0 %}
|
||||
<span class="badge bg-success">+{{ line.difference }}</span>
|
||||
{% elif line.difference < 0 %}
|
||||
<span class="badge bg-danger">{{ line.difference }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">0</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if line.processed %}
|
||||
<span class="badge bg-success">Обработана</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning">Не обработана</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> Строк инвентаризации не добавлено.
|
||||
<a href="{% url 'inventory:inventory-lines-add' inventory.pk %}" class="alert-link">Добавить строки</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
{% if inventory.status != 'completed' %}
|
||||
<a href="{% url 'inventory:inventory-lines-add' inventory.pk %}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Добавить строки
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'inventory:inventory-list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Вернуться к списку
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,69 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
|
||||
{% block inventory_title %}Новая инвентаризация{% endblock %}
|
||||
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">Начало новой инвентаризации</h4>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form method="post" class="form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.warehouse.id_for_label }}" class="form-label">
|
||||
{{ form.warehouse.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.warehouse }}
|
||||
{% if form.warehouse.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.warehouse.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.conducted_by.id_for_label }}" class="form-label">
|
||||
{{ form.conducted_by.label }}
|
||||
</label>
|
||||
{{ form.conducted_by }}
|
||||
{% if form.conducted_by.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.conducted_by.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<small class="text-muted">Кто проводит инвентаризацию?</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.notes.id_for_label }}" class="form-label">
|
||||
{{ form.notes.label }}
|
||||
</label>
|
||||
{{ form.notes }}
|
||||
{% if form.notes.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.notes.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> Начать инвентаризацию
|
||||
</button>
|
||||
<a href="{% url 'inventory:inventory-list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Отменить
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
select, textarea, input {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,58 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
|
||||
{% block inventory_title %}Внесение результатов инвентаризации{% endblock %}
|
||||
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">Внесение результатов инвентаризации</h4>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<strong>Инвентаризация:</strong> {{ inventory.warehouse.name }} ({{ inventory.date|date:"d.m.Y" }})
|
||||
</div>
|
||||
|
||||
<form method="post" class="form">
|
||||
{% csrf_token %}
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Товар</th>
|
||||
<th>Кол-во в системе</th>
|
||||
<th>Кол-во по факту</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for line in lines %}
|
||||
<tr>
|
||||
<td>{{ line.product.name }}</td>
|
||||
<td>{{ line.quantity_system }}</td>
|
||||
<td>
|
||||
<input type="number" step="0.001" class="form-control" name="quantity_{{ line.id }}" value="{{ line.quantity_fact }}">
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-muted">
|
||||
Нет товаров для инвентаризации
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> Сохранить результаты
|
||||
</button>
|
||||
<a href="{% url 'inventory:inventory-detail' inventory.pk %}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Отменить
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,96 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
|
||||
{% block inventory_title %}История инвентаризаций{% endblock %}
|
||||
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Инвентаризации</h4>
|
||||
<a href="{% url 'inventory:inventory-create' %}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-circle"></i> Новая инвентаризация
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
{% if inventories %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Склад</th>
|
||||
<th>Статус</th>
|
||||
<th>Провёл</th>
|
||||
<th>Дата</th>
|
||||
<th class="text-end">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for inventory in inventories %}
|
||||
<tr>
|
||||
<td><strong>{{ inventory.warehouse.name }}</strong></td>
|
||||
<td>
|
||||
{% if inventory.status == 'draft' %}
|
||||
<span class="badge bg-secondary">Черновик</span>
|
||||
{% elif inventory.status == 'processing' %}
|
||||
<span class="badge bg-warning">В обработке</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success">Завершена</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ inventory.conducted_by|default:"—" }}</td>
|
||||
<td>{{ inventory.date|date:"d.m.Y H:i" }}</td>
|
||||
<td class="text-end">
|
||||
<a href="{% url 'inventory:inventory-detail' inventory.pk %}" class="btn btn-sm btn-outline-info">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1">Первая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Предыдущая</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ num }}</span>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Следующая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Последняя</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> Инвентаризаций не найдено.
|
||||
<a href="{% url 'inventory:inventory-create' %}" class="alert-link">Начать новую инвентаризацию</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}Журнал операций{% endblock %}
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Журнал всех складских операций</h4></div><div class="card-body">{% if movements %}<table class="table table-hover table-sm"><thead><tr><th>Товар</th><th>Изменение</th><th>Причина</th><th>Дата</th></tr></thead><tbody>{% for m in movements %}<tr><td>{{ m.product.name }}</td><td>{% if m.change > 0 %}<span class="badge bg-success">+{{ m.change }}</span>{% else %}<span class="badge bg-danger">{{ m.change }}</span>{% endif %}</td><td>{{ m.get_reason_display }}</td><td>{{ m.created_at|date:"d.m.Y H:i" }}</td></tr>{% endfor %}</tbody></table>{% else %}<div class="alert alert-info">Операций не найдено.</div>{% endif %}</div></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}Новое резервирование{% endblock %}
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Резервирование товара</h4></div><div class="card-body"><form method="post">{% csrf_token %}<div class="mb-3"><label class="form-label">{{ form.product.label }} *</label>{{ form.product }}</div><div class="mb-3"><label class="form-label">{{ form.warehouse.label }} *</label>{{ form.warehouse }}</div><div class="mb-3"><label class="form-label">{{ form.quantity.label }} *</label>{{ form.quantity }}</div><div class="mb-3"><label class="form-label">{{ form.order_item.label }}</label>{{ form.order_item }}</div><div class="d-flex gap-2"><button type="submit" class="btn btn-primary">Сохранить</button><a href="{% url 'inventory:reservation-list' %}" class="btn btn-secondary">Отмена</a></div></form></div></div>
|
||||
<style>select,input{width:100%;}</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}Резервирования{% endblock %}
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Активные резервирования <a href="{% url 'inventory:reservation-create' %}" class="btn btn-primary btn-sm float-end"><i class="bi bi-plus-circle"></i> Новое</a></h4></div><div class="card-body">{% if reservations %}<table class="table table-hover table-sm"><thead><tr><th>Товар</th><th>Кол-во</th><th>Склад</th><th>Зарезервировано</th><th class="text-end">Действия</th></tr></thead><tbody>{% for r in reservations %}<tr><td>{{ r.product.name }}</td><td>{{ r.quantity }}</td><td>{{ r.warehouse.name }}</td><td>{{ r.reserved_at|date:"d.m.Y" }}</td><td class="text-end"><a href="{% url 'inventory:reservation-update' r.pk %}" class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil"></i></a></td></tr>{% endfor %}</tbody></table>{% else %}<div class="alert alert-info">Резервирований не найдено.</div>{% endif %}</div></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}Изменение резервирования{% endblock %}
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Изменение статуса резервирования</h4></div><div class="card-body"><form method="post">{% csrf_token %}<div class="mb-3"><label class="form-label">{{ form.status.label }}</label>{{ form.status }}</div><div class="d-flex gap-2"><button type="submit" class="btn btn-primary">Сохранить</button><a href="{% url 'inventory:reservation-list' %}" class="btn btn-secondary">Отмена</a></div></form></div></div>
|
||||
<style>select{width:100%;}</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,55 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
|
||||
{% block inventory_title %}Отмена продажи{% endblock %}
|
||||
|
||||
{% block inventory_content %}
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h4 class="mb-0">Подтверждение отмены продажи</h4>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<strong>Внимание!</strong> Вы собираетесь отменить продажу.
|
||||
</div>
|
||||
|
||||
<p class="text-muted">
|
||||
Это действие удалит запись о продаже и может повлиять на остатки товара на складе.
|
||||
</p>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<h5>Информация о продаже:</h5>
|
||||
<ul class="mb-0">
|
||||
<li><strong>Товар:</strong> {{ sale.product.name }}</li>
|
||||
<li><strong>Склад:</strong> {{ sale.warehouse.name }}</li>
|
||||
<li><strong>Количество:</strong> {{ sale.quantity }} шт</li>
|
||||
<li><strong>Цена продажи:</strong> {{ sale.sale_price }} ₽</li>
|
||||
<li><strong>Статус:</strong>
|
||||
{% if sale.processed %}
|
||||
Обработана
|
||||
{% else %}
|
||||
Ожидает обработки
|
||||
{% endif %}
|
||||
</li>
|
||||
{% if sale.document_number %}
|
||||
<li><strong>Номер документа:</strong> {{ sale.document_number }}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form method="post" class="mt-4">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i> Подтвердить отмену
|
||||
</button>
|
||||
<a href="{% url 'inventory:sale-list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Вернуться
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
138
myproject/inventory/templates/inventory/sale/sale_detail.html
Normal file
138
myproject/inventory/templates/inventory/sale/sale_detail.html
Normal file
@@ -0,0 +1,138 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
|
||||
{% block inventory_title %}Детали продажи{% endblock %}
|
||||
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Продажа: {{ sale.product.name }}</h4>
|
||||
<a href="{% url 'inventory:sale-list' %}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Вернуться
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<h5>Информация о продаже</h5>
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<th>Товар:</th>
|
||||
<td><strong>{{ sale.product.name }}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Склад:</th>
|
||||
<td>{{ sale.warehouse.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Количество:</th>
|
||||
<td><strong>{{ sale.quantity }} шт</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Цена продажи:</th>
|
||||
<td><strong>{{ sale.sale_price }} ₽</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Сумма:</th>
|
||||
<td><strong>{{ sale.quantity|add:0|multiply:sale.sale_price }} ₽</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h5>Дополнительная информация</h5>
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<th>Статус:</th>
|
||||
<td>
|
||||
{% if sale.processed %}
|
||||
<span class="badge bg-success">Обработана (FIFO применена)</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning">Ожидает обработки</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Дата продажи:</th>
|
||||
<td>{{ sale.date|date:"d.m.Y H:i" }}</td>
|
||||
</tr>
|
||||
{% if sale.order %}
|
||||
<tr>
|
||||
<th>Связанный заказ:</th>
|
||||
<td><code>{{ sale.order.order_number }}</code></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if sale.document_number %}
|
||||
<tr>
|
||||
<th>Номер документа:</th>
|
||||
<td><code>{{ sale.document_number }}</code></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h5 class="mt-4">Распределение по партиям (FIFO)</h5>
|
||||
<p class="text-muted">Какие партии товара использовались в этой продаже:</p>
|
||||
|
||||
{% if allocations %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Партия</th>
|
||||
<th>Дата создания</th>
|
||||
<th>Количество использовано</th>
|
||||
<th>Закупочная цена</th>
|
||||
<th>Сумма закупки</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for allocation in allocations %}
|
||||
<tr>
|
||||
<td>
|
||||
<code>Партия #{{ allocation.batch.id }}</code>
|
||||
</td>
|
||||
<td>{{ allocation.batch.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ allocation.quantity }} шт</td>
|
||||
<td>{{ allocation.cost_price }} ₽</td>
|
||||
<td><strong>{{ allocation.quantity|add:0|multiply:allocation.cost_price }} ₽</strong></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot class="table-light">
|
||||
<tr>
|
||||
<th colspan="2">Итого:</th>
|
||||
<th>{{ sale.quantity }} шт</th>
|
||||
<th colspan="2">
|
||||
<strong>
|
||||
{% comment %} Сумма всех закупочных цен {% endcomment %}
|
||||
Средняя стоимость
|
||||
</strong>
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> Распределение по партиям ещё не выполнено.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<a href="{% url 'inventory:sale-update' sale.pk %}" class="btn btn-primary">
|
||||
<i class="bi bi-pencil"></i> Редактировать
|
||||
</a>
|
||||
<a href="{% url 'inventory:sale-delete' sale.pk %}" class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</a>
|
||||
<a href="{% url 'inventory:sale-list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Вернуться к списку
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
127
myproject/inventory/templates/inventory/sale/sale_form.html
Normal file
127
myproject/inventory/templates/inventory/sale/sale_form.html
Normal file
@@ -0,0 +1,127 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
|
||||
{% block inventory_title %}
|
||||
{% if form.instance.pk %}
|
||||
Редактирование продажи
|
||||
{% else %}
|
||||
Новая продажа
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">
|
||||
{% if form.instance.pk %}
|
||||
Редактирование продажи
|
||||
{% else %}
|
||||
Регистрация новой продажи
|
||||
{% endif %}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form method="post" class="form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.product.id_for_label }}" class="form-label">
|
||||
{{ form.product.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.product }}
|
||||
{% if form.product.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.product.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.warehouse.id_for_label }}" class="form-label">
|
||||
{{ form.warehouse.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.warehouse }}
|
||||
{% if form.warehouse.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.warehouse.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.quantity.id_for_label }}" class="form-label">
|
||||
{{ form.quantity.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.quantity }}
|
||||
{% if form.quantity.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.quantity.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.sale_price.id_for_label }}" class="form-label">
|
||||
{{ form.sale_price.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.sale_price }}
|
||||
{% if form.sale_price.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.sale_price.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.order.id_for_label }}" class="form-label">
|
||||
{{ form.order.label }}
|
||||
</label>
|
||||
{{ form.order }}
|
||||
{% if form.order.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.order.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.document_number.id_for_label }}" class="form-label">
|
||||
{{ form.document_number.label }}
|
||||
</label>
|
||||
{{ form.document_number }}
|
||||
{% if form.document_number.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.document_number.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i>
|
||||
{% if form.instance.pk %}
|
||||
Сохранить
|
||||
{% else %}
|
||||
Создать
|
||||
{% endif %}
|
||||
</button>
|
||||
<a href="{% url 'inventory:sale-list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Отменить
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
select, textarea, input {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
113
myproject/inventory/templates/inventory/sale/sale_list.html
Normal file
113
myproject/inventory/templates/inventory/sale/sale_list.html
Normal file
@@ -0,0 +1,113 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
|
||||
{% block inventory_title %}История продаж{% endblock %}
|
||||
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Продажи товара (FIFO)</h4>
|
||||
<a href="{% url 'inventory:sale-create' %}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-circle"></i> Новая продажа
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
{% if sales %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Товар</th>
|
||||
<th>Склад</th>
|
||||
<th>Количество</th>
|
||||
<th>Цена продажи</th>
|
||||
<th>Заказ</th>
|
||||
<th>Статус</th>
|
||||
<th>Дата</th>
|
||||
<th class="text-end">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for sale in sales %}
|
||||
<tr>
|
||||
<td><strong>{{ sale.product.name }}</strong></td>
|
||||
<td>{{ sale.warehouse.name }}</td>
|
||||
<td>{{ sale.quantity }} шт</td>
|
||||
<td>{{ sale.sale_price }} ₽</td>
|
||||
<td>
|
||||
{% if sale.order %}
|
||||
<code>{{ sale.order.order_number }}</code>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if sale.processed %}
|
||||
<span class="badge bg-success">Обработана</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning">Ожидает</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ sale.date|date:"d.m.Y H:i" }}</td>
|
||||
<td class="text-end">
|
||||
<a href="{% url 'inventory:sale-detail' sale.pk %}" class="btn btn-sm btn-outline-info" title="Просмотр деталей">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'inventory:sale-update' sale.pk %}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<a href="{% url 'inventory:sale-delete' sale.pk %}" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Пагинация -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1">Первая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Предыдущая</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ num }}</span>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Следующая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Последняя</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> Продаж не найдено.
|
||||
<a href="{% url 'inventory:sale-create' %}" class="alert-link">Зарегистрировать новую продажу</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}Остатки товара{% endblock %}
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">{{ stock.product.name }} на {{ stock.warehouse.name }}</h4></div><div class="card-body"><table class="table table-borderless"><tr><th>Товар:</th><td><strong>{{ stock.product.name }}</strong></td></tr><tr><th>Склад:</th><td>{{ stock.warehouse.name }}</td></tr><tr><th>Доступно:</th><td><strong>{{ stock.quantity_available }} шт</strong></td></tr><tr><th>Зарезервировано:</th><td>{{ stock.quantity_reserved }} шт</td></tr><tr><th>Свободно:</th><td><strong>{{ stock.quantity_free }} шт</strong></td></tr><tr><th>Последнее обновление:</th><td>{{ stock.updated_at|date:"d.m.Y H:i" }}</td></tr></table><a href="{% url 'inventory:stock-list' %}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Вернуться</a></div></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}Остатки товаров{% endblock %}
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Остатки на складах</h4></div><div class="card-body">{% if stocks %}<table class="table table-hover table-sm"><thead><tr><th>Товар</th><th>Склад</th><th>Доступно</th><th>Зарезервировано</th><th>Свободно</th><th>Последний обновления</th></tr></thead><tbody>{% for stock in stocks %}<tr><td><a href="{% url 'inventory:stock-detail' stock.pk %}">{{ stock.product.name }}</a></td><td>{{ stock.warehouse.name }}</td><td>{{ stock.quantity_available }}</td><td>{{ stock.quantity_reserved }}</td><td><strong>{{ stock.quantity_free }}</strong></td><td>{{ stock.updated_at|date:"d.m.Y H:i" }}</td></tr>{% endfor %}</tbody></table>{% else %}<div class="alert alert-info">Остатки не найдены.</div>{% endif %}</div></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}Отмена перемещения{% endblock %}
|
||||
{% block inventory_content %}<div class="card border-danger"><div class="card-header bg-danger text-white"><h4 class="mb-0">Подтверждение</h4></div><div class="card-body"><div class="alert alert-warning"><i class="bi bi-exclamation-triangle"></i> Отменить перемещение товара?</div><form method="post">{% csrf_token %}<div class="d-flex gap-2"><button type="submit" class="btn btn-danger">Подтвердить</button><a href="{% url 'inventory:transfer-list' %}" class="btn btn-secondary">Отмена</a></div></form></div></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}Перемещение товара{% endblock %}
|
||||
{% block inventory_content %}
|
||||
<div class="card"><div class="card-header"><h4 class="mb-0">Перемещение товара</h4></div><div class="card-body"><form method="post">{% csrf_token %}<div class="mb-3"><label class="form-label">{{ form.batch.label }} *</label>{{ form.batch }}</div><div class="mb-3"><label class="form-label">{{ form.from_warehouse.label }} *</label>{{ form.from_warehouse }}</div><div class="mb-3"><label class="form-label">{{ form.to_warehouse.label }} *</label>{{ form.to_warehouse }}</div><div class="mb-3"><label class="form-label">{{ form.quantity.label }} *</label>{{ form.quantity }}</div><div class="mb-3"><label class="form-label">{{ form.document_number.label }}</label>{{ form.document_number }}</div><div class="d-flex gap-2"><button type="submit" class="btn btn-primary">Сохранить</button><a href="{% url 'inventory:transfer-list' %}" class="btn btn-secondary">Отмена</a></div></form></div></div>
|
||||
<style>select,textarea,input{width:100%;}</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}Перемещение товаров{% endblock %}
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Перемещение товаров между складами <a href="{% url 'inventory:transfer-create' %}" class="btn btn-primary btn-sm float-end"><i class="bi bi-plus-circle"></i> Новое</a></h4></div><div class="card-body">{% if transfers %}<div class="table-responsive"><table class="table table-hover table-sm"><thead><tr><th>Товар</th><th>Из</th><th>В</th><th>Кол-во</th><th>Дата</th><th class="text-end">Действия</th></tr></thead><tbody>{% for t in transfers %}<tr><td>{{ t.batch.product.name }}</td><td>{{ t.from_warehouse.name }}</td><td>{{ t.to_warehouse.name }}</td><td>{{ t.quantity }}</td><td>{{ t.date|date:"d.m.Y" }}</td><td class="text-end"><a href="{% url 'inventory:transfer-update' t.pk %}" class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil"></i></a><a href="{% url 'inventory:transfer-delete' t.pk %}" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></a></td></tr>{% endfor %}</tbody></table></div>{% else %}<div class="alert alert-info">Перемещений не найдено.</div>{% endif %}</div></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,41 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
|
||||
{% block inventory_title %}Удаление склада{% endblock %}
|
||||
|
||||
{% block inventory_content %}
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h4 class="mb-0">Подтверждение удаления</h4>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<strong>Внимание!</strong> Вы собираетесь удалить (деактивировать) склад.
|
||||
</div>
|
||||
|
||||
<p class="text-muted">
|
||||
Этот склад будет деактивирован и скрыт из основного списка.
|
||||
</p>
|
||||
|
||||
<h5>Склад: <strong>{{ warehouse.name }}</strong></h5>
|
||||
|
||||
{% if warehouse.description %}
|
||||
<p class="text-muted">{{ warehouse.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="mt-4">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i> Подтвердить удаление
|
||||
</button>
|
||||
<a href="{% url 'inventory:warehouse-list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Отменить
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,86 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
|
||||
{% block inventory_title %}
|
||||
{% if form.instance.pk %}
|
||||
Редактирование склада
|
||||
{% else %}
|
||||
Создание нового склада
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">
|
||||
{% if form.instance.pk %}
|
||||
Редактирование: {{ form.instance.name }}
|
||||
{% else %}
|
||||
Создание нового склада
|
||||
{% endif %}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form method="post" class="form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label">
|
||||
{{ form.name.label }}
|
||||
</label>
|
||||
<input type="text" class="form-control {% if form.name.errors %}is-invalid{% endif %}"
|
||||
id="{{ form.name.id_for_label }}" name="{{ form.name.html_name }}"
|
||||
value="{{ form.name.value|default:'' }}" required>
|
||||
{% if form.name.errors %}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.name.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.description.id_for_label }}" class="form-label">
|
||||
{{ form.description.label }}
|
||||
</label>
|
||||
<textarea class="form-control {% if form.description.errors %}is-invalid{% endif %}"
|
||||
id="{{ form.description.id_for_label }}" name="{{ form.description.html_name }}"
|
||||
rows="4">{{ form.description.value|default:'' }}</textarea>
|
||||
{% if form.description.errors %}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.description.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input"
|
||||
id="{{ form.is_active.id_for_label }}" name="{{ form.is_active.html_name }}"
|
||||
{% if form.is_active.value %}checked{% endif %}>
|
||||
<label class="form-check-label" for="{{ form.is_active.id_for_label }}">
|
||||
{{ form.is_active.label }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i>
|
||||
{% if form.instance.pk %}
|
||||
Сохранить
|
||||
{% else %}
|
||||
Создать
|
||||
{% endif %}
|
||||
</button>
|
||||
<a href="{% url 'inventory:warehouse-list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Отменить
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,98 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
|
||||
{% block inventory_title %}Управление складами{% endblock %}
|
||||
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Список складов</h4>
|
||||
<a href="{% url 'inventory:warehouse-create' %}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-circle"></i> Новый склад
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
{% if warehouses %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Описание</th>
|
||||
<th>Статус</th>
|
||||
<th>Дата создания</th>
|
||||
<th class="text-end">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for warehouse in warehouses %}
|
||||
<tr>
|
||||
<td><strong>{{ warehouse.name }}</strong></td>
|
||||
<td>{{ warehouse.description|truncatewords:10 }}</td>
|
||||
<td>
|
||||
{% if warehouse.is_active %}
|
||||
<span class="badge bg-success">Активен</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Неактивен</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ warehouse.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td class="text-end">
|
||||
<a href="{% url 'inventory:warehouse-update' warehouse.pk %}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<a href="{% url 'inventory:warehouse-delete' warehouse.pk %}" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Пагинация -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1">Первая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Предыдущая</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ num }}</span>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Следующая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Последняя</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> Складов не найдено.
|
||||
<a href="{% url 'inventory:warehouse-create' %}" class="alert-link">Создать новый</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}Отмена списания{% endblock %}
|
||||
{% block inventory_content %}
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white"><h4 class="mb-0">Подтверждение отмены</h4></div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning"><i class="bi bi-exclamation-triangle"></i> <strong>Внимание!</strong> Вы собираетесь отменить списание товара.</div>
|
||||
<form method="post"><{% csrf_token %}<div class="d-flex gap-2"><button type="submit" class="btn btn-danger"><i class="bi bi-trash"></i> Подтвердить</button><a href="{% url 'inventory:writeoff-list' %}" class="btn btn-secondary"><i class="bi bi-x-circle"></i> Отмена</a></div></form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,174 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}{% if form.instance.pk %}Редактирование списания{% else %}Новое списание{% endif %}{% endblock %}
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
<div class="card-header"><h4 class="mb-0">{% if form.instance.pk %}Редактирование{% else %}Создание{% endif %} списания</h4></div>
|
||||
<div class="card-body">
|
||||
<!-- Ошибки формы -->
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<strong><i class="bi bi-exclamation-triangle"></i> Ошибка:</strong>
|
||||
{% for error in form.non_field_errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Поле Партия -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.batch.label }} <span class="text-danger">*</span></label>
|
||||
{{ form.batch }}
|
||||
{% if form.batch.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.batch.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<!-- Информация об остатке партии -->
|
||||
<div id="batch-info" class="mt-2 p-2 bg-light border rounded" style="display:none;">
|
||||
<small class="text-muted">
|
||||
Остаток в партии: <strong id="batch-quantity">0</strong> шт
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Поле Количество -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.quantity.label }} <span class="text-danger">*</span></label>
|
||||
{{ form.quantity }}
|
||||
{% if form.quantity.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.quantity.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<small class="text-muted d-block mt-1">
|
||||
Введите количество товара для списания
|
||||
</small>
|
||||
<!-- Предупреждение о превышении остатка -->
|
||||
<div id="quantity-warning" class="alert alert-warning mt-2" style="display:none;">
|
||||
<i class="bi bi-exclamation-circle"></i>
|
||||
<strong>Внимание!</strong> Вы пытаетесь списать <strong id="warning-qty">0</strong> шт,
|
||||
а в партии только <strong id="warning-batch">0</strong> шт.
|
||||
Недостаток: <strong id="warning-shortage" class="text-danger">0</strong> шт.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Поле Причина -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.reason.label }}</label>
|
||||
{{ form.reason }}
|
||||
{% if form.reason.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.reason.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Поле Номер документа -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.document_number.label }}</label>
|
||||
{{ form.document_number }}
|
||||
{% if form.document_number.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.document_number.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Поле Примечания -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.notes.label }}</label>
|
||||
{{ form.notes }}
|
||||
{% if form.notes.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.notes.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Кнопки действия -->
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary" id="submit-btn"><i class="bi bi-check-circle"></i> Сохранить</button>
|
||||
<a href="{% url 'inventory:writeoff-list' %}" class="btn btn-secondary"><i class="bi bi-x-circle"></i> Отмена</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
select, textarea, input {
|
||||
width: 100%;
|
||||
}
|
||||
.invalid-feedback {
|
||||
color: #dc3545;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const batchSelect = document.querySelector('#id_batch');
|
||||
const quantityInput = document.querySelector('#id_quantity');
|
||||
const batchInfo = document.getElementById('batch-info');
|
||||
const batchQuantitySpan = document.getElementById('batch-quantity');
|
||||
const quantityWarning = document.getElementById('quantity-warning');
|
||||
const warningQty = document.getElementById('warning-qty');
|
||||
const warningBatch = document.getElementById('warning-batch');
|
||||
const warningShortage = document.getElementById('warning-shortage');
|
||||
|
||||
// Функция для получения остатка партии
|
||||
function getBatchQuantity() {
|
||||
if (!batchSelect.value) {
|
||||
batchInfo.style.display = 'none';
|
||||
quantityWarning.style.display = 'none';
|
||||
return null;
|
||||
}
|
||||
|
||||
// Получаем текст option и парсим остаток
|
||||
const selectedOption = batchSelect.options[batchSelect.selectedIndex];
|
||||
const optionText = selectedOption.text;
|
||||
|
||||
// Пытаемся найти количество в скобках (формат: "Product - Остаток: X шт")
|
||||
const match = optionText.match(/Остаток:\s*(\d+(?:[.,]\d+)?)/);
|
||||
if (match) {
|
||||
const qty = parseFloat(match[1].replace(',', '.'));
|
||||
return qty;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Функция для обновления информации и предупреждений
|
||||
function updateBatchInfo() {
|
||||
const batchQty = getBatchQuantity();
|
||||
|
||||
if (batchQty !== null) {
|
||||
batchQuantitySpan.textContent = batchQty;
|
||||
batchInfo.style.display = 'block';
|
||||
} else {
|
||||
batchInfo.style.display = 'none';
|
||||
quantityWarning.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для проверки количества
|
||||
function checkQuantity() {
|
||||
const batchQty = getBatchQuantity();
|
||||
const qty = parseFloat(quantityInput.value) || 0;
|
||||
|
||||
if (batchQty !== null && qty > 0) {
|
||||
if (qty > batchQty) {
|
||||
warningQty.textContent = qty;
|
||||
warningBatch.textContent = batchQty;
|
||||
warningShortage.textContent = (qty - batchQty).toFixed(3);
|
||||
quantityWarning.style.display = 'block';
|
||||
} else {
|
||||
quantityWarning.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
quantityWarning.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// События
|
||||
batchSelect.addEventListener('change', updateBatchInfo);
|
||||
quantityInput.addEventListener('input', checkQuantity);
|
||||
|
||||
// Инициализация
|
||||
updateBatchInfo();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,49 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% block inventory_title %}История списаний{% endblock %}
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Списания товара</h4>
|
||||
<a href="{% url 'inventory:writeoff-create' %}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-circle"></i> Новое списание
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if writeoffs %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Товар</th>
|
||||
<th>Количество</th>
|
||||
<th>Причина</th>
|
||||
<th>Дата</th>
|
||||
<th class="text-end">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for writeoff in writeoffs %}
|
||||
<tr>
|
||||
<td><strong>{{ writeoff.batch.product.name }}</strong></td>
|
||||
<td>{{ writeoff.quantity }} шт</td>
|
||||
<td><span class="badge bg-warning">{{ writeoff.get_reason_display }}</span></td>
|
||||
<td>{{ writeoff.date|date:"d.m.Y H:i" }}</td>
|
||||
<td class="text-end">
|
||||
<a href="{% url 'inventory:writeoff-update' writeoff.pk %}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<a href="{% url 'inventory:writeoff-delete' writeoff.pk %}" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">Списаний не найдено.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user