forked from erp-dev/erp
255 lines
8.3 KiB
Python
255 lines
8.3 KiB
Python
from django.db import models
|
||
from django.core.exceptions import ValidationError
|
||
from flower.common import ModelBase
|
||
from basic_info import models as basic_info_models
|
||
from functools import reduce
|
||
import decimal
|
||
|
||
|
||
# ==================== 枚举类定义 ====================
|
||
|
||
class StockChangeSourceEnum(models.IntegerChoices):
|
||
"""库存变动来源类型"""
|
||
|
||
PURCHASE = 1, '采购'
|
||
SALES_RETURN = 2, '销退'
|
||
TRANSPORT_IN = 3, '调入'
|
||
RECHECK_ADD = 4, '盘盈'
|
||
|
||
SALES = 5, '销售'
|
||
PURCHASE_RETURN = 6, '采购退货'
|
||
TRANSPORT_OUT = 7, '调出'
|
||
RECHECK_REMOVE = 8, '盘亏'
|
||
|
||
|
||
class StockChangeTypeEnum(models.IntegerChoices):
|
||
"""库存变动类型"""
|
||
|
||
ADD = 1, '入库'
|
||
REMOVE = 2, '出库'
|
||
|
||
|
||
# ==================== 模型定义 ====================
|
||
|
||
class PurchaseOrder(ModelBase):
|
||
"""采购单模型"""
|
||
|
||
id = models.BigAutoField(primary_key=True)
|
||
supplier = models.ForeignKey(basic_info_models.Supplier, on_delete=models.PROTECT, related_name='purchase_orders', verbose_name='供应商')
|
||
order_date = models.DateField(verbose_name='订单日期')
|
||
total_amount = models.DecimalField(max_digits=15, decimal_places=2, verbose_name='总金额')
|
||
remarks = models.TextField(blank=True, null=True, verbose_name='备注')
|
||
|
||
def __str__(self):
|
||
return f'采购订单 {self.id} - {self.supplier.name}'
|
||
|
||
class Meta:
|
||
verbose_name = '采购单'
|
||
verbose_name_plural = '采购单'
|
||
|
||
|
||
class StockChangeRecord(ModelBase):
|
||
"""库存变动记录模型"""
|
||
|
||
id = models.BigAutoField(primary_key=True)
|
||
type = models.IntegerField(choices=StockChangeTypeEnum.choices, verbose_name='变动类型')
|
||
warehouse = models.ForeignKey(
|
||
basic_info_models.WareHouse,
|
||
on_delete=models.PROTECT,
|
||
related_name='stock_change_records',
|
||
verbose_name='仓库',
|
||
)
|
||
source_type = models.IntegerField(
|
||
choices=StockChangeSourceEnum.choices,
|
||
verbose_name='变动来源',
|
||
)
|
||
created_by = models.ForeignKey(
|
||
'auth.User',
|
||
on_delete=models.PROTECT,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='操作人',
|
||
)
|
||
source_id = models.BigIntegerField(null=True, blank=True, verbose_name='来源单据ID')
|
||
is_finished = models.BooleanField(default=False, verbose_name='完成出入库')
|
||
finished_at = models.DateTimeField(null=True, blank=True, verbose_name='完成时间')
|
||
remarks = models.TextField(blank=True, null=True, verbose_name='备注')
|
||
|
||
def __str__(self):
|
||
return f'出入库记录 {self.id} - 仓库:{self.warehouse.name}'
|
||
|
||
class Meta:
|
||
verbose_name = '出入库记录'
|
||
verbose_name_plural = '出入库记录'
|
||
|
||
def clean(self):
|
||
if self.type == StockChangeTypeEnum.ADD:
|
||
if self.source_type > 4:
|
||
raise ValidationError('入库记录的来源类型无效')
|
||
elif self.type == StockChangeTypeEnum.REMOVE:
|
||
if self.source_type < 5:
|
||
raise ValidationError('出库记录的来源类型无效')
|
||
|
||
def save(self, *args, **kwargs):
|
||
self.full_clean()
|
||
super().save(*args, **kwargs)
|
||
|
||
@property
|
||
def is_incoming(self) -> bool:
|
||
"""判断是否为入库记录"""
|
||
return self.type == StockChangeTypeEnum.ADD
|
||
|
||
@property
|
||
def is_outgoing(self) -> bool:
|
||
"""判断是否为出库记录"""
|
||
return self.type == StockChangeTypeEnum.REMOVE
|
||
|
||
@property
|
||
def num_of_rolls(self) -> int:
|
||
"""获取匹数"""
|
||
self.details.count()
|
||
return self.details.count()
|
||
|
||
@property
|
||
def delta_num_of_rolls(self) -> int:
|
||
"""计算库存变动的匹数增减"""
|
||
|
||
# 入库为正,出库为负
|
||
if self.type == StockChangeTypeEnum.ADD:
|
||
return self.num_of_rolls
|
||
elif self.type == StockChangeTypeEnum.REMOVE:
|
||
return self.num_of_rolls * -1
|
||
else:
|
||
raise ValueError("未知的库存变动类型")
|
||
|
||
|
||
class StockChangeDetail(ModelBase):
|
||
"""库存变动明细模型"""
|
||
|
||
id = models.BigAutoField(primary_key=True)
|
||
product = models.ForeignKey(
|
||
basic_info_models.Product,
|
||
on_delete=models.PROTECT,
|
||
related_name='stock_change_details',
|
||
verbose_name='产品',
|
||
)
|
||
unit = models.IntegerField(
|
||
verbose_name='单位',
|
||
choices=basic_info_models.ProductUnitEnum.choices,
|
||
)
|
||
stock_change_record = models.ForeignKey(
|
||
StockChangeRecord,
|
||
on_delete=models.CASCADE,
|
||
related_name='details',
|
||
verbose_name='库存变动记录',
|
||
)
|
||
quantity = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='数量')
|
||
unit = models.IntegerField(
|
||
verbose_name='单位',
|
||
choices=basic_info_models.ProductUnitEnum.choices,
|
||
)
|
||
|
||
def __str__(self):
|
||
return f'库存变动明细 {self.id} - 记录ID: {self.stock_change_record.id}'
|
||
|
||
class Meta:
|
||
verbose_name = '库存变动明细'
|
||
verbose_name_plural = '库存变动明细'
|
||
|
||
|
||
class Inventory(ModelBase):
|
||
"""库存模型"""
|
||
|
||
id = models.BigAutoField(primary_key=True)
|
||
product = models.ForeignKey(
|
||
basic_info_models.Product,
|
||
on_delete=models.PROTECT,
|
||
related_name='inventories',
|
||
verbose_name='产品',
|
||
)
|
||
warehouse = models.ForeignKey(
|
||
basic_info_models.WareHouse,
|
||
on_delete=models.PROTECT,
|
||
related_name='inventories',
|
||
verbose_name='仓库',
|
||
)
|
||
quantity = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='库存数量')
|
||
num_of_rolls = models.IntegerField(default=1, verbose_name='匹数')
|
||
minimum_quantity = models.IntegerField(null=True, blank=True, verbose_name='最低库存量')
|
||
spec = models.CharField(max_length=100, blank=True, null=True, verbose_name='规格')
|
||
description = models.TextField(blank=True, null=True, verbose_name='备注描述')
|
||
|
||
def __str__(self):
|
||
return f'库存 {self.product.name} - 仓库: {self.warehouse.name}'
|
||
|
||
class Meta:
|
||
verbose_name = '库存'
|
||
verbose_name_plural = '库存'
|
||
unique_together = ('product', 'warehouse')
|
||
|
||
|
||
class StockSnapshot(ModelBase):
|
||
"""库存变动快照模型"""
|
||
|
||
id = models.BigAutoField(primary_key=True)
|
||
product = models.ForeignKey(
|
||
basic_info_models.Product,
|
||
on_delete=models.PROTECT,
|
||
related_name='stock_snapshots',
|
||
verbose_name='产品',
|
||
)
|
||
warehouse = models.ForeignKey(
|
||
basic_info_models.WareHouse,
|
||
on_delete=models.PROTECT,
|
||
related_name='stock_snapshots',
|
||
verbose_name='仓库',
|
||
)
|
||
delta = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='库存变动数量')
|
||
quantity_before = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='变动前库存数量')
|
||
quantity_after = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='变动后库存数量')
|
||
stock_change_record = models.ForeignKey(
|
||
StockChangeRecord,
|
||
on_delete=models.PROTECT,
|
||
related_name='stock_snapshots',
|
||
verbose_name='关联的库存变动记录',
|
||
)
|
||
unit = models.IntegerField(
|
||
verbose_name='单位',
|
||
choices=basic_info_models.ProductUnitEnum.choices,
|
||
)
|
||
num_of_rolls = models.IntegerField(default=1, verbose_name='匹数')
|
||
|
||
# offset_to表示该快照被冲抵到的目标快照ID
|
||
# 当offset_to不为空时,cancelled不可为True 及 offset_id只能为空
|
||
# 因为该记录是冲抵记录无法再取消,也不可能冲抵的同时被冲抵
|
||
offset_to = models.BigIntegerField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='冲抵目标ID',
|
||
)
|
||
offset_at = models.DateTimeField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='冲抵时间',
|
||
)
|
||
|
||
# 当cancelled为True时,offset_id不可为空,表示该快照已被冲抵
|
||
cancelled = models.BooleanField(default=False, verbose_name='已取消')
|
||
cancelled_at = models.DateTimeField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='取消时间',
|
||
)
|
||
offset_id = models.BigIntegerField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='冲抵记录ID',
|
||
)
|
||
|
||
def __str__(self):
|
||
return f'库存快照 {self.id} - 产品: {self.product.name}'
|
||
|
||
class Meta:
|
||
verbose_name = '库存快照'
|
||
verbose_name_plural = '库存快照'
|