采购件,半成品,产品页面初步完成
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
# 文件路径: inventory-backend/app/__init__.py
|
||||
|
||||
from flask import Flask
|
||||
from config import Config
|
||||
from app.extensions import db, migrate, cors
|
||||
@ -22,15 +23,18 @@ def create_app():
|
||||
# 注册入库聚合模块 (Inbound)
|
||||
try:
|
||||
# 指向聚合文件: app/api/v1/inbound/__init__.py
|
||||
# 该文件里应该包含了 buy, semi, base, product 的聚合逻辑
|
||||
from app.api.v1.inbound import inbound_bp
|
||||
|
||||
# 注册父蓝图,路由前缀为 /api/v1/inbound
|
||||
# 最终路由效果:
|
||||
# /api/v1/inbound + /buy/list -> /api/v1/inbound/buy/list
|
||||
# /api/v1/inbound + /semi/list -> /api/v1/inbound/semi/list
|
||||
# /api/v1/inbound + /buy/list -> /api/v1/inbound/buy/list
|
||||
# /api/v1/inbound + /semi/list -> /api/v1/inbound/semi/list
|
||||
# /api/v1/inbound + /product/list -> /api/v1/inbound/product/list
|
||||
# /api/v1/inbound + /base/search -> /api/v1/inbound/base/search
|
||||
app.register_blueprint(inbound_bp, url_prefix='/api/v1/inbound')
|
||||
|
||||
print("✅ Inbound (Buy & Semi) 模块注册成功")
|
||||
print("✅ Inbound (Buy, Semi, Product, Base) 模块注册成功")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"❌ 错误: Inbound 模块导入失败: {e}")
|
||||
@ -40,14 +44,22 @@ def create_app():
|
||||
# =========================================================
|
||||
with app.app_context():
|
||||
try:
|
||||
# ✅ 修正点:引用新路径 (不再引用 app.models.stock)
|
||||
from app.models.inbound.buy import StockBuy
|
||||
from app.models.inbound.semi import StockSemi
|
||||
# 1. 基础物料
|
||||
from app.models.material import MaterialBase
|
||||
# 2. 采购入库
|
||||
from app.models.inbound.buy import StockBuy
|
||||
# 3. 半成品入库
|
||||
from app.models.inbound.semi import StockSemi
|
||||
# 4. 成品入库 (新增)
|
||||
from app.models.inbound.product import StockProduct
|
||||
|
||||
# 如果是开发环境且没有迁移文件,可以取消注释下面这行来创建表
|
||||
# 开发环境如果需要自动建表,可以取消注释
|
||||
# db.create_all()
|
||||
|
||||
except ImportError as e:
|
||||
# 建议打印错误,防止因为文件名拼写错误导致静默失败
|
||||
print(f"⚠️ 模型预加载失败: {e}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 模型预加载发生未知错误: {e}")
|
||||
|
||||
return app
|
||||
@ -1,22 +1,14 @@
|
||||
from flask import Blueprint
|
||||
|
||||
# 1. 导入子模块蓝图
|
||||
# 注意:确保 .buy, .semi, .base 文件在同级目录下真实存在
|
||||
from .buy import inbound_buy_bp
|
||||
from .semi import inbound_semi_bp
|
||||
|
||||
# 如果你还有 base.py 文件,就取消注释下面这行
|
||||
from .base import inbound_base_bp
|
||||
# 导入 product
|
||||
from .product import inbound_product_bp
|
||||
|
||||
# 2. 创建父级聚合蓝图
|
||||
inbound_bp = Blueprint('inbound', __name__)
|
||||
|
||||
# 3. 挂载子蓝图
|
||||
# 访问地址: /api/v1/inbound/buy/list
|
||||
inbound_bp.register_blueprint(inbound_buy_bp, url_prefix='/buy')
|
||||
|
||||
# 访问地址: /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')
|
||||
inbound_bp.register_blueprint(inbound_base_bp, url_prefix='/base')
|
||||
# 挂载 product,前缀改为 /product
|
||||
inbound_bp.register_blueprint(inbound_product_bp, url_prefix='/product')
|
||||
@ -1,51 +1,55 @@
|
||||
# 文件路径: app/api/v1/inbound/base.py
|
||||
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():
|
||||
# ==============================================================================
|
||||
# 1. 搜索接口 (GET /api/v1/inbound/base/search)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/search', methods=['GET'])
|
||||
def search_base():
|
||||
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
|
||||
})
|
||||
keyword = request.args.get('keyword', '')
|
||||
data = MaterialBaseService.search_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
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 2. 新增基础信息 (POST)
|
||||
# 路由: /api/v1/inbound/base/
|
||||
# ------------------------------------------------------------------
|
||||
# ==============================================================================
|
||||
# 2. 列表接口 (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) # 前端传的是 pageNum
|
||||
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', None)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 3. 新增接口 (POST /api/v1/inbound/base/)
|
||||
# 注意:前端 material_base.ts 可能会请求 / 或 /add,这里统一匹配
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/', methods=['POST'])
|
||||
def add_material():
|
||||
def create():
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
@ -53,44 +57,37 @@ def add_material():
|
||||
|
||||
MaterialBaseService.create_material(data)
|
||||
return jsonify({"code": 200, "msg": "新增成功"})
|
||||
except ValueError as ve:
|
||||
# 捕获业务逻辑验证错误(如名称重复)
|
||||
return jsonify({"code": 400, "msg": str(ve)}), 400
|
||||
except ValueError as e:
|
||||
# 捕获业务逻辑验证错误 (如名称为空)
|
||||
return jsonify({"code": 400, "msg": str(e)}), 400
|
||||
except Exception as e:
|
||||
# 捕获系统错误
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": "系统错误"}), 500
|
||||
return jsonify({"code": 500, "msg": f"系统错误: {str(e)}"}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 3. 修改基础信息 (PUT)
|
||||
# 路由: /api/v1/inbound/base/
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_base_bp.route('/', methods=['PUT'])
|
||||
def update_material():
|
||||
# ==============================================================================
|
||||
# 4. 修改接口 (PUT /api/v1/inbound/base/<id>)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/<int:id>', methods=['PUT'])
|
||||
def update(id):
|
||||
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": "更新成功"})
|
||||
MaterialBaseService.update_material(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>
|
||||
# ------------------------------------------------------------------
|
||||
# ==============================================================================
|
||||
# 5. 删除接口 (DELETE /api/v1/inbound/base/<id>)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/<int:id>', methods=['DELETE'])
|
||||
def delete_material(id):
|
||||
def delete(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
|
||||
@ -0,0 +1,50 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
# 引用更名后的服务
|
||||
from app.services.inbound.product_service import ProductInboundService
|
||||
import traceback
|
||||
|
||||
# 蓝图命名改为 inbound_product_bp
|
||||
inbound_product_bp = Blueprint('inbound_product', __name__)
|
||||
|
||||
@inbound_product_bp.route('/search-base', methods=['GET'])
|
||||
def search_base():
|
||||
try:
|
||||
data = ProductInboundService.search_base_material(request.args.get('keyword', ''))
|
||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||
except Exception as e:
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
@inbound_product_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)
|
||||
keyword = request.args.get('keyword', '')
|
||||
result = ProductInboundService.get_list(page, limit, keyword)
|
||||
return jsonify({"code": 200, "msg": "success", "data": result})
|
||||
except Exception as e:
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
@inbound_product_bp.route('/submit', methods=['POST'])
|
||||
def submit():
|
||||
try:
|
||||
ProductInboundService.handle_inbound(request.get_json())
|
||||
return jsonify({"code": 200, "msg": "入库成功"})
|
||||
except Exception as e:
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
@inbound_product_bp.route('/<int:id>', methods=['PUT'])
|
||||
def update(id):
|
||||
try:
|
||||
ProductInboundService.update_inbound(id, request.get_json())
|
||||
return jsonify({"code": 200, "msg": "更新成功"})
|
||||
except Exception as e:
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
@inbound_product_bp.route('/<int:id>', methods=['DELETE'])
|
||||
def delete(id):
|
||||
try:
|
||||
ProductInboundService.delete_inbound(id)
|
||||
return jsonify({"code": 200, "msg": "删除成功"})
|
||||
except Exception as e:
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
@ -1,6 +1,5 @@
|
||||
# app/models/buy.py
|
||||
# app/models/inbound/buy.py
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class StockBuy(db.Model):
|
||||
@ -10,74 +9,54 @@ class StockBuy(db.Model):
|
||||
"""
|
||||
__tablename__ = 'stock_buy'
|
||||
|
||||
# 主键
|
||||
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))
|
||||
in_date = db.Column(db.Date)
|
||||
barcode = db.Column(db.String(100)) # 条码
|
||||
serial_number = db.Column(db.String(100)) # 序列号
|
||||
batch_number = db.Column(db.String(100)) # 批号
|
||||
barcode = db.Column(db.String(100))
|
||||
serial_number = db.Column(db.String(100))
|
||||
batch_number = db.Column(db.String(100))
|
||||
|
||||
# --- 数量 ---
|
||||
# 状态
|
||||
status = db.Column(db.String(50))
|
||||
inspection_status = db.Column(db.String(50))
|
||||
warehouse_location = 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)) # 在库/出库/损耗
|
||||
inspection_status = db.Column(db.String(50)) # 未检/合格/不合格
|
||||
warehouse_location = db.Column(db.String(100))
|
||||
|
||||
# --- 财务与商务 ---
|
||||
# 财务与商务
|
||||
unit_price = db.Column(db.Numeric(19, 4), default=0)
|
||||
total_price = db.Column(db.Numeric(19, 4), default=0)
|
||||
currency = db.Column(db.String(20), default='CNY')
|
||||
exchange_rate = db.Column(db.Numeric(15, 6), default=1.0)
|
||||
|
||||
supplier_name = db.Column(db.String(255))
|
||||
|
||||
# [关键映射区]:Python属性名 = DB列名
|
||||
# 前端传 purchaser -> 存入 buyer_name
|
||||
buyer_name = db.Column(db.String(100))
|
||||
|
||||
# 前端传 purchaser_email -> 存入 buyer_email
|
||||
buyer_email = db.Column(db.String(100))
|
||||
|
||||
# 前端传 source_link -> 存入 original_link
|
||||
original_link = db.Column(db.Text)
|
||||
|
||||
buyer_name = db.Column(db.String(100)) # 对应 SQL: buyer_name
|
||||
buyer_email = db.Column(db.String(100)) # 对应 SQL: buyer_email
|
||||
original_link = db.Column(db.Text) # 对应 SQL: original_link
|
||||
detail_link = db.Column(db.Text)
|
||||
arrival_photo = db.Column(db.Text)
|
||||
|
||||
# [这就是报错缺失的字段],请确保执行了 ALTER TABLE
|
||||
remark = db.Column(db.Text)
|
||||
# 注意:SQL 中没有 remark 字段,这里已移除
|
||||
|
||||
# 【关系定义】
|
||||
# 建立与 MaterialBase 的关系,方便通过 stock.material 访问基础信息
|
||||
# 关系定义
|
||||
material = db.relationship('MaterialBase', back_populates='stock_buys')
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
序列化:将模型转换为字典,主要用于单条查询或内部调用
|
||||
列表查询主要依赖 Service 层的手动构建以提高性能
|
||||
"""
|
||||
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,
|
||||
'inbound_date': self.in_date.strftime('%Y-%m-%d') if self.in_date else '',
|
||||
'barcode': self.barcode,
|
||||
@ -86,27 +65,23 @@ class StockBuy(db.Model):
|
||||
'warehouse_loc': self.warehouse_location,
|
||||
'status': self.status,
|
||||
'inspection_status': self.inspection_status,
|
||||
'remark': self.remark,
|
||||
|
||||
# 数量 (转为float防止json序列化报错)
|
||||
'in_quantity': float(self.in_quantity or 0),
|
||||
'qty_inbound': 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), # 兼容字段
|
||||
'qty_stock': float(self.stock_quantity or 0),
|
||||
'available_quantity': float(self.available_quantity or 0),
|
||||
'qty_available': float(self.available_quantity or 0), # 兼容字段
|
||||
'qty_available': float(self.available_quantity or 0),
|
||||
|
||||
# 财务
|
||||
'unit_price': float(self.unit_price or 0),
|
||||
'total_price': float(self.total_price or 0),
|
||||
'currency': self.currency,
|
||||
'exchange_rate': float(self.exchange_rate or 1.0),
|
||||
|
||||
# 商务 (字段映射)
|
||||
'supplier_name': self.supplier_name,
|
||||
'purchaser': self.buyer_name, # 映射回前端
|
||||
'purchaser_email': self.buyer_email, # 映射回前端
|
||||
'source_link': self.original_link, # 映射回前端
|
||||
'purchaser': self.buyer_name,
|
||||
'purchaser_email': self.buyer_email,
|
||||
'source_link': self.original_link,
|
||||
'detail_link': self.detail_link,
|
||||
'arrival_photo': self.arrival_photo
|
||||
}
|
||||
102
inventory-backend/app/models/inbound/product.py
Normal file
102
inventory-backend/app/models/inbound/product.py
Normal file
@ -0,0 +1,102 @@
|
||||
# app/models/inbound/product.py
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
class StockProduct(db.Model):
|
||||
"""
|
||||
成品入库库存表
|
||||
对应数据库表: stock_product
|
||||
"""
|
||||
__tablename__ = 'stock_product'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
base_id = db.Column(db.Integer, db.ForeignKey('material_base.id'), nullable=False)
|
||||
|
||||
sku = db.Column(db.String(100))
|
||||
production_date = db.Column(db.Date)
|
||||
barcode = db.Column(db.String(100))
|
||||
serial_number = db.Column(db.String(100))
|
||||
# SQL 无 batch_number
|
||||
|
||||
# 数量
|
||||
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))
|
||||
|
||||
# 生产与成本
|
||||
bom_code = db.Column('bom_id', db.String(100)) # 映射 SQL: bom_id
|
||||
bom_version = db.Column(db.String(50))
|
||||
work_order_code = db.Column('work_order_id', db.String(100)) # 映射 SQL: work_order_id
|
||||
|
||||
raw_material_cost = db.Column(db.Numeric(19, 4), default=0)
|
||||
manual_cost = db.Column(db.Numeric(19, 4), default=0)
|
||||
|
||||
production_manager = db.Column('producer_name', db.String(100)) # 映射 SQL: producer_name
|
||||
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)
|
||||
|
||||
# 成品特有字段
|
||||
sale_price = db.Column(db.Numeric(19, 4), default=0)
|
||||
inspection_report_link = db.Column(db.Text)
|
||||
order_id = db.Column(db.String(100))
|
||||
|
||||
# 关系定义
|
||||
material = db.relationship('MaterialBase', back_populates='stock_products')
|
||||
|
||||
def to_dict(self):
|
||||
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,
|
||||
'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,
|
||||
'inbound_date': self.production_date.strftime('%Y-%m-%d') if self.production_date else '',
|
||||
'barcode': self.barcode,
|
||||
'serial_number': self.serial_number,
|
||||
'warehouse_loc': self.warehouse_location,
|
||||
'status': self.status,
|
||||
|
||||
'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,
|
||||
'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,
|
||||
|
||||
'sale_price': float(self.sale_price or 0),
|
||||
'inspection_report_link': self.inspection_report_link,
|
||||
'order_id': self.order_id
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
# app/models/inbound/semi.py
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class StockSemi(db.Model):
|
||||
@ -10,80 +9,43 @@ class StockSemi(db.Model):
|
||||
"""
|
||||
__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 无 batch_number,此处移除
|
||||
|
||||
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)) # 仓库位
|
||||
# 状态与位置
|
||||
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))
|
||||
# 半成品特有字段 (SQL 字段映射)
|
||||
bom_code = db.Column('bom_id', db.String(100)) # 映射 SQL: bom_id
|
||||
bom_version = db.Column(db.String(50))
|
||||
work_order_code = db.Column('work_order_id', db.String(100)) # 映射 SQL: work_order_id
|
||||
|
||||
# 工单 相关
|
||||
# 数据库列名: 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)
|
||||
|
||||
# 成本 相关
|
||||
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_manager = db.Column('producer_name', db.String(100)) # 映射 SQL: producer_name
|
||||
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) # 详细信息链接
|
||||
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
|
||||
@ -91,47 +53,35 @@ class StockSemi(db.Model):
|
||||
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), # 兼容字段
|
||||
'qty_inbound': float(self.in_quantity or 0),
|
||||
'stock_quantity': float(self.stock_quantity or 0),
|
||||
'qty_stock': 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), # 兼容字段
|
||||
'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, # 前端展示总成本用
|
||||
|
||||
'unit_total_cost': unit_total,
|
||||
'production_manager': self.production_manager,
|
||||
|
||||
# 时间范围 (SQL存的是字符串,直接返回即可,或者根据需要拆分)
|
||||
# 如果 service 层存的是 "Start ~ End",这里直接返回
|
||||
'production_time_range': self.production_time_range,
|
||||
# 为了兼容前端分开的时间字段(如果有):
|
||||
# 简单的时间拆分逻辑,防止 split 报错
|
||||
'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,7 +1,5 @@
|
||||
# app/models/material.py
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class MaterialBase(db.Model):
|
||||
"""
|
||||
@ -10,78 +8,50 @@ class MaterialBase(db.Model):
|
||||
"""
|
||||
__tablename__ = 'material_base'
|
||||
|
||||
# 1. 基础字段 (保持不变)
|
||||
# 1. 基础字段
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(255), nullable=False, comment='基础信息名称')
|
||||
|
||||
# 类别 (对应 SQL: category)
|
||||
category = db.Column(db.String(100), comment='类别')
|
||||
|
||||
# 类型 (对应 SQL: material_type) -> 前端 prop="type"
|
||||
material_type = db.Column(db.String(100), comment='类型')
|
||||
|
||||
# 规格型号 (对应 SQL: spec_model) -> 前端 prop="spec"
|
||||
spec_model = db.Column(db.String(255), comment='规格型号')
|
||||
|
||||
unit = db.Column(db.String(50), comment='计量单位')
|
||||
|
||||
# 可见等级
|
||||
visibility_level = db.Column(db.Integer, default=0, comment='信息可见等级')
|
||||
|
||||
# 链接与图片
|
||||
manual_link = db.Column(db.Text, comment='通用说明书链接')
|
||||
product_image = db.Column(db.Text, comment='通用产品图链接')
|
||||
manual_link = db.Column(db.Text, comment='通用说明书')
|
||||
product_image = db.Column(db.Text, comment='通用产品图')
|
||||
|
||||
# 启用状态
|
||||
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)
|
||||
|
||||
# ============================================================
|
||||
# 关联关系区域 (修改重点)
|
||||
# 关联关系区域
|
||||
# ============================================================
|
||||
|
||||
# 1. 关联采购库存 (StockBuy) - 保持不变
|
||||
# 注意:确保 app/models/inbound/buy.py 中的 StockBuy 定义了 back_populates='material'
|
||||
# 1. 关联采购库存 (StockBuy)
|
||||
stock_buys = db.relationship('StockBuy', back_populates='material', lazy='dynamic')
|
||||
|
||||
# 2. 【新增】关联半成品库存 (StockSemi)
|
||||
# 注意:确保 app/models/inbound/semi.py 中的 StockSemi 定义了 back_populates='material'
|
||||
# 这样以后可以通过 material.stock_semis 来访问该物料下的所有半成品库存记录
|
||||
# 2. 关联半成品库存 (StockSemi)
|
||||
stock_semis = db.relationship('StockSemi', back_populates='material', lazy='dynamic')
|
||||
|
||||
# 3. 关联成品库存 (StockProduct)
|
||||
stock_products = db.relationship('StockProduct', back_populates='material', lazy='dynamic')
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
序列化方法:将模型转换为字典,供API返回JSON使用
|
||||
序列化方法
|
||||
"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'category': self.category,
|
||||
|
||||
# =========================================
|
||||
# 关键映射区 (保持不变)
|
||||
# =========================================
|
||||
# 数据库叫 material_type -> 前端叫 type
|
||||
'type': self.material_type,
|
||||
|
||||
# 数据库叫 spec_model -> 前端叫 spec
|
||||
'spec': self.spec_model,
|
||||
|
||||
'type': self.material_type, # 前端字段映射
|
||||
'spec': self.spec_model, # 前端字段映射
|
||||
'unit': self.unit,
|
||||
|
||||
# 驼峰命名适配
|
||||
'visibilityLevel': self.visibility_level,
|
||||
'generalManual': self.manual_link,
|
||||
'generalImage': self.product_image,
|
||||
|
||||
# 状态处理
|
||||
'isEnabled': 1 if self.is_enabled else 0,
|
||||
|
||||
# 时间字段保持注释
|
||||
# 'createTime': self.create_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(self, 'create_time') and self.create_time else None
|
||||
}
|
||||
@ -0,0 +1,173 @@
|
||||
from app.extensions import db
|
||||
from app.models.material import MaterialBase
|
||||
# 引用新的 product 路径
|
||||
from app.models.inbound.product import StockProduct
|
||||
from datetime import datetime
|
||||
from sqlalchemy import or_, func
|
||||
import traceback
|
||||
|
||||
|
||||
class ProductInboundService:
|
||||
@staticmethod
|
||||
def search_base_material(keyword):
|
||||
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
|
||||
})
|
||||
return results
|
||||
except:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def handle_inbound(data):
|
||||
try:
|
||||
base_id = data.get('base_id')
|
||||
if not base_id: raise ValueError("必须选择基础物料")
|
||||
material = MaterialBase.query.get(base_id)
|
||||
if not material: raise ValueError("物料不存在")
|
||||
|
||||
in_date_val = datetime.utcnow().date()
|
||||
if data.get('in_date'):
|
||||
try:
|
||||
in_date_val = datetime.strptime(str(data['in_date'])[:10], '%Y-%m-%d').date()
|
||||
except:
|
||||
pass
|
||||
|
||||
in_qty = float(data.get('in_quantity') or 0)
|
||||
|
||||
p_start = data.get('production_start_time', '')
|
||||
p_end = data.get('production_end_time', '')
|
||||
time_range = f"{p_start} ~ {p_end}" if p_start or p_end else None
|
||||
|
||||
new_stock = StockProduct(
|
||||
base_id=material.id,
|
||||
sku=data.get('sku'),
|
||||
production_date=in_date_val,
|
||||
barcode=data.get('barcode'),
|
||||
serial_number=data.get('serial_number'),
|
||||
|
||||
status='在库',
|
||||
warehouse_location=data.get('warehouse_location'),
|
||||
|
||||
in_quantity=in_qty,
|
||||
stock_quantity=in_qty,
|
||||
available_quantity=in_qty,
|
||||
|
||||
bom_code=data.get('bom_code'),
|
||||
bom_version=data.get('bom_version'),
|
||||
work_order_code=data.get('work_order_code'),
|
||||
production_manager=data.get('production_manager'),
|
||||
production_time_range=time_range,
|
||||
|
||||
raw_material_cost=float(data.get('raw_material_cost') or 0),
|
||||
manual_cost=float(data.get('manual_cost') or 0),
|
||||
|
||||
quality_status=data.get('quality_status', '合格'),
|
||||
quality_report_link=data.get('quality_report_link'),
|
||||
detail_link=data.get('detail_link'),
|
||||
|
||||
sale_price=float(data.get('sale_price') or 0),
|
||||
inspection_report_link=data.get('inspection_report_link'),
|
||||
order_id=data.get('order_id')
|
||||
)
|
||||
|
||||
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:
|
||||
stock = StockProduct.query.get(stock_id)
|
||||
if not stock: raise ValueError("记录不存在")
|
||||
|
||||
fields = [
|
||||
'sku', 'barcode', 'serial_number', 'warehouse_location',
|
||||
'status', 'quality_status', 'bom_code', 'bom_version',
|
||||
'work_order_code', 'production_manager', 'quality_report_link',
|
||||
'detail_link', 'inspection_report_link', 'order_id'
|
||||
]
|
||||
for f in fields:
|
||||
if f in data: setattr(stock, f, data[f])
|
||||
|
||||
if 'sale_price' in data: stock.sale_price = float(data['sale_price'])
|
||||
if 'raw_material_cost' in data: stock.raw_material_cost = float(data['raw_material_cost'])
|
||||
if 'manual_cost' in data: stock.manual_cost = float(data['manual_cost'])
|
||||
|
||||
if 'in_quantity' in data:
|
||||
new_qty = float(data['in_quantity'])
|
||||
diff = new_qty - float(stock.in_quantity)
|
||||
stock.in_quantity = new_qty
|
||||
stock.stock_quantity = float(stock.stock_quantity) + diff
|
||||
stock.available_quantity = float(stock.available_quantity) + diff
|
||||
|
||||
if 'production_start_time' in data or 'production_end_time' in data:
|
||||
old_range = stock.production_time_range or " ~ "
|
||||
parts = old_range.split(' ~ ')
|
||||
start = data.get('production_start_time', parts[0] if len(parts) > 0 else '')
|
||||
end = data.get('production_end_time', parts[1] if len(parts) > 1 else '')
|
||||
stock.production_time_range = f"{start} ~ {end}"
|
||||
|
||||
db.session.commit()
|
||||
return stock
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def delete_inbound(stock_id):
|
||||
try:
|
||||
stock = StockProduct.query.get(stock_id)
|
||||
if stock:
|
||||
db.session.delete(stock)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def get_list(page, limit, keyword=None):
|
||||
try:
|
||||
query = db.session.query(StockProduct).outerjoin(MaterialBase, StockProduct.base_id == MaterialBase.id)
|
||||
if keyword:
|
||||
query = query.filter(or_(
|
||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||
StockProduct.serial_number.ilike(f'%{keyword}%'),
|
||||
StockProduct.work_order_code.ilike(f'%{keyword}%'),
|
||||
StockProduct.order_id.ilike(f'%{keyword}%')
|
||||
))
|
||||
pagination = query.order_by(StockProduct.id.desc()).paginate(page=page, per_page=limit, error_out=False)
|
||||
|
||||
base_ids = list(set([i.base_id for i in pagination.items]))
|
||||
stock_map = {}
|
||||
if base_ids:
|
||||
aggs = db.session.query(
|
||||
StockProduct.base_id,
|
||||
func.sum(StockProduct.stock_quantity).label('s'),
|
||||
func.sum(StockProduct.available_quantity).label('a')
|
||||
).filter(StockProduct.base_id.in_(base_ids)).group_by(StockProduct.base_id).all()
|
||||
for a in aggs: stock_map[a.base_id] = {'s': float(a.s or 0), 'a': float(a.a or 0)}
|
||||
|
||||
items = []
|
||||
for item in pagination.items:
|
||||
d = item.to_dict()
|
||||
stats = stock_map.get(item.base_id, {'s': 0, 'a': 0})
|
||||
d['sum_stock'] = stats['s']
|
||||
d['sum_available'] = stats['a']
|
||||
items.append(d)
|
||||
|
||||
return {"total": pagination.total, "items": items}
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return {"total": 0, "items": []}
|
||||
Reference in New Issue
Block a user