Files
TermiNexus fbf1bb7d6e refactor(gerp): 应用按子应用目录规约重组,清单外置并拆分路由
- 新增 gvsds/apps/gerp/apps/<appid>/:talk/mailbox/gitea/oa/ehrm/dingcalc_finance/
  product_order/shop/store_value/wallet/enterprise,各应用自带 models/manager/views/urls
  与 apps.gerp_manifests(JSON 清单,$schema 指向 apps/gerp_manifests.schema.json)
- 删除 app_manifests.py 内联 _LEGACY_MANIFESTS,改为扫描各应用清单文件(未知键/缺必填键即判非法并跳过)
- 路由按应用拆:/API/Mail/、/API/Gitea/、/API/Oa/ 由扁平直挂改为 include 子应用 urls.py
  (reverse() 断言 19 个 name 的路径逐字不变)
- AppConfig 移至 gerp_app.py 并改显式点分路径注册,规避 apps/ 目录与 Django <app>.apps 同名冲突
- gerp/models.py 裁剪已搬迁模型(保留 _NewUuidBytes 与平台模型),修复 views/__init__.py 与 oidc.py 导入
- AssetDownloadView:优先按 FileAsset.StoragePath 定位并回退 ./resource 旧布局,修上传成功却下载 404
- 同批落地 Talk 应用后端(app 已迁入 apps/talk,gerp.0046 状态认领迁移)
- 注意:app.ini 被 .gitignore 忽略,INSTALLED_APPS 需在部署环境手工同步 gvsds.apps.gerp.gerp_app.GerpConfig
2026-09-26 23:46:00 +08:00

