Унификация генерации номеров документов и оптимизация кода
- Унифицирован формат номеров документов: 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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user