feat: упростить создание заказов и рефакторинг единиц измерения

- Добавить inline-редактирование цен в списке товаров
- Оптимизировать карточки товаров в POS-терминале
- Рефакторинг моделей единиц измерения
- Миграция unit -> base_unit в SalesUnit
- Улучшить UI форм создания/редактирования товаров

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-17 03:34:43 +03:00
parent 928b340486
commit 2f1f0621e6
24 changed files with 1079 additions and 227 deletions

View File

@@ -292,7 +292,6 @@
<thead class="table-light">
<tr>
<th>Название</th>
<th>Единица</th>
<th class="text-end">Коэфф.</th>
<th class="text-end">Цена</th>
<th class="text-center">Мин. кол-во</th>
@@ -306,9 +305,6 @@
{{ su.name }}
{% if su.is_default %}<span class="badge bg-success ms-1">По умолчанию</span>{% endif %}
</td>
<td>
<span class="badge bg-secondary">{{ su.unit.short_name }}</span>
</td>
<td class="text-end">{{ su.conversion_factor }}</td>
<td class="text-end">
{% if su.sale_price %}

View File

@@ -581,6 +581,157 @@
<hr class="my-4">
<!-- Блок: Единицы продажи -->
{% if sales_unit_formset %}
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-gradient" style="background: linear-gradient(135deg, #28a745 0%, #20c997 100%);">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0 text-white">
<i class="bi bi-box-seam"></i> Единицы продажи
</h5>
<a href="{% url 'products:unit-list' %}" class="btn btn-sm btn-light" target="_blank">
<i class="bi bi-rulers"></i> Справочник единиц
</a>
</div>
</div>
<div class="card-body">
<p class="text-muted small mb-3">
Настройте, в каких единицах продается товар (ветка, кг, штука).
Коэффициент указывает, сколько единиц продажи получается из 1 базовой единицы.
</p>
{{ sales_unit_formset.management_form }}
<!-- Шаблон для новых форм (скрыт) -->
<template id="empty-sales-unit-template">
{% with form=sales_unit_formset.empty_form %}
<div class="sales-unit-row border rounded p-3 mb-2">
<div class="row g-2 align-items-end">
<div class="col-md-2">
<label class="form-label small">Ед. измерения</label>
{{ form.unit }}
</div>
<div class="col-md-2">
<label class="form-label small">Название</label>
{{ form.name }}
</div>
<div class="col-md-1">
<label class="form-label small">Коэфф.</label>
{{ form.conversion_factor }}
</div>
<div class="col-md-1">
<label class="form-label small">Цена</label>
{{ form.price }}
</div>
<div class="col-md-1">
<label class="form-label small">Скидка</label>
{{ form.sale_price }}
</div>
<div class="col-md-1">
<label class="form-label small">Мин.кол</label>
{{ form.min_quantity }}
</div>
<div class="col-md-1">
<label class="form-label small">Шаг</label>
{{ form.quantity_step }}
</div>
<div class="col-md-1">
<label class="form-label small">Поз.</label>
{{ form.position }}
</div>
<div class="col-md-1 text-center">
<label class="form-label small d-block">По умолч.</label>
{{ form.is_default }}
</div>
<div class="col-md-1">
<div class="d-flex gap-1">
<div class="form-check" title="Активна">
{{ form.is_active }}
</div>
</div>
</div>
</div>
</div>
{% endwith %}
</template>
<div id="sales-units-container">
{% for form in sales_unit_formset %}
<div class="sales-unit-row border rounded p-3 mb-2 {% if form.instance.pk %}bg-light{% endif %}">
<div class="row g-2 align-items-end">
{% if form.instance.pk %}
<input type="hidden" name="{{ form.prefix }}-id" value="{{ form.instance.pk }}">
{% endif %}
<div class="col-md-2">
<label class="form-label small">Ед. измерения</label>
{{ form.unit }}
</div>
<div class="col-md-2">
<label class="form-label small">Название</label>
{{ form.name }}
</div>
<div class="col-md-1">
<label class="form-label small">Коэфф.</label>
{{ form.conversion_factor }}
</div>
<div class="col-md-1">
<label class="form-label small">Цена</label>
{{ form.price }}
</div>
<div class="col-md-1">
<label class="form-label small">Скидка</label>
{{ form.sale_price }}
</div>
<div class="col-md-1">
<label class="form-label small">Мин.кол</label>
{{ form.min_quantity }}
</div>
<div class="col-md-1">
<label class="form-label small">Шаг</label>
{{ form.quantity_step }}
</div>
<div class="col-md-1">
<label class="form-label small">Поз.</label>
{{ form.position }}
</div>
<div class="col-md-1 text-center">
<label class="form-label small d-block">По умолч.</label>
{{ form.is_default }}
</div>
<div class="col-md-1">
<div class="d-flex gap-1">
<div class="form-check" title="Активна">
{{ form.is_active }}
</div>
{% if form.instance.pk %}
<div class="form-check" title="Удалить">
{{ form.DELETE }}
</div>
{% endif %}
</div>
</div>
</div>
{% if form.errors %}
<div class="text-danger small mt-1">
{% for field, errors in form.errors.items %}
{{ field }}: {{ errors|join:", " }}
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
<button type="button" class="btn btn-outline-success btn-sm mt-2" id="add-sales-unit">
<i class="bi bi-plus-circle"></i> Добавить единицу продажи
</button>
</div>
</div>
{% endif %}
<hr class="my-4">
<!-- Блок 2.5: Информация о наличии (только при редактировании) -->
{% if object %}
<div class="mb-4 p-3 bg-info-light rounded border border-info">
@@ -686,6 +837,58 @@ document.addEventListener('DOMContentLoaded', function() {
});
}, 250);
});
// === Динамическое добавление единиц продажи ===
const addButton = document.getElementById('add-sales-unit');
const container = document.getElementById('sales-units-container');
const totalFormsInput = document.querySelector('[name="sales_units-TOTAL_FORMS"]');
if (addButton && container && totalFormsInput) {
addButton.addEventListener('click', function() {
const formCount = parseInt(totalFormsInput.value);
const template = document.getElementById('empty-sales-unit-template');
if (template) {
// Клонируем содержимое шаблона
const newRow = template.content.cloneNode(true);
const rowDiv = newRow.querySelector('.sales-unit-row');
// Обновляем имена и id полей
rowDiv.querySelectorAll('input, select').forEach(input => {
const name = input.getAttribute('name');
const id = input.getAttribute('id');
if (name) {
// Заменяем __prefix__ на текущий индекс
input.setAttribute('name', name.replace('__prefix__', formCount));
}
if (id) {
input.setAttribute('id', id.replace('__prefix__', formCount));
}
// Устанавливаем значения по умолчанию
if (input.type === 'checkbox') {
if (input.name.includes('is_active')) {
input.checked = true;
} else {
input.checked = false;
}
} else if (input.type !== 'hidden') {
if (input.name.includes('min_quantity') || input.name.includes('quantity_step')) {
input.value = '1';
} else if (input.name.includes('position')) {
input.value = formCount;
} else if (input.name.includes('conversion_factor')) {
input.value = '1';
}
}
});
container.appendChild(rowDiv);
totalFormsInput.value = formCount + 1;
}
});
}
});
</script>
{% endblock %}

