forked from erp-dev/erp
feat: stock module
This commit is contained in:
182
stock/models.py
182
stock/models.py
@@ -1,9 +1,13 @@
|
||||
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
|
||||
import decimal
|
||||
|
||||
|
||||
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='订单日期')
|
||||
@@ -18,7 +22,114 @@ class PurchaseOrder(ModelBase):
|
||||
verbose_name_plural = '采购单'
|
||||
|
||||
|
||||
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 StockChangeRecord(ModelBase):
|
||||
"""库存变动记录模型"""
|
||||
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
type = models.IntegerField(choices=StockChangeTypeEnum.choices, verbose_name='变动类型')
|
||||
product = models.ForeignKey(
|
||||
basic_info_models.Product,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='stock_change_records',
|
||||
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='变动来源',
|
||||
)
|
||||
unit = models.IntegerField(
|
||||
verbose_name='单位',
|
||||
choices=basic_info_models.ProductUnitEnum.choices,
|
||||
)
|
||||
num_of_rolls = models.IntegerField(
|
||||
default=1,
|
||||
verbose_name='匹数',
|
||||
)
|
||||
created_by = models.ForeignKey(
|
||||
'auth.User',
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='操作人',
|
||||
)
|
||||
quantity = models.DecimalField(max_digits=10, decimal_places=2, 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} - 产品: {self.product.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 delta(self) -> decimal.Decimal:
|
||||
"""计算库存变动的数量增减"""
|
||||
|
||||
# 入库为正,出库为负
|
||||
if self.type == StockChangeTypeEnum.ADD:
|
||||
return self.quantity
|
||||
elif self.type == StockChangeTypeEnum.REMOVE:
|
||||
return -self.quantity
|
||||
else:
|
||||
raise ValueError("未知的库存变动类型")
|
||||
|
||||
|
||||
class Inventory(ModelBase):
|
||||
"""库存模型"""
|
||||
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
product = models.ForeignKey(
|
||||
basic_info_models.Product,
|
||||
@@ -32,8 +143,8 @@ class Inventory(ModelBase):
|
||||
related_name='inventories',
|
||||
verbose_name='仓库',
|
||||
)
|
||||
quantity = models.IntegerField(verbose_name='库存数量')
|
||||
num_of_rolls = models.IntegerField(null=True, blank=True, 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='备注描述')
|
||||
@@ -44,3 +155,70 @@ class Inventory(ModelBase):
|
||||
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.snapshot_date} - 产品: {self.product.name}'
|
||||
|
||||
class Meta:
|
||||
verbose_name = '库存快照'
|
||||
verbose_name_plural = '库存快照'
|
||||
|
||||
Reference in New Issue
Block a user