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

728 lines
25 KiB
Python
Raw Permalink 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 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 FrontendPageTypeEnum(models.TextChoices):
"""前端页面类型枚举"""
MAIN = 'main', '主菜单'
SUB = 'sub', '子菜单'
class FrontendPage(ModelBase):
"""
前端页面/菜单项
用于管理前端可见页面,与员工岗位关联实现权限控制。
数据来源docs/menu_keys.json通过 sync_frontend_pages 命令同步。
"""
id = models.BigAutoField(primary_key=True)
key = models.CharField(
max_length=100,
unique=True,
db_index=True,
verbose_name='页面唯一标识',
help_text='前端路由 key如 plate-order-kanban'
)
label = models.CharField(max_length=100, verbose_name='页面名称')
page_type = models.CharField(
max_length=10,
choices=FrontendPageTypeEnum.choices,
default=FrontendPageTypeEnum.SUB,
verbose_name='页面类型'
)
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='children',
verbose_name='父级菜单'
)
sort_order = models.IntegerField(default=0, verbose_name='排序')
is_active = models.BooleanField(default=True, verbose_name='是否启用')
def __str__(self):
return f'{self.label} ({self.key})'
class Meta:
verbose_name = '前端页面'
verbose_name_plural = '前端页面'
ordering = ['sort_order', 'id']
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='预留字段')
visible_pages = models.ManyToManyField(
'FrontendPage',
blank=True,
related_name='employee_types',
verbose_name='可见页面',
help_text='授予主菜单将自动包含其所有子菜单'
)
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 = '客户资料'
constraints = [
models.UniqueConstraint(
fields=['merchant', 'name'],
name='uniq_customer_merchant_name',
),
]
permissions = [
('view_all_customers', '查看所有客户资料'),
]
class CustomerAddress(ModelBase):
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey(
'Merchant',
on_delete=models.PROTECT,
related_name='customer_addresses',
verbose_name='所属商户',
)
customer = models.ForeignKey(
'Customer',
on_delete=models.PROTECT,
related_name='addresses',
verbose_name='客户',
)
address = models.CharField(max_length=255, verbose_name='地址')
contact_name = models.CharField(max_length=100, blank=True, default='', verbose_name='联系人')
contact_phone = models.CharField(max_length=50, blank=True, default='', verbose_name='联系电话')
area = models.CharField(max_length=100, blank=True, default='', verbose_name='地区')
coordinates = models.CharField(
max_length=100,
null=True,
blank=True,
verbose_name='经纬度',
help_text='经纬度字符串,格式由调用方决定',
)
is_default = models.BooleanField(default=False, verbose_name='是否默认地址')
remark = models.CharField(max_length=200, blank=True, default='', verbose_name='备注')
extra = models.JSONField(null=True, blank=True, verbose_name='扩展信息')
created_by = models.ForeignKey(
'Employee',
on_delete=models.PROTECT,
related_name='created_customer_addresses',
null=True,
blank=True,
verbose_name='创建者',
)
deleted_at = models.DateTimeField(null=True, blank=True, verbose_name='删除时间')
deleted_by = models.ForeignKey(
'Employee',
on_delete=models.SET_NULL,
related_name='deleted_customer_addresses',
null=True,
blank=True,
verbose_name='删除者',
)
def __str__(self):
return f'{self.customer.name} - {self.address}'
class Meta:
verbose_name = '客户地址'
verbose_name_plural = '客户地址'
ordering = ['-is_default', '-created_at', '-id']
indexes = [
models.Index(fields=['merchant', 'customer']),
models.Index(fields=['deleted_at']),
models.Index(fields=['is_default']),
]
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='员工状态',
)
wecom_user_id = models.CharField(
max_length=128,
unique=True,
null=True,
blank=True,
verbose_name='企业微信用户ID',
help_text='企业微信 OAuth2 返回的 user_id',
)
@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 TransportVehicle(ModelBase):
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='transport_vehicles', verbose_name='所属商户')
name = models.CharField(max_length=100, verbose_name='车辆名称')
license_plate = models.CharField(max_length=20, verbose_name='车牌号码')
def __str__(self):
return f'{self.name} ({self.license_plate})'
class Meta:
verbose_name = '运输车辆'
verbose_name_plural = '运输车辆'
unique_together = ('merchant', 'license_plate')
class TransportVehicleMaterialCapacity(ModelBase):
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='transport_vehicle_material_capacities', verbose_name='所属商户')
transport_vehicle = models.ForeignKey(
TransportVehicle,
on_delete=models.CASCADE,
related_name='material_capacities',
verbose_name='运输车辆',
)
material_name = models.CharField(max_length=100, verbose_name='物料名')
capacity = models.PositiveIntegerField(verbose_name='容量(条)')
def __str__(self):
return f'{self.transport_vehicle.name}-{self.material_name}:{self.capacity}'
class Meta:
verbose_name = '运输车辆物料容量'
verbose_name_plural = '运输车辆物料容量'
unique_together = ('transport_vehicle', 'material_name')
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', '自动创建出入库记录任务'