913 lines
50 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.
"""模块:GERP 数据模型
定义企业资源管理核心表:租户、公司、部门、角色、权限、岗位模板、
人事职务标签、工作流、消息、菜单、资产、负债、
现金簿、背包、物品、个人主页等。
权限体系四大分层:
1. 组织人事层:Tenant → Company → Department,员工通过 UserRoleDepartment/UserRoleWorkgroup 关联表归属部门/工作组
2. 岗位模板层:PositionTemplate + PositionTemplatePermission,权限初始化快照
3. 角色权限载体层:Role + RolePermission,运行时鉴权实体
4. 鉴权规则层:操作权限校验(角色汇总)+ 数据权限校验(组织属性)
所有 ORM 字段与方法遵循帕斯卡命名法(首字母大写、缩写大写、单单词小写)。"""
from gvsdsdk.model_base import QModel
import datetime
import uuid
from django.db import models
from gvsds.apps.core.models import UUIDField, User
def _NewUuidBytes():
"""生成随机 16 字节 UUID(主键默认值,迁移可序列化)。"""
return uuid.uuid4().bytes
class Tenant(QModel):
"""租户表——多租户架构核心"""
TenantUUID = UUIDField(primary_key=True, verbose_name='租户UUID')
TenantName = models.CharField(max_length=256, verbose_name='租户名称')
TenantStatus = models.SmallIntegerField(default=1, verbose_name='租户状态')
TenantExpireTime = models.DateTimeField(verbose_name='到期时间')
ReviewStatus = models.SmallIntegerField(default=0, verbose_name='审核状态')
ReviewRemark = models.CharField(max_length=512, default='', verbose_name='审核备注')
ReviewerUUID = UUIDField(null=True, blank=True, verbose_name='审核人UUID')
ReviewTime = models.DateTimeField(null=True, blank=True, verbose_name='审核时间')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'Tenant'
app_label = 'gerp'
ordering = ['-CreateTime']
verbose_name = '租户'
verbose_name_plural = '租户'
def __str__(self):
return self.TenantName
class CompanyInfo(QModel):
"""企业信息表——工商注册与企业详情"""
CompanyInfoUUID = UUIDField(primary_key=True, verbose_name='企业信息UUID')
TenantUUID = UUIDField(null=True, blank=True, db_column='TenantUUID', verbose_name='租户UUID')
EnterpriseName = models.CharField(max_length=256, verbose_name='企业全称')
EnterpriseShortName = models.CharField(max_length=100, verbose_name='企业简称')
CreditCode = models.CharField(max_length=64, unique=True, verbose_name='统一社会信用代码')
EnterpriseType = models.CharField(max_length=32, default='', verbose_name='企业类型')
OwnershipType = models.CharField(max_length=32, default='', verbose_name='所有制类型')
Industry = models.CharField(max_length=32, default='', verbose_name='行业')
Scale = models.CharField(max_length=32, default='', verbose_name='规模')
RegisteredCapital = models.CharField(max_length=64, default='', verbose_name='注册资本')
EstablishDate = models.CharField(max_length=32, default='', verbose_name='成立日期')
BusinessScope = models.TextField(default='', verbose_name='经营范围')
BusinessStatus = models.CharField(max_length=32, default='', verbose_name='经营状态')
RegProvince = models.CharField(max_length=32, default='', verbose_name='注册省份')
RegCity = models.CharField(max_length=32, default='', verbose_name='注册城市')
RegDistrict = models.CharField(max_length=32, default='', verbose_name='注册区县')
RegAddress = models.CharField(max_length=512, default='', verbose_name='注册地址')
OfficeProvince = models.CharField(max_length=32, default='', verbose_name='办公省份')
OfficeCity = models.CharField(max_length=32, default='', verbose_name='办公城市')
OfficeDistrict = models.CharField(max_length=32, default='', verbose_name='办公区县')
OfficeAddress = models.CharField(max_length=512, default='', verbose_name='办公地址')
Phone = models.CharField(max_length=32, default='', verbose_name='电话')
Phone2 = models.CharField(max_length=32, default='', verbose_name='电话2')
Fax = models.CharField(max_length=32, default='', verbose_name='传真')
Email = models.CharField(max_length=128, default='', verbose_name='邮箱')
Hotline = models.CharField(max_length=32, default='', verbose_name='热线')
ServicePhone = models.CharField(max_length=32, default='', verbose_name='客服电话')
Website = models.CharField(max_length=256, default='', verbose_name='网站')
Wechat = models.CharField(max_length=128, default='', verbose_name='微信')
WechatMini = models.CharField(max_length=128, default='', verbose_name='微信小程序')
Weibo = models.CharField(max_length=128, default='', verbose_name='微博')
LinkedIn = models.CharField(max_length=256, default='', verbose_name='领英')
Bank = models.CharField(max_length=32, default='', verbose_name='开户银行')
BankAccount = models.CharField(max_length=64, default='', verbose_name='银行账号')
BankBranch = models.CharField(max_length=128, default='', verbose_name='开户支行')
BankProvince = models.CharField(max_length=32, default='', verbose_name='银行省份')
BankCity = models.CharField(max_length=32, default='', verbose_name='银行城市')
AccountName = models.CharField(max_length=128, default='', verbose_name='账户名')
BankCode = models.CharField(max_length=32, default='', verbose_name='银行代码')
AccountType = models.CharField(max_length=32, default='', verbose_name='账户类型')
AccountStatus = models.CharField(max_length=32, default='', verbose_name='账户状态')
TaxBank = models.CharField(max_length=128, default='', verbose_name='税务银行')
TaxAccount = models.CharField(max_length=64, default='', verbose_name='税务账号')
TaxBankCode = models.CharField(max_length=32, default='', verbose_name='税务银行代码')
SalaryBank = models.CharField(max_length=128, default='', verbose_name='薪资银行')
SalaryAccount = models.CharField(max_length=64, default='', verbose_name='薪资账号')
SocialSecurityBank = models.CharField(max_length=128, default='', verbose_name='社保银行')
SocialSecurityAccount = models.CharField(max_length=64, default='', verbose_name='社保账号')
LegalPersonName = models.CharField(max_length=64, default='', verbose_name='法人姓名')
LegalPersonIDCard = models.CharField(max_length=32, default='', db_column='LegalPersonIdCard', verbose_name='法人身份证')
LegalPersonGender = models.CharField(max_length=16, default='', verbose_name='法人性别')
LegalPersonBirthday = models.CharField(max_length=32, default='', verbose_name='法人生日')
LegalPersonNationality = models.CharField(max_length=32, default='', verbose_name='法人国籍')
LegalPersonEducation = models.CharField(max_length=32, default='', verbose_name='法人学历')
LegalPersonPhone = models.CharField(max_length=32, default='', verbose_name='法人电话')
LegalPersonEmail = models.CharField(max_length=128, default='', verbose_name='法人邮箱')
LegalPersonAddress = models.CharField(max_length=256, default='', verbose_name='法人地址')
LegalPersonPosition = models.CharField(max_length=32, default='', verbose_name='法人职务')
LegalPersonShareRatio = models.CharField(max_length=16, default='', verbose_name='法人持股比例')
LegalPersonAppointment = models.CharField(max_length=32, default='', verbose_name='法人任职')
OperatorName = models.CharField(max_length=64, default='', verbose_name='经办人姓名')
OperatorIDCard = models.CharField(max_length=32, default='', db_column='OperatorIdCard', verbose_name='经办人身份证')
OperatorPhone = models.CharField(max_length=32, default='', verbose_name='经办人电话')
OperatorEmail = models.CharField(max_length=128, default='', verbose_name='经办人邮箱')
OperatorDepartment = models.CharField(max_length=128, default='', verbose_name='经办人部门')
OperatorPosition = models.CharField(max_length=64, default='', verbose_name='经办人职务')
FinanceManagerName = models.CharField(max_length=64, default='', verbose_name='财务负责人')
FinanceManagerPhone = models.CharField(max_length=32, default='', verbose_name='财务电话')
FinanceManagerEmail = models.CharField(max_length=128, default='', verbose_name='财务邮箱')
FinanceManagerIDCard = models.CharField(max_length=32, default='', db_column='FinanceManagerIdCard', verbose_name='财务身份证')
Remark = models.TextField(default='', verbose_name='备注')
ReviewStatus = models.SmallIntegerField(default=0, verbose_name='审核状态')
ReviewRemark = models.CharField(max_length=512, default='', verbose_name='审核备注')
ReviewerUUID = UUIDField(null=True, blank=True, verbose_name='审核人UUID')
ReviewTime = models.DateTimeField(null=True, blank=True, verbose_name='审核时间')
CreatorUUID = UUIDField(null=True, blank=True, verbose_name='创建人UUID')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'CompanyInfo'
app_label = 'gerp'
ordering = ['-CreateTime']
verbose_name = '企业信息'
verbose_name_plural = '企业信息'
class Company(QModel):
"""公司表——系统内组织机构"""
CompanyUUID = UUIDField(primary_key=True, verbose_name='公司UUID')
TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID')
ParentCompanyUUID = UUIDField(null=True, blank=True, db_column='ParentCompanyUUID', verbose_name='上级公司UUID')
CompanyName = models.CharField(max_length=256, verbose_name='公司全称')
CompanyShort = models.CharField(max_length=100, verbose_name='公司简称')
CompanyIcon = models.CharField(max_length=512, null=True, blank=True, db_column='CompanyIcon', verbose_name='公司图标')
CompanyStatus = models.SmallIntegerField(default=1, verbose_name='公司状态')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'Company'
app_label = 'gerp'
ordering = ['-CreateTime']
verbose_name = '公司'
verbose_name_plural = '公司'
def __str__(self):
return self.CompanyName
class Department(QModel):
"""部门表——组织架构层级"""
DeptUUID = UUIDField(primary_key=True, verbose_name='部门UUID')
TenantUUID = UUIDField(null=True, blank=True, verbose_name='租户UUID')
CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID')
ParentDeptUUID = UUIDField(null=True, blank=True, db_column='ParentDeptUUID', verbose_name='上级部门UUID')
DeptName = models.CharField(max_length=256, verbose_name='部门名称')
DeptSort = models.IntegerField(default=0, verbose_name='排序')
DeptStatus = models.SmallIntegerField(default=1, verbose_name='部门状态')
PositionCategoryUUID = UUIDField(null=True, blank=True, db_column='PositionCategoryUUID', verbose_name='绑定通用部门UUID')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'Dept'
app_label = 'gerp'
ordering = ['DeptSort', '-CreateTime']
verbose_name = '部门'
verbose_name_plural = '部门'
def __str__(self):
return self.DeptName
class FileAsset(QModel):
"""文件资产表——文件存储与引用管理"""
AssetUUID = UUIDField(primary_key=True, verbose_name='资产UUID')
TenantUUID = UUIDField(null=True, blank=True, verbose_name='租户UUID')
CompanyUUID = UUIDField(null=True, blank=True, db_column='CompanyUUID', verbose_name='公司UUID')
FileName = models.CharField(max_length=256, verbose_name='文件名')
FileSHA1 = models.CharField(max_length=40, unique=True, verbose_name='文件SHA1')
FileSize = models.IntegerField(verbose_name='文件大小')
MimeType = models.CharField(max_length=128, verbose_name='MIME类型')
StoragePath = models.CharField(max_length=512, verbose_name='存储路径')
UploaderUUID = UUIDField(verbose_name='上传者UUID')
RefCount = models.IntegerField(default=0, verbose_name='引用计数')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'FileAsset'
app_label = 'gerp'
ordering = ['-CreateTime']
verbose_name = '文件资产'
verbose_name_plural = '文件资产'
class WorkOrder(QModel):
"""工单表——任务与问题跟踪"""
OrderUUID = UUIDField(primary_key=True, verbose_name='工单UUID')
Title = models.CharField(max_length=256, verbose_name='标题')
Description = models.CharField(max_length=4096, default='', verbose_name='描述')
Category = models.CharField(max_length=64, default='other', verbose_name='分类')
Priority = models.SmallIntegerField(default=0, verbose_name='优先级')
Status = models.SmallIntegerField(default=0, verbose_name='状态')
CreatorUUID = UUIDField(db_index=True, db_column='CreatorUUID', verbose_name='创建人UUID')
AssigneeUUID = UUIDField(null=True, blank=True, db_column='AssigneeUUID', verbose_name='指派人UUID')
Resolution = models.CharField(max_length=4096, default='', verbose_name='解决方案')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
UpdateTime = models.DateTimeField(null=True, blank=True, verbose_name='更新时间')
CloseTime = models.DateTimeField(null=True, blank=True, verbose_name='关闭时间')
class Meta:
db_table = 'WorkOrder'
app_label = 'gerp'
ordering = ['-CreateTime']
verbose_name = '工单'
verbose_name_plural = '工单'
class Message(QModel):
"""消息表——系统通知与站内信"""
MessageUUID = UUIDField(primary_key=True, verbose_name='消息UUID')
SenderUUID = UUIDField(null=True, blank=True, db_column='SenderUUID', verbose_name='发送者UUID')
ReceiverUUID = UUIDField(db_index=True, db_column='ReceiverUUID', verbose_name='接收者UUID')
Title = models.CharField(max_length=256, verbose_name='标题')
Content = models.TextField(default='', verbose_name='内容')
MsgType = models.SmallIntegerField(default=0, verbose_name='消息类型')
IsRead = models.SmallIntegerField(default=0, verbose_name='已读')
RelatedUUID = models.CharField(max_length=64, default='', verbose_name='关联UUID')
RelatedType = models.CharField(max_length=32, default='', verbose_name='关联类型')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'Message'
app_label = 'gerp'
ordering = ['-CreateTime']
verbose_name = '消息'
verbose_name_plural = '消息'
class Menu(QModel):
"""菜单表——系统导航与功能入口"""
MenuUUID = UUIDField(primary_key=True, verbose_name='菜单UUID')
TenantUUID = UUIDField(null=True, blank=True, verbose_name='租户UUID')
CompanyUUID = UUIDField(null=True, blank=True, db_column='CompanyUUID', verbose_name='公司UUID')
ParentMenuUUID = UUIDField(null=True, blank=True, db_column='ParentMenuUUID', verbose_name='上级菜单UUID')
MenuName = models.CharField(max_length=128, verbose_name='菜单名称')
MenuTitle = models.CharField(max_length=128, verbose_name='菜单标题')
MenuIcon = models.CharField(max_length=128, default='', verbose_name='菜单图标')
MenuLink = models.CharField(max_length=256, default='', verbose_name='菜单链接')
MenuSort = models.SmallIntegerField(default=0, verbose_name='排序')
PermissionLevel = models.FloatField(default=1.0, verbose_name='权限级别')
PermissionAttr = models.CharField(max_length=256, default='', verbose_name='权限属性')
MenuStatus = models.SmallIntegerField(default=1, verbose_name='菜单状态')
MenuType = models.SmallIntegerField(default=0, verbose_name='菜单类型')
class Meta:
db_table = 'Menu'
app_label = 'gerp'
verbose_name = '菜单'
verbose_name_plural = '菜单'
class LegacyDepartment(QModel):
"""遗留部门表——历史数据兼容"""
DepartmentUUID = UUIDField(primary_key=True, verbose_name='部门UUID')
DepartmentName = models.CharField(max_length=256, unique=True, verbose_name='部门名称')
DepartmentEnglishName = models.CharField(max_length=256, unique=True, verbose_name='部门英文名')
DepartmentIntroduction = models.CharField(max_length=1024, verbose_name='部门介绍')
class Meta:
db_table = 'Department'
app_label = 'gerp'
verbose_name = '遗留部门'
verbose_name_plural = '遗留部门'
class DepartmentSMTM(QModel):
"""部门主管表——部门负责人映射"""
SMTMUUID = UUIDField(primary_key=True, verbose_name='关联UUID')
DepartmentUUID = UUIDField(verbose_name='部门UUID')
UserUUID = UUIDField(verbose_name='用户UUID')
PersonInChargeStatus = models.SmallIntegerField(default=0, verbose_name='负责人状态')
class Meta:
db_table = 'DepartmentSMTM'
app_label = 'gerp'
verbose_name = '部门主管'
verbose_name_plural = '部门主管'
class Workgroup(QModel):
"""工作组表——跨部门协作单元,归属特定公司"""
WorkgroupUUID = UUIDField(primary_key=True, verbose_name='工作组UUID')
TenantUUID = UUIDField(null=True, blank=True, verbose_name='租户UUID')
CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID')
WorkgroupName = models.CharField(max_length=256, verbose_name='工作组名称')
WorkgroupEnglishName = models.CharField(max_length=256, verbose_name='工作组英文名')
WorkgroupIntroduction = models.CharField(max_length=1024, verbose_name='工作组介绍')
WorkgroupType = models.SmallIntegerField(default=0, verbose_name='工作组类型') # 0=部门工作组,1=跨部门工作组
WorkgroupTags = models.CharField(max_length=512, null=True, blank=True, verbose_name='工作组标签') # 逗号分隔,如 团队,项目组
class Meta:
db_table = 'Workgroup'
app_label = 'gerp'
verbose_name = '工作组'
verbose_name_plural = '工作组'
class WorkgroupRole(QModel):
"""工作组角色关联表——工作组与角色的多对多映射
工作组只绑定角色,不直接绑定用户。用户通过持有该角色即成为工作组成员。
RoleType 标识该角色在工作组中的类型:0=组员,1=组长。
CompanyUUID 冗余存储用于 SAAS 数据隔离过滤(与 UserRole 表设计一致)。
"""
WorkgroupRoleUUID = UUIDField(primary_key=True, verbose_name='关联UUID')
WorkgroupUUID = UUIDField(db_column='WorkgroupUUID', verbose_name='工作组UUID')
RoleUUID = UUIDField(verbose_name='角色UUID')
TenantUUID = UUIDField(null=True, blank=True, verbose_name='租户UUID')
CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID')
RoleType = models.SmallIntegerField(default=0, verbose_name='角色类型')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'WorkgroupRole'
app_label = 'gerp'
verbose_name = '工作组角色关联'
verbose_name_plural = '工作组角色关联'
class UserRoleWorkgroup(QModel):
"""用户-角色对 与 工作组 的多对多关联表
解决"一个 (用户↔角色) 对可同时加入多个工作组"的需求:
UserRole 表自身只有单值 WorkgroupUUID 字段(一对一只挂一个工作组),
本表通过 UserRoleUUID + WorkgroupUUID 建立多对多映射,一个用户-角色对可挂多个工作组。
CompanyUUID 冗余存储用于 SAAS 数据隔离过滤(与 UserRole 表设计一致)。
"""
UserRoleWorkgroupUUID = UUIDField(primary_key=True, verbose_name='关联UUID')
UserRoleUUID = UUIDField(verbose_name='用户-角色关联UUID')
WorkgroupUUID = UUIDField(db_column='WorkgroupUUID', verbose_name='工作组UUID')
TenantUUID = UUIDField(null=True, blank=True, verbose_name='租户UUID')
CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID')
# 职务不再存于本表:工作组指向公司的员工关系,职务复用 UserRoleDepartmentPosition 多值子表
RoleType = models.SmallIntegerField(default=0, verbose_name='角色类型')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'UserRoleWorkgroup'
app_label = 'gerp'
verbose_name = '用户角色工作组关联'
verbose_name_plural = '用户角色工作组关联'
class UserRoleDepartment(QModel):
"""用户-角色对 与 部门 的多对多关联表
解决"一个 (用户↔角色) 对可同时加入多个部门"的需求:
UserRole 表自身只有单值 DeptUUID 字段(且数据库主键是联合主键 (UserUUID, RoleUUID),
同一用户-角色对只能存在一行,挂新部门会撞 1062 Duplicate entry),
本表通过 UserRoleUUID + DeptUUID 建立多对多映射,一个用户-角色对可挂多个部门。
CompanyUUID 冗余存储用于 SAAS 数据隔离过滤(与 UserRole 表设计一致)。
"""
UserRoleDepartmentUUID = UUIDField(primary_key=True, verbose_name='关联UUID')
UserRoleUUID = UUIDField(verbose_name='用户-角色关联UUID')
DeptUUID = UUIDField(db_column='DeptUUID', verbose_name='部门UUID')
TenantUUID = UUIDField(null=True, blank=True, verbose_name='租户UUID')
CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID')
# PositionUUID 已多值化:职务改存 UserRoleDepartmentPosition 子表(身兼数职)
RoleType = models.SmallIntegerField(default=0, verbose_name='角色类型')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'UserRoleDepartment'
app_label = 'gerp'
verbose_name = '用户角色部门关联'
verbose_name_plural = '用户角色部门关联'
# (UserRoleDepartmentPosition 已下沉 gvsdsdk.models —— 职务多值子表统一由 SDK 提供)
class ItemReference(QModel):
"""物品引用表——物品模板定义"""
ItemUUID = UUIDField(primary_key=True, verbose_name='物品UUID')
ItemPublisherUserUUID = UUIDField(verbose_name='发布者UUID')
ItemName = models.CharField(max_length=128, verbose_name='物品名称')
ItemIcon = models.CharField(max_length=512, verbose_name='物品图标')
ItemIconColor = models.CharField(max_length=6, verbose_name='图标颜色')
ItemTags = models.JSONField(verbose_name='标签')
ItemDescription = models.TextField(verbose_name='描述')
ItemMetaData = models.JSONField(verbose_name='元数据')
class Meta:
db_table = 'ItemReference'
app_label = 'gerp'
verbose_name = '物品模板'
verbose_name_plural = '物品模板'
class Backpack(QModel):
"""背包表——用户物品栏"""
BackpackItemUUID = UUIDField(primary_key=True, verbose_name='背包物品UUID')
UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID')
ItemTemplateUUID = UUIDField(db_column='ItemTemplateUUID', verbose_name='物品模板UUID')
_ItemName = models.CharField(max_length=128, null=True, blank=True, verbose_name='物品名称缓存')
_ItemIcon = models.CharField(max_length=512, null=True, blank=True, verbose_name='物品图标缓存')
_ItemIconColor = models.CharField(max_length=6, null=True, blank=True, verbose_name='图标颜色缓存')
_ItemTags = models.JSONField(null=True, blank=True, verbose_name='标签缓存')
_ItemMetaData = models.JSONField(null=True, blank=True, verbose_name='元数据缓存')
_ItemDescription = models.TextField(null=True, blank=True, verbose_name='描述缓存')
ItemLength = models.BigIntegerField(verbose_name='数量')
class Meta:
db_table = 'Backpack'
app_label = 'gerp'
verbose_name = '背包'
verbose_name_plural = '背包'
@property
def ItemReference(self):
"""获取物品模板引用"""
return self.ItemTemplateUUID
@property
def ItemNameResolved(self):
"""解析物品名称——优先使用缓存值"""
ref = self.ItemTemplateUUID
return self._ItemName or (ref.ItemName if ref else None)
@property
def ItemIconResolved(self):
"""解析物品图标——优先使用缓存值"""
ref = self.ItemTemplateUUID
return self._ItemIcon or (ref.ItemIcon if ref else None)
class SimulatedTrigger(QModel):
"""模拟触发器表——自动化规则引擎"""
SimulatedTriggerUUID = UUIDField(primary_key=True, verbose_name='触发器UUID')
SimulatedTriggerPublisherUserUUID = UUIDField(verbose_name='发布者UUID')
SimulatedTriggerName = models.CharField(max_length=128, verbose_name='触发器名称')
SimulatedTriggerFilePath = models.CharField(max_length=512, verbose_name='文件路径')
SimulatedTriggerCode = models.TextField(verbose_name='触发器代码')
class Meta:
db_table = 'SimulatedTrigger'
app_label = 'gerp'
verbose_name = '模拟触发器'
verbose_name_plural = '模拟触发器'
class CashReceiptsPaymentsLedger(QModel):
"""现金收支台账表——财务流水记录"""
CashBookUUID = UUIDField(primary_key=True, verbose_name='台账UUID')
CashBookCreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
CashBookTransactionType = models.SmallIntegerField(default=0, verbose_name='交易类型')
CashBookAmount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='金额')
CashBookCurrency = models.CharField(max_length=10, default='CNY', verbose_name='币种')
CashBookDescription = models.CharField(max_length=512, verbose_name='描述')
CashBookTransactionTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='交易时间')
CashBookUserUUID = UUIDField(verbose_name='用户UUID')
CashBookStatus = models.SmallIntegerField(default=0, verbose_name='状态')
CashBookRemark = models.CharField(max_length=1024, null=True, blank=True, verbose_name='备注')
CashBookMetaData = models.JSONField(null=True, blank=True, verbose_name='元数据')
CashBookCategory = models.CharField(max_length=50, default='\u5176\u5b83', verbose_name='分类')
CashBookPaymentMethod = models.CharField(max_length=50, default='', verbose_name='支付方式')
CashBookPaymentDetails = models.CharField(max_length=512, null=True, blank=True, verbose_name='支付详情')
CashBookInvoiceNo = models.CharField(max_length=100, null=True, blank=True, verbose_name='发票号')
CashBookTaxRate = models.DecimalField(max_digits=4, decimal_places=2, default=0.00, verbose_name='税率')
CashBookTaxAmount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='税额')
CashBookNetAmount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='净额')
CashBookProjectUUID = UUIDField(null=True, blank=True, verbose_name='项目UUID')
CashBookOrderUUID = UUIDField(null=True, blank=True, verbose_name='订单UUID')
CashBookDepartmentUUID = UUIDField(null=True, blank=True, verbose_name='部门UUID')
CashBookAttachmentPath = models.CharField(max_length=512, null=True, blank=True, verbose_name='附件路径')
CashBookExchangeRate = models.DecimalField(max_digits=10, decimal_places=4, default=1.0000, verbose_name='汇率')
class Meta:
db_table = 'CashBook'
app_label = 'gerp'
verbose_name = '现金收支台账'
verbose_name_plural = '现金收支台账'
indexes = [
models.Index(fields=['CashBookTransactionTime'], name='idx_transaction_time'),
models.Index(fields=['CashBookTransactionType'], name='idx_transaction_type'),
models.Index(fields=['CashBookStatus'], name='idx_status'),
models.Index(fields=['CashBookCategory'], name='idx_category'),
]
class Asset(QModel):
"""资产表——企业资产记录"""
AssetUUID = UUIDField(primary_key=True, verbose_name='资产UUID')
AssetType = models.CharField(max_length=50, verbose_name='资产类型')
AssetAmount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='金额')
AssetCurrency = models.CharField(max_length=10, default='CNY', verbose_name='币种')
AssetDescription = models.CharField(max_length=512, verbose_name='描述')
AssetCreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
AssetUserUUID = UUIDField(verbose_name='用户UUID')
AssetStatus = models.SmallIntegerField(default=0, verbose_name='状态')
AssetMetaData = models.JSONField(null=True, blank=True, verbose_name='元数据')
class Meta:
db_table = 'Asset'
app_label = 'gerp'
verbose_name = '资产'
verbose_name_plural = '资产'
class Liability(QModel):
"""负债表——企业负债记录"""
LiabilityUUID = UUIDField(primary_key=True, verbose_name='负债UUID')
LiabilityType = models.CharField(max_length=50, verbose_name='负债类型')
LiabilityAmount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='金额')
LiabilityCurrency = models.CharField(max_length=10, default='CNY', verbose_name='币种')
LiabilityDescription = models.CharField(max_length=512, verbose_name='描述')
LiabilityCreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
LiabilityUserUUID = UUIDField(verbose_name='用户UUID')
LiabilityStatus = models.SmallIntegerField(default=0, verbose_name='状态')
LiabilityMetaData = models.JSONField(null=True, blank=True, verbose_name='元数据')
class Meta:
db_table = 'Liability'
app_label = 'gerp'
verbose_name = '负债'
verbose_name_plural = '负债'
class UserIDCard(QModel):
"""用户身份证表——实名信息"""
UserUUID = UUIDField(primary_key=True, verbose_name='用户UUID')
Name = models.CharField(max_length=128, verbose_name='姓名')
OldName = models.CharField(max_length=128, default='', verbose_name='曾用名')
Born = models.DateField(verbose_name='出生日期')
Sex = models.CharField(max_length=10, verbose_name='性别')
Ethnic = models.CharField(max_length=20, verbose_name='民族')
IdentificationNumber = models.CharField(max_length=20, verbose_name='身份证号')
StudentRegistrationNumber = models.CharField(max_length=30, default='', verbose_name='学籍号')
ChangeStudentStatus = models.JSONField(default=list, verbose_name='学籍变动')
CorrespondenceAddress = models.JSONField(default=list, verbose_name='通讯地址')
HeadImage = models.CharField(max_length=4096, default='', verbose_name='头像')
InformationAboutFamilyMembers = models.JSONField(default=dict, verbose_name='家庭成员信息')
Appointments = models.JSONField(default=dict, verbose_name='任职信息')
ReadmeReport = models.TextField(default='', verbose_name='自述报告')
class Meta:
db_table = 'IDCard'
app_label = 'gerp'
verbose_name = '身份证'
verbose_name_plural = '身份证'
class PersonalPage(QModel):
"""个人主页表——用户展示页面"""
PersonalPageUserUUID = UUIDField(primary_key=True, verbose_name='用户UUID')
PersonalPageName = models.CharField(max_length=256, unique=True, verbose_name='主页名称')
Subtitle = models.CharField(max_length=256, default='', verbose_name='副标题')
BackgroundLink = models.CharField(max_length=256, default='', verbose_name='背景链接')
Classification = models.CharField(max_length=256, default='', verbose_name='分类')
Github = models.CharField(max_length=256, default='', verbose_name='GitHub')
Wechat = models.CharField(max_length=256, default='', verbose_name='微信')
QQ = models.CharField(max_length=256, default='', db_column='Qq', verbose_name='QQ')
SelfIntroduction = models.TextField(default='', verbose_name='自我介绍')
ProjectExperience = models.CharField(max_length=256, default='', verbose_name='项目经验')
SatisfiedCustomers = models.CharField(max_length=256, default='', verbose_name='满意客户')
ReceiveAward = models.CharField(max_length=256, default='', verbose_name='获奖')
CoffeeCups = models.CharField(max_length=256, default='', verbose_name='咖啡杯数')
Achievement1Icon = models.CharField(max_length=256, default='', verbose_name='成就1图标')
Achievement1Name = models.CharField(max_length=256, default='', verbose_name='成就1名称')
Achievement1Time = models.CharField(max_length=256, default='', verbose_name='成就1时间')
Achievement1Introduction = models.TextField(default='', verbose_name='成就1介绍')
Achievement2Icon = models.CharField(max_length=256, default='', verbose_name='成就2图标')
Achievement2Name = models.CharField(max_length=256, default='', verbose_name='成就2名称')
Achievement2Time = models.CharField(max_length=256, default='', verbose_name='成就2时间')
Achievement2Introduction = models.TextField(default='', verbose_name='成就2介绍')
Achievement3Icon = models.CharField(max_length=256, default='', verbose_name='成就3图标')
Achievement3Name = models.CharField(max_length=256, default='', verbose_name='成就3名称')
Achievement3Time = models.CharField(max_length=256, default='', verbose_name='成就3时间')
Achievement3Introduction = models.TextField(default='', verbose_name='成就3介绍')
Achievement4Icon = models.CharField(max_length=256, default='', verbose_name='成就4图标')
Achievement4Name = models.CharField(max_length=256, default='', verbose_name='成就4名称')
Achievement4Time = models.CharField(max_length=256, default='', verbose_name='成就4时间')
Achievement4Introduction = models.TextField(default='', verbose_name='成就4介绍')
Tags = models.CharField(max_length=256, default='', verbose_name='标签')
Markdown = models.TextField(default='', verbose_name='Markdown')
class Meta:
db_table = 'PersonalPage'
app_label = 'gerp'
verbose_name = '个人主页'
verbose_name_plural = '个人主页'
class UserSettings(QModel):
"""用户设置表——存储每位用户的个性化系统设置"""
UserUUID = UUIDField(unique=True, db_column='UserUUID', verbose_name='用户UUID')
SettingsData = models.TextField(default='', verbose_name='设置数据')
class Meta:
db_table = 'UserSettings'
app_label = 'gerp'
verbose_name = '用户设置'
verbose_name_plural = '用户设置'
class AuditLog(QModel):
"""操作审计日志表——记录敏感表的写操作(增/删/改)数据快照
仅记录 User/Role/Permission/Tenant/Company 等敏感表的写操作,
按公司隔离查询(CompanyUUID 字段),含变更前后的数据快照。
通过管理命令 python manage.py CleanAuditLogs --days=90 自动清理过期日志。
"""
AuditLogUUID = UUIDField(primary_key=True, verbose_name='日志UUID')
TenantUUID = UUIDField(null=True, blank=True, db_column='TenantUUID', verbose_name='租户UUID')
CompanyUUID = UUIDField(null=True, blank=True, db_column='CompanyUUID', verbose_name='公司UUID')
UserUUID = UUIDField(null=True, blank=True, db_column='UserUUID', verbose_name='操作人UUID')
TableName = models.CharField(max_length=64, db_index=True, verbose_name='表名')
Action = models.CharField(max_length=16, verbose_name='操作类型')
TargetUUID = UUIDField(null=True, blank=True, verbose_name='目标记录UUID')
IPAddress = models.CharField(max_length=64, default='', verbose_name='IP地址')
OldData = models.JSONField(null=True, blank=True, verbose_name='变更前数据')
NewData = models.JSONField(null=True, blank=True, verbose_name='变更后数据')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, db_index=True, verbose_name='创建时间')
class Meta:
db_table = 'AuditLog'
app_label = 'gerp'
ordering = ['-CreateTime']
verbose_name = '操作审计日志'
verbose_name_plural = '操作审计日志'
def __str__(self):
return f'{self.TableName}:{self.Action} @ {self.CreateTime}'
class AbnormalUserLog(QModel):
"""异常用户攻击日志表——记录恶意越权/攻击请求的完整指纹
由 AttackDetectMiddleware 自动记录:403/429 响应、重复鉴权失败、
越权访问尝试等。按公司隔离查询,用于安全审计与攻击溯源。
"""
LogUUID = UUIDField(primary_key=True, verbose_name='日志UUID')
TenantUUID = UUIDField(null=True, blank=True, db_column='TenantUUID', verbose_name='租户UUID')
CompanyUUID = UUIDField(null=True, blank=True, db_column='CompanyUUID', verbose_name='公司UUID')
UserUUID = UUIDField(null=True, blank=True, db_column='UserUUID', verbose_name='用户UUID')
AccountID = models.CharField(max_length=64, default='', db_index=True, verbose_name='账号标识')
IPAddress = models.CharField(max_length=64, default='', db_index=True, verbose_name='客户端IP')
RealIP = models.CharField(max_length=64, default='', verbose_name='真实IP')
XForwardedFor = models.CharField(max_length=512, default='', verbose_name='XFF头')
UserAgent = models.CharField(max_length=512, default='', verbose_name='User-Agent')
RequestMethod = models.CharField(max_length=16, default='', verbose_name='请求方法')
RequestPath = models.CharField(max_length=512, default='', db_index=True, verbose_name='请求路径')
RequestParams = models.JSONField(null=True, blank=True, verbose_name='查询参数')
RequestBody = models.TextField(default='', verbose_name='请求体摘要')
RequestHeaders = models.JSONField(null=True, blank=True, verbose_name='请求头')
AttemptType = models.CharField(max_length=32, default='', db_index=True, verbose_name='攻击类型')
ResponseStatus = models.IntegerField(default=0, verbose_name='响应状态码')
Remark = models.CharField(max_length=512, default='', verbose_name='备注')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, db_index=True, verbose_name='创建时间')
class Meta:
db_table = 'AbnormalUserLog'
app_label = 'gerp'
ordering = ['-CreateTime']
verbose_name = '异常用户攻击日志'
verbose_name_plural = '异常用户攻击日志'
def __str__(self):
return f'{self.AttemptType}:{self.IPAddress} @ {self.CreateTime}'
class App(QModel):
"""应用/功能注册表——企业功能与个人应用的统一实体
企业视角叫"功能"(租户安装后全员可用),个人视角叫"应用"(个人启用后自用)。
投放形态 AppType:
enterprise —— 企业功能:仅租户管理员安装后该租户可见
personal —— 个人应用:个人启用后仅本人可见
both —— 混合:企业装=全员,个人装=自用
企业授权 AuthRequired=1:个人使用须经租户管理员审批授权(UserApp.ApplyStatus 状态机)。
"""
AppUUID = UUIDField(primary_key=True, verbose_name='应用UUID')
AppCode = models.CharField(max_length=64, unique=True, verbose_name='应用编码')
AppName = models.CharField(max_length=128, verbose_name='应用名称')
AppIcon = models.CharField(max_length=256, default='', verbose_name='应用图标')
AppDesc = models.TextField(default='', verbose_name='应用描述')
AppType = models.CharField(max_length=16, default='both', verbose_name='投放形态(enterprise/personal/both)')
AuthRequired = models.SmallIntegerField(default=0, verbose_name='是否需企业授权(0/1)')
MenuRootUUID = UUIDField(null=True, blank=True, verbose_name='关联菜单根UUID(安装后注入)')
DefaultPerms = models.TextField(default='', verbose_name='默认权限码集(;分隔)')
FrontendRoute = models.CharField(max_length=256, default='', verbose_name='前端路由(安装后跳转)')
FrontendType = models.CharField(max_length=16, default='vue', verbose_name='前端类型(vue/legacy)')
LegacySrc = models.CharField(max_length=256, default='', verbose_name='旧版页面路径(legacy)')
AppSort = models.IntegerField(default=0, verbose_name='排序')
AppStatus = models.SmallIntegerField(default=1, verbose_name='状态(1启用/0禁用)')
StrictPositionVisible = models.SmallIntegerField(default=0, verbose_name='严格职务可见(0宽松/1严格)')
RequirePositionBinding = models.SmallIntegerField(default=0, verbose_name='强制职务绑定(0/1,应用清单声明默认要求职务)')
# 远端注入(MSYNC):Source=msync 的应用由子服务器上报注册
Source = models.CharField(max_length=16, default='local', verbose_name='来源(local/msync)')
ServiceName = models.CharField(max_length=128, default='', blank=True, verbose_name='提供方服务名')
ServiceDomain = models.CharField(max_length=256, default='', blank=True, verbose_name='提供方服务域名')
ServiceOnline = models.SmallIntegerField(default=1, verbose_name='提供方服务在线(1在线/0离线)')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'App'
app_label = 'gerp'
ordering = ['AppSort', 'CreateTime']
verbose_name = '应用/功能'
verbose_name_plural = '应用/功能'
def __str__(self):
return self.AppName
class TenantApp(QModel):
"""企业安装/授权——租户与应用的关系
InstallStatus: 1=已安装(启用) / 0=已停用
AuthStatus: 0=未授权 / 1=已授权 / 2=已拒绝(AuthRequired 应用的企业级开通状态)
"""
TenantAppUUID = UUIDField(primary_key=True, verbose_name='企业安装记录UUID')
TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID')
AppUUID = UUIDField(db_column='AppUUID', verbose_name='应用UUID')
InstallStatus = models.SmallIntegerField(default=1, verbose_name='安装状态(1启用/0停用)')
AuthStatus = models.SmallIntegerField(default=0, verbose_name='企业授权状态(0/1/2)')
InstallTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='安装时间')
AuthBy = UUIDField(null=True, blank=True, verbose_name='企业授权人UUID')
AuthTime = models.DateTimeField(null=True, blank=True, verbose_name='企业授权时间')
class Meta:
db_table = 'TenantApp'
app_label = 'gerp'
unique_together = (('TenantUUID', 'AppUUID'),)
verbose_name = '企业应用安装'
verbose_name_plural = '企业应用安装'
class CompanyApp(QModel):
"""企业应用安装(公司维度)——租户可选定一个或多个公司安装
与 TenantApp(租户全量安装)互补:
- TenantApp :全租户安装(租户下所有公司生效)
- CompanyApp :指定公司安装(仅该公司生效)
企业功能可用判定:租户已装 或 当前公司已装。
InstallStatus: 1=已安装(启用) / 0=已停用
"""
CompanyAppUUID = UUIDField(primary_key=True, verbose_name='公司安装记录UUID')
TenantUUID = UUIDField(null=True, blank=True, verbose_name='租户UUID')
CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID')
AppUUID = UUIDField(db_column='AppUUID', verbose_name='应用UUID')
InstallStatus = models.SmallIntegerField(default=1, verbose_name='安装状态(1启用/0停用)')
InstallTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='安装时间')
InstallBy = UUIDField(null=True, blank=True, verbose_name='安装人UUID')
class Meta:
db_table = 'CompanyApp'
app_label = 'gerp'
unique_together = (('CompanyUUID', 'AppUUID'),)
verbose_name = '企业应用安装(公司维度)'
verbose_name_plural = '企业应用安装(公司维度)'
class UserApp(QModel):
"""个人应用启用/申请——用户与应用的关系
UseStatus: 1=已启用 / 0=已停用
ApplyStatus: 0=未申请 / 1=申请中 / 2=已授权 / 3=已拒绝(AuthRequired 应用的审批状态机)
"""
UserAppUUID = UUIDField(primary_key=True, verbose_name='个人应用记录UUID')
UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID')
AppUUID = UUIDField(db_column='AppUUID', verbose_name='应用UUID')
UseStatus = models.SmallIntegerField(default=1, verbose_name='使用状态(1启用/0停用)')
ApplyStatus = models.SmallIntegerField(default=0, verbose_name='授权申请状态(0/1/2/3)')
ApplyTime = models.DateTimeField(null=True, blank=True, verbose_name='申请时间')
ApproveBy = UUIDField(null=True, blank=True, verbose_name='审批人UUID')
ApproveTime = models.DateTimeField(null=True, blank=True, verbose_name='审批时间')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'UserApp'
app_label = 'gerp'
unique_together = (('UserUUID', 'AppUUID'),)
verbose_name = '个人应用'
verbose_name_plural = '个人应用'
class AppConfig(QModel):
"""应用配置键值表——按租户+公司隔离的应用运行参数
每个应用可声明若干配置键(ConfigKey),管理员(有安装权限者)可在
"配置"入口编辑配置值。
配置粒度:
- CompanyUUID 为空(NULL)→ 租户级默认配置(全租户共享)
- CompanyUUID 有值 → 公司级配置(覆盖租户默认)
读取回退链:公司级 → 租户默认 → 模板默认值。
键值约定(由应用自行定义与读取,如 [stalwart] 域名映射):
ConfigKey 配置键名(如 SMTP_HOST / DOMAIN)
ConfigValue 配置值(自由文本)
"""
AppConfigUUID = UUIDField(primary_key=True, verbose_name='应用配置UUID')
TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID')
CompanyUUID = UUIDField(null=True, blank=True, db_column='CompanyUUID', verbose_name='公司UUID(空=租户级默认配置)')
AppUUID = UUIDField(db_column='AppUUID', verbose_name='应用UUID')
ConfigKey = models.CharField(max_length=128, verbose_name='配置键名')
ConfigValue = models.TextField(default='', verbose_name='配置值')
UpdateBy = UUIDField(null=True, blank=True, verbose_name='更新人UUID')
UpdateTime = models.DateTimeField(null=True, blank=True, verbose_name='更新时间')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'AppConfig'
app_label = 'gerp'
unique_together = (('TenantUUID', 'CompanyUUID', 'AppUUID', 'ConfigKey'),)
verbose_name = '应用配置'
verbose_name_plural = '应用配置'
class AppPosition(QModel):
"""应用与职务绑定——只有绑定职务的用户才可见/使用该应用
机制:
- 无该应用的绑定记录(AppPosition 为空)→ 全员可见(兼容存量应用)
- 有绑定记录 → 用户职务集合(UserRoleDepartmentPosition)必须与绑定职务有交集
生效点:应用市场 / 我的应用 / 菜单注入 / API 硬校验(AppManager.ListMarket/ListMyApps/CheckAccess)。
"""
AppPositionUUID = UUIDField(primary_key=True, verbose_name='绑定UUID')
AppUUID = UUIDField(db_column='AppUUID', verbose_name='应用UUID')
PositionUUID = UUIDField(db_column='PositionUUID', verbose_name='职务UUID')
TenantUUID = UUIDField(null=True, blank=True, verbose_name='租户UUID')
CompanyUUID = UUIDField(null=True, blank=True, db_column='CompanyUUID', verbose_name='公司UUID')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'AppPosition'
app_label = 'gerp'
unique_together = (('AppUUID', 'PositionUUID'),)
verbose_name = '应用职务绑定'
verbose_name_plural = '应用职务绑定'
# (AppVirtualPosition / AppVirtualBinding 已下沉 gvsdsdk.models —— 应用虚拟职务统一由 SDK 提供)
# ==================== 从 gvsdsdk 导入重复模型 ====================
# 以下模型原在 gerp/models.py 定义,现统一依赖 gvsdsdk.models(映射 SAAS 主库)
# 注意:gvsdsdk 模型用 UUIDField(非 ForeignKey),不支持 select_related 和关联属性访问
from gvsdsdk.models import (
Role, Permission, UserRole, RolePermission,
UserABAC, Password, PositionTemplate, PositionTemplatePermission,
Position, PositionCategory, UserRoleDepartmentPosition,
AppVirtualPosition, AppVirtualBinding,
)
# ==================== GERP 子应用模型(新目录规约) ====================
# 子应用模型定义在 gvsds/apps/gerp/apps/<appenglishid>/models.py(app_label 仍为 'gerp',
# 迁移仍集中在 gerp/migrations/)。此处在模型加载阶段统一导入并把公开名字注入本模块命名空间:
# 1) 保证 Django 加载 gerp.models 时即完成子应用模型注册
# 2) 保持历史导入路径 `from gvsds.apps.gerp.models import X` 继续可用
#
# 约定:子应用模型若要引用本模块的平台模型,必须用绝对导入
# `from gvsds.apps.gerp.models import X`;子应用包不得在 import 时导入模型
# (否则会在 app 注册阶段触发 AppRegistryNotReady)。
from .apps import ImportSubAppModels as _ImportSubAppModels # noqa: E402
_ImportSubAppModels(namespace=globals())