Добавлена система трансформации товаров

Реализована полная система трансформации товаров (превращение одного товара в другой).
Пример: белая гипсофила → крашеная гипсофила.

Особенности реализации:
- Резервирование входных товаров в статусе draft
- FIFO списание входных товаров при проведении
- Автоматический расчёт себестоимости выходных товаров
- Возможность отмены как черновиков, так и проведённых трансформаций

Модели (inventory/models.py):
- Transformation: документ трансформации (draft/completed/cancelled)
- TransformationInput: входные товары (списание)
- TransformationOutput: выходные товары (оприходование)
- Добавлен статус 'converted_to_transformation' в Reservation
- Добавлен тип 'transformation' в DocumentCounter

Бизнес-логика (inventory/services/transformation_service.py):
- TransformationService с методами CRUD
- Валидация наличия товаров
- Автоматическая генерация номеров документов

Сигналы (inventory/signals.py):
- Автоматическое резервирование входных товаров
- FIFO списание при проведении
- Создание партий выходных товаров
- Откат операций при отмене

Интерфейс без Django Admin:
- Список трансформаций (list.html)
- Форма создания (form.html)
- Детальный просмотр с добавлением товаров (detail.html)
- Интеграция с компонентом поиска товаров
- 8 views для полного CRUD + проведение/отмена

Миграция:
- 0003_alter_documentcounter_counter_type_and_more.py

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-25 18:27:31 +03:00
parent 56850e790e
commit 30ee077963
12 changed files with 1682 additions and 4 deletions

View File

@@ -8,7 +8,8 @@ from inventory.models import (
Warehouse, StockBatch, Incoming, IncomingBatch, Sale, WriteOff, Transfer,
Inventory, InventoryLine, Reservation, Stock, StockMovement,
SaleBatchAllocation, Showcase, WriteOffDocument, WriteOffDocumentItem,
IncomingDocument, IncomingDocumentItem
IncomingDocument, IncomingDocumentItem, Transformation, TransformationInput,
TransformationOutput
)
@@ -512,3 +513,69 @@ class IncomingDocumentItemAdmin(admin.ModelAdmin):
def total_cost_display(self, obj):
return f"{obj.total_cost:.2f}"
total_cost_display.short_description = 'Сумма'
# ===== TRANSFORMATION =====
class TransformationInputInline(admin.TabularInline):
model = TransformationInput
extra = 1
fields = ['product', 'quantity']
autocomplete_fields = ['product']
class TransformationOutputInline(admin.TabularInline):
model = TransformationOutput
extra = 1
fields = ['product', 'quantity', 'stock_batch']
autocomplete_fields = ['product']
readonly_fields = ['stock_batch']
@admin.register(Transformation)
class TransformationAdmin(admin.ModelAdmin):
list_display = ['document_number', 'warehouse', 'status_display', 'date', 'employee', 'inputs_count', 'outputs_count']
list_filter = ['status', 'warehouse', 'date']
search_fields = ['document_number', 'comment']
readonly_fields = ['document_number', 'date', 'created_at', 'updated_at']
inlines = [TransformationInputInline, TransformationOutputInline]
autocomplete_fields = ['warehouse', 'employee']
fieldsets = (
('Основная информация', {
'fields': ('document_number', 'warehouse', 'status', 'employee')
}),
('Детали', {
'fields': ('comment', 'date', 'created_at', 'updated_at')
}),
)
def save_model(self, request, obj, form, change):
if not obj.pk:
# Генерируем номер документа при создании
from inventory.models import DocumentCounter
next_num = DocumentCounter.get_next_value('transformation')
obj.document_number = f"TR-{next_num:05d}"
obj.employee = request.user
super().save_model(request, obj, form, change)
def status_display(self, obj):
colors = {
'draft': '#6c757d',
'completed': '#28a745',
'cancelled': '#dc3545',
}
return format_html(
'<span style="color: {}; font-weight: bold;">{}</span>',
colors.get(obj.status, '#6c757d'),
obj.get_status_display()
)
status_display.short_description = 'Статус'
def inputs_count(self, obj):
return obj.inputs.count()
inputs_count.short_description = 'Входов'
def outputs_count(self, obj):
return obj.outputs.count()
outputs_count.short_description = 'Выходов'