feat: implement dynamic inspection requirement logic based on material master data
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
# 文件路径: app/api/v1/inbound/base.py
|
||||
|
||||
from flask import Blueprint, request, jsonify, send_file, g
|
||||
from flask import Blueprint, request, jsonify, send_file, g, current_app
|
||||
from app.extensions import db
|
||||
from app.services.inbound.base_service import MaterialBaseService
|
||||
from app.utils.decorators import login_required, permission_required, audit_log
|
||||
@ -410,3 +410,49 @@ def batch_set_warning():
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f"批量设置预警失败: {str(e)}")
|
||||
return jsonify({"code": 500, "msg": f"批量设置预警失败: {str(e)}"}), 500
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 2.6 批量设置强制质检 API (POST /api/v1/inbound/base/batch-inspection)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/batch-inspection', methods=['POST'])
|
||||
@permission_required('material_list:operation')
|
||||
def batch_set_inspection():
|
||||
"""
|
||||
批量设置物料强制质检标记
|
||||
请求体格式: {
|
||||
"ids": [1, 2, 3],
|
||||
"isInspectionRequired": true
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"code": 400, "msg": "No data provided"}), 400
|
||||
|
||||
ids = data.get('ids', [])
|
||||
is_inspection_required = bool(data.get('isInspectionRequired', False))
|
||||
|
||||
if not ids:
|
||||
return jsonify({"code": 400, "msg": "请选择要设置的物料"}), 400
|
||||
|
||||
updated_count = 0
|
||||
for base_id in ids:
|
||||
material = MaterialBase.query.get(base_id)
|
||||
if material:
|
||||
material.is_inspection_required = is_inspection_required
|
||||
updated_count += 1
|
||||
|
||||
db.session.commit()
|
||||
return jsonify({
|
||||
"code": 200,
|
||||
"msg": f"批量设置成功,已更新 {updated_count} 条记录",
|
||||
"data": {
|
||||
"updated": updated_count
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f"批量设置强制质检失败: {str(e)}")
|
||||
return jsonify({"code": 500, "msg": f"批量设置强制质检失败: {str(e)}"}), 500
|
||||
|
||||
@ -31,6 +31,9 @@ class MaterialBase(db.Model):
|
||||
# 启用状态
|
||||
is_enabled = db.Column(db.Boolean, default=True, comment='是否启用')
|
||||
|
||||
# 强制质检标记(采购入库时必须上传检测报告)
|
||||
is_inspection_required = db.Column(db.Boolean, default=False, comment='是否强制要求质检')
|
||||
|
||||
# ============================================================
|
||||
# 关联关系区域
|
||||
# ============================================================
|
||||
@ -81,6 +84,8 @@ class MaterialBase(db.Model):
|
||||
'generalImage': parse_list(self.product_image),
|
||||
# 【核心修改】:直接返回布尔值,不再转成 1 或 0
|
||||
'isEnabled': bool(self.is_enabled),
|
||||
# 强制质检标记
|
||||
'isInspectionRequired': bool(self.is_inspection_required),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -81,6 +81,8 @@ class StockBuy(db.Model):
|
||||
'category': self.base.category if self.base else '',
|
||||
'unit': self.base.unit if self.base else '',
|
||||
'material_type': self.base.material_type if self.base else '',
|
||||
# 强制质检标记
|
||||
'isInspectionRequired': bool(self.base.is_inspection_required) if self.base else False,
|
||||
|
||||
'sku': self.sku,
|
||||
'inbound_date': self.in_date.strftime('%Y-%m-%d') if self.in_date else '',
|
||||
|
||||
@ -75,7 +75,9 @@ class MaterialBaseService:
|
||||
'category': item.category,
|
||||
'unit': item.unit,
|
||||
'type': item.material_type,
|
||||
'status': '启用'
|
||||
'status': '启用',
|
||||
# 强制质检标记
|
||||
'isInspectionRequired': bool(item.is_inspection_required)
|
||||
})
|
||||
return results
|
||||
except Exception as e:
|
||||
|
||||
@ -94,6 +94,22 @@ class BuyInboundService:
|
||||
if not material: raise ValueError("所选物料不存在")
|
||||
if not material.is_enabled: raise ValueError(f"物料【{material.name}】已停用")
|
||||
|
||||
# ============================================================
|
||||
# 强制质检校验:如果物料标记为强制质检,则必须提供到检状态和检测报告
|
||||
# ============================================================
|
||||
if material.is_inspection_required:
|
||||
inspection_status = data.get('inspection_status')
|
||||
if not inspection_status:
|
||||
raise ValueError(f"物料【{material.name}】为强管控物料,必须选择到检状态")
|
||||
|
||||
# 检查检测报告:文件列表或外部链接至少有一个
|
||||
# 前端会将外部链接添加到 inspection_report 数组中一起提交
|
||||
inspection_report_list = data.get('inspection_report', [])
|
||||
has_report_file = inspection_report_list and len(inspection_report_list) > 0
|
||||
|
||||
if not has_report_file:
|
||||
raise ValueError(f"物料【{material.name}】为强管控物料,必须提供检测报告文件或外部链接")
|
||||
|
||||
BuyInboundService._check_unique(
|
||||
base_id=base_id,
|
||||
serial_number=data.get('serial_number'),
|
||||
@ -171,7 +187,36 @@ class BuyInboundService:
|
||||
try:
|
||||
stock = StockBuy.query.get(stock_id)
|
||||
if not stock: raise ValueError("记录不存在")
|
||||
BuyInboundService._check_unique(base_id=data.get('base_id', stock.base_id),
|
||||
|
||||
# 获取物料信息用于校验
|
||||
base_id = data.get('base_id', stock.base_id)
|
||||
material = MaterialBase.query.get(base_id)
|
||||
|
||||
# ============================================================
|
||||
# 强制质检校验:如果物料标记为强制质检,则必须提供到检状态和检测报告
|
||||
# ============================================================
|
||||
if material and material.is_inspection_required:
|
||||
inspection_status = data.get('inspection_status', stock.inspection_status)
|
||||
if not inspection_status:
|
||||
raise ValueError(f"物料【{material.name}】为强管控物料,必须选择到检状态")
|
||||
|
||||
# 检查检测报告:文件列表至少有一个
|
||||
inspection_report_list = data.get('inspection_report')
|
||||
if inspection_report_list is None:
|
||||
# 如果没有传入,使用现有的
|
||||
import json as json_module
|
||||
try:
|
||||
existing_reports = json_module.loads(stock.inspection_report) if stock.inspection_report else []
|
||||
except:
|
||||
existing_reports = []
|
||||
has_report_file = existing_reports and len(existing_reports) > 0
|
||||
else:
|
||||
has_report_file = inspection_report_list and len(inspection_report_list) > 0
|
||||
|
||||
if not has_report_file:
|
||||
raise ValueError(f"物料【{material.name}】为强管控物料,必须提供检测报告文件或外部链接")
|
||||
|
||||
BuyInboundService._check_unique(base_id=base_id,
|
||||
serial_number=data.get('serial_number', stock.serial_number),
|
||||
batch_number=data.get('batch_number', stock.batch_number),
|
||||
exclude_id=stock_id)
|
||||
|
||||
@ -61,3 +61,12 @@ export function batchSetWarning(data: any[]) {
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 6. 批量设置强制质检
|
||||
export function batchSetInspection(data: { ids: number[], isInspectionRequired: boolean }) {
|
||||
return request({
|
||||
url: '/inbound/base/batch-inspection',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
@ -146,6 +146,17 @@
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 批量质检设置按钮 (需要 operation 权限) -->
|
||||
<el-button
|
||||
v-if="userStore.hasPermission('material_list:operation')"
|
||||
type="danger"
|
||||
plain
|
||||
@click="openBatchInspectionDialog"
|
||||
style="margin-right: 10px"
|
||||
>
|
||||
<el-icon style="margin-right: 5px"><CircleCheck /></el-icon>批量质检设置
|
||||
</el-button>
|
||||
|
||||
<el-button v-if="userStore.hasPermission('material_list:operation')" type="primary" @click="handleAdd" style="margin-right: 10px">
|
||||
<el-icon style="margin-right: 5px"><Plus /></el-icon>新增
|
||||
</el-button>
|
||||
@ -295,6 +306,13 @@
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="columns.isInspectionRequired.visible" prop="isInspectionRequired" label="强制质检" min-width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.isInspectionRequired ? 'danger' : 'info'" size="small">
|
||||
{{ scope.row.isInspectionRequired ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="userStore.hasPermission('material_list:operation')" label="操作" min-width="200" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button v-if="userStore.hasPermission('material_list:operation')" link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
@ -526,13 +544,41 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 批量质检设置弹窗 -->
|
||||
<el-dialog v-model="inspectionDialog.visible" title="批量质检设置" width="500px" append-to-body destroy-on-close>
|
||||
<el-alert
|
||||
:title="`已选择 ${inspectionDialog.selectedCount} 条物料进行批量质检设置`"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px"
|
||||
/>
|
||||
<el-form label-width="180px">
|
||||
<el-form-item label="是否强制要求入库上传检测报告">
|
||||
<el-switch
|
||||
v-model="inspectionForm.isInspectionRequired"
|
||||
active-text="是"
|
||||
inactive-text="否"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div style="color: #909399; font-size: 12px; margin-top: -10px;">
|
||||
开启后,这些物料在采购入库时必须上传检测报告或外部链接
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="inspectionDialog.visible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitBatchInspection" :loading="inspectionLoading">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, nextTick, computed } from 'vue';
|
||||
import { Plus, Document, Refresh, Setting, Rank, Camera, Link, Download, Bell } from '@element-plus/icons-vue';
|
||||
import { Plus, Document, Refresh, Setting, Rank, Camera, Link, Download, Bell, CircleCheck } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
@ -544,7 +590,8 @@ import {
|
||||
delMaterialBase,
|
||||
getMaterialBaseOptions,
|
||||
exportAssetStatistics,
|
||||
batchSetWarning
|
||||
batchSetWarning,
|
||||
batchSetInspection
|
||||
} from '@/api/material_base';
|
||||
import { uploadFile, deleteFile } from '@/api/common/upload';
|
||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue';
|
||||
@ -691,6 +738,17 @@ const warningRules = {
|
||||
]
|
||||
};
|
||||
|
||||
// 批量质检设置相关
|
||||
const inspectionDialog = reactive({
|
||||
visible: false,
|
||||
selectedCount: 0,
|
||||
selectedIds: [] as number[]
|
||||
});
|
||||
const inspectionLoading = ref(false);
|
||||
const inspectionForm = reactive({
|
||||
isInspectionRequired: false
|
||||
});
|
||||
|
||||
const columns = reactive({
|
||||
id: { visible: false },
|
||||
companyName: { visible: true },
|
||||
@ -703,7 +761,8 @@ const columns = reactive({
|
||||
inventory: { visible: true },
|
||||
available: { visible: true },
|
||||
files: { visible: true },
|
||||
isEnabled: { visible: true }
|
||||
isEnabled: { visible: true },
|
||||
isInspectionRequired: { visible: true }
|
||||
});
|
||||
|
||||
// 列与权限Code的映射关系(数据库中的code)
|
||||
@ -719,7 +778,8 @@ const permissionMap: Record<string, string> = {
|
||||
inventory: 'material_list:inventoryCount',
|
||||
available: 'material_list:availableCount',
|
||||
files: 'material_list:files',
|
||||
isEnabled: 'material_list:isEnabled'
|
||||
isEnabled: 'material_list:isEnabled',
|
||||
isInspectionRequired: 'material_list:operation'
|
||||
};
|
||||
|
||||
// 根据用户权限初始化列显示状态
|
||||
@ -1247,6 +1307,47 @@ const submitWarning = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 打开批量质检设置对话框
|
||||
const openBatchInspectionDialog = () => {
|
||||
// 获取当前勾选的物料
|
||||
const selected = tableRef.value?.getSelectionRows() || [];
|
||||
if (selected.length === 0) {
|
||||
ElMessage.warning('请先勾选需要设置质检的物料');
|
||||
return;
|
||||
}
|
||||
|
||||
inspectionDialog.selectedIds = selected.map((row: MaterialBaseVO) => row.id);
|
||||
inspectionDialog.selectedCount = selected.length;
|
||||
inspectionForm.isInspectionRequired = false; // 默认重置为否
|
||||
inspectionDialog.visible = true;
|
||||
};
|
||||
|
||||
// 提交批量质检设置
|
||||
const submitBatchInspection = async () => {
|
||||
if (inspectionDialog.selectedIds.length === 0) {
|
||||
ElMessage.warning('请先勾选物料');
|
||||
return;
|
||||
}
|
||||
|
||||
inspectionLoading.value = true;
|
||||
try {
|
||||
await batchSetInspection({
|
||||
ids: inspectionDialog.selectedIds,
|
||||
isInspectionRequired: inspectionForm.isInspectionRequired
|
||||
});
|
||||
|
||||
ElMessage.success('批量质检设置成功');
|
||||
inspectionDialog.visible = false;
|
||||
selectedItems.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
getList();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.msg || '设置失败');
|
||||
} finally {
|
||||
inspectionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 表格行样式(根据预警状态)
|
||||
const tableRowClassName = ({ row }: { row: MaterialBaseVO }) => {
|
||||
// 只有拥有 view_warning 权限且有预警状态时才显示特殊样式
|
||||
|
||||
@ -821,6 +821,10 @@ const printCopies = ref(1)
|
||||
|
||||
const entryMode = ref('batch')
|
||||
const modeLocked = ref(false)
|
||||
|
||||
// 强制质检标记
|
||||
const isCurrentMaterialInspectionRequired = ref(false)
|
||||
|
||||
const dialogImageUrl = ref('')
|
||||
const dialogVisibleImage = ref(false)
|
||||
const arrivalFileList = ref<any[]>([])
|
||||
@ -1136,10 +1140,23 @@ const onMaterialSelected = (val: number) => {
|
||||
form.category = item.category
|
||||
form.unit = item.unit
|
||||
form.material_type = item.type
|
||||
// 保存强制质检标记
|
||||
isCurrentMaterialInspectionRequired.value = item.isInspectionRequired || false
|
||||
// 更新表单校验规则
|
||||
updateInspectionRules()
|
||||
checkHistoryAndSetMode(item.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 动态更新质检相关校验规则
|
||||
const updateInspectionRules = () => {
|
||||
if (formRef.value) {
|
||||
// 清除旧的校验结果
|
||||
formRef.value.clearValidate('inspection_status')
|
||||
formRef.value.clearValidate('inspection_report')
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// 校验规则
|
||||
// ------------------------------------
|
||||
@ -1159,12 +1176,39 @@ const validateIdentity = (rule: any, value: any, callback: any) => {
|
||||
else if (entryMode.value === 'batch' && !form.batch_number && rule.field === 'batch_number') callback(new Error('批号必填'))
|
||||
else callback()
|
||||
}
|
||||
const rules = {
|
||||
base_id: [{required: true, message: '请选择物料', trigger: 'change'}],
|
||||
in_quantity: [{required: true, message: '请输入数量', trigger: 'blur'}],
|
||||
serial_number: [{validator: validateIdentity, trigger: 'blur'}, {validator: validateUnique, trigger: 'blur'}],
|
||||
batch_number: [{validator: validateIdentity, trigger: 'blur'}, {validator: validateUnique, trigger: 'blur'}]
|
||||
}
|
||||
const rules = computed(() => {
|
||||
const baseRules = {
|
||||
base_id: [{required: true, message: '请选择物料', trigger: 'change'}],
|
||||
in_quantity: [{required: true, message: '请输入数量', trigger: 'blur'}],
|
||||
serial_number: [{validator: validateIdentity, trigger: 'blur'}, {validator: validateUnique, trigger: 'blur'}],
|
||||
batch_number: [{validator: validateIdentity, trigger: 'blur'}, {validator: validateUnique, trigger: 'blur'}]
|
||||
}
|
||||
|
||||
// 如果当前物料需要强制质检,添加质检相关校验
|
||||
if (isCurrentMaterialInspectionRequired.value) {
|
||||
// 到检状态必填
|
||||
baseRules.inspection_status = [
|
||||
{ required: true, message: '该物料为强管控物料,必须选择到检状态', trigger: 'change' }
|
||||
]
|
||||
// 检测报告必填(通过自定义校验规则:文件或外部链接至少有一个)
|
||||
baseRules.inspection_report = [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
const hasFile = form.inspection_report && form.inspection_report.length > 0
|
||||
const hasLink = inspection_report_url.value && inspection_report_url.value.trim() !== ''
|
||||
if (!hasFile && !hasLink) {
|
||||
callback(new Error('该物料为强管控物料,必须提供检测报告文件或链接'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return baseRules
|
||||
})
|
||||
|
||||
const checkHistoryAndSetMode = async (baseId: number) => {
|
||||
try {
|
||||
@ -1366,7 +1410,10 @@ const handleUpdate = (row: any) => {
|
||||
inspection_report_url.value = reportLinks.length > 0 ? reportLinks[0] : ''
|
||||
if (row.serial_number) { entryMode.value = 'serial'; form.serial_number = row.serial_number; form.batch_number = '' }
|
||||
else { entryMode.value = 'batch'; form.batch_number = row.batch_number; form.serial_number = '' }
|
||||
materialOptions.value = [{ id: row.base_id, name: row.material_name, spec: row.spec_model, category: row.category, company_name: row.company_name }]
|
||||
materialOptions.value = [{ id: row.base_id, name: row.material_name, spec: row.spec_model, category: row.category, company_name: row.company_name, isInspectionRequired: row.isInspectionRequired }]
|
||||
// 设置强制质检标记
|
||||
isCurrentMaterialInspectionRequired.value = row.isInspectionRequired || false
|
||||
updateInspectionRules()
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
@ -1598,6 +1645,8 @@ const confirmPrint = async () => {
|
||||
const resetForm = () => {
|
||||
materialOptions.value = []; arrivalFileList.value = []; reportFileList.value = []; inspection_report_url.value = ''
|
||||
searchPage.value = 1; hasNextPage.value = true; searchKeyword.value = '';
|
||||
// 重置强制质检标记
|
||||
isCurrentMaterialInspectionRequired.value = false
|
||||
Object.assign(form, {
|
||||
id: undefined, base_id: undefined,
|
||||
company_name: '',
|
||||
|
||||
Reference in New Issue
Block a user