1
0
forked from erp-dev/erp
Files
erpnew/stock/models.py

365 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 StockFreezeStatusEnum(models.IntegerChoices):
"""库存冻结状态类型"""
FROZEN = 1, '冻结中'
CANCELLED = 2, '已取消'
COMPLETED = 3, '已成单'
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)
merchant = models.ForeignKey(
basic_info_models.Merchant,
on_delete=models.PROTECT,
related_name='purchase_orders',
verbose_name='所属商户'
)
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)
merchant = models.ForeignKey(
basic_info_models.Merchant,
on_delete=models.PROTECT,
related_name='stock_change_records',
verbose_name='所属商户'
)
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)
merchant = models.ForeignKey(
basic_info_models.Merchant,
on_delete=models.PROTECT,
related_name='stock_change_details',
verbose_name='所属商户'
)
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)
merchant = models.ForeignKey(
basic_info_models.Merchant,
on_delete=models.PROTECT,
related_name='inventories',
verbose_name='所属商户'
)
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)
merchant = models.ForeignKey(
basic_info_models.Merchant,
on_delete=models.PROTECT,
related_name='stock_snapshots',
verbose_name='所属商户'
)
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 = '库存快照'
class StockFreeze(ModelBase):
"""库存冻结模型"""
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey(
basic_info_models.Merchant,
on_delete=models.PROTECT,
related_name='stock_freezes',
verbose_name='所属商户'
)
product = models.ForeignKey(
basic_info_models.Product,
on_delete=models.PROTECT,
related_name='stock_freezes',
verbose_name='产品',
)
warehouse = models.ForeignKey(
basic_info_models.WareHouse,
on_delete=models.PROTECT,
related_name='stock_freezes',
verbose_name='仓库',
)
stock_detail = models.ForeignKey(
StockChangeDetail,
on_delete=models.PROTECT,
related_name='stock_freezes',
verbose_name='关联的库存变动明细',
)
quantity = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='冻结数量')
unit = models.IntegerField(
verbose_name='单位',
choices=basic_info_models.ProductUnitEnum.choices,
)
status = models.IntegerField(
choices=StockFreezeStatusEnum.choices,
default=StockFreezeStatusEnum.FROZEN,
verbose_name='冻结状态',
)
frozen_by = models.ForeignKey(
'auth.User',
on_delete=models.PROTECT,
verbose_name='冻结操作人',
)
frozen_with = models.BigIntegerField(null=True, blank=True, verbose_name='冻结关联ID')
cancelled_by = models.ForeignKey(
'auth.User',
on_delete=models.PROTECT,
null=True,
blank=True,
related_name='cancelled_stock_freezes',
verbose_name='取消操作人',
)
completed_by = models.ForeignKey(
'auth.User',
on_delete=models.PROTECT,
null=True,
blank=True,
related_name='completed_stock_freezes',
verbose_name='成单操作人',
)
completed_at = models.DateTimeField(null=True, blank=True, verbose_name='成单时间')
completed_with = models.BigIntegerField(null=True, blank=True, verbose_name='成单关联ID')
cancelled_at = models.DateTimeField(null=True, blank=True, verbose_name='取消时间')
reason = models.TextField(blank=True, null=True, verbose_name='冻结原因')
def __str__(self):
return f'库存冻结 {self.id} - 产品: {self.product.name}'
class Meta:
verbose_name = '库存冻结'
verbose_name_plural = '库存冻结'