1
0
forked from erp-dev/erp
Files
erpnew/stock/models.py
2025-11-04 18:04:27 +08:00

225 lines
7.5 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
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='订单日期')
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 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,
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.snapshot_date} - 产品: {self.product.name}'
class Meta:
verbose_name = '库存快照'
verbose_name_plural = '库存快照'