Files
octopus/myproject/inventory/admin.py
Andrey Smakotin c534e27c41 refactor: подготовка к стандартизации Transfer моделей
Текущее состояние перед рефакторингом Transfer → TransferDocument.
Все изменения с последнего коммита по улучшению системы поступлений.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-26 19:55:50 +03:00

526 lines
19 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, Sale, WriteOff, Transfer,
Inventory, InventoryLine, Reservation, Stock, StockMovement,
SaleBatchAllocation, Showcase, WriteOffDocument, WriteOffDocumentItem,
IncomingDocument, IncomingDocumentItem, Transformation, TransformationInput,
TransformationOutput
)
# ===== 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 = 'Количество'
# ===== 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)
msg_parts = [
f"Инвентаризация {inventory.warehouse.name}: "
f"обработано {result['processed_lines']} строк."
]
if result.get('writeoff_document'):
msg_parts.append(
f"Создан документ списания: {result['writeoff_document'].document_number} (черновик)."
)
if result.get('incoming_document'):
msg_parts.append(
f"Создан документ оприходования: {result['incoming_document'].document_number} (черновик)."
)
self.message_user(request, ' '.join(msg_parts))
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')
# ===== INCOMING DOCUMENT (документы поступления) =====
class IncomingDocumentItemInline(admin.TabularInline):
model = IncomingDocumentItem
extra = 0
fields = ('product', 'quantity', 'cost_price', 'notes')
raw_id_fields = ('product',)
@admin.register(IncomingDocument)
class IncomingDocumentAdmin(admin.ModelAdmin):
list_display = ('document_number', 'warehouse', 'status_display', 'receipt_type_display', 'date', 'items_count', 'total_quantity_display', 'created_by', 'created_at')
list_filter = ('status', 'warehouse', 'receipt_type', 'date', 'created_at')
search_fields = ('document_number', 'warehouse__name', 'supplier_name')
date_hierarchy = 'date'
readonly_fields = ('document_number', 'created_at', 'updated_at', 'confirmed_at', 'confirmed_by')
inlines = [IncomingDocumentItemInline]
fieldsets = (
('Документ', {
'fields': ('document_number', 'warehouse', 'status', 'date', 'receipt_type', 'supplier_name', '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 receipt_type_display(self, obj):
colors = {
'supplier': '#0d6efd',
'inventory': '#0dcaf0',
'adjustment': '#198754',
}
return format_html(
'<span style="color: {}; font-weight: bold;">{}</span>',
colors.get(obj.receipt_type, '#6c757d'),
obj.get_receipt_type_display()
)
receipt_type_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(IncomingDocumentItem)
class IncomingDocumentItemAdmin(admin.ModelAdmin):
list_display = ('document', 'product', 'quantity', 'cost_price', 'total_cost_display', 'created_at')
list_filter = ('document__status', 'document__receipt_type', 'created_at')
search_fields = ('product__name', 'document__document_number')
raw_id_fields = ('product', 'document')
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 = 'Выходов'