Унификация генерации номеров документов и оптимизация кода

- Унифицирован формат номеров документов: IN-XXXXXX (6 цифр), как WO-XXXXXX и MOVE-XXXXXX
- Убрано дублирование функции _extract_number_from_document_number
- Оптимизирована инициализация счетчика incoming: быстрая проверка перед полной инициализацией
- Удален неиспользуемый файл utils.py (функциональность перенесена в document_generator.py)
- Все функции генерации номеров используют единый подход через DocumentCounter.get_next_value()
This commit is contained in:
2025-12-21 00:51:08 +03:00
parent 78dc9e9801
commit 375ec5366a
14 changed files with 1873 additions and 147 deletions

View File

@@ -761,6 +761,7 @@ class DocumentCounter(models.Model):
COUNTER_TYPE_CHOICES = [
('transfer', 'Перемещение товара'),
('writeoff', 'Списание товара'),
('incoming', 'Поступление товара'),
]
counter_type = models.CharField(
@@ -1100,3 +1101,205 @@ class WriteOffDocumentItem(models.Model):
def total_cost(self):
"""Себестоимость позиции (средневзвешенная из cost_price товара)"""
return self.quantity * (self.product.cost_price or Decimal('0'))
class IncomingDocument(models.Model):
"""
Документ поступления товара на склад.
Сценарий использования:
1. Создается черновик (draft)
2. В течение дня добавляются товары (IncomingDocumentItem)
3. В конце смены документ проводится (confirmed) → создаются IncomingBatch и Incoming
4. Сигнал автоматически создает StockBatch и обновляет Stock
"""
STATUS_CHOICES = [
('draft', 'Черновик'),
('confirmed', 'Проведён'),
('cancelled', 'Отменён'),
]
document_number = models.CharField(
max_length=100,
unique=True,
db_index=True,
verbose_name="Номер документа"
)
warehouse = models.ForeignKey(
Warehouse,
on_delete=models.PROTECT,
related_name='incoming_documents',
verbose_name="Склад"
)
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='draft',
db_index=True,
verbose_name="Статус"
)
date = models.DateField(
verbose_name="Дата документа",
help_text="Дата, к которой относится поступление"
)
receipt_type = models.CharField(
max_length=20,
choices=IncomingBatch.RECEIPT_TYPE_CHOICES,
default='supplier',
db_index=True,
verbose_name="Тип поступления"
)
supplier_name = models.CharField(
max_length=200,
blank=True,
null=True,
verbose_name="Наименование поставщика",
help_text="Заполняется для типа 'Поступление от поставщика'"
)
notes = models.TextField(
blank=True,
null=True,
verbose_name="Примечания"
)
# Аудит
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='created_incoming_documents',
verbose_name="Создал"
)
confirmed_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='confirmed_incoming_documents',
verbose_name="Провёл"
)
confirmed_at = models.DateTimeField(
null=True,
blank=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 = ['-date', '-created_at']
indexes = [
models.Index(fields=['document_number']),
models.Index(fields=['warehouse', 'status']),
models.Index(fields=['date']),
models.Index(fields=['receipt_type']),
models.Index(fields=['-created_at']),
]
def __str__(self):
return f"{self.document_number} ({self.get_status_display()})"
@property
def total_quantity(self):
"""Общее количество товаров в документе"""
return self.items.aggregate(
total=models.Sum('quantity')
)['total'] or Decimal('0')
@property
def total_cost(self):
"""Общая себестоимость поступления"""
return sum(item.total_cost for item in self.items.select_related('product'))
@property
def can_edit(self):
"""Можно ли редактировать документ"""
return self.status == 'draft'
@property
def can_confirm(self):
"""Можно ли провести документ"""
return self.status == 'draft' and self.items.exists()
@property
def can_cancel(self):
"""Можно ли отменить документ"""
return self.status == 'draft'
class IncomingDocumentItem(models.Model):
"""
Строка документа поступления.
При создании:
- Товар добавляется в черновик документа
- Резервирование НЕ создается (товар еще не поступил)
При проведении документа:
1. Создается IncomingBatch с номером документа
2. Создается Incoming запись для каждого товара
3. Сигнал create_stock_batch_on_incoming автоматически создает StockBatch
"""
document = models.ForeignKey(
IncomingDocument,
on_delete=models.CASCADE,
related_name='items',
verbose_name="Документ"
)
product = models.ForeignKey(
Product,
on_delete=models.PROTECT,
related_name='incoming_document_items',
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="Создан")
updated_at = models.DateTimeField(auto_now=True, verbose_name="Обновлён")
class Meta:
verbose_name = "Позиция документа поступления"
verbose_name_plural = "Позиции документа поступления"
ordering = ['id']
indexes = [
models.Index(fields=['document']),
models.Index(fields=['product']),
]
def __str__(self):
return f"{self.product.name}: {self.quantity} шт @ {self.cost_price}"
@property
def total_cost(self):
"""Себестоимость позиции (quantity * cost_price)"""
return self.quantity * self.cost_price