基础信息读取错误,未修改完成
This commit is contained in:
96
inventory-backend/app/api/v1/inbound/base.py
Normal file
96
inventory-backend/app/api/v1/inbound/base.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
# 修改为这一行,指向 app/services/inbound/base_service.py
|
||||||
|
from app.services.inbound.base_service import MaterialBaseService
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
# 定义蓝图
|
||||||
|
# name='inbound_base' 确保全局唯一,防止和其他蓝图重名
|
||||||
|
inbound_base_bp = Blueprint('inbound_base', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 1. 获取基础信息列表 (GET)
|
||||||
|
# 路由: /api/v1/inbound/base/list
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@inbound_base_bp.route('/list', methods=['GET'])
|
||||||
|
def get_list():
|
||||||
|
try:
|
||||||
|
# 获取分页参数
|
||||||
|
page = request.args.get('pageNum', 1, type=int)
|
||||||
|
limit = request.args.get('pageSize', 10, type=int)
|
||||||
|
|
||||||
|
# 获取筛选参数
|
||||||
|
filters = {
|
||||||
|
"keyword": request.args.get('keyword'),
|
||||||
|
"category": request.args.get('category'),
|
||||||
|
"type": request.args.get('type'),
|
||||||
|
"isEnabled": request.args.get('isEnabled')
|
||||||
|
}
|
||||||
|
|
||||||
|
# 调用 Service 层逻辑
|
||||||
|
result = MaterialBaseService.get_list(page, limit, filters)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": result
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 2. 新增基础信息 (POST)
|
||||||
|
# 路由: /api/v1/inbound/base/
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@inbound_base_bp.route('/', methods=['POST'])
|
||||||
|
def add_material():
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({"code": 400, "msg": "No data provided"}), 400
|
||||||
|
|
||||||
|
MaterialBaseService.create_material(data)
|
||||||
|
return jsonify({"code": 200, "msg": "新增成功"})
|
||||||
|
except ValueError as ve:
|
||||||
|
# 捕获业务逻辑验证错误(如名称重复)
|
||||||
|
return jsonify({"code": 400, "msg": str(ve)}), 400
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({"code": 500, "msg": "系统错误"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 3. 修改基础信息 (PUT)
|
||||||
|
# 路由: /api/v1/inbound/base/
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@inbound_base_bp.route('/', methods=['PUT'])
|
||||||
|
def update_material():
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
if not data or not data.get('id'):
|
||||||
|
return jsonify({"code": 400, "msg": "ID不能为空"}), 400
|
||||||
|
|
||||||
|
MaterialBaseService.update_material(data.get('id'), data)
|
||||||
|
return jsonify({"code": 200, "msg": "更新成功"})
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 4. 删除基础信息 (DELETE)
|
||||||
|
# 路由: /api/v1/inbound/base/<id>
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@inbound_base_bp.route('/<int:id>', methods=['DELETE'])
|
||||||
|
def delete_material(id):
|
||||||
|
try:
|
||||||
|
MaterialBaseService.delete_material(id)
|
||||||
|
return jsonify({"code": 200, "msg": "删除成功"})
|
||||||
|
except ValueError as ve:
|
||||||
|
# 捕获依赖检查错误(如已被库存引用)
|
||||||
|
return jsonify({"code": 400, "msg": str(ve)}), 400
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||||
@ -1,106 +0,0 @@
|
|||||||
from flask import Blueprint, request, jsonify
|
|
||||||
|
|
||||||
# 确保这两个引用路径是存在的,如果报错说明文件没建好
|
|
||||||
try:
|
|
||||||
from app.services.stock_service import StockService
|
|
||||||
from app.schemas.stock_schema import stock_buy_schema
|
|
||||||
except ImportError as e:
|
|
||||||
# 如果服务还没写好,这里会打印错误,防止整个后端起不来
|
|
||||||
print(f"❌ 导入服务出错: {e}")
|
|
||||||
StockService = None
|
|
||||||
stock_buy_schema = None
|
|
||||||
|
|
||||||
stocks_bp = Blueprint('stocks', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# 1. 获取入库列表
|
|
||||||
# URL: /api/v1/stocks/inbound (GET)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
@stocks_bp.route('/inbound', methods=['GET'])
|
|
||||||
def get_inbound_list():
|
|
||||||
if not StockService:
|
|
||||||
return jsonify({'code': 500, 'msg': '后端服务未初始化'}), 500
|
|
||||||
|
|
||||||
try:
|
|
||||||
page = request.args.get('page', 1, type=int)
|
|
||||||
limit = request.args.get('pageSize', 10, type=int)
|
|
||||||
|
|
||||||
# 调用 Service 层获取数据
|
|
||||||
result = StockService.get_list(page, limit)
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'code': 200,
|
|
||||||
'msg': 'success',
|
|
||||||
'data': result
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
print(f"获取列表报错: {e}")
|
|
||||||
return jsonify({'code': 500, 'msg': '服务器内部错误'}), 500
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# 2. 新增入库单
|
|
||||||
# URL: /api/v1/stocks/inbound (POST)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
@stocks_bp.route('/inbound', methods=['POST'])
|
|
||||||
def create_inbound():
|
|
||||||
if not StockService:
|
|
||||||
return jsonify({'code': 500, 'msg': '后端服务未初始化'}), 500
|
|
||||||
|
|
||||||
json_data = request.get_json()
|
|
||||||
if not json_data:
|
|
||||||
return jsonify({'code': 400, 'msg': '没有接收到数据'}), 400
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 1. 参数校验 (Marshmallow Schema)
|
|
||||||
data = stock_buy_schema.load(json_data)
|
|
||||||
|
|
||||||
# 2. 调用业务逻辑
|
|
||||||
new_stock = StockService.create_inbound(data)
|
|
||||||
|
|
||||||
# 3. 返回成功
|
|
||||||
# 注意:确保 new_stock 对象有 to_dict() 方法,否则这里会报错
|
|
||||||
return jsonify({
|
|
||||||
'code': 200,
|
|
||||||
'msg': '入库成功',
|
|
||||||
'data': new_stock.to_dict() if hasattr(new_stock, 'to_dict') else str(new_stock)
|
|
||||||
}), 201
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# 捕获校验错误或数据库错误
|
|
||||||
print(f"入库报错: {e}")
|
|
||||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# 3. 更新入库单
|
|
||||||
# URL: /api/v1/stocks/inbound/<id> (PUT)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
@stocks_bp.route('/inbound/<int:id>', methods=['PUT'])
|
|
||||||
def update_inbound(id):
|
|
||||||
if not StockService:
|
|
||||||
return jsonify({'code': 500, 'msg': '后端服务未初始化'}), 500
|
|
||||||
|
|
||||||
json_data = request.get_json()
|
|
||||||
try:
|
|
||||||
StockService.update_inbound(id, json_data)
|
|
||||||
return jsonify({'code': 200, 'msg': '更新成功'})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# 4. 删除入库单
|
|
||||||
# URL: /api/v1/stocks/inbound/<id> (DELETE)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
@stocks_bp.route('/inbound/<int:id>', methods=['DELETE'])
|
|
||||||
def delete_inbound(id):
|
|
||||||
if not StockService:
|
|
||||||
return jsonify({'code': 500, 'msg': '后端服务未初始化'}), 500
|
|
||||||
|
|
||||||
try:
|
|
||||||
StockService.delete_inbound(id)
|
|
||||||
return jsonify({'code': 200, 'msg': '删除成功'})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
|
||||||
@ -1,32 +1,62 @@
|
|||||||
#material.py
|
# app/models/material.py
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
class MaterialBase(db.Model):
|
class MaterialBase(db.Model):
|
||||||
|
"""
|
||||||
|
基础信息表模型
|
||||||
|
对应数据库表: material_base
|
||||||
|
"""
|
||||||
__tablename__ = 'material_base'
|
__tablename__ = 'material_base'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
name = db.Column(db.String(255), nullable=False) # 名称
|
name = db.Column(db.String(255), nullable=False, comment='基础信息名称')
|
||||||
category = db.Column(db.String(100)) # 类别
|
category = db.Column(db.String(100), comment='类别') # 例如: 采购件, 自制件
|
||||||
material_type = db.Column(db.String(100)) # 类型
|
material_type = db.Column(db.String(100), comment='类型') # 例如: 电子料, 结构件 (对应前端 type)
|
||||||
spec_model = db.Column(db.String(255)) # 规格型号
|
spec_model = db.Column(db.String(255), comment='规格型号') # (对应前端 spec)
|
||||||
unit = db.Column(db.String(50)) # 计量单位
|
unit = db.Column(db.String(50), comment='计量单位')
|
||||||
visibility_level = db.Column(db.Integer, default=0) # 信息可见等级
|
|
||||||
manual_link = db.Column(db.Text) # 通用说明书
|
# 根据你提供的代码,可见等级设为 Integer,默认为 0
|
||||||
product_image = db.Column(db.Text) # 通用产品图
|
visibility_level = db.Column(db.Integer, default=0, comment='信息可见等级')
|
||||||
is_enabled = db.Column(db.Boolean, default=True) # 是否启用
|
|
||||||
|
manual_link = db.Column(db.Text, comment='通用说明书链接') # (对应前端 generalManual)
|
||||||
|
product_image = db.Column(db.Text, comment='通用产品图链接') # (对应前端 generalImage)
|
||||||
|
|
||||||
|
is_enabled = db.Column(db.Boolean, default=True, comment='是否启用')
|
||||||
|
|
||||||
|
# 时间字段(建议加上,用于排序或记录)
|
||||||
|
create_time = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
update_time = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
# 【核心关联】
|
# 【核心关联】
|
||||||
# 这里定义了反向关系,lazy='dynamic' 允许我们后续做 count() 查询
|
# 关联采购库存表,lazy='dynamic' 允许使用 .count() 等查询方法
|
||||||
# cascade='all, delete-orphan' 并不是在这里用的,因为我们是手动控制逻辑
|
|
||||||
stock_buys = db.relationship('StockBuy', back_populates='material', lazy='dynamic')
|
stock_buys = db.relationship('StockBuy', back_populates='material', lazy='dynamic')
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
"""
|
||||||
|
序列化方法:将模型转换为字典,供API返回JSON使用
|
||||||
|
此处进行了字段名的映射,以适配 list.vue 前端的 prop 属性
|
||||||
|
"""
|
||||||
return {
|
return {
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'category': self.category,
|
'category': self.category,
|
||||||
'material_type': self.material_type,
|
|
||||||
'spec_model': self.spec_model,
|
# --- 字段映射区域 (后端字段 -> 前端字段) ---
|
||||||
'unit': self.unit
|
'type': self.material_type, # 前端 prop="type"
|
||||||
|
'spec': self.spec_model, # 前端 prop="spec"
|
||||||
|
'unit': self.unit,
|
||||||
|
|
||||||
|
# 转为驼峰命名,适配前端习惯
|
||||||
|
'visibilityLevel': self.visibility_level,
|
||||||
|
'generalManual': self.manual_link,
|
||||||
|
'generalImage': self.product_image,
|
||||||
|
|
||||||
|
# Element Plus Switch 组件通常接受 1/0 或 true/false
|
||||||
|
# 这里转为 1/0 方便前端 el-switch :active-value="1" :inactive-value="0"
|
||||||
|
'isEnabled': 1 if self.is_enabled else 0,
|
||||||
|
|
||||||
|
# 补充时间信息
|
||||||
|
'createTime': self.create_time.strftime('%Y-%m-%d %H:%M:%S') if self.create_time else None
|
||||||
}
|
}
|
||||||
139
inventory-backend/app/services/inbound/base_service.py
Normal file
139
inventory-backend/app/services/inbound/base_service.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
from app.extensions import db
|
||||||
|
from app.models.material import MaterialBase
|
||||||
|
from app.models.stock import StockBuy # 需要引入库存表做删除时的依赖检查
|
||||||
|
from sqlalchemy import or_
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialBaseService:
|
||||||
|
@staticmethod
|
||||||
|
def get_list(page, limit, filters=None):
|
||||||
|
"""
|
||||||
|
获取基础信息列表
|
||||||
|
:param page: 页码
|
||||||
|
:param limit: 每页条数
|
||||||
|
:param filters: 筛选条件字典 {keyword, category, type, isEnabled}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
query = MaterialBase.query
|
||||||
|
|
||||||
|
if filters:
|
||||||
|
# 1. 关键词模糊搜索 (名称 或 规格型号)
|
||||||
|
if filters.get('keyword'):
|
||||||
|
kw = f"%{filters['keyword']}%"
|
||||||
|
query = query.filter(or_(
|
||||||
|
MaterialBase.name.like(kw),
|
||||||
|
MaterialBase.spec_model.like(kw)
|
||||||
|
))
|
||||||
|
|
||||||
|
# 2. 精确筛选
|
||||||
|
if filters.get('category'):
|
||||||
|
query = query.filter_by(category=filters['category'])
|
||||||
|
|
||||||
|
if filters.get('type'):
|
||||||
|
query = query.filter_by(material_type=filters['type'])
|
||||||
|
|
||||||
|
if filters.get('isEnabled') is not None:
|
||||||
|
# 前端传 1/0,转为 Boolean
|
||||||
|
is_active = bool(int(filters['isEnabled']))
|
||||||
|
query = query.filter_by(is_enabled=is_active)
|
||||||
|
|
||||||
|
# 按 ID 倒序排列
|
||||||
|
pagination = query.order_by(MaterialBase.id.desc()).paginate(page=page, per_page=limit, error_out=False)
|
||||||
|
|
||||||
|
items = [item.to_dict() for item in pagination.items]
|
||||||
|
return {"total": pagination.total, "items": items}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"查询基础信息列表失败: {e}")
|
||||||
|
# 生产环境建议记录日志
|
||||||
|
return {"total": 0, "items": []}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_material(data):
|
||||||
|
"""新增基础信息"""
|
||||||
|
try:
|
||||||
|
# 0. 基础校验
|
||||||
|
if not data.get('name') or not data.get('spec'):
|
||||||
|
raise ValueError("名称和规格型号不能为空")
|
||||||
|
|
||||||
|
# 1. 查重 (名称+规格型号 唯一)
|
||||||
|
exist = MaterialBase.query.filter_by(
|
||||||
|
name=data['name'],
|
||||||
|
spec_model=data['spec']
|
||||||
|
).first()
|
||||||
|
if exist:
|
||||||
|
raise ValueError(f"已存在相同名称和规格的数据 (ID: {exist.id})")
|
||||||
|
|
||||||
|
# 2. 创建对象 (注意前端驼峰 -> 后端下划线映射)
|
||||||
|
new_material = MaterialBase(
|
||||||
|
name=data['name'],
|
||||||
|
spec_model=data['spec'], # 映射
|
||||||
|
category=data.get('category'),
|
||||||
|
material_type=data.get('type'), # 映射
|
||||||
|
unit=data.get('unit'),
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(new_material)
|
||||||
|
db.session.commit()
|
||||||
|
return new_material
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
raise e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_material(m_id, data):
|
||||||
|
"""修改基础信息"""
|
||||||
|
try:
|
||||||
|
material = MaterialBase.query.get(m_id)
|
||||||
|
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']
|
||||||
|
if 'type' in data: material.material_type = data['type']
|
||||||
|
if 'unit' in data: material.unit = data['unit']
|
||||||
|
if 'visibilityLevel' in data: material.visibility_level = data['visibilityLevel']
|
||||||
|
if 'generalManual' in data: material.manual_link = data['generalManual']
|
||||||
|
if 'generalImage' in data: material.product_image = data['generalImage']
|
||||||
|
|
||||||
|
if 'isEnabled' in data:
|
||||||
|
material.is_enabled = bool(int(data['isEnabled']))
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return material
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
raise e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_material(m_id):
|
||||||
|
"""删除基础信息 (带依赖检查)"""
|
||||||
|
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} 条库存记录引用,请先清理库存或仅禁用此条目。")
|
||||||
|
|
||||||
|
# 2. 执行删除
|
||||||
|
db.session.delete(material)
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"删除基础信息失败: {e}")
|
||||||
|
raise e
|
||||||
@ -1,119 +0,0 @@
|
|||||||
from app.extensions import db
|
|
||||||
from app.models.stock import StockBuy
|
|
||||||
from app.models.material import MaterialBase
|
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
|
||||||
|
|
||||||
|
|
||||||
class StockService:
|
|
||||||
@staticmethod
|
|
||||||
def create_inbound(data):
|
|
||||||
"""
|
|
||||||
处理入库逻辑:
|
|
||||||
1. 根据 SKU 查找物料。
|
|
||||||
2. 如果没找到,创建新物料 (MaterialBase)。
|
|
||||||
3. 创建入库单 (StockBuy)。
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
sku = data.get('sku_code')
|
|
||||||
material_id = data.get('material_id')
|
|
||||||
|
|
||||||
# --- 第一步:确定 material_id ---
|
|
||||||
|
|
||||||
# 如果前端没传 ID,或者传了但我们想二次确认,都通过 SKU 查一遍
|
|
||||||
existing_material = MaterialBase.query.filter_by(sku_code=sku).first()
|
|
||||||
|
|
||||||
if existing_material:
|
|
||||||
# 场景 A: 物料已存在 -> 直接使用其 ID
|
|
||||||
material_id = existing_material.id
|
|
||||||
else:
|
|
||||||
# 场景 B: 物料不存在 -> 自动创建新物料
|
|
||||||
if not data.get('material_name'):
|
|
||||||
raise ValueError(f"SKU [{sku}] 是新物料,必须填写【物料名称】才能入库。")
|
|
||||||
|
|
||||||
new_material = MaterialBase(
|
|
||||||
sku_code=sku,
|
|
||||||
name=data.get('material_name'),
|
|
||||||
spec_model=data.get('spec_model'),
|
|
||||||
unit=data.get('unit'),
|
|
||||||
category=data.get('category')
|
|
||||||
)
|
|
||||||
db.session.add(new_material)
|
|
||||||
db.session.flush() # 关键:将对象刷入暂存区,以获取自增的 ID
|
|
||||||
material_id = new_material.id
|
|
||||||
|
|
||||||
# --- 第二步:创建入库单 ---
|
|
||||||
|
|
||||||
qty = data.get('qty_inbound')
|
|
||||||
price = data.get('price_unit', 0)
|
|
||||||
|
|
||||||
new_stock = StockBuy(
|
|
||||||
material_id=material_id,
|
|
||||||
inbound_date=data.get('inbound_date'),
|
|
||||||
batch_no=data.get('batch_no'),
|
|
||||||
warehouse_loc=data.get('warehouse_loc'),
|
|
||||||
supplier_name=data.get('supplier_name'),
|
|
||||||
|
|
||||||
# 数量逻辑:初始时,当前量 = 可用量 = 入库量
|
|
||||||
qty_inbound=qty,
|
|
||||||
qty_current=qty,
|
|
||||||
qty_available=qty,
|
|
||||||
|
|
||||||
# 财务逻辑
|
|
||||||
price_unit=price,
|
|
||||||
price_total=float(qty) * float(price) if qty else 0
|
|
||||||
)
|
|
||||||
|
|
||||||
db.session.add(new_stock)
|
|
||||||
db.session.commit() # 统一提交事务
|
|
||||||
|
|
||||||
return new_stock
|
|
||||||
|
|
||||||
except SQLAlchemyError as e:
|
|
||||||
db.session.rollback() # 数据库报错回滚
|
|
||||||
raise e
|
|
||||||
except ValueError as e:
|
|
||||||
db.session.rollback() # 业务报错回滚
|
|
||||||
raise e
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_list(page, per_page):
|
|
||||||
"""获取分页列表"""
|
|
||||||
pagination = StockBuy.query.order_by(StockBuy.inbound_date.desc()).paginate(
|
|
||||||
page=page, per_page=per_page, error_out=False
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
'items': [item.to_dict() for item in pagination.items],
|
|
||||||
'total': pagination.total,
|
|
||||||
'pages': pagination.pages,
|
|
||||||
'current_page': pagination.page
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def update_inbound(stock_id, data):
|
|
||||||
"""更新入库单信息 (通常不允许改物料本身,只改入库相关)"""
|
|
||||||
stock = StockBuy.query.get_or_404(stock_id)
|
|
||||||
|
|
||||||
if 'warehouse_loc' in data: stock.warehouse_loc = data['warehouse_loc']
|
|
||||||
if 'supplier_name' in data: stock.supplier_name = data['supplier_name']
|
|
||||||
if 'batch_no' in data: stock.batch_no = data['batch_no']
|
|
||||||
if 'price_unit' in data: stock.price_unit = data['price_unit']
|
|
||||||
|
|
||||||
# 如果修改了数量,需要级联更新当前库存
|
|
||||||
if 'qty_inbound' in data:
|
|
||||||
old_qty = float(stock.qty_inbound)
|
|
||||||
new_qty = float(data['qty_inbound'])
|
|
||||||
diff = new_qty - old_qty
|
|
||||||
|
|
||||||
stock.qty_inbound = new_qty
|
|
||||||
stock.qty_current = float(stock.qty_current) + diff
|
|
||||||
stock.qty_available = float(stock.qty_available) + diff
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
return stock
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def delete_inbound(stock_id):
|
|
||||||
"""删除入库单"""
|
|
||||||
stock = StockBuy.query.get_or_404(stock_id)
|
|
||||||
db.session.delete(stock)
|
|
||||||
db.session.commit()
|
|
||||||
@ -5,4 +5,8 @@ app = create_app()
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# debug=True 修改代码后会自动重启
|
# debug=True 修改代码后会自动重启
|
||||||
|
print("\n====== 当前所有注册路由 ======")
|
||||||
|
for rule in app.url_map.iter_rules():
|
||||||
|
print(f"{rule} -> {rule.endpoint}")
|
||||||
|
print("==============================\n")
|
||||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
44
inventory-web/src/api/material_base.ts
Normal file
44
inventory-web/src/api/material_base.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 1. 获取基础信息列表
|
||||||
|
export function listMaterialBase(params: any) {
|
||||||
|
return request({
|
||||||
|
url: '/inbound/base/list',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 新增基础信息
|
||||||
|
export function addMaterialBase(data: any) {
|
||||||
|
return request({
|
||||||
|
url: '/inbound/base/',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 修改基础信息 (包含状态启用/禁用)
|
||||||
|
export function updateMaterialBase(data: any) {
|
||||||
|
return request({
|
||||||
|
url: '/inbound/base/',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 删除基础信息
|
||||||
|
export function delMaterialBase(id: number) {
|
||||||
|
return request({
|
||||||
|
url: `/inbound/base/${id}`, // 注意这里是反引号,用于拼接 URL
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 获取详情 (可选,用于编辑回显)
|
||||||
|
export function getMaterialBase(id: number) {
|
||||||
|
return request({
|
||||||
|
url: `/inbound/base/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -19,7 +19,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// 2. 基础物料 (对应 views/material/list.vue)
|
// 2. 基础信息 (对应 views/material/list.vue)
|
||||||
{
|
{
|
||||||
path: '/material',
|
path: '/material',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
@ -28,9 +28,9 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
{
|
{
|
||||||
path: 'index',
|
path: 'index',
|
||||||
name: 'MaterialBase',
|
name: 'MaterialBase',
|
||||||
// 基础物料列表
|
// 基础信息列表
|
||||||
component: () => import('@/views/material/list.vue'),
|
component: () => import('@/views/material/list.vue'),
|
||||||
meta: { title: '基础物料', icon: 'Box' }
|
meta: { title: '基础信息', icon: 'Box' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
<el-button type="success" size="large" @click="handleNav('/material/index')">
|
<el-button type="success" size="large" @click="handleNav('/material/index')">
|
||||||
<el-icon style="margin-right: 5px"><Box /></el-icon>
|
<el-icon style="margin-right: 5px"><Box /></el-icon>
|
||||||
基础物料
|
基础信息
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<el-button type="warning" size="large" @click="handleNav('/operation/borrow')">
|
<el-button type="warning" size="large" @click="handleNav('/operation/borrow')">
|
||||||
|
|||||||
@ -3,39 +3,311 @@
|
|||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<el-button type="primary">
|
<el-button type="primary" @click="handleAdd">
|
||||||
<el-icon style="margin-right: 5px"><Plus /></el-icon>新增物料
|
<el-icon style="margin-right: 5px"><Plus /></el-icon>新增基础信息
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
<el-input placeholder="请输入物料名称或编码" style="width: 200px; margin-right: 10px;" />
|
<el-input
|
||||||
<el-select placeholder="物料类别" style="width: 150px; margin-right: 10px;">
|
v-model="queryParams.keyword"
|
||||||
<el-option label="采购件" value="elec" />
|
placeholder="请输入基础信息名称或规格"
|
||||||
<el-option label="半成品" value="struct" />
|
style="width: 220px; margin-right: 10px;"
|
||||||
<el-option label="成品" value="elec" />
|
clearable
|
||||||
<el-option label="服务权益" value="struct" />
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-select v-model="queryParams.category" placeholder="基础信息类别" clearable style="width: 150px; margin-right: 10px;">
|
||||||
|
<el-option label="采购件" value="PURCHASE" />
|
||||||
|
<el-option label="自制件" value="SELF_MADE" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-button type="primary" plain>搜索</el-button>
|
|
||||||
|
<el-select v-model="queryParams.type" placeholder="物料类型" clearable style="width: 150px; margin-right: 10px;">
|
||||||
|
<el-option label="电子料" value="ELEC" />
|
||||||
|
<el-option label="结构件" value="STRUCT" />
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<el-select v-model="queryParams.isEnabled" placeholder="状态" clearable style="width: 100px; margin-right: 10px;">
|
||||||
|
<el-option label="启用" :value="1" />
|
||||||
|
<el-option label="禁用" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<el-button type="primary" plain @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button plain @click="resetQuery">重置</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table :data="tableData" border stripe style="width: 100%; margin-top: 20px">
|
<el-table
|
||||||
<el-table-column prop="code" label="物料编码" width="120" />
|
v-loading="loading"
|
||||||
<el-table-column prop="name" label="物料名称" width="180" />
|
:data="tableData"
|
||||||
<el-table-column prop="spec" label="规格型号" />
|
border
|
||||||
<el-table-column prop="unit" label="单位" width="80" />
|
stripe
|
||||||
<el-table-column prop="category" label="类别" width="120" />
|
style="width: 100%; margin-top: 20px"
|
||||||
<el-table-column label="操作" width="150">
|
>
|
||||||
<template #default>
|
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||||
<el-button link type="primary" size="small">编辑</el-button>
|
|
||||||
<el-button link type="danger" size="small">删除</el-button>
|
<el-table-column prop="name" label="基础信息名称" min-width="150" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<el-table-column prop="category" label="类别" width="100" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag v-if="scope.row.category === 'PURCHASE'">采购件</el-tag>
|
||||||
|
<el-tag v-else-if="scope.row.category === 'SELF_MADE'" type="success">自制件</el-tag>
|
||||||
|
<el-tag v-else type="info">{{ scope.row.category || '-' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="type" label="类型" width="100" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<span v-if="scope.row.type === 'ELEC'">电子料</span>
|
||||||
|
<span v-else-if="scope.row.type === 'STRUCT'">结构件</span>
|
||||||
|
<span v-else>{{ scope.row.type || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="spec" label="规格型号" min-width="150" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<el-table-column prop="unit" label="单位" width="70" align="center" />
|
||||||
|
|
||||||
|
<el-table-column prop="visibilityLevel" label="可见等级" width="90" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
L{{ scope.row.visibilityLevel }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="资料" width="100" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
v-if="scope.row.generalImage"
|
||||||
|
link type="primary"
|
||||||
|
:icon="Picture"
|
||||||
|
title="查看图片"
|
||||||
|
@click="openLink(scope.row.generalImage)"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
v-if="scope.row.generalManual"
|
||||||
|
link type="primary"
|
||||||
|
:icon="Document"
|
||||||
|
title="查看说明书"
|
||||||
|
@click="openLink(scope.row.generalManual)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="isEnabled" label="是否启用" width="100" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-switch
|
||||||
|
v-model="scope.row.isEnabled"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
:loading="scope.row.statusLoading"
|
||||||
|
@change="handleStatusChange(scope.row)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||||
|
<el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px; text-align: right;">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="total, prev, pager, next, sizes"
|
||||||
|
:total="total"
|
||||||
|
v-model:current-page="queryParams.pageNum"
|
||||||
|
v-model:page-size="queryParams.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
@size-change="getList"
|
||||||
|
@current-change="getList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { Plus, Picture, Document } from '@element-plus/icons-vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
|
||||||
|
// 【关键修改】引入刚才定义的 API 文件
|
||||||
|
import {
|
||||||
|
listMaterialBase,
|
||||||
|
delMaterialBase,
|
||||||
|
updateMaterialBase
|
||||||
|
} from '@/api/material_base';
|
||||||
|
|
||||||
|
// --- 类型定义 ---
|
||||||
|
interface MaterialBaseVO {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
category: string;
|
||||||
|
type: string;
|
||||||
|
spec: string;
|
||||||
|
unit: string;
|
||||||
|
visibilityLevel: number;
|
||||||
|
generalManual?: string;
|
||||||
|
generalImage?: string;
|
||||||
|
isEnabled: number; // 1 or 0
|
||||||
|
statusLoading?: boolean; // 辅助字段
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueryParams {
|
||||||
|
pageNum: number;
|
||||||
|
pageSize: number;
|
||||||
|
keyword: string;
|
||||||
|
category: string;
|
||||||
|
type: string;
|
||||||
|
isEnabled?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 响应式数据 ---
|
||||||
|
const loading = ref(false);
|
||||||
|
const total = ref(0);
|
||||||
|
const tableData = ref<MaterialBaseVO[]>([]);
|
||||||
|
|
||||||
|
const queryParams = reactive<QueryParams>({
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
keyword: '',
|
||||||
|
category: '',
|
||||||
|
type: '',
|
||||||
|
isEnabled: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- 业务逻辑方法 ---
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
const getList = () => {
|
||||||
|
loading.value = true;
|
||||||
|
// 调用 API 文件中的 listMaterialBase
|
||||||
|
listMaterialBase(queryParams)
|
||||||
|
.then((response: any) => {
|
||||||
|
// 我们的 request.ts 已经处理了 code!=200,这里直接拿 data
|
||||||
|
if (response && response.data) {
|
||||||
|
tableData.value = response.data.items;
|
||||||
|
total.value = response.data.total;
|
||||||
|
} else {
|
||||||
|
tableData.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
tableData.value = [];
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNum = 1;
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryParams.keyword = '';
|
||||||
|
queryParams.category = '';
|
||||||
|
queryParams.type = '';
|
||||||
|
queryParams.isEnabled = undefined;
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增
|
||||||
|
const handleAdd = () => {
|
||||||
|
ElMessage.info("请实现新增弹窗逻辑");
|
||||||
|
// 逻辑:dialogVisible.value = true
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
const handleEdit = (row: MaterialBaseVO) => {
|
||||||
|
console.log("点击编辑", row);
|
||||||
|
ElMessage.info(`准备编辑 ID: ${row.id}`);
|
||||||
|
// 逻辑:调用 getMaterialBase(row.id) 回显数据
|
||||||
|
};
|
||||||
|
|
||||||
|
// 状态切换 (实时保存)
|
||||||
|
const handleStatusChange = (row: MaterialBaseVO) => {
|
||||||
|
row.statusLoading = true;
|
||||||
|
const text = row.isEnabled === 1 ? "启用" : "停用";
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
id: row.id,
|
||||||
|
isEnabled: row.isEnabled
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调用 API 文件中的 updateMaterialBase
|
||||||
|
updateMaterialBase(updateData)
|
||||||
|
.then(() => {
|
||||||
|
ElMessage.success(`已${text} "${row.name}"`);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 失败回滚
|
||||||
|
row.isEnabled = row.isEnabled === 1 ? 0 : 1;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
row.statusLoading = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const handleDelete = (row: MaterialBaseVO) => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`是否确认删除名称为 "${row.name}" 的数据项? \n如果该物料已有库存记录,删除将会被拒绝。`,
|
||||||
|
"警告",
|
||||||
|
{
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning"
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
// 调用 API 文件中的 delMaterialBase
|
||||||
|
delMaterialBase(row.id)
|
||||||
|
.then(() => {
|
||||||
|
ElMessage.success("删除成功");
|
||||||
|
if (tableData.value.length === 1 && queryParams.pageNum > 1) {
|
||||||
|
queryParams.pageNum--;
|
||||||
|
}
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
// 取消
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开链接
|
||||||
|
const openLink = (url: string) => {
|
||||||
|
if (!url) return;
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.filter-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user