Files
octopus/myproject/inventory/admin.py
Andrey Smakotin cd5b8c3ef2 Добавлена поддержка документов списания в админке и сигналах
- Зарегистрированы модели WriteOffDocument и WriteOffDocumentItem в админке
- Настроен inline для позиций документа в админке
- Добавлены цветовые индикаторы статусов документа
- Настроены фильтры, поиск и сортировка для удобной работы
- Добавлен сигнал release_reservation_on_writeoff_item_delete
- Автоматическое освобождение резервов при удалении позиций через админку
- Защита от утечки резервов при прямом удалении через ORM
2025-12-10 23:38:48 +03:00

417 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.db.models import Sum
from decimal import Decimal
from inventory.models import (
Warehouse, StockBatch, Incoming, IncomingBatch, Sale, WriteOff, Transfer,
Inventory, InventoryLine, Reservation, Stock, StockMovement,
SaleBatchAllocation, Showcase, WriteOffDocument, WriteOffDocumentItem
)
# ===== SHOWCASE =====
@admin.register(Showcase)
class ShowcaseAdmin(admin.ModelAdmin):
list_display = ('name', 'warehouse', 'is_default', 'is_active', 'created_at')
list_filter = ('is_active', 'is_default', 'warehouse', 'created_at')
search_fields = ('name', 'warehouse__name')
date_hierarchy = 'created_at'
fieldsets = (
('Основная информация', {
'fields': ('name', 'warehouse', 'description', 'is_active', 'is_default')
}),
('Даты', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
readonly_fields = ('created_at', 'updated_at')
# ===== WAREHOUSE =====
@admin.register(Warehouse)
class WarehouseAdmin(admin.ModelAdmin):
list_display = ('name', 'is_default_display', 'is_active', 'created_at')
list_filter = ('is_active', 'is_default', 'created_at')
search_fields = ('name',)
fieldsets = (
('Основная информация', {
'fields': ('name', 'description', 'is_active', 'is_default')
}),
('Даты', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
readonly_fields = ('created_at', 'updated_at')
def is_default_display(self, obj):
if obj.is_default:
return format_html('<span style="color: #ff9900; font-weight: bold;">★ По умолчанию</span>')
return '-'
is_default_display.short_description = 'По умолчанию'
# ===== STOCK BATCH =====
@admin.register(StockBatch)
class StockBatchAdmin(admin.ModelAdmin):
list_display = ('product', 'warehouse', 'quantity_display', 'cost_price', 'created_at', 'is_active')
list_filter = ('warehouse', 'is_active', 'created_at')
search_fields = ('product__name', 'product__sku', 'warehouse__name')
date_hierarchy = 'created_at'
fieldsets = (
('Партия', {
'fields': ('product', 'warehouse', 'quantity', 'is_active')
}),
('Финансы', {
'fields': ('cost_price',)
}),
('Даты', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
readonly_fields = ('created_at', 'updated_at')
def quantity_display(self, obj):
if obj.quantity <= 0:
color = '#ff0000' # красный
elif obj.quantity < 10:
color = '#ff9900' # оранжевый
else:
color = '#008000' # зелёный
return format_html(
'<span style="color: {}; font-weight: bold;">{}</span>',
color,
f'{obj.quantity} шт'
)
quantity_display.short_description = 'Количество'
# ===== INCOMING BATCH =====
@admin.register(IncomingBatch)
class IncomingBatchAdmin(admin.ModelAdmin):
list_display = ('document_number', 'warehouse', 'supplier_name', 'items_count', 'created_at')
list_filter = ('warehouse', 'created_at')
search_fields = ('document_number', 'supplier_name')
date_hierarchy = 'created_at'
fieldsets = (
('Партия поступления', {
'fields': ('document_number', 'warehouse', 'supplier_name', 'notes')
}),
('Даты', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
readonly_fields = ('created_at', 'updated_at')
def items_count(self, obj):
return obj.items.count()
items_count.short_description = 'Товаров'
# ===== INCOMING =====
@admin.register(Incoming)
class IncomingAdmin(admin.ModelAdmin):
list_display = ('product', 'batch', 'quantity', 'cost_price', 'created_at')
list_filter = ('batch__warehouse', 'created_at', 'product')
search_fields = ('product__name', 'batch__document_number')
date_hierarchy = 'created_at'
fieldsets = (
('Товар в партии', {
'fields': ('batch', 'product', 'quantity', 'cost_price', 'stock_batch')
}),
('Дата', {
'fields': ('created_at',),
'classes': ('collapse',)
}),
)
readonly_fields = ('created_at', 'stock_batch')
# ===== SALE BATCH ALLOCATION (INLINE) =====
class SaleBatchAllocationInline(admin.TabularInline):
model = SaleBatchAllocation
extra = 0
readonly_fields = ('batch', 'quantity', 'cost_price')
can_delete = False
fields = ('batch', 'quantity', 'cost_price')
# ===== SALE =====
@admin.register(Sale)
class SaleAdmin(admin.ModelAdmin):
list_display = ('product', 'warehouse', 'quantity', 'sale_price', 'order_display', 'processed_display', 'date')
list_filter = ('warehouse', 'processed', 'date')
search_fields = ('product__name', 'order__order_number')
date_hierarchy = 'date'
fieldsets = (
('Продажа', {
'fields': ('product', 'warehouse', 'quantity', 'sale_price', 'order')
}),
('Статус', {
'fields': ('processed',)
}),
('Документ', {
'fields': ('document_number',)
}),
('Дата', {
'fields': ('date',),
'classes': ('collapse',)
}),
)
readonly_fields = ('date',)
inlines = [SaleBatchAllocationInline]
def order_display(self, obj):
if obj.order:
return f"ORD-{obj.order.order_number}"
return "-"
order_display.short_description = 'Заказ'
def processed_display(self, obj):
if obj.processed:
return format_html('<span style="color: green;">✓ Обработана</span>')
return format_html('<span style="color: red;">✗ Ожидает</span>')
processed_display.short_description = 'Статус'
# ===== WRITE OFF =====
@admin.register(WriteOff)
class WriteOffAdmin(admin.ModelAdmin):
list_display = ('batch', 'quantity', 'reason_display', 'cost_price', 'date')
list_filter = ('reason', 'date', 'batch__warehouse')
search_fields = ('batch__product__name', 'document_number')
date_hierarchy = 'date'
fieldsets = (
('Списание', {
'fields': ('batch', 'quantity', 'reason', 'cost_price')
}),
('Документ', {
'fields': ('document_number', 'notes')
}),
('Дата', {
'fields': ('date',),
'classes': ('collapse',)
}),
)
readonly_fields = ('date', 'cost_price')
def reason_display(self, obj):
return obj.get_reason_display()
reason_display.short_description = 'Причина'
# ===== TRANSFER =====
@admin.register(Transfer)
class TransferAdmin(admin.ModelAdmin):
list_display = ('batch', 'from_warehouse', 'to_warehouse', 'quantity', 'date')
list_filter = ('date', 'from_warehouse', 'to_warehouse')
search_fields = ('batch__product__name', 'document_number')
date_hierarchy = 'date'
fieldsets = (
('Перемещение', {
'fields': ('batch', 'from_warehouse', 'to_warehouse', 'quantity', 'new_batch')
}),
('Документ', {
'fields': ('document_number',)
}),
('Дата', {
'fields': ('date',),
'classes': ('collapse',)
}),
)
readonly_fields = ('date', 'new_batch')
# ===== INVENTORY LINE (INLINE) =====
class InventoryLineInline(admin.TabularInline):
model = InventoryLine
extra = 1
fields = ('product', 'quantity_system', 'quantity_fact', 'difference', 'processed')
readonly_fields = ('difference', 'processed')
# ===== INVENTORY =====
@admin.register(Inventory)
class InventoryAdmin(admin.ModelAdmin):
list_display = ('warehouse', 'status_display', 'date', 'conducted_by')
list_filter = ('status', 'date', 'warehouse')
search_fields = ('warehouse__name', 'conducted_by')
date_hierarchy = 'date'
fieldsets = (
('Инвентаризация', {
'fields': ('warehouse', 'status', 'conducted_by', 'notes')
}),
('Дата', {
'fields': ('date',),
'classes': ('collapse',)
}),
)
readonly_fields = ('date',)
inlines = [InventoryLineInline]
actions = ['process_inventory']
def status_display(self, obj):
colors = {
'draft': '#ff9900', # оранжевый
'processing': '#0099ff', # синий
'completed': '#008000' # зелёный
}
return format_html(
'<span style="color: {}; font-weight: bold;">{}</span>',
colors.get(obj.status, '#000000'),
obj.get_status_display()
)
status_display.short_description = 'Статус'
def process_inventory(self, request, queryset):
from inventory.services import InventoryProcessor
for inventory in queryset:
result = InventoryProcessor.process_inventory(inventory.id)
self.message_user(
request,
f"Инвентаризация {inventory.warehouse.name}: "
f"обработано {result['processed_lines']} строк, "
f"создано {result['writeoffs_created']} списаний и "
f"{result['incomings_created']} приходов"
)
process_inventory.short_description = 'Обработать инвентаризацию'
# ===== RESERVATION =====
@admin.register(Reservation)
class ReservationAdmin(admin.ModelAdmin):
list_display = ('product', 'warehouse', 'quantity', 'status_display', 'context_info', 'reserved_at')
list_filter = ('status', 'reserved_at', 'warehouse', 'showcase')
search_fields = ('product__name', 'order_item__order__order_number', 'showcase__name')
date_hierarchy = 'reserved_at'
fieldsets = (
('Резерв', {
'fields': ('product', 'warehouse', 'quantity', 'status', 'order_item', 'showcase')
}),
('Даты', {
'fields': ('reserved_at', 'released_at', 'converted_at')
}),
)
readonly_fields = ('reserved_at', 'released_at', 'converted_at')
def status_display(self, obj):
colors = {
'reserved': '#0099ff', # синий
'released': '#ff0000', # красный
'converted_to_sale': '#008000' # зелёный
}
return format_html(
'<span style="color: {}; font-weight: bold;">{}</span>',
colors.get(obj.status, '#000000'),
obj.get_status_display()
)
status_display.short_description = 'Статус'
def context_info(self, obj):
if obj.order_item:
return format_html(
'<span style="color: #0066cc;">📎 Заказ ORD-{}</span>',
obj.order_item.order.order_number
)
elif obj.showcase:
return format_html(
'<span style="color: #ff9900;">🌺 Витрина: {}</span>',
obj.showcase.name
)
return "-"
context_info.short_description = 'Контекст'
# ===== STOCK =====
@admin.register(Stock)
class StockAdmin(admin.ModelAdmin):
list_display = ('product', 'warehouse', 'quantity_available', 'quantity_reserved', 'quantity_free', 'updated_at')
list_filter = ('warehouse', 'updated_at')
search_fields = ('product__name', 'product__sku', 'warehouse__name')
fieldsets = (
('Остаток', {
'fields': ('product', 'warehouse', 'quantity_available', 'quantity_reserved')
}),
('Дата', {
'fields': ('updated_at',),
'classes': ('collapse',)
}),
)
readonly_fields = ('quantity_available', 'quantity_reserved', 'updated_at')
# ===== STOCK MOVEMENT (для аудита) =====
@admin.register(StockMovement)
class StockMovementAdmin(admin.ModelAdmin):
list_display = ('product', 'change', 'reason', 'order', 'created_at')
list_filter = ('reason', 'created_at')
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')