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', '自动创建出入库记录任务'