Добавлена поддержка документов списания в админке и сигналах

- Зарегистрированы модели WriteOffDocument и WriteOffDocumentItem в админке
- Настроен inline для позиций документа в админке
- Добавлены цветовые индикаторы статусов документа
- Настроены фильтры, поиск и сортировка для удобной работы
- Добавлен сигнал release_reservation_on_writeoff_item_delete
- Автоматическое освобождение резервов при удалении позиций через админку
- Защита от утечки резервов при прямом удалении через ORM
This commit is contained in:
2025-12-10 23:38:48 +03:00
parent 865cdbbb8b
commit cd5b8c3ef2
2 changed files with 76 additions and 2 deletions

View File

@@ -7,7 +7,7 @@ from decimal import Decimal
from inventory.models import (
Warehouse, StockBatch, Incoming, IncomingBatch, Sale, WriteOff, Transfer,
Inventory, InventoryLine, Reservation, Stock, StockMovement,
SaleBatchAllocation, Showcase
SaleBatchAllocation, Showcase, WriteOffDocument, WriteOffDocumentItem
)
@@ -356,3 +356,61 @@ class StockMovementAdmin(admin.ModelAdmin):
search_fields = ('product__name', 'order__order_number')
date_hierarchy = 'created_at'
readonly_fields = ('created_at',)
# ===== WRITEOFF DOCUMENT (документы списания) =====
class WriteOffDocumentItemInline(admin.TabularInline):
model = WriteOffDocumentItem
extra = 0
fields = ('product', 'quantity', 'reason', 'notes', 'reservation')
readonly_fields = ('reservation',)
raw_id_fields = ('product',)
@admin.register(WriteOffDocument)
class WriteOffDocumentAdmin(admin.ModelAdmin):
list_display = ('document_number', 'warehouse', 'status_display', 'date', 'items_count', 'total_quantity_display', 'created_by', 'created_at')
list_filter = ('status', 'warehouse', 'date', 'created_at')
search_fields = ('document_number', 'warehouse__name')
date_hierarchy = 'date'
readonly_fields = ('document_number', 'created_at', 'updated_at', 'confirmed_at', 'confirmed_by')
inlines = [WriteOffDocumentItemInline]
fieldsets = (
('Документ', {
'fields': ('document_number', 'warehouse', 'status', 'date', 'notes')
}),
('Аудит', {
'fields': ('created_by', 'created_at', 'confirmed_by', 'confirmed_at', 'updated_at'),
'classes': ('collapse',)
}),
)
def status_display(self, obj):
colors = {
'draft': '#ff9900',
'confirmed': '#008000',
'cancelled': '#ff0000',
}
return format_html(
'<span style="color: {}; font-weight: bold;">{}</span>',
colors.get(obj.status, '#000000'),
obj.get_status_display()
)
status_display.short_description = 'Статус'
def items_count(self, obj):
return obj.items.count()
items_count.short_description = 'Позиций'
def total_quantity_display(self, obj):
return f"{obj.total_quantity} шт"
total_quantity_display.short_description = 'Всего'
@admin.register(WriteOffDocumentItem)
class WriteOffDocumentItemAdmin(admin.ModelAdmin):
list_display = ('document', 'product', 'quantity', 'reason', 'created_at')
list_filter = ('reason', 'document__status', 'created_at')
search_fields = ('product__name', 'document__document_number')
raw_id_fields = ('product', 'document', 'reservation')

View File

@@ -11,7 +11,7 @@ from django.utils import timezone
from decimal import Decimal
from orders.models import Order, OrderItem
from inventory.models import Reservation, Warehouse, Incoming, StockBatch, Sale, SaleBatchAllocation, Inventory, WriteOff, Stock
from inventory.models import Reservation, Warehouse, Incoming, StockBatch, Sale, SaleBatchAllocation, Inventory, WriteOff, Stock, WriteOffDocumentItem
from inventory.services import SaleProcessor
from inventory.services.batch_manager import StockBatchManager
from inventory.services.inventory_processor import InventoryProcessor
@@ -1357,3 +1357,19 @@ def update_kit_prices_on_product_change(sender, instance, created, **kwargs):
f"после изменения цены товара {instance.sku}: {e}",
exc_info=True
)
# ==================== WRITEOFF DOCUMENT SIGNALS ====================
@receiver(pre_delete, sender=WriteOffDocumentItem)
def release_reservation_on_writeoff_item_delete(sender, instance, **kwargs):
"""
Сигнал: При удалении позиции документа списания освобождаем связанный резерв.
Это fallback для случаев удаления напрямую через ORM/Admin,
минуя WriteOffDocumentService.remove_item().
"""
if instance.reservation and instance.reservation.status == 'reserved':
instance.reservation.status = 'released'
instance.reservation.released_at = timezone.now()
instance.reservation.save(update_fields=['status', 'released_at'])