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