Добавлена поддержка документов списания в админке и сигналах
- Зарегистрированы модели WriteOffDocument и WriteOffDocumentItem в админке - Настроен inline для позиций документа в админке - Добавлены цветовые индикаторы статусов документа - Настроены фильтры, поиск и сортировка для удобной работы - Добавлен сигнал release_reservation_on_writeoff_item_delete - Автоматическое освобождение резервов при удалении позиций через админку - Защита от утечки резервов при прямом удалении через ORM
This commit is contained in:
@@ -7,7 +7,7 @@ from decimal import Decimal
|
|||||||
from inventory.models import (
|
from inventory.models import (
|
||||||
Warehouse, StockBatch, Incoming, IncomingBatch, Sale, WriteOff, Transfer,
|
Warehouse, StockBatch, Incoming, IncomingBatch, Sale, WriteOff, Transfer,
|
||||||
Inventory, InventoryLine, Reservation, Stock, StockMovement,
|
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')
|
search_fields = ('product__name', 'order__order_number')
|
||||||
date_hierarchy = 'created_at'
|
date_hierarchy = 'created_at'
|
||||||
readonly_fields = ('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')
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from django.utils import timezone
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from orders.models import Order, OrderItem
|
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 import SaleProcessor
|
||||||
from inventory.services.batch_manager import StockBatchManager
|
from inventory.services.batch_manager import StockBatchManager
|
||||||
from inventory.services.inventory_processor import InventoryProcessor
|
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}",
|
f"после изменения цены товара {instance.sku}: {e}",
|
||||||
exc_info=True
|
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'])
|
||||||
|
|||||||
Reference in New Issue
Block a user