View File

@@ -22,9 +22,6 @@
<dt class="col-sm-4">Название:</dt>
<dd class="col-sm-8">{{ sales_unit.name }}</dd>
<dt class="col-sm-4">Единица:</dt>
<dd class="col-sm-8">{{ sales_unit.unit.name }} ({{ sales_unit.unit.short_name }})</dd>
<dt class="col-sm-4">Коэффициент:</dt>
<dd class="col-sm-8">{{ sales_unit.conversion_factor }}</dd>

View File

@@ -20,32 +20,18 @@
</div>
{% endif %}
<!-- Товар и Единица измерения -->
<div class="row mb-3">
<div class="col-md-6">
<label for="{{ form.product.id_for_label }}" class="form-label">
{{ form.product.label }} <span class="text-danger">*</span>
</label>
{{ form.product }}
{% if form.product.help_text %}
<small class="form-text text-muted">{{ form.product.help_text }}</small>
{% endif %}
{% if form.product.errors %}
<div class="text-danger">{{ form.product.errors }}</div>
{% endif %}
</div>
<div class="col-md-6">
<label for="{{ form.unit.id_for_label }}" class="form-label">
{{ form.unit.label }} <span class="text-danger">*</span>
</label>
{{ form.unit }}
{% if form.unit.help_text %}
<small class="form-text text-muted">{{ form.unit.help_text }}</small>
{% endif %}
{% if form.unit.errors %}
<div class="text-danger">{{ form.unit.errors }}</div>
{% endif %}
</div>
<!-- Товар -->
<div class="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.help_text %}
<small class="form-text text-muted">{{ form.product.help_text }}</small>
{% endif %}
{% if form.product.errors %}
<div class="text-danger">{{ form.product.errors }}</div>
{% endif %}
</div>
<!-- Название -->

