添加半成品页面进行数据
This commit is contained in:
@ -1,38 +1,53 @@
|
||||
# 文件路径: inventory-backend/app/__init__.py
|
||||
from flask import Flask
|
||||
from config import Config
|
||||
from app.extensions import db, migrate, cors
|
||||
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
|
||||
# 初始化插件
|
||||
# 1. 初始化插件
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
# 确保跨域配置正确,允许前端访问
|
||||
|
||||
# 确保跨域配置
|
||||
cors.init_app(app, resources={r"/api/*": {"origins": "*"}})
|
||||
|
||||
# --- 注册蓝图 ---
|
||||
# =========================================================
|
||||
# 2. 注册蓝图 (Blueprints)
|
||||
# =========================================================
|
||||
|
||||
# 1. 保持原有的 stocks 模块
|
||||
try:
|
||||
from app.api.v1.stocks import stocks_bp
|
||||
app.register_blueprint(stocks_bp, url_prefix='/api/v1/stocks')
|
||||
except ImportError as e:
|
||||
print(f"⚠️ 警告: 原有 stocks 蓝图导入失败: {e}")
|
||||
|
||||
# 2. 注册新的入库聚合蓝图
|
||||
# 核心:必须先导入,再注册。路径对应 app/api/v1/inbound/__init__.py
|
||||
# 注册入库聚合模块 (Inbound)
|
||||
try:
|
||||
# 指向聚合文件: app/api/v1/inbound/__init__.py
|
||||
from app.api.v1.inbound import inbound_bp
|
||||
# 最终路径结构:/api/v1/inbound/buy/list
|
||||
app.register_blueprint(inbound_bp, url_prefix='/api/v1/inbound')
|
||||
print("✅ 入库模块蓝图注册成功")
|
||||
except ImportError as e:
|
||||
print(f"❌ 严重错误: 入库模块 inbound 蓝图导入失败: {e}")
|
||||
|
||||
# 打印路由映射,仅在本地调试时建议开启
|
||||
# with app.app_context():
|
||||
# print(app.url_map)
|
||||
# 注册父蓝图,路由前缀为 /api/v1/inbound
|
||||
# 最终路由效果:
|
||||
# /api/v1/inbound + /buy/list -> /api/v1/inbound/buy/list
|
||||
# /api/v1/inbound + /semi/list -> /api/v1/inbound/semi/list
|
||||
app.register_blueprint(inbound_bp, url_prefix='/api/v1/inbound')
|
||||
|
||||
print("✅ Inbound (Buy & Semi) 模块注册成功")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"❌ 错误: Inbound 模块导入失败: {e}")
|
||||
|
||||
# =========================================================
|
||||
# 3. 预加载数据模型 (解决 relationship 找不到模型的问题)
|
||||
# =========================================================
|
||||
with app.app_context():
|
||||
try:
|
||||
# ✅ 修正点:引用新路径 (不再引用 app.models.stock)
|
||||
from app.models.inbound.buy import StockBuy
|
||||
from app.models.inbound.semi import StockSemi
|
||||
from app.models.material import MaterialBase
|
||||
|
||||
# 如果是开发环境且没有迁移文件,可以取消注释下面这行来创建表
|
||||
# db.create_all()
|
||||
except ImportError as e:
|
||||
print(f"⚠️ 模型预加载失败: {e}")
|
||||
|
||||
return app
|
||||
@ -1,18 +1,22 @@
|
||||
from flask import Blueprint
|
||||
|
||||
# 1. 导入同目录下的 buy 模块 (假设文件名为 buy.py)
|
||||
# 1. 导入子模块蓝图
|
||||
# 注意:确保 .buy, .semi, .base 文件在同级目录下真实存在
|
||||
from .buy import inbound_buy_bp
|
||||
from .semi import inbound_semi_bp
|
||||
|
||||
# 2. 【关键修改】导入同目录下的 base 模块
|
||||
# 使用相对导入 .base,这样 Python 就会去 app/api/v1/inbound/base.py 找
|
||||
# 如果你还有 base.py 文件,就取消注释下面这行
|
||||
from .base import inbound_base_bp
|
||||
|
||||
# 创建父级蓝图 'inbound'
|
||||
# 2. 创建父级聚合蓝图
|
||||
inbound_bp = Blueprint('inbound', __name__)
|
||||
|
||||
# 3. 挂载子蓝图
|
||||
# 最终路由将是: /api/v1/inbound/buy/...
|
||||
# 访问地址: /api/v1/inbound/buy/list
|
||||
inbound_bp.register_blueprint(inbound_buy_bp, url_prefix='/buy')
|
||||
|
||||
# 最终路由将是: /api/v1/inbound/base/...
|
||||
# 访问地址: /api/v1/inbound/semi/list
|
||||
inbound_bp.register_blueprint(inbound_semi_bp, url_prefix='/semi')
|
||||
|
||||
# 如果有 base
|
||||
inbound_bp.register_blueprint(inbound_base_bp, url_prefix='/base')
|
||||
@ -0,0 +1,91 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.services.inbound.semi_service import SemiInboundService
|
||||
import traceback
|
||||
|
||||
# 定义蓝图,url_prefix 通常在注册蓝图时指定,例如 /api/v1/inbound/semi
|
||||
inbound_semi_bp = Blueprint('inbound_semi', __name__)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 0. 基础物料搜索 (复用逻辑)
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_semi_bp.route('/search-base', methods=['GET'])
|
||||
def search_base():
|
||||
"""
|
||||
供前端下拉框远程搜索使用 (搜索半成品类型的基础物料)
|
||||
Query Param: keyword (名称或规格)
|
||||
"""
|
||||
try:
|
||||
keyword = request.args.get('keyword', '')
|
||||
# 这里复用 Service 中的搜索逻辑
|
||||
data = SemiInboundService.search_base_material(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. 获取半成品列表
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_semi_bp.route('/list', methods=['GET'])
|
||||
def get_list():
|
||||
try:
|
||||
page = request.args.get('page', 1, type=int)
|
||||
limit = request.args.get('pageSize', 15, type=int)
|
||||
# 支持按关键字搜索:BOM号、工单号、SN、批号等
|
||||
keyword = request.args.get('keyword', '')
|
||||
|
||||
result = SemiInboundService.get_list(page, limit, keyword)
|
||||
return jsonify({"code": 200, "msg": "success", "data": result})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 2. 新增半成品入库
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_semi_bp.route('/submit', methods=['POST'])
|
||||
def submit():
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"code": 400, "msg": "No data"}), 400
|
||||
|
||||
SemiInboundService.handle_inbound(data)
|
||||
return jsonify({"code": 200, "msg": "入库成功"})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 3. 更新半成品入库信息
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_semi_bp.route('/<int:id>', methods=['PUT'])
|
||||
def update_semi(id):
|
||||
try:
|
||||
data = request.get_json()
|
||||
SemiInboundService.update_inbound(id, data)
|
||||
return jsonify({"code": 200, "msg": "更新成功"})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 4. 删除半成品入库记录
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_semi_bp.route('/<int:id>', methods=['DELETE'])
|
||||
def delete_semi(id):
|
||||
try:
|
||||
SemiInboundService.delete_inbound(id)
|
||||
return jsonify({"code": 200, "msg": "删除成功"})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
@ -1,2 +1,13 @@
|
||||
# app/models/__init__.py
|
||||
|
||||
# 1. 基础物料
|
||||
from app.models.material import MaterialBase
|
||||
from app.models.stock import StockBuy
|
||||
|
||||
# 2. 采购入库 (指向新路径)
|
||||
from app.models.inbound.buy import StockBuy
|
||||
|
||||
# 3. 半成品入库 (指向新路径)
|
||||
from app.models.inbound.semi import StockSemi
|
||||
|
||||
# 如果有其他模型 (比如 sys_user 等),保留它们
|
||||
# from app.models.sys_user import SysUser
|
||||
0
inventory-backend/app/models/inbound/__init__.py
Normal file
0
inventory-backend/app/models/inbound/__init__.py
Normal file
@ -1,4 +1,4 @@
|
||||
# app/models/stock.py
|
||||
# app/models/buy.py
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
143
inventory-backend/app/models/inbound/semi.py
Normal file
143
inventory-backend/app/models/inbound/semi.py
Normal file
@ -0,0 +1,143 @@
|
||||
# app/models/inbound/semi.py
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class StockSemi(db.Model):
|
||||
"""
|
||||
半成品入库库存表
|
||||
对应数据库表: stock_semi
|
||||
"""
|
||||
__tablename__ = 'stock_semi'
|
||||
|
||||
# =========================================================
|
||||
# 1. 基础字段 (Strictly matching SQL Schema)
|
||||
# =========================================================
|
||||
|
||||
# 主键
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# 外键关联 material_base 表
|
||||
base_id = db.Column(db.Integer, db.ForeignKey('material_base.id'), nullable=False)
|
||||
|
||||
# 身份标识
|
||||
sku = db.Column(db.String(100))
|
||||
|
||||
# SQL字段名为 production_date, 对应前端的 "入库日期/生产日期"
|
||||
production_date = db.Column(db.Date)
|
||||
|
||||
barcode = db.Column(db.String(100)) # 条码
|
||||
serial_number = db.Column(db.String(100)) # 序列号
|
||||
|
||||
# 注意:提供的 SQL 中 stock_semi 没有 batch_number 字段,这里不定义,以免报错。
|
||||
# 如果后续数据库加上了该字段,请取消下方注释:
|
||||
# batch_number = db.Column(db.String(100))
|
||||
|
||||
# --- 数量 ---
|
||||
in_quantity = db.Column(db.Numeric(19, 4), default=0)
|
||||
stock_quantity = db.Column(db.Numeric(19, 4), default=0)
|
||||
available_quantity = db.Column(db.Numeric(19, 4), default=0)
|
||||
|
||||
# --- 状态与位置 ---
|
||||
status = db.Column(db.String(50)) # 在库/出库/损耗
|
||||
warehouse_location = db.Column(db.String(100)) # 仓库位
|
||||
|
||||
# =========================================================
|
||||
# 2. 半成品特有字段 (SQL 字段映射)
|
||||
# =========================================================
|
||||
|
||||
# BOM 相关
|
||||
# 数据库列名: bom_id, Python属性: bom_code (为了适配前端习惯)
|
||||
bom_code = db.Column('bom_id', db.String(100))
|
||||
bom_version = db.Column(db.String(50))
|
||||
|
||||
# 工单 相关
|
||||
# 数据库列名: work_order_id, Python属性: work_order_code
|
||||
work_order_code = db.Column('work_order_id', db.String(100))
|
||||
|
||||
# 成本 相关
|
||||
raw_material_cost = db.Column(db.Numeric(19, 4), default=0) # 原材料成本
|
||||
manual_cost = db.Column(db.Numeric(19, 4), default=0) # 手动/人工成本
|
||||
|
||||
# 生产信息
|
||||
# 数据库列名: producer_name, Python属性: production_manager
|
||||
production_manager = db.Column('producer_name', db.String(100))
|
||||
|
||||
# 生产起止时间 (SQL定义为 VARCHAR(255))
|
||||
production_time_range = db.Column(db.String(255))
|
||||
|
||||
# 质量与链接
|
||||
quality_status = db.Column(db.String(50)) # 质量状态
|
||||
quality_report_link = db.Column(db.Text) # 质量报告链接
|
||||
detail_link = db.Column(db.Text) # 详细信息链接
|
||||
|
||||
# =========================================================
|
||||
# 3. 关系定义
|
||||
# =========================================================
|
||||
# 建立与 MaterialBase 的关系
|
||||
# 注意:确保 MaterialBase 模型中定义了 back_populates='stock_semis'
|
||||
material = db.relationship('MaterialBase', back_populates='stock_semis')
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
序列化:将模型转换为字典,供API返回JSON使用
|
||||
在这里处理字段名称转换,确保前端能正确显示数据
|
||||
"""
|
||||
# 计算单件总成本 (原料 + 人工)
|
||||
raw_val = float(self.raw_material_cost or 0)
|
||||
man_val = float(self.manual_cost or 0)
|
||||
unit_total = raw_val + man_val
|
||||
|
||||
return {
|
||||
'id': self.id,
|
||||
'base_id': self.base_id,
|
||||
|
||||
# --- 级联基础信息 (防止 None 报错) ---
|
||||
'material_name': self.material.name if self.material else '',
|
||||
'spec_model': self.material.spec_model if self.material else '',
|
||||
'category': self.material.category if self.material else '',
|
||||
'unit': self.material.unit if self.material else '',
|
||||
'material_type': self.material.material_type if self.material else '',
|
||||
|
||||
# --- 实体信息 ---
|
||||
'sku': self.sku,
|
||||
# 将 production_date 映射回前端通用的 inbound_date
|
||||
'inbound_date': self.production_date.strftime('%Y-%m-%d') if self.production_date else '',
|
||||
'barcode': self.barcode,
|
||||
'serial_number': self.serial_number,
|
||||
# 'batch_number': self.batch_number, # SQL无此字段,暂不返回
|
||||
'warehouse_loc': self.warehouse_location,
|
||||
'status': self.status,
|
||||
|
||||
# --- 数量 (转为float防止json序列化报错) ---
|
||||
'in_quantity': float(self.in_quantity or 0),
|
||||
'qty_inbound': float(self.in_quantity or 0), # 兼容字段
|
||||
'stock_quantity': float(self.stock_quantity or 0),
|
||||
'qty_stock': float(self.stock_quantity or 0), # 兼容字段
|
||||
'available_quantity': float(self.available_quantity or 0),
|
||||
'qty_available': float(self.available_quantity or 0), # 兼容字段
|
||||
|
||||
# --- 半成品特有数据 ---
|
||||
'bom_code': self.bom_code,
|
||||
'bom_version': self.bom_version,
|
||||
'work_order_code': self.work_order_code,
|
||||
|
||||
'raw_material_cost': raw_val,
|
||||
'manual_cost': man_val,
|
||||
'unit_total_cost': unit_total, # 前端展示总成本用
|
||||
|
||||
'production_manager': self.production_manager,
|
||||
|
||||
# 时间范围 (SQL存的是字符串,直接返回即可,或者根据需要拆分)
|
||||
# 如果 service 层存的是 "Start ~ End",这里直接返回
|
||||
'production_time_range': self.production_time_range,
|
||||
# 为了兼容前端分开的时间字段(如果有):
|
||||
'production_start_time': self.production_time_range.split(' ~ ')[
|
||||
0] if self.production_time_range and ' ~ ' in self.production_time_range else '',
|
||||
'production_end_time': self.production_time_range.split(' ~ ')[
|
||||
1] if self.production_time_range and ' ~ ' in self.production_time_range else '',
|
||||
|
||||
'quality_status': self.quality_status,
|
||||
'quality_report_link': self.quality_report_link,
|
||||
'detail_link': self.detail_link
|
||||
}
|
||||
@ -10,7 +10,7 @@ class MaterialBase(db.Model):
|
||||
"""
|
||||
__tablename__ = 'material_base'
|
||||
|
||||
# 1. 基础字段 (必须与 SQL 建表语句完全一致)
|
||||
# 1. 基础字段 (保持不变)
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(255), nullable=False, comment='基础信息名称')
|
||||
|
||||
@ -32,26 +32,31 @@ class MaterialBase(db.Model):
|
||||
manual_link = db.Column(db.Text, comment='通用说明书链接')
|
||||
product_image = db.Column(db.Text, comment='通用产品图链接')
|
||||
|
||||
# 启用状态 (注意:SQL中是 boolean)
|
||||
# 启用状态
|
||||
is_enabled = db.Column(db.Boolean, default=True, comment='是否启用')
|
||||
|
||||
# ============================================================
|
||||
# ⚠️ 注意:你之前提供的 SQL 建表语句中【没有】下面这两个时间字段。
|
||||
# 如果数据库里没有这两列,代码运行到这里会报错 (UndefinedColumn)。
|
||||
# 我先将其注释掉。如果你确认数据库已经 Alter Table 加了这两列,请取消注释。
|
||||
# 时间字段 (保持你原本的注释状态,以免报错)
|
||||
# ============================================================
|
||||
# create_time = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
# update_time = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# 【核心关联】
|
||||
# 关联采购库存表 (StockBuy),lazy='dynamic' 允许后续做 .count() 统计
|
||||
# 确保 app/models/stock.py 中有 back_populates='material'
|
||||
# ============================================================
|
||||
# 关联关系区域 (修改重点)
|
||||
# ============================================================
|
||||
|
||||
# 1. 关联采购库存 (StockBuy) - 保持不变
|
||||
# 注意:确保 app/models/inbound/buy.py 中的 StockBuy 定义了 back_populates='material'
|
||||
stock_buys = db.relationship('StockBuy', back_populates='material', lazy='dynamic')
|
||||
|
||||
# 2. 【新增】关联半成品库存 (StockSemi)
|
||||
# 注意:确保 app/models/inbound/semi.py 中的 StockSemi 定义了 back_populates='material'
|
||||
# 这样以后可以通过 material.stock_semis 来访问该物料下的所有半成品库存记录
|
||||
stock_semis = db.relationship('StockSemi', back_populates='material', lazy='dynamic')
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
序列化方法:将模型转换为字典,供API返回JSON使用
|
||||
这里是解决【前端表格空白】最关键的地方
|
||||
"""
|
||||
return {
|
||||
'id': self.id,
|
||||
@ -59,7 +64,7 @@ class MaterialBase(db.Model):
|
||||
'category': self.category,
|
||||
|
||||
# =========================================
|
||||
# 关键映射区 (解决前后端字段名不一致问题)
|
||||
# 关键映射区 (保持不变)
|
||||
# =========================================
|
||||
# 数据库叫 material_type -> 前端叫 type
|
||||
'type': self.material_type,
|
||||
@ -74,11 +79,9 @@ class MaterialBase(db.Model):
|
||||
'generalManual': self.manual_link,
|
||||
'generalImage': self.product_image,
|
||||
|
||||
# 状态处理:前端 Switch 通常接受 boolean 或 1/0
|
||||
# 数据库里的 true -> 返回 1 (启用)
|
||||
# 数据库里的 false/None -> 返回 0 (禁用)
|
||||
# 状态处理
|
||||
'isEnabled': 1 if self.is_enabled else 0,
|
||||
|
||||
# 如果上方注释了 create_time,这里也要注释,否则会报错
|
||||
# 时间字段保持注释
|
||||
# 'createTime': self.create_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(self, 'create_time') and self.create_time else None
|
||||
}
|
||||
@ -1,18 +1,62 @@
|
||||
# 文件路径: app/services/inbound/base_service.py
|
||||
|
||||
from app.extensions import db
|
||||
from app.models.material import MaterialBase
|
||||
from app.models.stock import StockBuy # 需要引入库存表做删除时的依赖检查
|
||||
|
||||
# ==============================================================================
|
||||
# ✅ 正确的引用方式
|
||||
# ==============================================================================
|
||||
from app.models.inbound.buy import StockBuy # 引用采购库存模型
|
||||
from app.models.inbound.semi import StockSemi # 引用半成品库存模型
|
||||
from sqlalchemy import or_
|
||||
import traceback
|
||||
|
||||
|
||||
class MaterialBaseService:
|
||||
"""
|
||||
基础物料服务层
|
||||
负责处理 MaterialBase 的增删改查及搜索逻辑
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def search_material(keyword):
|
||||
"""
|
||||
根据关键字搜索已启用的基础物料
|
||||
(供 /api/v1/inbound/base/search 接口调用)
|
||||
"""
|
||||
try:
|
||||
if not keyword:
|
||||
return []
|
||||
|
||||
# 搜索名称或规格型号,且必须是启用的
|
||||
query = MaterialBase.query.filter(
|
||||
MaterialBase.is_enabled == True,
|
||||
or_(
|
||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||
)
|
||||
).limit(20)
|
||||
|
||||
results = []
|
||||
for item in query.all():
|
||||
results.append({
|
||||
'id': item.id,
|
||||
'name': item.name,
|
||||
'spec': item.spec_model,
|
||||
'category': item.category,
|
||||
'unit': item.unit,
|
||||
'type': item.material_type,
|
||||
'status': '启用'
|
||||
})
|
||||
return results
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_list(page, limit, filters=None):
|
||||
"""
|
||||
获取基础信息列表
|
||||
:param page: 页码
|
||||
:param limit: 每页条数
|
||||
:param filters: 筛选条件字典 {keyword, category, type, isEnabled}
|
||||
获取基础信息列表 (带分页和筛选)
|
||||
"""
|
||||
try:
|
||||
query = MaterialBase.query
|
||||
@ -46,7 +90,6 @@ class MaterialBaseService:
|
||||
|
||||
except Exception as e:
|
||||
print(f"查询基础信息列表失败: {e}")
|
||||
# 生产环境建议记录日志
|
||||
return {"total": 0, "items": []}
|
||||
|
||||
@staticmethod
|
||||
@ -65,16 +108,16 @@ class MaterialBaseService:
|
||||
if exist:
|
||||
raise ValueError(f"已存在相同名称和规格的数据 (ID: {exist.id})")
|
||||
|
||||
# 2. 创建对象 (注意前端驼峰 -> 后端下划线映射)
|
||||
# 2. 创建对象
|
||||
new_material = MaterialBase(
|
||||
name=data['name'],
|
||||
spec_model=data['spec'], # 映射
|
||||
spec_model=data['spec'],
|
||||
category=data.get('category'),
|
||||
material_type=data.get('type'), # 映射
|
||||
material_type=data.get('type'),
|
||||
unit=data.get('unit'),
|
||||
visibility_level=data.get('visibilityLevel'), # 映射
|
||||
manual_link=data.get('generalManual'), # 映射
|
||||
product_image=data.get('generalImage'), # 映射
|
||||
visibility_level=data.get('visibilityLevel'),
|
||||
manual_link=data.get('generalManual'),
|
||||
product_image=data.get('generalImage'),
|
||||
is_enabled=True if data.get('isEnabled', 1) == 1 else False
|
||||
)
|
||||
|
||||
@ -94,7 +137,7 @@ class MaterialBaseService:
|
||||
if not material:
|
||||
raise ValueError("数据不存在")
|
||||
|
||||
# 更新字段 (仅更新传入的字段)
|
||||
# 更新字段
|
||||
if 'name' in data: material.name = data['name']
|
||||
if 'spec' in data: material.spec_model = data['spec']
|
||||
if 'category' in data: material.category = data['category']
|
||||
@ -116,19 +159,32 @@ class MaterialBaseService:
|
||||
|
||||
@staticmethod
|
||||
def delete_material(m_id):
|
||||
"""删除基础信息 (带依赖检查)"""
|
||||
"""
|
||||
删除基础信息 (带依赖检查)
|
||||
✅ 已升级:同时检查采购库(Buy)和半成品库(Semi)
|
||||
"""
|
||||
try:
|
||||
material = MaterialBase.query.get(m_id)
|
||||
if not material:
|
||||
raise ValueError("数据不存在")
|
||||
|
||||
# 1. 依赖检查:如果该基础信息已经在库存表(StockBuy)中使用,禁止物理删除
|
||||
# 这里假设 StockBuy 表有一个外键或字段指向 MaterialBase (e.g., base_id)
|
||||
usage_count = StockBuy.query.filter_by(base_id=m_id).count()
|
||||
if usage_count > 0:
|
||||
raise ValueError(f"无法删除:该基础信息已被 {usage_count} 条库存记录引用,请先清理库存或仅禁用此条目。")
|
||||
# 1. 依赖检查:采购入库引用
|
||||
buy_usage_count = StockBuy.query.filter_by(base_id=m_id).count()
|
||||
|
||||
# 2. 执行删除
|
||||
# 2. 依赖检查:半成品入库引用
|
||||
semi_usage_count = StockSemi.query.filter_by(base_id=m_id).count()
|
||||
|
||||
total_usage = buy_usage_count + semi_usage_count
|
||||
|
||||
if total_usage > 0:
|
||||
raise ValueError(
|
||||
f"无法删除:该基础物料正被使用中。\n"
|
||||
f"- 采购库存记录: {buy_usage_count} 条\n"
|
||||
f"- 半成品库存记录: {semi_usage_count} 条\n"
|
||||
f"请先清理相关库存或仅‘禁用’此条目。"
|
||||
)
|
||||
|
||||
# 3. 执行删除
|
||||
db.session.delete(material)
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
from app.extensions import db
|
||||
from app.models.inbound.buy import StockBuy
|
||||
from app.models.material import MaterialBase
|
||||
from app.models.stock import StockBuy
|
||||
from datetime import datetime
|
||||
from sqlalchemy import or_, func # 引入 func 用于聚合计算
|
||||
from sqlalchemy import or_, func
|
||||
import traceback
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,339 @@
|
||||
from app.extensions import db
|
||||
from app.models.material import MaterialBase
|
||||
from app.models.inbound.semi import StockSemi
|
||||
from datetime import datetime
|
||||
from sqlalchemy import or_, func
|
||||
import traceback
|
||||
|
||||
|
||||
class SemiInboundService:
|
||||
@staticmethod
|
||||
def search_base_material(keyword):
|
||||
"""
|
||||
搜索基础物料,逻辑与采购入库基本一致。
|
||||
如果需要只搜索'半成品'类型的物料,可以在 filter 中增加条件。
|
||||
"""
|
||||
try:
|
||||
if not keyword:
|
||||
return []
|
||||
|
||||
query = MaterialBase.query.filter(
|
||||
MaterialBase.is_enabled == True,
|
||||
or_(
|
||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||
)
|
||||
).limit(20)
|
||||
|
||||
results = []
|
||||
for item in query.all():
|
||||
results.append({
|
||||
'id': item.id,
|
||||
'name': item.name,
|
||||
'spec': item.spec_model,
|
||||
'category': item.category,
|
||||
'unit': item.unit,
|
||||
'type': item.material_type,
|
||||
'status': '启用'
|
||||
})
|
||||
return results
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def handle_inbound(data):
|
||||
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} 的基础物料不存在")
|
||||
|
||||
# 1. 处理入库日期
|
||||
in_date_val = datetime.utcnow().date()
|
||||
if data.get('in_date'):
|
||||
try:
|
||||
date_str = str(data['in_date'])[:10]
|
||||
in_date_val = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 2. 处理生产时间 (前端传来的是字符串 "YYYY-MM-DD HH:mm:ss")
|
||||
p_start = None
|
||||
p_end = None
|
||||
if data.get('production_start_time'):
|
||||
try:
|
||||
p_start = datetime.strptime(str(data['production_start_time']), '%Y-%m-%d %H:%M:%S')
|
||||
except:
|
||||
pass
|
||||
if data.get('production_end_time'):
|
||||
try:
|
||||
p_end = datetime.strptime(str(data['production_end_time']), '%Y-%m-%d %H:%M:%S')
|
||||
except:
|
||||
pass
|
||||
|
||||
# 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. 创建记录
|
||||
new_stock = StockSemi(
|
||||
base_id=material.id,
|
||||
sku=data.get('sku'),
|
||||
in_date=in_date_val,
|
||||
|
||||
# 标识信息
|
||||
serial_number=data.get('serial_number'),
|
||||
batch_number=data.get('batch_number'),
|
||||
barcode=data.get('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,
|
||||
|
||||
# 成本信息 (半成品特有)
|
||||
raw_material_cost=raw_cost,
|
||||
manual_cost=manual_cost,
|
||||
unit_total_cost=unit_total_cost,
|
||||
total_price=total_value, # 数据库字段可能复用 total_price 或叫 total_value
|
||||
|
||||
# 链接
|
||||
quality_report_link=data.get('quality_report_link'),
|
||||
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()
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def update_inbound(stock_id, data):
|
||||
try:
|
||||
print(f"----- UPDATE SEMI DEBUG: ID={stock_id} -----")
|
||||
|
||||
stock = StockSemi.query.get(stock_id)
|
||||
if not stock:
|
||||
raise ValueError("记录不存在")
|
||||
|
||||
# 1. 简单字段映射
|
||||
field_mapping = {
|
||||
'sku': 'sku',
|
||||
'barcode': 'barcode',
|
||||
'warehouse_location': 'warehouse_location',
|
||||
'serial_number': 'serial_number',
|
||||
'batch_number': 'batch_number',
|
||||
'status': 'status',
|
||||
'quality_status': 'quality_status', # 质量状态
|
||||
|
||||
# 生产信息
|
||||
'bom_code': 'bom_code',
|
||||
'bom_version': 'bom_version',
|
||||
'work_order_code': 'work_order_code',
|
||||
'production_manager': 'production_manager',
|
||||
'quality_report_link': 'quality_report_link',
|
||||
'detail_link': 'detail_link'
|
||||
}
|
||||
|
||||
for frontend_key, db_attr in field_mapping.items():
|
||||
if frontend_key in data:
|
||||
setattr(stock, db_attr, data[frontend_key])
|
||||
|
||||
# 2. 处理时间更新
|
||||
if 'production_start_time' in data:
|
||||
try:
|
||||
if data['production_start_time']:
|
||||
stock.production_start_time = datetime.strptime(str(data['production_start_time']),
|
||||
'%Y-%m-%d %H:%M:%S')
|
||||
else:
|
||||
stock.production_start_time = None
|
||||
except:
|
||||
pass
|
||||
|
||||
if 'production_end_time' in data:
|
||||
try:
|
||||
if data['production_end_time']:
|
||||
stock.production_end_time = datetime.strptime(str(data['production_end_time']),
|
||||
'%Y-%m-%d %H:%M:%S')
|
||||
else:
|
||||
stock.production_end_time = None
|
||||
except:
|
||||
pass
|
||||
|
||||
# 3. 处理数量和成本变更联动
|
||||
qty_changed = False
|
||||
cost_changed = False
|
||||
|
||||
# 更新数量
|
||||
if 'in_quantity' in data:
|
||||
new_qty = float(data['in_quantity'])
|
||||
old_qty = float(stock.in_quantity)
|
||||
if new_qty != old_qty:
|
||||
diff = new_qty - old_qty
|
||||
stock.in_quantity = new_qty
|
||||
stock.stock_quantity = float(stock.stock_quantity) + diff
|
||||
stock.available_quantity = float(stock.available_quantity) + diff
|
||||
qty_changed = True
|
||||
|
||||
# 更新成本 (原材料 or 人工)
|
||||
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:
|
||||
stock.unit_total_cost = float(stock.raw_material_cost) + float(stock.manual_cost)
|
||||
|
||||
if cost_changed or qty_changed:
|
||||
stock.total_price = float(stock.in_quantity) * float(stock.unit_total_cost)
|
||||
|
||||
db.session.commit()
|
||||
print("----- UPDATE SEMI SUCCESS -----")
|
||||
return stock
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"----- UPDATE SEMI FAILED: {str(e)} -----")
|
||||
traceback.print_exc()
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def delete_inbound(stock_id):
|
||||
try:
|
||||
stock = StockSemi.query.get(stock_id)
|
||||
if not stock:
|
||||
raise ValueError("记录不存在")
|
||||
db.session.delete(stock)
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def get_list(page, limit, keyword=None):
|
||||
try:
|
||||
# 1. 查询分页数据
|
||||
query = db.session.query(StockSemi).outerjoin(MaterialBase, StockSemi.base_id == MaterialBase.id)
|
||||
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
or_(
|
||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||
MaterialBase.spec_model.ilike(f'%{keyword}%'),
|
||||
StockSemi.batch_number.ilike(f'%{keyword}%'),
|
||||
StockSemi.serial_number.ilike(f'%{keyword}%'),
|
||||
StockSemi.sku.ilike(f'%{keyword}%'),
|
||||
# 增加半成品特有的搜索字段
|
||||
StockSemi.work_order_code.ilike(f'%{keyword}%'),
|
||||
StockSemi.bom_code.ilike(f'%{keyword}%')
|
||||
)
|
||||
)
|
||||
|
||||
pagination = query.order_by(StockSemi.id.desc()).paginate(page=page, per_page=limit, error_out=False)
|
||||
|
||||
# 2. 聚合统计 (计算该物料的总库存)
|
||||
current_items = pagination.items
|
||||
base_ids = list(set([item.base_id for item in current_items if item.base_id]))
|
||||
|
||||
stock_map = {}
|
||||
if base_ids:
|
||||
aggregates = db.session.query(
|
||||
StockSemi.base_id,
|
||||
func.sum(StockSemi.stock_quantity).label('total_stock'),
|
||||
func.sum(StockSemi.available_quantity).label('total_avail')
|
||||
).filter(StockSemi.base_id.in_(base_ids)).group_by(StockSemi.base_id).all()
|
||||
|
||||
for agg in aggregates:
|
||||
stock_map[agg.base_id] = {
|
||||
'total_stock': float(agg.total_stock or 0),
|
||||
'total_avail': float(agg.total_avail or 0)
|
||||
}
|
||||
|
||||
items = []
|
||||
for item in current_items:
|
||||
mat_name = item.material.name if item.material else '未知物料'
|
||||
mat_spec = item.material.spec_model if item.material else ''
|
||||
mat_cat = item.material.category if item.material else ''
|
||||
mat_unit = item.material.unit if item.material else ''
|
||||
mat_type = item.material.material_type if item.material else ''
|
||||
|
||||
stats = stock_map.get(item.base_id, {'total_stock': 0, 'total_avail': 0})
|
||||
|
||||
d = {
|
||||
'id': item.id,
|
||||
'base_id': item.base_id,
|
||||
'material_name': mat_name,
|
||||
'spec_model': mat_spec,
|
||||
'category': mat_cat,
|
||||
'unit': mat_unit,
|
||||
'material_type': mat_type,
|
||||
|
||||
'sku': item.sku,
|
||||
'inbound_date': str(item.in_date) if item.in_date else '',
|
||||
'barcode': item.barcode,
|
||||
'serial_number': item.serial_number,
|
||||
'batch_number': item.batch_number,
|
||||
'status': item.status,
|
||||
'quality_status': item.quality_status, # 半成品使用质量状态
|
||||
|
||||
'qty_inbound': float(item.in_quantity or 0),
|
||||
'qty_stock': float(item.stock_quantity or 0),
|
||||
'qty_available': float(item.available_quantity or 0),
|
||||
|
||||
'sum_stock': stats['total_stock'],
|
||||
'sum_available': stats['total_avail'],
|
||||
|
||||
'warehouse_loc': item.warehouse_location,
|
||||
|
||||
# 半成品特有字段返回
|
||||
'bom_code': item.bom_code,
|
||||
'bom_version': item.bom_version,
|
||||
'work_order_code': item.work_order_code,
|
||||
'raw_material_cost': float(item.raw_material_cost or 0),
|
||||
'manual_cost': float(item.manual_cost or 0),
|
||||
'unit_total_cost': float(item.unit_total_cost or 0),
|
||||
'production_manager': item.production_manager,
|
||||
'production_start_time': str(item.production_start_time) if item.production_start_time else '',
|
||||
'production_end_time': str(item.production_end_time) if item.production_end_time else '',
|
||||
|
||||
'quality_report_link': item.quality_report_link,
|
||||
'detail_link': item.detail_link,
|
||||
'remark': item.remark
|
||||
}
|
||||
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": []}
|
||||
@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/jetbrains://idea/navigate/reference?project=inventory-web&path=public%2Firis.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>inventory-web</title>
|
||||
</head>
|
||||
|
||||
BIN
inventory-web/public/iris.png
Normal file
BIN
inventory-web/public/iris.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,45 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 1. 获取列表
|
||||
export function getSemiList(params: any) {
|
||||
return request({
|
||||
url: '/inbound/semi/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 2. 新增入库
|
||||
export function createSemiInbound(data: any) {
|
||||
return request({
|
||||
url: '/inbound/semi/submit',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 3. 更新入库
|
||||
export function updateSemiInbound(id: number, data: any) {
|
||||
return request({
|
||||
url: `/inbound/semi/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 4. 删除入库
|
||||
export function deleteSemiInbound(id: number) {
|
||||
return request({
|
||||
url: `/inbound/semi/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 5. 搜索基础物料
|
||||
export function searchMaterialBase(keyword: string) {
|
||||
return request({
|
||||
url: '/inbound/semi/search-base',
|
||||
method: 'get',
|
||||
params: { keyword }
|
||||
})
|
||||
}
|
||||
@ -1,11 +1,883 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="semi-module">
|
||||
<div class="header-tools">
|
||||
<div class="left-tools">
|
||||
<el-input
|
||||
v-model="queryParams.keyword"
|
||||
placeholder="🔍 搜索物料 / 批号 / SN / 工单号 / BOM编号..."
|
||||
class="search-input"
|
||||
clearable
|
||||
@clear="fetchData"
|
||||
@keyup.enter="fetchData"
|
||||
>
|
||||
<template #append>
|
||||
<el-button :icon="Search" @click="fetchData" />
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div class="right-tools">
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate" class="action-btn">半成品入库登记</el-button>
|
||||
<el-button :icon="Refresh" @click="fetchData" class="action-btn">刷新</el-button>
|
||||
|
||||
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
|
||||
<template #reference>
|
||||
<el-button :icon="Setting" class="action-btn">表头</el-button>
|
||||
</template>
|
||||
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
|
||||
<div class="col-group-title">基础信息</div>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12" v-for="c in baseColumns" :key="c.prop"><el-checkbox :label="c.prop">{{ c.label }}</el-checkbox></el-col>
|
||||
</el-row>
|
||||
<div class="col-group-title" style="margin-top:10px">生产与库存</div>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12" v-for="c in stockColumns" :key="c.prop"><el-checkbox :label="c.prop">{{ c.label }}</el-checkbox></el-col>
|
||||
</el-row>
|
||||
</el-checkbox-group>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
class="modern-table"
|
||||
highlight-current-row
|
||||
header-cell-class-name="table-header-gray"
|
||||
>
|
||||
<template v-for="col in allColumns" :key="col.prop">
|
||||
<el-table-column
|
||||
v-if="visibleColumnProps.includes(col.prop)"
|
||||
:prop="col.prop"
|
||||
:label="col.label"
|
||||
:min-width="col.minWidth || '140'"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="scope" v-if="['serial_number', 'batch_number'].includes(col.prop)">
|
||||
<span v-if="scope.row[col.prop]" :class="col.prop === 'serial_number' ? 'tag-sn' : 'tag-bn'">
|
||||
{{ scope.row[col.prop] }}
|
||||
</span>
|
||||
<span v-else class="text-placeholder">-</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<template #default="scope" v-else-if="col.prop === 'qty_stock'">
|
||||
<span class="stock-num">{{ scope.row.sum_stock }}</span>
|
||||
<el-tag size="small" type="info" effect="plain" class="sum-tag">总</el-tag>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="col.prop === 'qty_available'">
|
||||
<span class="avail-num">{{ scope.row.sum_available }}</span>
|
||||
<el-tag size="small" type="info" effect="plain" class="sum-tag">总</el-tag>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="col.prop === 'status'">
|
||||
<el-tag :type="getStatusType(scope.row.status)" effect="light" round>
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="col.prop === 'quality_status'">
|
||||
<el-tag :type="getQualityType(scope.row.quality_status)" effect="dark" size="small">
|
||||
{{ scope.row.quality_status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="['quality_report_link', 'detail_link'].includes(col.prop)">
|
||||
<el-link v-if="scope.row[col.prop]" type="primary" :href="scope.row[col.prop]" target="_blank" :underline="false">
|
||||
<el-icon><Link /></el-icon> 查看
|
||||
</el-link>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="['raw_material_cost', 'manual_cost'].includes(col.prop)">
|
||||
<span class="money-text">{{ formatMoney(scope.row[col.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="default" @click="handleUpdate(row)">编辑</el-button>
|
||||
<el-popconfirm title="确定删除该条记录吗?不可恢复。" @confirm="handleDelete(row)" width="220">
|
||||
<template #reference>
|
||||
<el-button link type="danger" size="default">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
class="pagination-bar"
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[15, 30, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogStatus === 'create' ? '新增半成品入库' : '编辑半成品信息'"
|
||||
width="1050px"
|
||||
top="5vh"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
class="stylish-dialog"
|
||||
>
|
||||
<el-form :model="form" label-width="110px" ref="formRef" :rules="rules" size="large" class="stylish-form">
|
||||
|
||||
<div class="form-card basic-card">
|
||||
<div class="card-title">
|
||||
<el-icon class="icon"><Box /></el-icon>
|
||||
<span>1. 基础信息</span>
|
||||
<span class="sub-title" v-if="dialogStatus === 'create'"> (请先搜索选择半成品物料)</span>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<el-row :gutter="24" v-if="dialogStatus === 'create'" style="margin-bottom: 20px;">
|
||||
<el-col :span="14">
|
||||
<el-form-item label="物料搜索" prop="base_id" class="highlight-label">
|
||||
<el-select
|
||||
v-model="form.base_id"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
placeholder="输入名称 / 规格型号进行模糊搜索..."
|
||||
:remote-method="handleSearchMaterial"
|
||||
:loading="searchLoading"
|
||||
style="width: 100%"
|
||||
@change="onMaterialSelected"
|
||||
size="large"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in materialOptions"
|
||||
:key="item.id"
|
||||
:label="item.name + ' [' + item.spec + ']'"
|
||||
:value="item.id"
|
||||
>
|
||||
<div class="option-item">
|
||||
<span class="opt-name">{{ item.name }}</span>
|
||||
<span class="opt-spec">{{ item.spec }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="10">
|
||||
<div class="info-alert">
|
||||
<el-icon><InfoFilled /></el-icon> 仅展示状态为“启用”的基础物料
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="read-only-grid">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" disabled class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="规格型号"><el-input v-model="form.spec_model" disabled class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" disabled class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" disabled class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" disabled class="is-text-view" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-card inbound-card">
|
||||
<div class="card-title">
|
||||
<el-icon class="icon"><House /></el-icon>
|
||||
<span>2. 入库详情</span>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="6"><el-form-item label="编码/SKU" prop="sku"><el-input v-model="form.sku" placeholder="选填" /></el-form-item></el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="入库日期" prop="in_date">
|
||||
<el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6"><el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" placeholder="扫描条码" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" placeholder="例如: B-01-01" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="identity-panel">
|
||||
<el-row>
|
||||
<el-col :span="24" style="margin-bottom: 12px;">
|
||||
<el-radio-group v-model="entryMode" @change="handleEntryModeChange" :disabled="modeLocked" class="custom-radio-group">
|
||||
<el-radio-button label="batch">按批号入库 (Batch)</el-radio-button>
|
||||
<el-radio-button label="serial">按序列号入库 (SN)</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span v-if="modeLocked" class="locked-msg"><el-icon><Lock /></el-icon> 根据历史记录已锁定入库方式</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="批号" prop="batch_number">
|
||||
<el-input
|
||||
v-model="form.batch_number"
|
||||
:placeholder="entryMode === 'batch' ? '系统生成...' : '不可用'"
|
||||
:disabled="entryMode === 'serial'"
|
||||
clearable
|
||||
>
|
||||
<template #prefix><span class="prefix-tag bn">BN</span></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="序列号" prop="serial_number">
|
||||
<el-input
|
||||
v-model="form.serial_number"
|
||||
:placeholder="entryMode === 'serial' ? '请扫描 SN...' : '不可用'"
|
||||
:disabled="entryMode === 'batch'"
|
||||
clearable
|
||||
>
|
||||
<template #prefix><span class="prefix-tag sn">SN</span></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="24" style="margin-top: 15px;">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="入库数量" prop="in_quantity">
|
||||
<el-input-number v-model="form.in_quantity" :min="1" controls-position="right" style="width:100%" class="strong-input" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<template v-if="dialogStatus === 'update'">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="当前库存" prop="stock_quantity">
|
||||
<el-input-number v-model="form.stock_quantity" disabled style="width:100%" :controls="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="当前可用" prop="available_quantity">
|
||||
<el-input-number v-model="form.available_quantity" disabled style="width:100%" :controls="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="库存状态" prop="status">
|
||||
<el-select v-model="form.status" style="width:100%">
|
||||
<el-option label="在库" value="在库" />
|
||||
<el-option label="出库" value="出库" />
|
||||
<el-option label="损耗" value="损耗" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-form-item label="质量状态" prop="quality_status">
|
||||
<el-select v-model="form.quality_status" style="width:100%">
|
||||
<el-option label="待检" value="待检"><span style="color:#909399">⚪ 待检</span></el-option>
|
||||
<el-option label="合格" value="合格"><span style="color:#67C23A">🟢 合格</span></el-option>
|
||||
<el-option label="不合格" value="不合格"><span style="color:#F56C6C">🔴 不合格</span></el-option>
|
||||
<el-option label="返修中" value="返修中"><span style="color:#E6A23C">🟠 返修中</span></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12"><el-form-item label="质量报告" prop="quality_report_link"><el-input v-model="form.quality_report_link" placeholder="报告链接 URL" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-card production-card">
|
||||
<div class="card-title">
|
||||
<el-icon class="icon"><Setting /></el-icon>
|
||||
<span>3. 生产与成本信息</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<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-row>
|
||||
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8"><el-form-item label="生产负责人"><el-input v-model="form.production_manager" /></el-form-item></el-col>
|
||||
<el-col :span="16">
|
||||
<el-form-item label="生产时间">
|
||||
<el-date-picker
|
||||
v-model="form.production_time_range"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="divider-text">成本核算 (单件)</div>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="原材料成本">
|
||||
<el-input-number v-model="form.raw_material_cost" :precision="2" controls-position="right" style="width:100%">
|
||||
<template #prefix>¥</template>
|
||||
</el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="手动/工时">
|
||||
<el-input-number v-model="form.manual_cost" :precision="2" controls-position="right" style="width:100%">
|
||||
<template #prefix>¥</template>
|
||||
</el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="单件总成本">
|
||||
<el-input-number v-model="form.unit_total_cost" :precision="2" disabled :controls="false" style="width:100%" class="total-price-input">
|
||||
<template #prefix>¥</template>
|
||||
</el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="24" style="margin-top:10px">
|
||||
<el-col :span="24"><el-form-item label="详情链接"><el-input v-model="form.detail_link" placeholder="外部生产系统详情页 http://" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="visible = false" size="large">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitForm" size="large" class="confirm-btn">
|
||||
{{ dialogStatus === 'create' ? '确认入库' : '保存修改' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch, computed } from 'vue'
|
||||
import { Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
// 假设这是半成品入库的API文件
|
||||
import {
|
||||
getSemiList,
|
||||
createSemiInbound,
|
||||
updateSemiInbound,
|
||||
deleteSemiInbound,
|
||||
searchMaterialBase
|
||||
} from '@/api/inbound/semi'
|
||||
|
||||
// ------------------------------------
|
||||
// 状态与变量
|
||||
// ------------------------------------
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const visible = ref(false)
|
||||
const searchLoading = ref(false)
|
||||
const dialogStatus = ref<'create' | 'update'>('create')
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const formRef = ref()
|
||||
const queryParams = reactive({ page: 1, pageSize: 15, keyword: '' })
|
||||
const materialOptions = ref<any[]>([])
|
||||
|
||||
const entryMode = ref('batch')
|
||||
const modeLocked = ref(false)
|
||||
|
||||
// 列定义
|
||||
const baseColumns = [
|
||||
{ prop: 'material_name', label: '名称' },
|
||||
{ prop: 'category', label: '类别' },
|
||||
{ prop: 'material_type', label: '类型' },
|
||||
{ prop: 'spec_model', label: '规格型号' },
|
||||
{ prop: 'unit', label: '单位' },
|
||||
]
|
||||
|
||||
// 半成品特有的列定义
|
||||
const stockColumns = [
|
||||
{ prop: 'id', label: 'ID', minWidth: '60' },
|
||||
{ prop: 'base_id', label: 'BaseID', minWidth: '80' },
|
||||
{ prop: 'sku', label: 'SKU', minWidth: '120' },
|
||||
{ prop: 'inbound_date', label: '入库日期', minWidth: '120' },
|
||||
{ prop: 'barcode', label: '条码', minWidth: '120' },
|
||||
{ prop: 'serial_number', label: '序列号', minWidth: '150' },
|
||||
{ prop: 'batch_number', label: '批号', minWidth: '150' },
|
||||
{ prop: 'status', label: '状态', minWidth: '100' },
|
||||
{ prop: 'quality_status', label: '质量状态', minWidth: '100' }, // 对应 inspection_status
|
||||
{ prop: 'qty_inbound', label: '入库量', minWidth: '100' },
|
||||
{ prop: 'qty_stock', label: '库存数', minWidth: '100' },
|
||||
{ prop: 'qty_available', label: '可用数', minWidth: '100' },
|
||||
{ prop: 'warehouse_loc', label: '库位', minWidth: '120' },
|
||||
// 新增字段
|
||||
{ prop: 'bom_code', label: 'BOM编号', minWidth: '120' },
|
||||
{ prop: 'bom_version', label: 'BOM版本', minWidth: '90' },
|
||||
{ prop: 'work_order_code', label: '工单号', minWidth: '120' },
|
||||
{ prop: 'raw_material_cost', label: '原料成本', minWidth: '100' },
|
||||
{ prop: 'manual_cost', label: '人工成本', minWidth: '100' },
|
||||
{ prop: 'production_manager', label: '生产负责人', minWidth: '100' },
|
||||
{ prop: 'production_start_time', label: '生产开始', minWidth: '160' },
|
||||
{ prop: 'production_end_time', label: '生产结束', minWidth: '160' },
|
||||
{ prop: 'quality_report_link', label: '质量报告', minWidth: '100' },
|
||||
{ prop: 'detail_link', label: '详情链接', minWidth: '100' },
|
||||
]
|
||||
|
||||
const allColumns = [...baseColumns, ...stockColumns]
|
||||
|
||||
// 表头持久化
|
||||
const STORAGE_KEY = 'stock_semi_visible_columns'
|
||||
const defaultColumns = [
|
||||
'material_name', 'spec_model', 'unit',
|
||||
'inbound_date', 'serial_number', 'batch_number', 'status', 'quality_status',
|
||||
'bom_code', 'work_order_code', 'qty_stock', 'qty_available'
|
||||
]
|
||||
|
||||
const getSavedColumns = () => {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY)
|
||||
return saved ? JSON.parse(saved) : defaultColumns
|
||||
} catch (e) {
|
||||
return defaultColumns
|
||||
}
|
||||
}
|
||||
|
||||
const visibleColumnProps = ref(getSavedColumns())
|
||||
|
||||
watch(visibleColumnProps, (newVal) => {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(newVal))
|
||||
}, { deep: true })
|
||||
|
||||
|
||||
const form = reactive({
|
||||
id: undefined,
|
||||
base_id: undefined as number | undefined,
|
||||
material_name: '', spec_model: '', category: '', unit: '', material_type: '',
|
||||
sku: '', barcode: '', in_date: '',
|
||||
serial_number: '', batch_number: '',
|
||||
status: '在库',
|
||||
quality_status: '合格', // 原 inspection_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: [] as string[], // [start, end]
|
||||
quality_report_link: '',
|
||||
detail_link: ''
|
||||
})
|
||||
|
||||
// ------------------------------------
|
||||
// 逻辑校验规则
|
||||
// ------------------------------------
|
||||
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
|
||||
if (rule.field === 'batch_number' && row.batch_number === value) return true
|
||||
return false
|
||||
})
|
||||
if (isDuplicate) {
|
||||
callback(new Error('编号重复'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const validateIdentity = (rule: any, value: any, callback: any) => {
|
||||
if (entryMode.value === 'serial' && !form.serial_number && rule.field === 'serial_number') {
|
||||
callback(new Error('SN必填'))
|
||||
} else if (entryMode.value === 'batch' && !form.batch_number && rule.field === 'batch_number') {
|
||||
callback(new Error('批号必填'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = {
|
||||
base_id: [{ required: true, message: '请选择物料', trigger: 'change' }],
|
||||
in_quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }],
|
||||
serial_number: [{ validator: validateIdentity, trigger: 'blur' }, { validator: validateUnique, trigger: 'blur' }],
|
||||
batch_number: [{ validator: validateIdentity, trigger: 'blur' }, { validator: validateUnique, trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// 核心逻辑函数
|
||||
// ------------------------------------
|
||||
const checkHistoryAndSetMode = async (baseId: number) => {
|
||||
try {
|
||||
const res: any = await getSemiList({ page: 1, pageSize: 1000 })
|
||||
const allItems = res.data.items || []
|
||||
const historyItems = allItems.filter((item: any) => item.base_id === baseId)
|
||||
|
||||
if (historyItems.length > 0) {
|
||||
modeLocked.value = true
|
||||
const latest = historyItems.sort((a: any, b: any) => b.id - a.id)[0]
|
||||
if (latest.serial_number) {
|
||||
entryMode.value = 'serial'
|
||||
form.serial_number = ''
|
||||
form.batch_number = ''
|
||||
} else {
|
||||
entryMode.value = 'batch'
|
||||
form.serial_number = ''
|
||||
const lastBatch = latest.batch_number || '000000'
|
||||
form.batch_number = incrementBatchNumber(lastBatch)
|
||||
}
|
||||
} else {
|
||||
modeLocked.value = false
|
||||
entryMode.value = 'batch'
|
||||
form.batch_number = '000001'
|
||||
}
|
||||
|
||||
if(formRef.value) {
|
||||
formRef.value.clearValidate('serial_number')
|
||||
formRef.value.clearValidate('batch_number')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
modeLocked.value = false
|
||||
entryMode.value = 'batch'
|
||||
form.batch_number = '000001'
|
||||
}
|
||||
}
|
||||
|
||||
const incrementBatchNumber = (batchStr: string) => {
|
||||
if (!batchStr || !/^\d+$/.test(batchStr)) return '000001'
|
||||
const num = parseInt(batchStr, 10)
|
||||
return (num + 1).toString().padStart(6, '0')
|
||||
}
|
||||
|
||||
const handleEntryModeChange = (val: string) => {
|
||||
if (val === 'batch') {
|
||||
form.serial_number = ''
|
||||
form.batch_number = '000001'
|
||||
if(formRef.value) formRef.value.clearValidate('serial_number')
|
||||
} else {
|
||||
form.batch_number = ''
|
||||
if(formRef.value) formRef.value.clearValidate('batch_number')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearchMaterial = async (query: string) => {
|
||||
if (query) {
|
||||
searchLoading.value = true
|
||||
try {
|
||||
const res: any = await searchMaterialBase(query)
|
||||
materialOptions.value = res.data || []
|
||||
} finally {
|
||||
searchLoading.value = false
|
||||
}
|
||||
} else {
|
||||
materialOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const onMaterialSelected = (val: number) => {
|
||||
const item = materialOptions.value.find(i => i.id === val)
|
||||
if (item) {
|
||||
form.material_name = item.name
|
||||
form.spec_model = item.spec
|
||||
form.category = item.category
|
||||
form.unit = item.unit
|
||||
form.material_type = item.type
|
||||
checkHistoryAndSetMode(item.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 自动计算单件总成本
|
||||
watch(() => [form.raw_material_cost, form.manual_cost], () => {
|
||||
form.unit_total_cost = Number((form.raw_material_cost + form.manual_cost).toFixed(2))
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await getSemiList(queryParams)
|
||||
tableData.value = res.data.items || []
|
||||
total.value = res.data.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogStatus.value = 'create'
|
||||
resetForm()
|
||||
form.in_date = dayjs().format('YYYY-MM-DD')
|
||||
modeLocked.value = false
|
||||
entryMode.value = 'batch'
|
||||
form.batch_number = ''
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
const handleUpdate = (row: any) => {
|
||||
dialogStatus.value = 'update'
|
||||
resetForm()
|
||||
modeLocked.value = true
|
||||
|
||||
form.id = row.id
|
||||
form.base_id = row.base_id
|
||||
form.material_name = row.material_name
|
||||
form.spec_model = row.spec_model
|
||||
form.category = row.category
|
||||
form.unit = row.unit
|
||||
form.material_type = row.material_type
|
||||
form.sku = row.sku
|
||||
form.barcode = row.barcode
|
||||
form.in_date = row.inbound_date
|
||||
form.warehouse_location = row.warehouse_loc
|
||||
|
||||
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 = ''
|
||||
}
|
||||
|
||||
form.status = row.status
|
||||
form.quality_status = row.quality_status // 质量状态
|
||||
form.in_quantity = Number(row.qty_inbound) || 0
|
||||
form.stock_quantity = Number(row.qty_stock) || 0
|
||||
form.available_quantity = Number(row.qty_available) || 0
|
||||
|
||||
// 映射新字段
|
||||
form.bom_code = row.bom_code
|
||||
form.bom_version = row.bom_version
|
||||
form.work_order_code = row.work_order_code
|
||||
form.raw_material_cost = Number(row.raw_material_cost) || 0
|
||||
form.manual_cost = Number(row.manual_cost) || 0
|
||||
form.production_manager = row.production_manager
|
||||
|
||||
if (row.production_start_time && row.production_end_time) {
|
||||
form.production_time_range = [row.production_start_time, row.production_end_time]
|
||||
} else {
|
||||
form.production_time_range = []
|
||||
}
|
||||
|
||||
form.quality_report_link = row.quality_report_link
|
||||
form.detail_link = row.detail_link
|
||||
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
try {
|
||||
// 解构时间范围
|
||||
const payload: any = {
|
||||
...form,
|
||||
in_quantity: Number(form.in_quantity),
|
||||
raw_material_cost: Number(form.raw_material_cost),
|
||||
manual_cost: Number(form.manual_cost),
|
||||
production_start_time: form.production_time_range?.[0] || null,
|
||||
production_end_time: form.production_time_range?.[1] || null
|
||||
}
|
||||
delete payload.production_time_range // 后端不需要数组
|
||||
|
||||
if (dialogStatus.value === 'create') {
|
||||
await createSemiInbound(payload)
|
||||
ElMessage.success('入库成功')
|
||||
} else {
|
||||
await updateSemiInbound(form.id!, payload)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
await fetchData()
|
||||
visible.value = false
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.msg || '操作失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await deleteSemiInbound(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (e) {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
materialOptions.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: [],
|
||||
quality_report_link: '', detail_link: ''
|
||||
})
|
||||
}
|
||||
|
||||
const getStatusType = (status: string) => {
|
||||
const map: any = { '在库': 'success', '出库': 'info', '损耗': 'danger' }
|
||||
return map[status] || 'warning'
|
||||
}
|
||||
|
||||
const getQualityType = (status: string) => {
|
||||
const map: any = { '合格': 'success', '不合格': 'danger', '待检': 'info', '返修中': 'warning' }
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
const formatMoney = (val: any) => {
|
||||
const num = Number(val)
|
||||
return isNaN(num) ? '-' : `¥ ${num.toFixed(2)}`
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 全局布局 */
|
||||
.semi-module {
|
||||
background: #f5f7fa;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 顶部工具栏 */
|
||||
.header-tools {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
background: #fff;
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
|
||||
}
|
||||
.left-tools { flex: 0 0 350px; }
|
||||
.right-tools { display: flex; gap: 10px; align-items: center; }
|
||||
.action-btn { font-weight: 500; }
|
||||
|
||||
/* 表格美化 */
|
||||
.modern-table {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
|
||||
}
|
||||
:deep(.table-header-gray th) {
|
||||
background-color: #f8f9fb !important;
|
||||
color: #606266;
|
||||
font-weight: 600;
|
||||
height: 50px;
|
||||
}
|
||||
.tag-sn { color: #409EFF; font-weight: bold; font-family: monospace; }
|
||||
.tag-bn { color: #67C23A; font-weight: bold; font-family: monospace; }
|
||||
.money-text { font-family: 'Consolas', monospace; color: #303133; }
|
||||
.stock-num { font-weight: bold; color: #333; font-size: 15px; }
|
||||
.avail-num { font-weight: bold; color: #67C23A; font-size: 15px; }
|
||||
.sum-tag { margin-left: 4px; transform: scale(0.9); }
|
||||
|
||||
/* 弹窗与表单美化 */
|
||||
.stylish-form .form-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e4e7ed;
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
background: #fcfcfc;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: #303133;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.card-title .icon { margin-right: 8px; font-size: 18px; color: #409EFF; }
|
||||
.card-title .sub-title { font-size: 12px; color: #909399; font-weight: normal; margin-left: 10px; }
|
||||
|
||||
.card-content { padding: 20px; }
|
||||
|
||||
/* 基础信息卡片 (蓝色调,示读) */
|
||||
.basic-card { border-left: 4px solid #409EFF; }
|
||||
.is-text-view :deep(.el-input__wrapper) {
|
||||
box-shadow: none !important;
|
||||
background-color: #f5f7fa;
|
||||
border-bottom: 1px solid #dcdfe6;
|
||||
border-radius: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
.is-text-view :deep(.el-input__inner) { color: #606266; font-weight: 500; }
|
||||
|
||||
/* 入库信息卡片 */
|
||||
.inbound-card { border-left: 4px solid #67C23A; }
|
||||
|
||||
/* 生产与成本卡片 (橙色调) */
|
||||
.production-card { border-left: 4px solid #E6A23C; }
|
||||
.production-card .card-title .icon { color: #E6A23C; }
|
||||
|
||||
|
||||
/* 身份区域 (SN/BN) */
|
||||
.identity-panel {
|
||||
background: #fffbf0;
|
||||
border: 1px dashed #e6a23c;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.custom-radio-group { margin-bottom: 10px; }
|
||||
.locked-msg { font-size: 12px; color: #e6a23c; margin-left: 15px; }
|
||||
.prefix-tag { font-weight: bold; font-size: 12px; padding: 0 5px; border-radius: 4px; }
|
||||
.prefix-tag.bn { color: #67C23A; background: #f0f9eb; }
|
||||
.prefix-tag.sn { color: #409EFF; background: #ecf5ff; }
|
||||
|
||||
/* 分割线 */
|
||||
.divider-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin: 30px 0 20px;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.divider-text::before, .divider-text::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.divider-text::before { margin-right: 15px; }
|
||||
.divider-text::after { margin-left: 15px; }
|
||||
|
||||
/* 底部按钮 */
|
||||
.dialog-footer { display: flex; justify-content: flex-end; gap: 15px; margin-top: 20px; }
|
||||
.info-alert { font-size: 12px; color: #909399; margin-top: 10px; display: flex; align-items: center; gap: 5px; }
|
||||
.option-item { display: flex; justify-content: space-between; width: 100%; }
|
||||
.opt-name { font-weight: bold; }
|
||||
.opt-spec { color: #8492a6; font-size: 13px; }
|
||||
.total-price-input :deep(.el-input__inner) { color: #F56C6C; font-weight: bold; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user