forked from erp-dev/erp
563 lines
20 KiB
Python
563 lines
20 KiB
Python
from decimal import Decimal
|
||
import json
|
||
|
||
from django.db import models
|
||
from flower.common import ModelBase
|
||
from django.contrib.auth.models import User
|
||
|
||
|
||
# ==================== 常量定义 ====================
|
||
METER_TO_YARD_RATE = 0.9144
|
||
YARD_TO_METER_RATE = 1.09361
|
||
|
||
|
||
# ==================== 枚举类定义 ====================
|
||
|
||
class WareHouseModeEnum(models.IntegerChoices):
|
||
"""仓库出入库模式枚举"""
|
||
RESTRICT_IN = 1, '严进宽出'
|
||
RESTRICT_IN_OUT = 2, '严进严出'
|
||
UNRESTRICTED = 3, '宽进宽出'
|
||
|
||
|
||
class WarehouseTypeEnum(models.IntegerChoices):
|
||
"""仓库类别枚举"""
|
||
WHOLE = 1, '整仓'
|
||
SCATTERED = 2, '散仓'
|
||
|
||
|
||
class MerchantTypeEnum(models.IntegerChoices):
|
||
"""商户类型枚举"""
|
||
STORE = 1, '布行'
|
||
FACTORY = 2, '印花厂'
|
||
|
||
|
||
class ProductUnitEnum(models.IntegerChoices):
|
||
"""产品单位枚举"""
|
||
METER = 1, '米'
|
||
YARD = 2, '码'
|
||
KG = 3, '公斤'
|
||
SEGMENT = 4, '段'
|
||
|
||
|
||
class EmployeeTypeEnum(models.TextChoices):
|
||
"""员工职位类型枚举"""
|
||
PRINTER = '打纸', '打纸'
|
||
ROLLING = '滚筒', '滚筒'
|
||
WARE = '仓库', '仓库'
|
||
DELIVERY = '送货', '送货'
|
||
|
||
|
||
class EmployeeStatusEnum(models.TextChoices):
|
||
"""员工状态枚举"""
|
||
ACTIVE = '在职', '在职'
|
||
INACTIVE = '离职', '离职'
|
||
|
||
|
||
class DeviceStatusEnum(models.TextChoices):
|
||
"""设备状态枚举"""
|
||
ACTIVE = '使用中', '使用中'
|
||
INACTIVE = '停用', '停用'
|
||
MAINTENANCE = '维修中', '维修中'
|
||
|
||
|
||
class DeviceTypeEnum(models.TextChoices):
|
||
"""设备类型枚举"""
|
||
ROLLING = '滚筒', '滚筒'
|
||
PRINTER = '打印', '打印'
|
||
|
||
|
||
# ==================== 模型定义 ====================
|
||
|
||
|
||
class EmployeeType(ModelBase):
|
||
"""员工职位类型"""
|
||
id = models.BigAutoField(primary_key=True)
|
||
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='employee_types', verbose_name='所属商户')
|
||
title = models.CharField(max_length=50, verbose_name='职位名称')
|
||
description = models.TextField(blank=True, null=True, verbose_name='职位描述')
|
||
reverse = models.CharField(max_length=100, blank=True, null=True, verbose_name='预留字段')
|
||
|
||
def __str__(self):
|
||
return self.title
|
||
|
||
class Meta:
|
||
verbose_name = '员工职位类型'
|
||
verbose_name_plural = '员工职位类型'
|
||
unique_together = ('merchant', 'title')
|
||
|
||
|
||
class QuickInput(ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
name = models.CharField(max_length=100, verbose_name='名')
|
||
value = models.CharField(max_length=200, verbose_name='值')
|
||
group = models.CharField(verbose_name='组')
|
||
|
||
def __str__(self):
|
||
return f'{self.group}-{self.name}'
|
||
|
||
class Meta:
|
||
verbose_name = '快捷输入'
|
||
verbose_name_plural = '快捷输入'
|
||
unique_together = ('name', 'group')
|
||
|
||
|
||
class Merchant(ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
name = models.CharField(max_length=100, verbose_name='商户名称')
|
||
type = models.IntegerField(choices=MerchantTypeEnum.choices, verbose_name='商户类型')
|
||
area = models.CharField(max_length=100, blank=True, null=True, verbose_name='地区')
|
||
email = models.EmailField(blank=True, null=True, verbose_name='电子邮箱')
|
||
mobile = models.CharField(max_length=20, blank=True, null=True, verbose_name='手机')
|
||
contact = models.TextField(blank=True, null=True, verbose_name='联系人')
|
||
auto_complete_stock_change = models.BooleanField(
|
||
default=False,
|
||
verbose_name='自动确认库存变动',
|
||
)
|
||
description = models.TextField(blank=True, null=True, verbose_name='备注描述')
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class Meta:
|
||
verbose_name = '商户资料'
|
||
verbose_name_plural = '商户资料'
|
||
|
||
|
||
class VehicleType(ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='vehicle_types', verbose_name='所属商户')
|
||
name = models.CharField(max_length=50, verbose_name='车辆类型名称')
|
||
description = models.TextField(blank=True, null=True, verbose_name='备注描述')
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class Meta:
|
||
verbose_name = '车辆类型'
|
||
verbose_name_plural = '车辆类型'
|
||
|
||
|
||
class WareHouse(ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='warehouses', verbose_name='所属商户')
|
||
name = models.CharField(max_length=100, verbose_name='仓库名称')
|
||
location = models.CharField(max_length=200, blank=True, null=True, verbose_name='仓库地址')
|
||
type = models.IntegerField(
|
||
choices=WarehouseTypeEnum.choices,
|
||
default=WarehouseTypeEnum.WHOLE,
|
||
verbose_name='仓库类别',
|
||
)
|
||
mode = models.IntegerField(
|
||
choices=WareHouseModeEnum.choices,
|
||
default=WareHouseModeEnum.RESTRICT_IN,
|
||
verbose_name='出入库模式',
|
||
)
|
||
contact = models.CharField(max_length=100, blank=True, null=True, verbose_name='联系人')
|
||
mobile = models.CharField(max_length=20, blank=True, null=True, verbose_name='手机')
|
||
area = models.CharField(max_length=100, blank=True, null=True, verbose_name='地区')
|
||
description = models.TextField(blank=True, null=True, verbose_name='备注描述')
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class Meta:
|
||
verbose_name = '仓库资料'
|
||
verbose_name_plural = '仓库资料'
|
||
|
||
|
||
class Supplier(ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='suppliers', verbose_name='所属商户')
|
||
name = models.CharField(max_length=100, verbose_name='供应商名称')
|
||
area = models.CharField(max_length=100, blank=True, null=True, verbose_name='地区')
|
||
email = models.EmailField(blank=True, null=True, verbose_name='电子邮箱')
|
||
mobile = models.CharField(max_length=20, blank=True, null=True, verbose_name='手机')
|
||
contact = models.TextField(blank=True, null=True, verbose_name='联系人')
|
||
description = models.TextField(blank=True, null=True, verbose_name='备注描述')
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class Meta:
|
||
verbose_name = '供应商'
|
||
verbose_name_plural = '供应商'
|
||
|
||
|
||
class BankAccount(ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='bank_accounts', verbose_name='所属商户')
|
||
name = models.CharField(max_length=100, verbose_name='名称')
|
||
auto_number = models.CharField(max_length=50, verbose_name='自动编号')
|
||
description = models.TextField(blank=True, null=True, verbose_name='备注描述')
|
||
attachment = models.FileField(upload_to='bank_accounts/', blank=True, null=True, verbose_name='附件')
|
||
|
||
def __str__(self):
|
||
return f'{self.name} - {self.auto_number}'
|
||
|
||
class Meta:
|
||
verbose_name = '银行账户'
|
||
verbose_name_plural = '银行账户'
|
||
|
||
|
||
class ProductCategory(ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='product_categories', verbose_name='所属商户')
|
||
name = models.CharField(max_length=100, verbose_name='类别名称')
|
||
description = models.TextField(blank=True, null=True, verbose_name='类别描述')
|
||
product_prefix = models.CharField(max_length=5, verbose_name='产品编号前缀')
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class Meta:
|
||
verbose_name = '产品类别'
|
||
verbose_name_plural = '产品类别'
|
||
|
||
|
||
class Product(ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, verbose_name='所属商户')
|
||
category = models.ForeignKey(
|
||
ProductCategory,
|
||
on_delete=models.PROTECT,
|
||
related_name='products',
|
||
verbose_name='产品类别',
|
||
)
|
||
name = models.CharField(max_length=100, verbose_name='产品名')
|
||
human_id = models.CharField(max_length=100, verbose_name='产品编号', blank=True)
|
||
color = models.CharField(max_length=50, blank=True, null=True, verbose_name='颜色')
|
||
width_size = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True, verbose_name='宽幅')
|
||
pieces = models.IntegerField(blank=True, null=True, verbose_name='件数')
|
||
segment_size = models.DecimalField(blank=True, null=True, verbose_name='一段尺寸', decimal_places=2, max_digits=8)
|
||
single_price_in = models.DecimalField(
|
||
max_digits=10,
|
||
decimal_places=2,
|
||
verbose_name='参考进价',
|
||
blank=True,
|
||
null=True,
|
||
)
|
||
single_price_out = models.DecimalField(
|
||
max_digits=10,
|
||
decimal_places=2,
|
||
verbose_name='参考售价',
|
||
blank=True,
|
||
null=True,
|
||
)
|
||
unit = models.IntegerField(
|
||
choices=ProductUnitEnum.choices,
|
||
default=ProductUnitEnum.METER,
|
||
verbose_name='单位',
|
||
)
|
||
spec = models.CharField(max_length=100, blank=True, null=True, verbose_name='规格')
|
||
description = models.TextField(blank=True, null=True, verbose_name='备注描述')
|
||
image = models.ImageField(
|
||
upload_to='product_images/',
|
||
blank=True,
|
||
null=True,
|
||
verbose_name='产品图片',
|
||
)
|
||
mdy_image_url = models.URLField(
|
||
max_length=500,
|
||
blank=True,
|
||
null=True,
|
||
verbose_name='明道云图片URL',
|
||
help_text='来自明道云的图片链接',
|
||
)
|
||
minimum_quantity = models.IntegerField(null=True, blank=True, verbose_name='最低库存量')
|
||
transform_rate = models.DecimalField(
|
||
max_digits=10,
|
||
decimal_places=4,
|
||
blank=True,
|
||
null=True,
|
||
verbose_name='公斤转换率',
|
||
)
|
||
from_mdy = models.BooleanField(
|
||
default=False,
|
||
verbose_name='是否来自明道云同步',
|
||
help_text='标记该产品是否通过明道云接口同步',
|
||
)
|
||
|
||
def get_primary_image_url(self):
|
||
"""
|
||
优先返回 mdy_image_url;若为空则尝试从 description JSON 中提取 original_file_full_path。
|
||
"""
|
||
if self.mdy_image_url:
|
||
return self.mdy_image_url
|
||
|
||
if not self.description:
|
||
return None
|
||
|
||
try:
|
||
payload = json.loads(self.description) if isinstance(self.description, str) else self.description
|
||
except (TypeError, ValueError, json.JSONDecodeError):
|
||
return None
|
||
|
||
if isinstance(payload, dict):
|
||
items = [payload]
|
||
elif isinstance(payload, list):
|
||
items = payload
|
||
else:
|
||
return None
|
||
|
||
for item in items:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
url = item.get('original_file_full_path') or item.get('DownloadUrl')
|
||
if url:
|
||
return url
|
||
return None
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
def save(
|
||
self,
|
||
*args,
|
||
force_insert=False,
|
||
force_update=False,
|
||
using=None,
|
||
update_fields=None,
|
||
):
|
||
super().save(
|
||
*args,
|
||
force_insert=force_insert,
|
||
force_update=force_update,
|
||
using=using,
|
||
update_fields=update_fields,
|
||
)
|
||
# Generate human_id if not set after saving to get the ID
|
||
if not self.human_id and self.category:
|
||
self.human_id = f'{self.category.product_prefix}{self.id:05d}'
|
||
super().save(
|
||
*args,
|
||
force_insert=False,
|
||
force_update=False,
|
||
using=using,
|
||
update_fields=['human_id'],
|
||
)
|
||
|
||
class Meta:
|
||
verbose_name = '产品资料'
|
||
verbose_name_plural = '产品资料'
|
||
|
||
|
||
class Customer(ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='customers', verbose_name='所属商户')
|
||
name = models.CharField(max_length=100, verbose_name='客户名称')
|
||
visible_employees = models.ManyToManyField(
|
||
'Employee',
|
||
blank=True,
|
||
related_name='visible_customers',
|
||
verbose_name='可见该客户的员工',
|
||
)
|
||
created_by = models.ForeignKey(
|
||
'Employee',
|
||
on_delete=models.PROTECT,
|
||
related_name='created_customers',
|
||
null=True,
|
||
verbose_name='创建者',
|
||
)
|
||
mobile = models.CharField(max_length=20, null=True, blank=True, verbose_name='手机')
|
||
area = models.CharField(max_length=100, blank=True, null=True, verbose_name='地区')
|
||
email = models.EmailField(blank=True, null=True, verbose_name='电子邮箱')
|
||
contact = models.CharField(blank=True, null=True, verbose_name='联系人')
|
||
description = models.TextField(blank=True, null=True, verbose_name='备注描述')
|
||
mdy_uid = models.CharField(
|
||
max_length=100,
|
||
unique=True,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='明道云UID',
|
||
help_text='对应明道云记录的唯一标识',
|
||
)
|
||
from_mdy = models.BooleanField(
|
||
default=False,
|
||
verbose_name='是否来自明道云同步',
|
||
help_text='标记该客户是否通过明道云接口同步',
|
||
)
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class Meta:
|
||
verbose_name = '客户资料'
|
||
verbose_name_plural = '客户资料'
|
||
permissions = [
|
||
('view_all_customers', '查看所有客户资料'),
|
||
]
|
||
|
||
|
||
class Employee(ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='employees', verbose_name='所属商户')
|
||
sys_user = models.OneToOneField(
|
||
User,
|
||
on_delete=models.PROTECT,
|
||
related_name='employee',
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='系统用户',
|
||
)
|
||
name = models.CharField(max_length=100, verbose_name='员工姓名')
|
||
position = models.ForeignKey(
|
||
'EmployeeType',
|
||
on_delete=models.PROTECT,
|
||
related_name='employees',
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='职位',
|
||
)
|
||
mobile = models.CharField(max_length=20, blank=True, null=True, verbose_name='手机')
|
||
area = models.CharField(max_length=100, blank=True, null=True, verbose_name='地区')
|
||
description = models.TextField(blank=True, null=True, verbose_name='备注描述')
|
||
status = models.CharField(
|
||
max_length=20,
|
||
choices=EmployeeStatusEnum.choices,
|
||
default=EmployeeStatusEnum.ACTIVE,
|
||
verbose_name='员工状态',
|
||
)
|
||
|
||
@property
|
||
def job_type(self) -> str:
|
||
"""
|
||
返回职位名称(向后兼容)
|
||
|
||
如果有关联的职位类型,返回其 title,否则返回空字符串
|
||
"""
|
||
if self.position:
|
||
return self.position.title
|
||
return ''
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class Meta:
|
||
verbose_name = '员工资料'
|
||
verbose_name_plural = '员工资料'
|
||
|
||
|
||
class UserProfile(ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
user = models.OneToOneField(
|
||
User,
|
||
on_delete=models.CASCADE,
|
||
related_name='profile',
|
||
verbose_name='关联用户',
|
||
)
|
||
merchant = models.ForeignKey(
|
||
'Merchant',
|
||
on_delete=models.PROTECT,
|
||
related_name='user_profiles',
|
||
verbose_name='所属商户',
|
||
)
|
||
description = models.TextField(blank=True, null=True, verbose_name='备注描述')
|
||
|
||
def __str__(self):
|
||
return f"{self.user.username}'s Profile"
|
||
|
||
class Meta:
|
||
verbose_name = '用户资料扩展'
|
||
verbose_name_plural = '用户资料扩展'
|
||
|
||
|
||
class DeviceInfo(ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='devices', verbose_name='所属商户')
|
||
name = models.CharField(max_length=100, verbose_name='设备名称')
|
||
type = models.CharField(
|
||
max_length=20,
|
||
choices=DeviceTypeEnum.choices,
|
||
default=DeviceTypeEnum.ROLLING,
|
||
verbose_name='设备类型',
|
||
)
|
||
status = models.CharField(
|
||
max_length=20,
|
||
choices=DeviceStatusEnum.choices,
|
||
default=DeviceStatusEnum.ACTIVE,
|
||
verbose_name='设备状态',
|
||
)
|
||
start_working_at = models.DateField(blank=True, null=True, verbose_name='开始服役日期')
|
||
stop_working_at = models.DateField(blank=True, null=True, verbose_name='停止服役日期')
|
||
is_occupied = models.BooleanField(default=False, verbose_name='是否被占用')
|
||
description = models.TextField(blank=True, null=True, verbose_name='备注描述')
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class Meta:
|
||
verbose_name = '设备资料'
|
||
verbose_name_plural = '设备资料'
|
||
|
||
|
||
class VehicleTransportRecord(ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='vehicle_transport_records', verbose_name='所属商户')
|
||
vehicle_type = models.ForeignKey(
|
||
VehicleType,
|
||
on_delete=models.PROTECT,
|
||
related_name='vehicles',
|
||
verbose_name='车次',
|
||
)
|
||
driver_name = models.CharField(max_length=100, verbose_name='司机姓名')
|
||
contact_number = models.CharField(max_length=20, blank=True, null=True, verbose_name='联系电话')
|
||
license_plate = models.CharField(max_length=20, verbose_name='车牌号码', null=True, blank=True)
|
||
delivery_date = models.DateField(verbose_name='送货日期')
|
||
|
||
def __str__(self):
|
||
return f'{self.vehicle_type}-{self.driver_name}-{self.delivery_date}'
|
||
|
||
class Meta:
|
||
verbose_name = '司机车次'
|
||
verbose_name_plural = '司机车次'
|
||
|
||
|
||
class MerchantSettingTypeEnum(models.TextChoices):
|
||
"""商户设置类型枚举"""
|
||
STR = 'str', '字符串'
|
||
INT = 'int', '整数'
|
||
FLOAT = 'float', '浮点数'
|
||
BOOL = 'bool', '布尔值'
|
||
|
||
|
||
class MerchantSetting(ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='settings', verbose_name='所属商户')
|
||
key = models.CharField(max_length=100, verbose_name='设置项')
|
||
type = models.CharField(
|
||
max_length=20,
|
||
choices=MerchantSettingTypeEnum.choices,
|
||
default=MerchantSettingTypeEnum.STR,
|
||
verbose_name='设置类型',
|
||
)
|
||
val_str = models.CharField(max_length=100, null=True, blank=True, verbose_name='设置值(字符串)')
|
||
val_int = models.IntegerField(null=True, blank=True, verbose_name='设置值(整数)')
|
||
val_float = models.FloatField(null=True, blank=True, verbose_name='设置值(浮点数)')
|
||
val_bool = models.BooleanField(null=True, blank=True, verbose_name='设置值(布尔值)')
|
||
description = models.TextField(blank=True, null=True, verbose_name='备注')
|
||
|
||
@property
|
||
def value(self):
|
||
if self.type == MerchantSettingTypeEnum.STR:
|
||
return self.val_str
|
||
elif self.type == MerchantSettingTypeEnum.INT:
|
||
return self.val_int
|
||
elif self.type == MerchantSettingTypeEnum.FLOAT:
|
||
return self.val_float
|
||
elif self.type == MerchantSettingTypeEnum.BOOL:
|
||
return self.val_bool
|
||
raise ValueError(f'Invalid setting type: {self.type}')
|
||
|
||
def __str__(self):
|
||
return f'{self.merchant.name} - {self.key}'
|
||
|
||
class Meta:
|
||
verbose_name = '商户设置'
|
||
verbose_name_plural = '商户设置'
|
||
|
||
|
||
class MerchantSettingKeyEnum(models.TextChoices):
|
||
"""商户设置项枚举"""
|
||
AUTO_CREATE_STOCK_CHANGE_TASKS = 'auto_create_stock_change_tasks', '自动创建出入库记录任务'
|