View File

@@ -17,19 +17,9 @@
<!-- Поиск и фильтры -->
<form method="get" class="mb-3">
<div class="row g-2">
<div class="col-md-4">
<div class="col-md-6">
<input type="text" class="form-control form-control-sm" name="q" value="{{ search_query }}" placeholder="Поиск по товару, артикулу, названию...">
</div>
<div class="col-md-2">
<select class="form-select form-select-sm" name="unit" onchange="this.form.submit()">
<option value="">Все единицы</option>
{% for unit in all_units %}
<option value="{{ unit.id }}" {% if unit_filter == unit.id|stringformat:"s" %}selected{% endif %}>
{{ unit.short_name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<select class="form-select form-select-sm" name="is_active" onchange="this.form.submit()">
<option value="">Все</option>
@@ -63,15 +53,14 @@
<table class="table table-hover table-sm align-middle">
<thead class="table-light">
<tr>
<th style="width: 25%;">Товар</th>
<th style="width: 15%;">Название единицы</th>
<th style="width: 10%;" class="text-center">Единица</th>
<th style="width: 28%;">Товар</th>
<th style="width: 18%;">Название единицы</th>
<th style="width: 10%;" class="text-end">Коэфф.</th>
<th style="width: 12%;" class="text-end">Цена</th>
<th style="width: 8%;" class="text-center">Мин.</th>
<th style="width: 8%;" class="text-center">Шаг</th>
<th style="width: 6%;" class="text-center">По умолч.</th>
<th style="width: 6%;" class="text-center">Статус</th>
<th style="width: 8%;" class="text-center">По умолч.</th>
<th style="width: 8%;" class="text-center">Статус</th>
<th style="width: 10%;" class="text-end">Действия</th>
</tr>
</thead>
@@ -87,9 +76,6 @@
{% endif %}
</td>
<td>{{ su.name }}</td>
<td class="text-center">
<span class="badge bg-secondary">{{ su.unit.short_name }}</span>
</td>
<td class="text-end"><code>{{ su.conversion_factor }}</code></td>
<td class="text-end">
{% if su.sale_price %}
@@ -142,7 +128,7 @@
<ul class="pagination pagination-sm justify-content-center mb-0">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}{% if unit_filter %}&unit={{ unit_filter }}{% endif %}{% if is_active_filter %}&is_active={{ is_active_filter }}{% endif %}{% if is_default_filter %}&is_default={{ is_default_filter }}{% endif %}">&laquo;</a>
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}{% if is_active_filter %}&is_active={{ is_active_filter }}{% endif %}{% if is_default_filter %}&is_default={{ is_default_filter }}{% endif %}">&laquo;</a>
</li>
{% endif %}
<li class="page-item active">
@@ -150,7 +136,7 @@
</li>
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}{% if unit_filter %}&unit={{ unit_filter }}{% endif %}{% if is_active_filter %}&is_active={{ is_active_filter }}{% endif %}{% if is_default_filter %}&is_default={{ is_default_filter }}{% endif %}">&raquo;</a>
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}{% if is_active_filter %}&is_active={{ is_active_filter }}{% endif %}{% if is_default_filter %}&is_default={{ is_default_filter }}{% endif %}">&raquo;</a>
</li>
{% endif %}
</ul>

View File

