refactor: подготовка к стандартизации Transfer моделей

Текущее состояние перед рефакторингом Transfer → TransferDocument.
Все изменения с последнего коммита по улучшению системы поступлений.

🤖 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-26 19:55:50 +03:00
parent 0da2995a74
commit c534e27c41
14 changed files with 198 additions and 313 deletions

View File

@@ -100,84 +100,9 @@ class StockBatch(models.Model):
return f"{self.product.name} на {self.warehouse.name} - Остаток: {self.quantity} шт @ {self.cost_price} за ед."
class IncomingBatch(models.Model):
"""
Партия поступления товара (один номер документа = одна партия).
Содержит один номер документа и может включать несколько товаров.
"""
RECEIPT_TYPE_CHOICES = [
('supplier', 'Поступление от поставщика'),
('inventory', 'Оприходование при инвентаризации'),
('adjustment', 'Оприходование без инвентаризации'),
]
warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE,
related_name='incoming_batches', verbose_name="Склад")
document_number = models.CharField(max_length=100, unique=True, db_index=True,
verbose_name="Номер документа")
receipt_type = models.CharField(
max_length=20,
choices=RECEIPT_TYPE_CHOICES,
default='supplier',
db_index=True,
verbose_name="Тип поступления"
)
supplier_name = models.CharField(max_length=200, blank=True, null=True,
verbose_name="Наименование поставщика")
notes = models.TextField(blank=True, null=True, verbose_name="Примечания")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата обновления")
class Meta:
verbose_name = "Партия поступления"
verbose_name_plural = "Партии поступлений"
ordering = ['-created_at']
indexes = [
models.Index(fields=['document_number']),
models.Index(fields=['warehouse']),
models.Index(fields=['receipt_type']),
models.Index(fields=['-created_at']),
]
def __str__(self):
total_items = self.items.count()
total_qty = self.items.aggregate(models.Sum('quantity'))['quantity__sum'] or 0
return f"Партия {self.document_number}: {total_items} товаров, {total_qty} шт"
class Incoming(models.Model):
"""
Товар в партии поступления. Много товаров = одна партия (IncomingBatch).
"""
batch = models.ForeignKey(IncomingBatch, on_delete=models.CASCADE,
related_name='items', verbose_name="Партия")
product = models.ForeignKey(Product, on_delete=models.CASCADE,
related_name='incomings', verbose_name="Товар")
quantity = models.DecimalField(max_digits=10, decimal_places=3, verbose_name="Количество")
cost_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Закупочная цена")
notes = models.TextField(blank=True, null=True, verbose_name="Примечания")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
stock_batch = models.ForeignKey(StockBatch, on_delete=models.SET_NULL, null=True, blank=True,
related_name='incomings', verbose_name="Складская партия")
class Meta:
verbose_name = "Товар в поступлении"
verbose_name_plural = "Товары в поступлениях"
ordering = ['-created_at']
indexes = [
models.Index(fields=['batch']),
models.Index(fields=['product']),
models.Index(fields=['-created_at']),
]
unique_together = [['batch', 'product']] # Один товар максимум один раз в партии
def __str__(self):
return f"{self.product.name}: {self.quantity} шт (партия {self.batch.document_number})"
@property
def can_edit(self):
"""Можно ли редактировать приход"""
return self.stock_batch is None
# Модели IncomingBatch и Incoming удалены - заменены на IncomingDocument/IncomingDocumentItem
# Теперь используется упрощенная архитектура:
# IncomingDocument → IncomingDocumentItem → StockBatch (напрямую при проведении)
class Sale(models.Model):
@@ -1207,8 +1132,8 @@ class IncomingDocument(models.Model):
Сценарий использования:
1. Создается черновик (draft)
2. В течение дня добавляются товары (IncomingDocumentItem)
3. В конце смены документ проводится (confirmed) → создаются IncomingBatch и Incoming
4. Сигнал автоматически создает StockBatch и обновляет Stock
3. В конце смены документ проводится (confirmed) → создается StockBatch напрямую
4. Stock автоматически обновляется
"""
STATUS_CHOICES = [
('draft', 'Черновик'),
@@ -1216,6 +1141,12 @@ class IncomingDocument(models.Model):
('cancelled', 'Отменён'),
]
RECEIPT_TYPE_CHOICES = [
('supplier', 'Поступление от поставщика'),
('inventory', 'Оприходование при инвентаризации'),
('adjustment', 'Оприходование без инвентаризации'),
]
document_number = models.CharField(
max_length=100,
unique=True,
@@ -1245,7 +1176,7 @@ class IncomingDocument(models.Model):
receipt_type = models.CharField(
max_length=20,
choices=IncomingBatch.RECEIPT_TYPE_CHOICES,
choices=RECEIPT_TYPE_CHOICES,
default='supplier',
db_index=True,
verbose_name="Тип поступления"
@@ -1355,9 +1286,8 @@ class IncomingDocumentItem(models.Model):
- Резервирование НЕ создается (товар еще не поступил)
При проведении документа:
1. Создается IncomingBatch с номером документа
2. Создается Incoming запись для каждого товара
3. Сигнал create_stock_batch_on_incoming автоматически создает StockBatch
1. Для каждой позиции напрямую создается StockBatch
2. Stock автоматически обновляется
"""
document = models.ForeignKey(
IncomingDocument,