将半成品和成品跟bom表进行相关联
This commit is contained in:
@ -25,6 +25,26 @@ def search_base():
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 0.5 [新增] BOM 搜索接口
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_product_bp.route('/search-bom', methods=['GET'])
|
||||
def search_bom():
|
||||
"""
|
||||
供前端下拉框远程搜索使用 (搜索BOM)
|
||||
"""
|
||||
try:
|
||||
keyword = request.args.get('keyword', '')
|
||||
data = ProductInboundService.search_bom_options(keyword)
|
||||
return jsonify({
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": data
|
||||
})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 1. 获取列表 (支持 status 多选筛选)
|
||||
|
||||
@ -29,6 +29,27 @@ def search_base():
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 0.5 [新增] BOM 搜索接口
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_semi_bp.route('/search-bom', methods=['GET'])
|
||||
def search_bom():
|
||||
"""
|
||||
供前端下拉框远程搜索使用 (搜索BOM)
|
||||
Query Param: keyword (编号或父件规格)
|
||||
"""
|
||||
try:
|
||||
keyword = request.args.get('keyword', '')
|
||||
data = SemiInboundService.search_bom_options(keyword)
|
||||
return jsonify({
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": data
|
||||
})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 1. 获取半成品列表
|
||||
|
||||
@ -11,26 +11,17 @@ import json
|
||||
class ProductInboundService:
|
||||
|
||||
# ============================================================
|
||||
# 0. 辅助:唯一性校验 (新增核心逻辑)
|
||||
# 0. 辅助:唯一性校验
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def _check_unique(serial_number, exclude_id=None):
|
||||
"""
|
||||
校验成品的唯一性
|
||||
:param serial_number: 序列号
|
||||
:param exclude_id: 排除的ID (编辑模式用)
|
||||
"""
|
||||
from app.models.inbound.product import StockProduct
|
||||
|
||||
# 成品强校验序列号 (SN) - SN应该是全局唯一的
|
||||
if serial_number:
|
||||
query = StockProduct.query.filter(StockProduct.serial_number == serial_number)
|
||||
if exclude_id:
|
||||
query = query.filter(StockProduct.id != exclude_id)
|
||||
|
||||
exists = query.first()
|
||||
if exists:
|
||||
# [修改] material -> base
|
||||
occupied_name = exists.base.name if (hasattr(exists, 'base') and exists.base) else "未知物料"
|
||||
raise ValueError(f"序列号【{serial_number}】已存在!被成品 [{occupied_name}] 占用,请核查。")
|
||||
|
||||
@ -40,10 +31,7 @@ class ProductInboundService:
|
||||
@staticmethod
|
||||
def search_base_material(keyword):
|
||||
try:
|
||||
# [核心修改] 只查询已启用的物料
|
||||
query = MaterialBase.query.filter(MaterialBase.is_enabled == True)
|
||||
|
||||
# 2. 动态条件:如果传入了关键词,则增加模糊匹配条件
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
or_(
|
||||
@ -51,11 +39,7 @@ class ProductInboundService:
|
||||
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||
)
|
||||
)
|
||||
|
||||
# 3. 排序与限制:按ID倒序,取最新20条
|
||||
query = query.order_by(MaterialBase.id.desc()).limit(20)
|
||||
|
||||
# 4. 结果封装
|
||||
results = []
|
||||
for item in query.all():
|
||||
results.append({
|
||||
@ -73,33 +57,69 @@ class ProductInboundService:
|
||||
return []
|
||||
|
||||
# ============================================================
|
||||
# 2. 新增入库逻辑 (强制北京时间 + 唯一性校验)
|
||||
# 1.5 [新增] BOM 搜索逻辑
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def search_bom_options(keyword):
|
||||
from app.models.bom import BomTable
|
||||
try:
|
||||
# 关联查询:BOM表 + 父件基础信息表
|
||||
query = db.session.query(
|
||||
BomTable.bom_no,
|
||||
BomTable.version,
|
||||
MaterialBase.name.label('parent_name'),
|
||||
MaterialBase.spec_model.label('parent_spec')
|
||||
).join(MaterialBase, BomTable.parent_id == MaterialBase.id)
|
||||
|
||||
# 只查询启用的BOM
|
||||
if hasattr(BomTable, 'is_enabled'):
|
||||
query = query.filter(BomTable.is_enabled == True)
|
||||
|
||||
if keyword:
|
||||
kw = f'%{keyword}%'
|
||||
# 支持搜索:BOM编号、父件名称、父件规格
|
||||
query = query.filter(
|
||||
or_(
|
||||
BomTable.bom_no.ilike(kw),
|
||||
MaterialBase.name.ilike(kw),
|
||||
MaterialBase.spec_model.ilike(kw)
|
||||
)
|
||||
)
|
||||
|
||||
# 去重并限制数量
|
||||
results = query.distinct().limit(20).all()
|
||||
|
||||
return [{
|
||||
'bom_no': r.bom_no,
|
||||
'version': r.version,
|
||||
'parent_name': r.parent_name,
|
||||
'parent_spec': r.parent_spec or ''
|
||||
} for r in results]
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
# ============================================================
|
||||
# 2. 新增入库逻辑
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def handle_inbound(data):
|
||||
from app.models.inbound.product import StockProduct
|
||||
|
||||
try:
|
||||
base_id = data.get('base_id')
|
||||
if not base_id: raise ValueError("必须选择基础物料")
|
||||
material = MaterialBase.query.get(base_id)
|
||||
if not material: raise ValueError("物料不存在")
|
||||
|
||||
# [核心修改] 后端二次校验:如果物料已停用,禁止入库
|
||||
if not material.is_enabled:
|
||||
raise ValueError(f"物料【{material.name}】已停用,无法办理新入库。")
|
||||
|
||||
# --- [核心修改] 执行唯一性校验 ---
|
||||
ProductInboundService._check_unique(
|
||||
serial_number=data.get('serial_number')
|
||||
)
|
||||
|
||||
# [核心修改] 强制北京时间
|
||||
beijing_tz = timezone(timedelta(hours=8))
|
||||
current_time = datetime.now(beijing_tz).replace(tzinfo=None)
|
||||
|
||||
in_date_val = current_time
|
||||
|
||||
if data.get('in_date'):
|
||||
try:
|
||||
date_str = str(data['in_date'])
|
||||
@ -113,12 +133,10 @@ class ProductInboundService:
|
||||
in_date_val = current_time
|
||||
|
||||
in_qty = float(data.get('in_quantity') or 0)
|
||||
|
||||
p_start = data.get('production_start_time', '')
|
||||
p_end = data.get('production_end_time', '')
|
||||
time_range = f"{p_start} ~ {p_end}" if p_start or p_end else None
|
||||
|
||||
# 全局流水号
|
||||
try:
|
||||
seq_sql = text("SELECT nextval('global_print_seq')")
|
||||
result = db.session.execute(seq_sql)
|
||||
@ -132,7 +150,6 @@ class ProductInboundService:
|
||||
photo_list = data.get('product_photo', [])
|
||||
quality_list = data.get('quality_report_link', [])
|
||||
inspection_list = data.get('inspection_report_link', [])
|
||||
|
||||
if not isinstance(photo_list, list): photo_list = []
|
||||
if not isinstance(quality_list, list): quality_list = []
|
||||
if not isinstance(inspection_list, list): inspection_list = []
|
||||
@ -141,39 +158,30 @@ class ProductInboundService:
|
||||
base_id=material.id,
|
||||
global_print_id=next_global_id,
|
||||
sku=generated_sku,
|
||||
production_date=in_date_val, # 存入 DateTime
|
||||
production_date=in_date_val,
|
||||
barcode=final_barcode,
|
||||
serial_number=data.get('serial_number'),
|
||||
|
||||
status=data.get('status', '在库'),
|
||||
warehouse_location=data.get('warehouse_location'),
|
||||
|
||||
in_quantity=in_qty,
|
||||
stock_quantity=in_qty,
|
||||
available_quantity=in_qty,
|
||||
|
||||
bom_code=data.get('bom_code'),
|
||||
bom_version=data.get('bom_version'),
|
||||
work_order_code=data.get('work_order_code'),
|
||||
production_manager=data.get('production_manager'),
|
||||
production_time_range=time_range,
|
||||
|
||||
raw_material_cost=float(data.get('raw_material_cost') or 0),
|
||||
manual_cost=float(data.get('manual_cost') or 0),
|
||||
|
||||
quality_status=data.get('quality_status', '合格'),
|
||||
|
||||
product_photo=json.dumps(photo_list),
|
||||
quality_report_link=json.dumps(quality_list),
|
||||
inspection_report_link=json.dumps(inspection_list),
|
||||
|
||||
detail_link=data.get('detail_link'),
|
||||
remark=data.get('remark'),
|
||||
|
||||
sale_price=float(data.get('sale_price') or 0),
|
||||
order_id=data.get('order_id')
|
||||
)
|
||||
|
||||
db.session.add(new_stock)
|
||||
db.session.commit()
|
||||
return new_stock
|
||||
@ -187,12 +195,10 @@ class ProductInboundService:
|
||||
@staticmethod
|
||||
def update_inbound(stock_id, data):
|
||||
from app.models.inbound.product import StockProduct
|
||||
|
||||
try:
|
||||
stock = StockProduct.query.get(stock_id)
|
||||
if not stock: raise ValueError("记录不存在")
|
||||
|
||||
# --- [核心修改] 编辑时也要校验唯一性 ---
|
||||
if 'serial_number' in data:
|
||||
ProductInboundService._check_unique(
|
||||
serial_number=data['serial_number'],
|
||||
@ -211,11 +217,9 @@ class ProductInboundService:
|
||||
if 'product_photo' in data:
|
||||
imgs = data['product_photo']
|
||||
if isinstance(imgs, list): stock.product_photo = json.dumps(imgs)
|
||||
|
||||
if 'quality_report_link' in data:
|
||||
imgs = data['quality_report_link']
|
||||
if isinstance(imgs, list): stock.quality_report_link = json.dumps(imgs)
|
||||
|
||||
if 'inspection_report_link' in data:
|
||||
imgs = data['inspection_report_link']
|
||||
if isinstance(imgs, list): stock.inspection_report_link = json.dumps(imgs)
|
||||
@ -267,7 +271,6 @@ class ProductInboundService:
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def get_outbound_history(stock_id):
|
||||
"""获取出库历史"""
|
||||
try:
|
||||
records = TransOutbound.query.filter_by(
|
||||
source_table='stock_product', stock_id=stock_id
|
||||
@ -284,7 +287,6 @@ class ProductInboundService:
|
||||
from app.models.inbound.product import StockProduct
|
||||
try:
|
||||
query = db.session.query(StockProduct).outerjoin(MaterialBase, StockProduct.base_id == MaterialBase.id)
|
||||
|
||||
if keyword:
|
||||
query = query.filter(or_(
|
||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||
@ -294,17 +296,12 @@ class ProductInboundService:
|
||||
StockProduct.order_id.ilike(f'%{keyword}%'),
|
||||
StockProduct.sku.ilike(f'%{keyword}%')
|
||||
))
|
||||
|
||||
# 类别筛选
|
||||
if category and category.strip():
|
||||
query = query.filter(MaterialBase.category == category.strip())
|
||||
# 类型筛选
|
||||
if material_type and material_type.strip():
|
||||
query = query.filter(MaterialBase.material_type == material_type.strip())
|
||||
|
||||
if not statuses:
|
||||
statuses = ['在库', '借库']
|
||||
|
||||
if '已出库' in statuses:
|
||||
query = query.filter(StockProduct.status.in_(statuses))
|
||||
else:
|
||||
@ -314,11 +311,8 @@ class ProductInboundService:
|
||||
StockProduct.stock_quantity > 0
|
||||
)
|
||||
)
|
||||
|
||||
# 按照 production_date (入库日期) 倒序排序
|
||||
pagination = query.order_by(StockProduct.production_date.desc()).paginate(page=page, per_page=limit,
|
||||
error_out=False)
|
||||
|
||||
current_items = pagination.items
|
||||
|
||||
def parse_img(json_str):
|
||||
@ -330,10 +324,7 @@ class ProductInboundService:
|
||||
|
||||
items = []
|
||||
for item in current_items:
|
||||
# [注意] 因为Model层已经修改了 to_dict 内部的 material -> base,所以这里直接调 to_dict 即可
|
||||
d = item.to_dict()
|
||||
|
||||
# 格式化日期
|
||||
date_display = ''
|
||||
if item.production_date:
|
||||
try:
|
||||
@ -341,30 +332,22 @@ class ProductInboundService:
|
||||
except:
|
||||
date_display = str(item.production_date)[:10]
|
||||
d['inbound_date'] = date_display
|
||||
|
||||
d['qty_stock'] = float(item.stock_quantity or 0)
|
||||
d['qty_available'] = float(item.available_quantity or 0)
|
||||
d['sum_stock'] = d['qty_stock']
|
||||
d['sum_available'] = d['qty_available']
|
||||
|
||||
d['product_photo'] = parse_img(item.product_photo)
|
||||
d['quality_report_link'] = parse_img(item.quality_report_link)
|
||||
d['inspection_report_link'] = parse_img(item.inspection_report_link)
|
||||
d['global_print_id'] = item.global_print_id
|
||||
|
||||
items.append(d)
|
||||
|
||||
return {"total": pagination.total, "items": items}
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return {"total": 0, "items": []}
|
||||
|
||||
# ============================================================
|
||||
# 7. 系统用户搜索
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def search_system_users(keyword):
|
||||
"""搜索系统用户(活跃状态)"""
|
||||
from app.models.system import SysUser
|
||||
try:
|
||||
query = SysUser.query.filter(SysUser.status == 'active')
|
||||
@ -385,9 +368,6 @@ class ProductInboundService:
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
# ============================================================
|
||||
# 8. 获取筛选选项(类别、类型)
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def get_filter_options():
|
||||
try:
|
||||
|
||||
@ -11,32 +11,20 @@ import json
|
||||
class SemiInboundService:
|
||||
|
||||
# ============================================================
|
||||
# 0. 辅助:唯一性校验 (新增核心逻辑)
|
||||
# 0. 辅助:唯一性校验
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def _check_unique(base_id, serial_number, batch_number, exclude_id=None):
|
||||
"""
|
||||
校验半成品的唯一性
|
||||
:param base_id: 基础物料ID
|
||||
:param serial_number: 序列号
|
||||
:param batch_number: 批号
|
||||
:param exclude_id: 排除的ID
|
||||
"""
|
||||
from app.models.inbound.semi import StockSemi
|
||||
|
||||
# 1. 序列号 (SN) 校验 - 全局唯一
|
||||
if serial_number:
|
||||
query = StockSemi.query.filter(StockSemi.serial_number == serial_number)
|
||||
if exclude_id:
|
||||
query = query.filter(StockSemi.id != exclude_id)
|
||||
|
||||
exists = query.first()
|
||||
if exists:
|
||||
# [修改] material -> base
|
||||
occupied_name = exists.base.name if (hasattr(exists, 'base') and exists.base) else "未知物料"
|
||||
raise ValueError(f"序列号【{serial_number}】已存在!被半成品 [{occupied_name}] 占用,请核查。")
|
||||
|
||||
# 2. 批号 (BN) 校验 - 同物料下不能重复开单
|
||||
if batch_number and base_id:
|
||||
query = StockSemi.query.filter(
|
||||
StockSemi.base_id == base_id,
|
||||
@ -44,7 +32,6 @@ class SemiInboundService:
|
||||
)
|
||||
if exclude_id:
|
||||
query = query.filter(StockSemi.id != exclude_id)
|
||||
|
||||
if query.first():
|
||||
raise ValueError(f"该物料已存在批号【{batch_number}】,请勿重复建单,建议在原批次上追加库存。")
|
||||
|
||||
@ -54,10 +41,7 @@ class SemiInboundService:
|
||||
@staticmethod
|
||||
def search_base_material(keyword):
|
||||
try:
|
||||
# [核心修改] 只查询已启用的物料
|
||||
query = MaterialBase.query.filter(MaterialBase.is_enabled == True)
|
||||
|
||||
# 如果有关键词,进行模糊匹配
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
or_(
|
||||
@ -65,19 +49,16 @@ class SemiInboundService:
|
||||
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||
)
|
||||
)
|
||||
|
||||
# 统一逻辑:按ID倒序,限制20条
|
||||
query = query.order_by(MaterialBase.id.desc()).limit(20)
|
||||
|
||||
results = []
|
||||
for item in query.all():
|
||||
results.append({
|
||||
'id': item.id,
|
||||
'name': item.name,
|
||||
'spec': item.spec_model, # 对应前端 item.spec
|
||||
'spec': item.spec_model,
|
||||
'category': item.category,
|
||||
'unit': item.unit,
|
||||
'type': item.material_type, # 对应前端 item.type
|
||||
'type': item.material_type,
|
||||
'status': '启用'
|
||||
})
|
||||
return results
|
||||
@ -85,39 +66,74 @@ class SemiInboundService:
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
# ============================================================
|
||||
# 1.5 [新增] BOM 搜索逻辑
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def search_bom_options(keyword):
|
||||
from app.models.bom import BomTable
|
||||
try:
|
||||
# 关联查询:BOM表 + 父件基础信息表
|
||||
query = db.session.query(
|
||||
BomTable.bom_no,
|
||||
BomTable.version,
|
||||
MaterialBase.name.label('parent_name'),
|
||||
MaterialBase.spec_model.label('parent_spec')
|
||||
).join(MaterialBase, BomTable.parent_id == MaterialBase.id)
|
||||
|
||||
# 只查询启用的BOM
|
||||
if hasattr(BomTable, 'is_enabled'):
|
||||
query = query.filter(BomTable.is_enabled == True)
|
||||
|
||||
if keyword:
|
||||
kw = f'%{keyword}%'
|
||||
# 支持搜索:BOM编号、父件名称、父件规格
|
||||
query = query.filter(
|
||||
or_(
|
||||
BomTable.bom_no.ilike(kw),
|
||||
MaterialBase.name.ilike(kw),
|
||||
MaterialBase.spec_model.ilike(kw)
|
||||
)
|
||||
)
|
||||
|
||||
# 去重并限制数量
|
||||
results = query.distinct().limit(20).all()
|
||||
|
||||
return [{
|
||||
'bom_no': r.bom_no,
|
||||
'version': r.version,
|
||||
'parent_name': r.parent_name,
|
||||
'parent_spec': r.parent_spec or ''
|
||||
} for r in results]
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
# ============================================================
|
||||
# 2. 新增入库逻辑
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def handle_inbound(data):
|
||||
from app.models.inbound.semi import StockSemi
|
||||
|
||||
try:
|
||||
base_id = data.get('base_id')
|
||||
if not base_id:
|
||||
raise ValueError("必须选择基础物料 (缺少 base_id)")
|
||||
|
||||
material = MaterialBase.query.get(base_id)
|
||||
if not material:
|
||||
raise ValueError(f"ID为 {base_id} 的基础物料不存在")
|
||||
|
||||
# [核心修改] 后端二次校验:如果物料已停用,禁止入库
|
||||
if not material.is_enabled:
|
||||
raise ValueError(f"物料【{material.name}】已停用,无法办理新入库。")
|
||||
|
||||
# --- [核心修改] 执行唯一性校验 ---
|
||||
SemiInboundService._check_unique(
|
||||
base_id=base_id,
|
||||
serial_number=data.get('serial_number'),
|
||||
batch_number=data.get('batch_number')
|
||||
)
|
||||
|
||||
# [核心修改] 强制北京时间
|
||||
beijing_tz = timezone(timedelta(hours=8))
|
||||
current_time = datetime.now(beijing_tz).replace(tzinfo=None)
|
||||
|
||||
in_date_val = current_time
|
||||
|
||||
if data.get('in_date'):
|
||||
try:
|
||||
date_str = str(data['in_date'])
|
||||
@ -130,7 +146,6 @@ class SemiInboundService:
|
||||
except ValueError:
|
||||
in_date_val = current_time
|
||||
|
||||
# 2. 处理生产时间
|
||||
p_start = None
|
||||
p_end = None
|
||||
if data.get('production_start_time'):
|
||||
@ -151,14 +166,12 @@ class SemiInboundService:
|
||||
elif isinstance(raw_range, str):
|
||||
time_range_str = raw_range
|
||||
|
||||
# 3. 处理数值和成本
|
||||
in_qty = float(data.get('in_quantity') or 0)
|
||||
raw_cost = float(data.get('raw_material_cost') or 0)
|
||||
manual_cost = float(data.get('manual_cost') or 0)
|
||||
unit_total_cost = raw_cost + manual_cost
|
||||
total_value = unit_total_cost * in_qty
|
||||
|
||||
# 4. 获取全局打印流水号
|
||||
next_global_id = 0
|
||||
try:
|
||||
seq_sql = text("SELECT nextval('global_print_seq')")
|
||||
@ -172,59 +185,47 @@ class SemiInboundService:
|
||||
final_sku = data.get('sku')
|
||||
if not final_sku:
|
||||
final_sku = generated_sku
|
||||
|
||||
final_barcode = data.get('barcode')
|
||||
if not final_barcode:
|
||||
final_barcode = final_sku
|
||||
|
||||
arrival_list = data.get('arrival_photo', [])
|
||||
quality_report_list = data.get('quality_report_link', [])
|
||||
|
||||
if not isinstance(arrival_list, list): arrival_list = []
|
||||
if not isinstance(quality_report_list, list): quality_report_list = []
|
||||
|
||||
# 8. 创建记录
|
||||
new_stock = StockSemi(
|
||||
base_id=material.id,
|
||||
global_print_id=next_global_id,
|
||||
sku=final_sku,
|
||||
production_date=in_date_val, # 存入 DateTime
|
||||
|
||||
production_date=in_date_val,
|
||||
serial_number=data.get('serial_number'),
|
||||
batch_number=data.get('batch_number'),
|
||||
barcode=final_barcode,
|
||||
|
||||
status='在库',
|
||||
quality_status=data.get('quality_status', '合格'),
|
||||
in_quantity=in_qty,
|
||||
stock_quantity=in_qty,
|
||||
available_quantity=in_qty,
|
||||
warehouse_location=data.get('warehouse_location'),
|
||||
|
||||
bom_code=data.get('bom_code'),
|
||||
bom_version=data.get('bom_version'),
|
||||
work_order_code=data.get('work_order_code'),
|
||||
production_manager=data.get('production_manager'),
|
||||
|
||||
production_start_time=p_start,
|
||||
production_end_time=p_end,
|
||||
production_time_range=time_range_str,
|
||||
|
||||
raw_material_cost=raw_cost,
|
||||
manual_cost=manual_cost,
|
||||
total_price=total_value,
|
||||
|
||||
arrival_photo=json.dumps(arrival_list),
|
||||
quality_report_link=json.dumps(quality_report_list),
|
||||
|
||||
detail_link=data.get('detail_link'),
|
||||
remark=data.get('remark')
|
||||
)
|
||||
|
||||
db.session.add(new_stock)
|
||||
db.session.commit()
|
||||
return new_stock
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print("----- SemiInboundService Error -----")
|
||||
@ -237,13 +238,11 @@ class SemiInboundService:
|
||||
@staticmethod
|
||||
def update_inbound(stock_id, data):
|
||||
from app.models.inbound.semi import StockSemi
|
||||
|
||||
try:
|
||||
stock = StockSemi.query.get(stock_id)
|
||||
if not stock:
|
||||
raise ValueError("记录不存在")
|
||||
|
||||
# --- [核心修改] 编辑时也要校验唯一性 ---
|
||||
new_base_id = data.get('base_id', stock.base_id)
|
||||
new_sn = data.get('serial_number', stock.serial_number)
|
||||
new_bn = data.get('batch_number', stock.batch_number)
|
||||
@ -270,7 +269,6 @@ class SemiInboundService:
|
||||
'detail_link': 'detail_link',
|
||||
'remark': 'remark'
|
||||
}
|
||||
|
||||
for frontend_key, db_attr in field_mapping.items():
|
||||
if frontend_key in data:
|
||||
setattr(stock, db_attr, data[frontend_key])
|
||||
@ -279,7 +277,6 @@ class SemiInboundService:
|
||||
imgs = data['arrival_photo']
|
||||
if isinstance(imgs, list):
|
||||
stock.arrival_photo = json.dumps(imgs)
|
||||
|
||||
if 'quality_report_link' in data:
|
||||
imgs = data['quality_report_link']
|
||||
if isinstance(imgs, list):
|
||||
@ -294,7 +291,6 @@ class SemiInboundService:
|
||||
stock.production_start_time = None
|
||||
except:
|
||||
pass
|
||||
|
||||
if 'production_end_time' in data:
|
||||
try:
|
||||
if data['production_end_time']:
|
||||
@ -304,7 +300,6 @@ class SemiInboundService:
|
||||
stock.production_end_time = None
|
||||
except:
|
||||
pass
|
||||
|
||||
if 'production_time_range' in data:
|
||||
raw_range = data['production_time_range']
|
||||
if isinstance(raw_range, list):
|
||||
@ -314,7 +309,6 @@ class SemiInboundService:
|
||||
|
||||
qty_changed = False
|
||||
cost_changed = False
|
||||
|
||||
if 'in_quantity' in data:
|
||||
new_qty = float(data['in_quantity'])
|
||||
diff = new_qty - float(stock.in_quantity)
|
||||
@ -323,22 +317,18 @@ class SemiInboundService:
|
||||
stock.stock_quantity = float(stock.stock_quantity) + diff
|
||||
stock.available_quantity = float(stock.available_quantity) + diff
|
||||
qty_changed = True
|
||||
|
||||
if 'raw_material_cost' in data:
|
||||
stock.raw_material_cost = float(data['raw_material_cost'])
|
||||
cost_changed = True
|
||||
|
||||
if 'manual_cost' in data:
|
||||
stock.manual_cost = float(data['manual_cost'])
|
||||
cost_changed = True
|
||||
|
||||
if cost_changed or qty_changed:
|
||||
unit_total = float(stock.raw_material_cost) + float(stock.manual_cost)
|
||||
stock.total_price = float(stock.in_quantity) * unit_total
|
||||
|
||||
db.session.commit()
|
||||
return stock
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
@ -365,7 +355,6 @@ class SemiInboundService:
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def get_outbound_history(stock_id):
|
||||
"""获取出库历史"""
|
||||
try:
|
||||
records = TransOutbound.query.filter_by(
|
||||
source_table='stock_semi', stock_id=stock_id
|
||||
@ -382,7 +371,6 @@ class SemiInboundService:
|
||||
from app.models.inbound.semi import StockSemi
|
||||
try:
|
||||
query = db.session.query(StockSemi).outerjoin(MaterialBase, StockSemi.base_id == MaterialBase.id)
|
||||
|
||||
if keyword:
|
||||
kw = f'%{keyword}%'
|
||||
query = query.filter(
|
||||
@ -396,17 +384,12 @@ class SemiInboundService:
|
||||
StockSemi.bom_code.ilike(kw)
|
||||
)
|
||||
)
|
||||
|
||||
# 类别筛选
|
||||
if category and category.strip():
|
||||
query = query.filter(MaterialBase.category == category.strip())
|
||||
# 类型筛选
|
||||
if material_type and material_type.strip():
|
||||
query = query.filter(MaterialBase.material_type == material_type.strip())
|
||||
|
||||
if not statuses:
|
||||
statuses = ['在库', '借库']
|
||||
|
||||
if '已出库' in statuses:
|
||||
query = query.filter(StockSemi.status.in_(statuses))
|
||||
else:
|
||||
@ -416,11 +399,8 @@ class SemiInboundService:
|
||||
StockSemi.stock_quantity > 0
|
||||
)
|
||||
)
|
||||
|
||||
# 按照 production_date (入库日期) 倒序排序
|
||||
pagination = query.order_by(StockSemi.production_date.desc()).paginate(page=page, per_page=limit,
|
||||
error_out=False)
|
||||
|
||||
current_items = pagination.items
|
||||
|
||||
def parse_img(json_str):
|
||||
@ -432,10 +412,7 @@ class SemiInboundService:
|
||||
|
||||
items = []
|
||||
for item in current_items:
|
||||
# [注意] 因为Model层已经修改了 to_dict 内部的 material -> base,所以这里直接调 to_dict 即可
|
||||
d = item.to_dict()
|
||||
|
||||
# 格式化展示日期
|
||||
date_display = ''
|
||||
if item.production_date:
|
||||
try:
|
||||
@ -443,30 +420,22 @@ class SemiInboundService:
|
||||
except:
|
||||
date_display = str(item.production_date)[:10]
|
||||
d['inbound_date'] = date_display
|
||||
|
||||
d['qty_stock'] = float(item.stock_quantity or 0)
|
||||
d['qty_available'] = float(item.available_quantity or 0)
|
||||
d['sum_stock'] = d['qty_stock']
|
||||
d['sum_available'] = d['qty_available']
|
||||
|
||||
d['arrival_photo'] = parse_img(item.arrival_photo)
|
||||
d['quality_report_link'] = parse_img(item.quality_report_link)
|
||||
d['global_print_id'] = item.global_print_id
|
||||
|
||||
items.append(d)
|
||||
|
||||
return {"total": pagination.total, "items": items}
|
||||
except Exception as e:
|
||||
print(f"List Error: {e}")
|
||||
traceback.print_exc()
|
||||
return {"total": 0, "items": []}
|
||||
|
||||
# ============================================================
|
||||
# 7. 系统用户搜索
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def search_system_users(keyword):
|
||||
"""搜索系统用户(活跃状态)"""
|
||||
from app.models.system import SysUser
|
||||
try:
|
||||
query = SysUser.query.filter(SysUser.status == 'active')
|
||||
@ -487,9 +456,6 @@ class SemiInboundService:
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
# ============================================================
|
||||
# 8. 获取筛选选项(类别、类型)
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def get_filter_options():
|
||||
try:
|
||||
|
||||
@ -41,6 +41,15 @@ export function searchMaterialBase(keyword: string) {
|
||||
})
|
||||
}
|
||||
|
||||
// 搜索BOM (新增)
|
||||
export function searchBom(keyword: string) {
|
||||
return request({
|
||||
url: '/inbound/product/search-bom',
|
||||
method: 'get',
|
||||
params: { keyword }
|
||||
})
|
||||
}
|
||||
|
||||
// 用户建议
|
||||
export function getUserSuggestions(params: any) {
|
||||
return request({
|
||||
|
||||
@ -44,6 +44,15 @@ export function searchMaterialBase(keyword: string) {
|
||||
})
|
||||
}
|
||||
|
||||
// 5.5 搜索BOM (新增)
|
||||
export function searchBom(keyword: string) {
|
||||
return request({
|
||||
url: '/inbound/semi/search-bom',
|
||||
method: 'get',
|
||||
params: { keyword }
|
||||
})
|
||||
}
|
||||
|
||||
// 用户建议
|
||||
export function getUserSuggestions(params: any) {
|
||||
return request({
|
||||
|
||||
@ -276,8 +276,36 @@
|
||||
<div class="card-title"><el-icon class="icon"><Setting /></el-icon><span>3. 生产与销售信息</span></div>
|
||||
<div class="card-content">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8"><el-form-item label="BOM编号"><el-input v-model="form.bom_code" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="BOM版本"><el-input v-model="form.bom_version" /></el-form-item></el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-form-item label="BOM编号">
|
||||
<el-select
|
||||
v-model="form.bom_code"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
placeholder="搜规格/编号"
|
||||
:remote-method="handleSearchBom"
|
||||
:loading="bomSearchLoading"
|
||||
@change="handleBomSelect"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in bomOptions"
|
||||
:key="`${item.bom_no}_${item.version}`"
|
||||
:label="item.bom_no"
|
||||
:value="`${item.bom_no}###${item.version}`"
|
||||
>
|
||||
<span style="float: left; font-weight: bold;">{{ item.bom_no }}</span>
|
||||
<span style="float: right; color: #8492a6; font-size: 13px; margin-left: 10px;">
|
||||
{{ item.version }} <span v-if="item.parent_spec">({{ item.parent_spec }})</span>
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8"><el-form-item label="BOM版本"><el-input v-model="form.bom_version" placeholder="自动填充" readonly/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="工单号"><el-input v-model="form.work_order_code" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
@ -353,10 +381,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { Plus, Setting, Refresh, Search, Box, House, Link, InfoFilled, Printer, Camera, Picture } from '@element-plus/icons-vue'
|
||||
// 修复点:引入 ElLoading
|
||||
import { ElMessage, ElLoading } from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
import { getProductList, createProductInbound, updateProductInbound, deleteProductInbound, searchMaterialBase } from '@/api/inbound/product'
|
||||
import {
|
||||
getProductList,
|
||||
createProductInbound,
|
||||
updateProductInbound,
|
||||
deleteProductInbound,
|
||||
searchMaterialBase,
|
||||
searchBom // [新增]
|
||||
} from '@/api/inbound/product'
|
||||
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||
import { getLabelPreview, executePrint } from '@/api/common/print'
|
||||
@ -372,6 +406,10 @@ const formRef = ref()
|
||||
const queryParams = reactive({ page: 1, pageSize: 100, keyword: '', statuses: ['在库', '借库'] })
|
||||
const materialOptions = ref<any[]>([])
|
||||
|
||||
// BOM 搜索相关
|
||||
const bomSearchLoading = ref(false)
|
||||
const bomOptions = ref<any[]>([])
|
||||
|
||||
// 打印相关变量
|
||||
const printVisible = ref(false)
|
||||
const printLoading = ref(false)
|
||||
@ -382,14 +420,12 @@ const currentPrintData = ref<any>({})
|
||||
// 图片/拍照相关
|
||||
const dialogImageUrl = ref('')
|
||||
const dialogVisibleImage = ref(false)
|
||||
// 3个独立的列表
|
||||
const productPhotoList = ref<any[]>([]) // 成品实拍
|
||||
const qualityFileList = ref<any[]>([]) // 质量报告
|
||||
const inspectionFileList = ref<any[]>([]) // 检测报告
|
||||
|
||||
const cameraDialogVisible = ref(false)
|
||||
const cameraRef = ref<InstanceType<typeof WebRtcCamera> | null>(null)
|
||||
// 定义当前触发拍照的字段
|
||||
const currentCameraField = ref<'product_photo' | 'quality_report_link' | 'inspection_report_link'>('product_photo')
|
||||
const quality_url = ref('')
|
||||
const inspection_url = ref('')
|
||||
@ -433,11 +469,31 @@ const form = reactive({
|
||||
})
|
||||
|
||||
// ------------------------------------
|
||||
// 校验规则 (前端 pre-check)
|
||||
// BOM Search Logic
|
||||
// ------------------------------------
|
||||
const handleSearchBom = async (query: string) => {
|
||||
bomSearchLoading.value = true
|
||||
try {
|
||||
const res: any = await searchBom(query)
|
||||
bomOptions.value = res.data || []
|
||||
} finally { bomSearchLoading.value = false }
|
||||
}
|
||||
const handleBomSelect = (val: string) => {
|
||||
if (!val) {
|
||||
form.bom_code = ''
|
||||
form.bom_version = ''
|
||||
return
|
||||
}
|
||||
const [code, version] = val.split('###')
|
||||
form.bom_code = code
|
||||
form.bom_version = version
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// Validation Logic
|
||||
// ------------------------------------
|
||||
const validateUnique = (rule: any, value: string, callback: any) => {
|
||||
if (!value) return callback()
|
||||
// 简单的列表前端查重
|
||||
const isDuplicate = tableData.value.some((row: any) => {
|
||||
if (dialogStatus.value === 'update' && row.id === form.id) return false
|
||||
if (rule.field === 'serial_number' && row.serial_number === value) return true
|
||||
@ -469,7 +525,6 @@ const handleSearchMaterial = async (query: string) => {
|
||||
const onMaterialSelected = (val: number) => {
|
||||
const item = materialOptions.value.find(i => i.id === val)
|
||||
if (item) {
|
||||
// Auto-populate readonly fields
|
||||
form.material_name = item.name
|
||||
form.spec_model = item.spec
|
||||
form.material_type = item.type
|
||||
@ -482,11 +537,9 @@ const onMaterialSelected = (val: number) => {
|
||||
// Autocomplete (Manager) - 后端驱动
|
||||
// ------------------------------------
|
||||
const querySearchManager = async (query: string, cb: any) => {
|
||||
// 后续从后端获取用户建议
|
||||
cb([])
|
||||
}
|
||||
const handleManagerSelect = (item: any) => {
|
||||
// 无需保存历史
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
@ -530,6 +583,10 @@ const handleUpdate = (row: any) => {
|
||||
const iLinks = iReports.filter(r => isExternalLink(r))
|
||||
inspection_url.value = iLinks.length > 0 ? iLinks[0] : ''
|
||||
materialOptions.value = [{ id: row.base_id, name: row.material_name, spec: row.spec_model, category: row.category, isHistory: false }]
|
||||
// 回显BOM
|
||||
if (form.bom_code) {
|
||||
bomOptions.value = [{ bom_no: form.bom_code, version: form.bom_version }]
|
||||
}
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
@ -563,9 +620,6 @@ const triggerCamera = (field: any) => {
|
||||
cameraDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 修复核心:拍照上传回调逻辑
|
||||
// ------------------------------------------------------------------------
|
||||
const handleCameraConfirm = async (file: File) => {
|
||||
if (!beforeAvatarUpload(file)) {
|
||||
cameraDialogVisible.value = false;
|
||||
@ -573,25 +627,18 @@ const handleCameraConfirm = async (file: File) => {
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// 使用 ElLoading.service 替代报错的 ElMessage.loading
|
||||
const loadingMsg = ElLoading.service({
|
||||
lock: true,
|
||||
text: '照片处理中...',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
});
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
const res: any = await uploadFile(formData);
|
||||
if (res.code === 200) {
|
||||
const newUrl = res.data.url;
|
||||
const field = currentCameraField.value; // 根据触发时记录的字段
|
||||
|
||||
// 添加到表单数据
|
||||
const field = currentCameraField.value;
|
||||
form[field].push(newUrl);
|
||||
|
||||
// 更新对应的显示列表
|
||||
const previewItem = { name: newUrl.split('/').pop(), url: getImageUrl(newUrl) };
|
||||
if (field === 'product_photo') {
|
||||
productPhotoList.value.push(previewItem);
|
||||
@ -600,7 +647,6 @@ const handleCameraConfirm = async (file: File) => {
|
||||
} else if (field === 'inspection_report_link') {
|
||||
inspectionFileList.value.push(previewItem);
|
||||
}
|
||||
|
||||
ElMessage.success('拍照上传成功');
|
||||
success = true;
|
||||
} else {
|
||||
@ -639,7 +685,6 @@ const submitForm = async () => {
|
||||
} else { await updateProductInbound(form.id!, payload); ElMessage.success('更新成功') }
|
||||
visible.value = false; fetchData()
|
||||
} catch(e:any) {
|
||||
// 捕获后端报错
|
||||
ElMessage.error(e.msg || '操作失败')
|
||||
} finally { submitting.value = false }
|
||||
}
|
||||
@ -654,7 +699,7 @@ const handlePrint = async (row: any) => {
|
||||
}
|
||||
const confirmPrint = async () => { printing.value = true; try { await executePrint(currentPrintData.value); ElMessage.success('已发送'); printVisible.value = false } catch (e: any) { ElMessage.error('打印失败') } finally { printing.value = false } }
|
||||
const resetForm = () => {
|
||||
materialOptions.value = []; productPhotoList.value = []; qualityFileList.value = []; inspectionFileList.value = []; quality_url.value = ''; inspection_url.value = ''
|
||||
materialOptions.value = []; bomOptions.value = []; productPhotoList.value = []; qualityFileList.value = []; inspectionFileList.value = []; quality_url.value = ''; inspection_url.value = ''
|
||||
Object.assign(form, { id: undefined, base_id: undefined, material_name: '', spec_model: '', material_type: '', category: '', unit: '', sku: '', barcode: '', serial_number: '', in_date: '', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', status: '在库', quality_status: '合格', bom_code: '', bom_version: '', work_order_code: '', order_id: '', production_manager: '', production_time_range: [], raw_material_cost: 0, manual_cost: 0, sale_price: 0, quality_report_link: [], inspection_report_link: [], product_photo: [], detail_link: '' })
|
||||
}
|
||||
const getStatusType = (s:string) => ({'在库':'success','出库':'info','借库':'warning','损耗':'danger'}[s]||'warning')
|
||||
|
||||
@ -373,8 +373,36 @@
|
||||
<div class="divider-text">生产任务信息</div>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8"><el-form-item label="工单号"><el-input v-model="form.work_order_code" placeholder="WO-xxx"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="BOM编号"><el-input v-model="form.bom_code" placeholder="BOM-xxx"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="BOM版本"><el-input v-model="form.bom_version" placeholder="v1.0"/></el-form-item></el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-form-item label="BOM编号">
|
||||
<el-select
|
||||
v-model="form.bom_code"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
placeholder="搜规格/编号"
|
||||
:remote-method="handleSearchBom"
|
||||
:loading="bomSearchLoading"
|
||||
@change="handleBomSelect"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in bomOptions"
|
||||
:key="`${item.bom_no}_${item.version}`"
|
||||
:label="item.bom_no"
|
||||
:value="`${item.bom_no}###${item.version}`"
|
||||
>
|
||||
<span style="float: left; font-weight: bold;">{{ item.bom_no }}</span>
|
||||
<span style="float: right; color: #8492a6; font-size: 13px; margin-left: 10px;">
|
||||
{{ item.version }} <span v-if="item.parent_spec">({{ item.parent_spec }})</span>
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8"><el-form-item label="BOM版本"><el-input v-model="form.bom_version" placeholder="自动填充" readonly/></el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="24">
|
||||
@ -452,6 +480,7 @@ import {
|
||||
updateSemiInbound,
|
||||
deleteSemiInbound,
|
||||
searchMaterialBase,
|
||||
searchBom, // [新增]
|
||||
getFilterOptions
|
||||
} from '@/api/inbound/semi'
|
||||
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
||||
@ -474,6 +503,10 @@ const categoryOptions = ref<string[]>([])
|
||||
const typeOptions = ref<string[]>([])
|
||||
const materialOptions = ref<any[]>([])
|
||||
|
||||
// BOM 搜索相关
|
||||
const bomSearchLoading = ref(false)
|
||||
const bomOptions = ref<any[]>([])
|
||||
|
||||
// 打印相关变量
|
||||
const printVisible = ref(false)
|
||||
const printLoading = ref(false)
|
||||
@ -542,15 +575,35 @@ const form = reactive({
|
||||
production_manager: '', production_time_range: [] as string[], arrival_photo: [] as string[], quality_report_link: [] as string[], detail_link: ''
|
||||
})
|
||||
|
||||
// ------------------------------------
|
||||
// BOM Search Logic
|
||||
// ------------------------------------
|
||||
const handleSearchBom = async (query: string) => {
|
||||
bomSearchLoading.value = true
|
||||
try {
|
||||
const res: any = await searchBom(query)
|
||||
bomOptions.value = res.data || []
|
||||
} finally { bomSearchLoading.value = false }
|
||||
}
|
||||
const handleBomSelect = (val: string) => {
|
||||
// val 格式为 bom_no###version
|
||||
if (!val) {
|
||||
form.bom_code = ''
|
||||
form.bom_version = ''
|
||||
return
|
||||
}
|
||||
const [code, version] = val.split('###')
|
||||
form.bom_code = code
|
||||
form.bom_version = version
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// Autocomplete & Search Logic (后端 API 驱动)
|
||||
// ------------------------------------
|
||||
const querySearchManager = async (query: string, cb: any) => {
|
||||
// 后续会从后端获取用户建议,暂时先返回空列表
|
||||
cb([])
|
||||
}
|
||||
const handleManagerSelect = (item: any) => {
|
||||
// 无需保存历史
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
@ -568,13 +621,11 @@ const handleSearchMaterial = async (query: string) => {
|
||||
const onMaterialSelected = (val: number) => {
|
||||
const item = materialOptions.value.find(i => i.id === val)
|
||||
if (item) {
|
||||
// Populate form fields
|
||||
form.material_name = item.name
|
||||
form.spec_model = item.spec
|
||||
form.category = item.category
|
||||
form.unit = item.unit
|
||||
form.material_type = item.type
|
||||
// Trigger batch/serial logic specific to Semi
|
||||
checkHistoryAndSetMode(item.id)
|
||||
}
|
||||
}
|
||||
@ -587,7 +638,6 @@ const validateUnique = (rule: any, value: string, callback: any) => {
|
||||
const isDuplicate = tableData.value.some((row: any) => {
|
||||
if (dialogStatus.value === 'update' && row.id === form.id) return false
|
||||
if (rule.field === 'serial_number' && row.serial_number === value) return true
|
||||
// 批号校验需要同时匹配物料
|
||||
if (rule.field === 'batch_number' && row.batch_number === value && row.base_id === form.base_id) return true
|
||||
return false
|
||||
})
|
||||
@ -699,6 +749,10 @@ const handleUpdate = (row: any) => {
|
||||
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, isHistory: false }]
|
||||
// 回显BOM,如果存在
|
||||
if (form.bom_code) {
|
||||
bomOptions.value = [{ bom_no: form.bom_code, version: form.bom_version }]
|
||||
}
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
@ -741,10 +795,7 @@ const handleCameraConfirm = async (file: File) => {
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// 修复点:使用 ElLoading
|
||||
const loadingMsg = ElLoading.service({ text: '照片上传中...', background: 'rgba(0, 0, 0, 0.7)' });
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
const res: any = await uploadFile(formData);
|
||||
@ -796,7 +847,6 @@ const submitForm = async () => {
|
||||
} else { await updateSemiInbound(form.id!, payload); ElMessage.success('更新成功') }
|
||||
await fetchData(); visible.value = false
|
||||
} catch (e: any) {
|
||||
// 捕获后端报错
|
||||
ElMessage.error(e.msg || '操作失败')
|
||||
} finally { submitting.value = false }
|
||||
}
|
||||
@ -811,7 +861,7 @@ const handlePrint = async (row: any) => {
|
||||
}
|
||||
const confirmPrint = async () => { printing.value = true; try { await executePrint(currentPrintData.value); ElMessage.success('指令已发送'); printVisible.value = false } catch (e: any) { ElMessage.error(e.msg || '打印失败') } finally { printing.value = false } }
|
||||
const resetForm = () => {
|
||||
materialOptions.value = []; arrivalFileList.value = []; reportFileList.value = []; quality_report_url.value = ''
|
||||
materialOptions.value = []; bomOptions.value = []; arrivalFileList.value = []; reportFileList.value = []; quality_report_url.value = ''
|
||||
Object.assign(form, { id: undefined, base_id: undefined, material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', bom_code: '', bom_version: '', work_order_code: '', raw_material_cost: 0, manual_cost: 0, unit_total_cost: 0, production_manager: '', production_time_range: [], arrival_photo: [], quality_report_link: [], detail_link: '' })
|
||||
}
|
||||
const getStatusType = (status: string) => { const map: any = { '在库': 'success', '出库': 'info', '借库': 'warning', '损耗': 'danger' }; return map[status] || 'warning' }
|
||||
|
||||
Reference in New Issue
Block a user