· models/base.py:新增 purchase_link 列(text)并加入 to_dict(purchaseLink)
· field_permissions.py:读过滤映射 purchaseLink → material_list:purchaseLink
· inbound/base.py:POST 与 PUT 两处 field_to_perm 同时补入(漏掉任一处,
新增/修改就会各缺一半)
· base_service.py:create_material 写入、update_material 按字段存在与否更新
★ 写侧刻意**显式纳入映射**而非依赖「不在映射中→默认允许」的兜底分支 ——
那个分支正是上一轮附件备注「谁都能写」的成因,新字段不再走它。
验证(6 个角色)
SUPER_ADMIN / SUPERVISOR / WAREHOUSE_MGR / INBOUND → 读✓ 写✓
OUTBOUND / SALES → 读✗ 写✗
读写逐角色一致;超管走 material_list:* 通配分支、过滤整段跳过(已单独复核)。
148 lines
6.8 KiB
Python
148 lines
6.8 KiB
Python
# app/models/base.py
|
||
from app.extensions import db
|
||
from pgvector.sqlalchemy import Vector
|
||
import json
|
||
|
||
|
||
class MaterialBase(db.Model):
|
||
"""
|
||
基础信息表模型
|
||
对应数据库表: material_base
|
||
"""
|
||
__tablename__ = 'material_base'
|
||
|
||
# 1. 基础字段
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
company_name = db.Column(db.String(255), index=True, comment='所属公司') # ★ 多租户过滤高频列
|
||
|
||
name = db.Column(db.String(255), nullable=False, index=True, comment='名称') # ★ 模糊搜索/精确定位高频列
|
||
common_name = db.Column(db.String(255), comment='俗名')
|
||
category = db.Column(db.String(100), index=True, comment='类别') # ★ 分类统计/过滤高频列
|
||
material_type = db.Column(db.String(100), index=True, comment='类型') # ★ 类型分组/过滤高频列
|
||
spec_model = db.Column(db.String(255), index=True, comment='规格型号') # ★ 模糊搜索/精确匹配高频列
|
||
unit = db.Column(db.String(50), comment='计量单位')
|
||
|
||
# 可见等级
|
||
visibility_level = db.Column(db.Integer, default=0, comment='信息可见等级')
|
||
|
||
# 链接与图片 (现在存储 JSON 字符串)
|
||
manual_link = db.Column(db.Text, comment='通用说明书')
|
||
product_image = db.Column(db.Text, comment='通用产品图')
|
||
|
||
# 备注字段(纯文本,与文件 URL 分离存储)
|
||
product_image_remark = db.Column(db.Text, default='', comment='产品图备注')
|
||
manual_link_remark = db.Column(db.Text, default='', comment='说明书备注')
|
||
|
||
# 采购链接:补货时直接跳转的购买地址(淘宝 / 1688 等)。
|
||
# ★ 用 Text 而非 String(N):外链常带很长的查询串,截断后会得到打不开的地址。
|
||
purchase_link = db.Column(db.Text, default='', comment='采购链接')
|
||
|
||
# 启用状态
|
||
is_enabled = db.Column(db.Boolean, default=True, index=True, comment='是否启用') # ★ 状态过滤高频列
|
||
|
||
# 强制质检标记(采购入库时必须上传检测报告)
|
||
is_inspection_required = db.Column(db.Boolean, default=False, comment='是否强制要求质检')
|
||
|
||
# 出库/借库需审批标记:命中该物料的出/借申请仍需走审批;否则默认自动通过、直接交库管执行
|
||
is_approval_required = db.Column(db.Boolean, default=False, comment='出库/借库需审批')
|
||
|
||
# 参考价格
|
||
reference_price = db.Column(db.Numeric(10, 2), nullable=True, comment='参考价格')
|
||
|
||
# CLIP 视觉向量(用于以图搜图)
|
||
img_embedding = db.Column(Vector(512), nullable=True)
|
||
|
||
# ============================================================
|
||
# 关联关系区域
|
||
# ============================================================
|
||
|
||
# 1. 关联采购库存 (StockBuy)
|
||
stock_buys = db.relationship('StockBuy', back_populates='base', lazy='dynamic')
|
||
|
||
# 2. 关联半成品库存 (StockSemi)
|
||
stock_semis = db.relationship('StockSemi', back_populates='base', lazy='dynamic')
|
||
|
||
# 3. 关联成品库存 (StockProduct)
|
||
stock_products = db.relationship('StockProduct', back_populates='base', lazy='dynamic')
|
||
|
||
# 4. 关联服务库存 (StockService)
|
||
stock_services = db.relationship('StockService', back_populates='base', lazy='dynamic')
|
||
|
||
# 5. 关联预警设置 (MaterialWarningSetting)
|
||
warning_settings = db.relationship('MaterialWarningSetting', back_populates='material', lazy='dynamic', cascade='all, delete-orphan')
|
||
|
||
def to_dict(self):
|
||
"""
|
||
序列化方法
|
||
"""
|
||
|
||
# 辅助解析函数:将数据库存储的 JSON 字符串转为 List
|
||
def parse_list(json_str):
|
||
if not json_str:
|
||
return []
|
||
try:
|
||
# 兼容旧数据:如果不是 JSON 格式(比如是单个 URL),则包装成 list
|
||
if not json_str.startswith('['):
|
||
return [json_str]
|
||
return json.loads(json_str)
|
||
except:
|
||
return []
|
||
|
||
return {
|
||
'id': self.id,
|
||
'companyName': self.company_name,
|
||
'name': self.name,
|
||
'commonName': self.common_name,
|
||
'category': self.category,
|
||
'type': self.material_type,
|
||
'spec': self.spec_model,
|
||
'unit': self.unit,
|
||
'visibilityLevel': self.visibility_level,
|
||
'generalManual': parse_list(self.manual_link),
|
||
'generalImage': parse_list(self.product_image),
|
||
'productImageRemark': self.product_image_remark or '',
|
||
'manualLinkRemark': self.manual_link_remark or '',
|
||
'purchaseLink': self.purchase_link or '',
|
||
# 【核心修改】:直接返回布尔值,不再转成 1 或 0
|
||
'isEnabled': bool(self.is_enabled),
|
||
# 强制质检标记
|
||
'isInspectionRequired': bool(self.is_inspection_required),
|
||
# 出库/借库需审批标记
|
||
'isApprovalRequired': bool(self.is_approval_required),
|
||
# 参考价格
|
||
'referencePrice': float(self.reference_price) if self.reference_price is not None else None,
|
||
}
|
||
|
||
|
||
class MaterialWarningSetting(db.Model):
|
||
"""
|
||
物料预警设置表模型
|
||
对应数据库表: material_warning_settings
|
||
"""
|
||
__tablename__ = 'material_warning_settings'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
base_id = db.Column(db.Integer, db.ForeignKey('material_base.id'), nullable=False, comment='物料基础信息ID')
|
||
is_enabled = db.Column(db.Boolean, default=False, comment='是否启用预警')
|
||
yellow_threshold = db.Column(db.Numeric(10, 2), nullable=True, comment='黄色预警阈值')
|
||
red_threshold = db.Column(db.Numeric(10, 2), nullable=True, comment='红色预警阈值')
|
||
yellow_emails = db.Column(db.String(500), nullable=True, comment='黄色预警通知邮箱')
|
||
red_emails = db.Column(db.String(500), nullable=True, comment='红色预警通知邮箱')
|
||
is_ordered = db.Column(db.Boolean, default=False, comment='是否已处理采购')
|
||
last_notified_at = db.Column(db.DateTime, nullable=True, comment='上次邮件通知时间')
|
||
|
||
# 关联关系
|
||
material = db.relationship('MaterialBase', back_populates='warning_settings')
|
||
|
||
def to_dict(self):
|
||
return {
|
||
'id': self.id,
|
||
'baseId': self.base_id,
|
||
'isEnabled': bool(self.is_enabled),
|
||
'yellowThreshold': float(self.yellow_threshold) if self.yellow_threshold is not None else None,
|
||
'redThreshold': float(self.red_threshold) if self.red_threshold is not None else None,
|
||
'yellowEmails': self.yellow_emails or '',
|
||
'redEmails': self.red_emails or '',
|
||
'isOrdered': bool(self.is_ordered),
|
||
'lastNotifiedAt': self.last_notified_at.strftime('%Y-%m-%d %H:%M:%S') if self.last_notified_at else None
|
||
} |