@@ -0,0 +1,67 @@
{% extends 'base.html' %}
{% block title %}Удаление единицы измерения{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-6">
<!-- Заголовок -->
<div class="d-flex align-items-center justify-content-between mb-4">
<h4 class="mb-0"><i class="bi bi-trash text-danger"></i> Удаление единицы измерения</h4>
<a href="{% url 'products:unit-list' %}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i> К списку
</a>
</div>
<div class="card">
<div class="card-body">
{% if can_delete %}
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle"></i>
Вы уверены, что хотите удалить единицу измерения <strong>"{{ unit.name }}"</strong> ({{ unit.code }})?
</div>
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-between">
<a href="{% url 'products:unit-list' %}" class="btn btn-outline-secondary">
Отмена
</a>
<button type="submit" class="btn btn-danger">
<i class="bi bi-trash"></i> Удалить
</button>
</div>
</form>
{% else %}
<div class="alert alert-danger">
<i class="bi bi-x-circle"></i>
<strong>Невозможно удалить единицу измерения "{{ unit.name }}"</strong>
</div>
<p>Эта единица измерения используется в:</p>
<ul>
{% if products_using > 0 %}
<li><strong>{{ products_using }}</strong> товарах (как базовая единица)</li>
{% endif %}
{% if sales_units_using > 0 %}
<li><strong>{{ sales_units_using }}</strong> единицах продажи</li>
{% endif %}
</ul>
<p class="text-muted">
Перед удалением необходимо переназначить эти товары и единицы продажи на другую единицу измерения.
</p>
<div class="d-flex justify-content-start">
<a href="{% url 'products:unit-list' %}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Назад к списку
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,110 @@
{% extends 'base.html' %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-6">
<!-- Заголовок -->
<div class="d-flex align-items-center justify-content-between mb-4">
<h4 class="mb-0"><i class="bi bi-rulers text-primary"></i> {{ title }}</h4>
<a href="{% url 'products:unit-list' %}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i> К списку
</a>
</div>
<!-- Форма -->
<div class="card">
<div class="card-body">
<form method="post" novalidate>
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-danger">
{% for error in form.non_field_errors %}
<p class="mb-0">{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.code.id_for_label }}" class="form-label">
Код <span class="text-danger">*</span>
</label>
{{ form.code }}
{% if form.code.help_text %}
<div class="form-text">{{ form.code.help_text }}</div>
{% endif %}
{% if form.code.errors %}
<div class="invalid-feedback d-block">
{% for error in form.code.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.short_name.id_for_label }}" class="form-label">
Сокращение
</label>
{{ form.short_name }}
{% if form.short_name.help_text %}
<div class="form-text">{{ form.short_name.help_text }}</div>
{% endif %}
</div>
</div>
<div class="mb-3">
<label for="{{ form.name.id_for_label }}" class="form-label">
Название <span class="text-danger">*</span>
</label>
{{ form.name }}
{% if form.name.help_text %}
<div class="form-text">{{ form.name.help_text }}</div>
{% endif %}
{% if form.name.errors %}
<div class="invalid-feedback d-block">
{% for error in form.name.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.position.id_for_label }}" class="form-label">
Позиция
</label>
{{ form.position }}
{% if form.position.help_text %}
<div class="form-text">{{ form.position.help_text }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3 d-flex align-items-end">
<div class="form-check">
{{ form.is_active }}
<label class="form-check-label" for="{{ form.is_active.id_for_label }}">
Активна
</label>
</div>
</div>
</div>
<hr>
<div class="d-flex justify-content-between">
<a href="{% url 'products:unit-list' %}" class="btn btn-outline-secondary">
Отмена
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> {{ submit_text }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -9,7 +9,7 @@
<!-- Заголовок -->
<div class="d-flex align-items-center justify-content-between mb-4">
<h4 class="mb-0"><i class="bi bi-rulers text-primary"></i> Единицы измерения</h4>
<a href="{% url 'admin:products_unitofmeasure_add' %}" class="btn btn-primary btn-sm">
<a href="{% url 'products:unit-create' %}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-circle"></i> Добавить единицу
</a>
</div>
@@ -53,9 +53,12 @@
<td><span class="badge bg-secondary">{{ unit.short_name }}</span></td>
<td class="text-center">{{ unit.position }}</td>
<td class="text-center">
<span class="badge {% if unit.usage_count > 0 %}bg-success{% else %}bg-secondary{% endif %}">
<span class="badge {% if unit.usage_count > 0 %}bg-success{% else %}bg-secondary{% endif %}" title="Единицы продажи">
{{ unit.usage_count }}
</span>
<span class="badge {% if unit.products_count > 0 %}bg-primary{% else %}bg-secondary{% endif %}" title="Товаров">
{{ unit.products_count }}
</span>
</td>
<td class="text-center">
{% if unit.is_active %}
@@ -66,9 +69,12 @@
</td>
<td class="text-end">
<div class="btn-group btn-group-sm">
<a href="{% url 'admin:products_unitofmeasure_change' unit.pk %}" class="btn btn-outline-secondary" title="Изменить">
<a href="{% url 'products:unit-update' unit.pk %}" class="btn btn-outline-secondary" title="Изменить">
<i class="bi bi-pencil"></i>
</a>
<a href="{% url 'products:unit-delete' unit.pk %}" class="btn btn-outline-danger" title="Удалить">
<i class="bi bi-trash"></i>
</a>
</div>
</td>
</tr>