增加入库记录页面,同时修正三组入库的时间问题
This commit is contained in:
@ -4,6 +4,8 @@ from .semi import inbound_semi_bp
|
||||
from .base import inbound_base_bp
|
||||
# 导入 product
|
||||
from .product import inbound_product_bp
|
||||
# ★ [新增] 导入 summary
|
||||
from .inbound_summary import bp as inbound_summary_bp
|
||||
|
||||
inbound_bp = Blueprint('inbound', __name__)
|
||||
|
||||
@ -12,3 +14,6 @@ inbound_bp.register_blueprint(inbound_semi_bp, url_prefix='/semi')
|
||||
inbound_bp.register_blueprint(inbound_base_bp, url_prefix='/base')
|
||||
# 挂载 product,前缀改为 /product
|
||||
inbound_bp.register_blueprint(inbound_product_bp, url_prefix='/product')
|
||||
|
||||
# ★ [新增] 挂载 summary, url 变成 /api/v1/inbound/summary/list
|
||||
inbound_bp.register_blueprint(inbound_summary_bp, url_prefix='/summary')
|
||||
35
inventory-backend/app/api/v1/inbound/inbound_summary.py
Normal file
35
inventory-backend/app/api/v1/inbound/inbound_summary.py
Normal file
@ -0,0 +1,35 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.services.inbound.inbound_summary_service import InboundSummaryService
|
||||
|
||||
# 定义蓝图
|
||||
bp = Blueprint('inbound_summary', __name__)
|
||||
|
||||
@bp.route('/list', methods=['GET'])
|
||||
def get_list():
|
||||
try:
|
||||
# 获取参数
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int) # 默认每页20
|
||||
keyword = request.args.get('keyword', '')
|
||||
start_date = request.args.get('start_date')
|
||||
end_date = request.args.get('end_date')
|
||||
source_type = request.args.get('source_type') # 可选:筛选 specific table
|
||||
|
||||
result = InboundSummaryService.get_list(
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
keyword=keyword,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
source_type=source_type
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
'data': result
|
||||
})
|
||||
except Exception as e:
|
||||
# 生产环境建议记录详细日志
|
||||
print(f"Inbound Summary Error: {str(e)}")
|
||||
return jsonify({'code': 500, 'msg': str(e)}), 500
|
||||
@ -15,7 +15,7 @@ class StockBuy(db.Model):
|
||||
|
||||
# 身份标识
|
||||
sku = db.Column(db.String(100))
|
||||
in_date = db.Column(db.Date)
|
||||
in_date = db.Column(db.DateTime)
|
||||
barcode = db.Column(db.String(100))
|
||||
serial_number = db.Column(db.String(100))
|
||||
batch_number = db.Column(db.String(100))
|
||||
|
||||
@ -15,7 +15,7 @@ class StockProduct(db.Model):
|
||||
|
||||
# 身份标识
|
||||
sku = db.Column(db.String(100))
|
||||
production_date = db.Column(db.Date)
|
||||
production_date = db.Column(db.DateTime)
|
||||
barcode = db.Column(db.String(100))
|
||||
serial_number = db.Column(db.String(100))
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ class StockSemi(db.Model):
|
||||
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)
|
||||
production_date = db.Column(db.DateTime)
|
||||
barcode = db.Column(db.String(100))
|
||||
serial_number = db.Column(db.String(100))
|
||||
batch_number = db.Column(db.String(100))
|
||||
|
||||
@ -40,7 +40,7 @@ class BuyInboundService:
|
||||
return []
|
||||
|
||||
# ============================================================
|
||||
# 2. 新增入库逻辑
|
||||
# 2. 新增入库逻辑 (修改:精确到时间)
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def handle_inbound(data):
|
||||
@ -51,14 +51,24 @@ class BuyInboundService:
|
||||
material = MaterialBase.query.get(base_id)
|
||||
if not material: raise ValueError("物料不存在")
|
||||
|
||||
in_date_val = datetime.utcnow().date()
|
||||
# [核心修改] 默认使用当前时间(含时分秒),不再截取 .date()
|
||||
current_time = datetime.now()
|
||||
in_date_val = current_time
|
||||
|
||||
if data.get('in_date'):
|
||||
try:
|
||||
date_str = str(data['in_date'])
|
||||
if len(date_str) > 10: date_str = date_str[:10]
|
||||
in_date_val = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
# 如果前端传了时分秒,尝试直接解析
|
||||
if len(date_str) > 10:
|
||||
in_date_val = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
|
||||
else:
|
||||
# 如果只传了日期,手动补上当前时间的时分秒,保证同日入库的排序正确
|
||||
d_temp = datetime.strptime(date_str, '%Y-%m-%d')
|
||||
in_date_val = datetime(d_temp.year, d_temp.month, d_temp.day,
|
||||
current_time.hour, current_time.minute, current_time.second)
|
||||
except:
|
||||
pass
|
||||
# 解析失败则使用当前时间作为兜底
|
||||
in_date_val = current_time
|
||||
|
||||
in_qty = float(data.get('in_quantity') or 0)
|
||||
u_price = float(data.get('unit_price') or 0)
|
||||
@ -80,10 +90,10 @@ class BuyInboundService:
|
||||
global_print_id=next_global_id,
|
||||
sku=generated_sku,
|
||||
barcode=final_barcode,
|
||||
in_date=in_date_val,
|
||||
in_date=in_date_val, # 存入 DateTime 对象
|
||||
serial_number=data.get('serial_number'),
|
||||
batch_number=data.get('batch_number'),
|
||||
status=data.get('status', '在库'), # 默认在库
|
||||
status=data.get('status', '在库'),
|
||||
in_quantity=in_qty,
|
||||
stock_quantity=in_qty,
|
||||
available_quantity=in_qty,
|
||||
@ -185,7 +195,7 @@ class BuyInboundService:
|
||||
return []
|
||||
|
||||
# ============================================================
|
||||
# 6. 获取列表 (核心逻辑修改)
|
||||
# 6. 获取列表 (修改:按时间倒序排序 + 展示只显示日期)
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def get_list(page, limit, keyword=None, statuses=None):
|
||||
@ -196,7 +206,7 @@ class BuyInboundService:
|
||||
try:
|
||||
query = db.session.query(StockBuy).outerjoin(MaterialBase, StockBuy.base_id == MaterialBase.id)
|
||||
|
||||
# 1. 关键词搜索 (覆盖所有关键字段)
|
||||
# 1. 关键词搜索
|
||||
if keyword:
|
||||
kw = f'%{keyword}%'
|
||||
query = query.filter(
|
||||
@ -210,24 +220,13 @@ class BuyInboundService:
|
||||
)
|
||||
)
|
||||
|
||||
# 2. 状态筛选与零库存隐藏逻辑
|
||||
# 用户要求:
|
||||
# - 默认显示:'在库', '借库'。
|
||||
# - 零库存规则:库存为0时,不在页面显示(除非筛选了'已出库')。
|
||||
|
||||
# 2. 状态筛选
|
||||
if not statuses:
|
||||
# 默认情况:只查 '在库' 和 '借库'
|
||||
statuses = ['在库', '借库']
|
||||
|
||||
# 构建筛选条件
|
||||
# 如果筛选条件中 包含 '已出库',则允许显示 stock_quantity >= 0 (即显示所有)
|
||||
# 如果筛选条件中 不包含 '已出库',则强制要求 stock_quantity > 0 (隐藏零库存)
|
||||
|
||||
if '已出库' in statuses:
|
||||
# 用户想看已出库的,直接按状态查,不做数量限制
|
||||
query = query.filter(StockBuy.status.in_(statuses))
|
||||
else:
|
||||
# 用户不想看已出库的,按状态查 AND 数量必须 > 0
|
||||
query = query.filter(
|
||||
and_(
|
||||
StockBuy.status.in_(statuses),
|
||||
@ -235,7 +234,8 @@ class BuyInboundService:
|
||||
)
|
||||
)
|
||||
|
||||
pagination = query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit, error_out=False)
|
||||
# [核心修改] 按照入库时间倒序排序 (从近到远)
|
||||
pagination = query.order_by(StockBuy.in_date.desc()).paginate(page=page, per_page=limit, error_out=False)
|
||||
current_items = pagination.items
|
||||
|
||||
def parse_img(json_str):
|
||||
@ -247,10 +247,17 @@ class BuyInboundService:
|
||||
|
||||
items = []
|
||||
for item in current_items:
|
||||
# 获取单行数据,不再进行聚合计算
|
||||
qty_stock = float(item.stock_quantity or 0)
|
||||
qty_avail = float(item.available_quantity or 0)
|
||||
|
||||
# [核心修改] 格式化展示日期,去掉时分秒
|
||||
date_display = ''
|
||||
if item.in_date:
|
||||
try:
|
||||
date_display = item.in_date.strftime('%Y-%m-%d')
|
||||
except:
|
||||
date_display = str(item.in_date)[:10]
|
||||
|
||||
d = {
|
||||
'id': item.id,
|
||||
'base_id': item.base_id,
|
||||
@ -261,7 +268,7 @@ class BuyInboundService:
|
||||
'material_type': item.material.material_type if item.material else '',
|
||||
|
||||
'sku': item.sku,
|
||||
'inbound_date': str(item.in_date) if item.in_date else '',
|
||||
'inbound_date': date_display, # 前端展示用的日期字符串
|
||||
'barcode': item.barcode,
|
||||
'serial_number': item.serial_number,
|
||||
'batch_number': item.batch_number,
|
||||
@ -273,8 +280,6 @@ class BuyInboundService:
|
||||
'qty_stock': qty_stock,
|
||||
'qty_available': qty_avail,
|
||||
|
||||
# 解除挂钩:不再返回所有批次的总和,直接返回当前批次的数量
|
||||
# 为了兼容前端字段名,这里直接用当前行数量填充
|
||||
'sum_stock': qty_stock,
|
||||
'sum_available': qty_avail,
|
||||
|
||||
|
||||
@ -0,0 +1,190 @@
|
||||
from sqlalchemy import select, literal, union_all, desc, asc, func, or_, cast, String, Numeric, Date
|
||||
from app.extensions import db
|
||||
from app.models.inbound.buy import StockBuy
|
||||
from app.models.inbound.semi import StockSemi
|
||||
from app.models.inbound.product import StockProduct
|
||||
from app.models.base import MaterialBase
|
||||
import traceback
|
||||
|
||||
|
||||
class InboundSummaryService:
|
||||
|
||||
@staticmethod
|
||||
def get_list(page=1, per_page=10, keyword=None, start_date=None, end_date=None, source_type=None):
|
||||
"""
|
||||
聚合查询:
|
||||
1. 联合 StockBuy, StockSemi, StockProduct 三张表
|
||||
2. 关联 MaterialBase 获取名称规格
|
||||
3. 计算动态状态 (库存耗尽显示已出库)
|
||||
4. 排序:默认按入库日期倒序 (最近的在前)
|
||||
"""
|
||||
try:
|
||||
# =========================================================
|
||||
# 1. 构建三个子查询 (Subqueries)
|
||||
# =========================================================
|
||||
|
||||
# --- A. 采购件 (StockBuy) ---
|
||||
q_buy = db.session.query(
|
||||
StockBuy.id.label('id'),
|
||||
StockBuy.base_id.label('base_id'),
|
||||
StockBuy.sku.label('sku'),
|
||||
StockBuy.in_date.label('inbound_date'),
|
||||
StockBuy.in_quantity.label('in_qty'),
|
||||
StockBuy.stock_quantity.label('current_qty'),
|
||||
|
||||
cast(StockBuy.supplier_name, String).label('source_info'),
|
||||
StockBuy.status.label('orig_status'),
|
||||
cast(StockBuy.batch_number, String).label('batch_number'),
|
||||
cast(literal('buy'), String).label('source_type')
|
||||
)
|
||||
|
||||
# --- B. 半成品 (StockSemi) ---
|
||||
q_semi = db.session.query(
|
||||
StockSemi.id.label('id'),
|
||||
StockSemi.base_id.label('base_id'),
|
||||
StockSemi.sku.label('sku'),
|
||||
StockSemi.production_date.label('inbound_date'),
|
||||
StockSemi.in_quantity.label('in_qty'),
|
||||
StockSemi.stock_quantity.label('current_qty'),
|
||||
|
||||
cast(StockSemi.production_manager, String).label('source_info'),
|
||||
StockSemi.status.label('orig_status'),
|
||||
cast(StockSemi.batch_number, String).label('batch_number'),
|
||||
cast(literal('semi'), String).label('source_type')
|
||||
)
|
||||
|
||||
# --- C. 成品 (StockProduct) ---
|
||||
q_product = db.session.query(
|
||||
StockProduct.id.label('id'),
|
||||
StockProduct.base_id.label('base_id'),
|
||||
StockProduct.sku.label('sku'),
|
||||
StockProduct.production_date.label('inbound_date'),
|
||||
StockProduct.in_quantity.label('in_qty'),
|
||||
StockProduct.stock_quantity.label('current_qty'),
|
||||
|
||||
cast(StockProduct.production_manager, String).label('source_info'),
|
||||
StockProduct.status.label('orig_status'),
|
||||
cast(StockProduct.serial_number, String).label('batch_number'),
|
||||
cast(literal('product'), String).label('source_type')
|
||||
)
|
||||
|
||||
# =========================================================
|
||||
# 2. 组合查询 (UNION ALL)
|
||||
# =========================================================
|
||||
combined_query = union_all(q_buy, q_semi, q_product)
|
||||
cte = combined_query.subquery()
|
||||
|
||||
# =========================================================
|
||||
# 3. 主查询:关联 MaterialBase
|
||||
# =========================================================
|
||||
query = db.session.query(
|
||||
cte,
|
||||
MaterialBase.name.label('material_name'),
|
||||
MaterialBase.spec_model.label('spec_model'),
|
||||
MaterialBase.category.label('category'),
|
||||
MaterialBase.material_type.label('material_type')
|
||||
).outerjoin(
|
||||
MaterialBase, cte.c.base_id == MaterialBase.id
|
||||
)
|
||||
|
||||
# =========================================================
|
||||
# 4. 过滤条件
|
||||
# =========================================================
|
||||
if keyword:
|
||||
rule = or_(
|
||||
cte.c.sku.ilike(f'%{keyword}%'),
|
||||
cte.c.source_info.ilike(f'%{keyword}%'),
|
||||
cte.c.batch_number.ilike(f'%{keyword}%'),
|
||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||
)
|
||||
query = query.filter(rule)
|
||||
|
||||
if start_date and end_date:
|
||||
query = query.filter(cte.c.inbound_date.between(start_date, end_date))
|
||||
|
||||
if source_type:
|
||||
query = query.filter(cte.c.source_type == source_type)
|
||||
|
||||
# =========================================================
|
||||
# 5. 获取总数
|
||||
# =========================================================
|
||||
count_query = db.session.query(func.count()) \
|
||||
.select_from(cte) \
|
||||
.outerjoin(MaterialBase, cte.c.base_id == MaterialBase.id)
|
||||
|
||||
if keyword:
|
||||
count_query = count_query.filter(rule)
|
||||
if start_date and end_date:
|
||||
count_query = count_query.filter(cte.c.inbound_date.between(start_date, end_date))
|
||||
if source_type:
|
||||
count_query = count_query.filter(cte.c.source_type == source_type)
|
||||
|
||||
total = count_query.scalar() or 0
|
||||
|
||||
# =========================================================
|
||||
# 6. 排序与分页
|
||||
# =========================================================
|
||||
# ★★★ 修改处:优先按入库日期倒序排列 (最近的在前) ★★★
|
||||
# 如果日期相同,再按 SKU 排序,保证分页稳定性
|
||||
query = query.order_by(desc(cte.c.inbound_date), asc(cte.c.sku))
|
||||
|
||||
pagination = query.limit(per_page).offset((page - 1) * per_page).all()
|
||||
|
||||
# =========================================================
|
||||
# 7. 数据格式化
|
||||
# =========================================================
|
||||
items = []
|
||||
type_map = {
|
||||
'buy': '采购入库',
|
||||
'semi': '半成品生产',
|
||||
'product': '成品完工'
|
||||
}
|
||||
|
||||
for row in pagination:
|
||||
date_str = ""
|
||||
if row.inbound_date:
|
||||
try:
|
||||
date_str = row.inbound_date.strftime('%Y-%m-%d')
|
||||
except Exception:
|
||||
date_str = str(row.inbound_date)
|
||||
|
||||
in_qty = float(row.in_qty) if row.in_qty is not None else 0.0
|
||||
current_qty = float(row.current_qty) if row.current_qty is not None else 0.0
|
||||
|
||||
# 状态逻辑
|
||||
final_status = row.orig_status
|
||||
if current_qty <= 0:
|
||||
final_status = "已出库"
|
||||
elif current_qty < in_qty:
|
||||
final_status = "部分出库"
|
||||
|
||||
items.append({
|
||||
'id': row.id,
|
||||
'sku': row.sku or "",
|
||||
'name': row.material_name or "未知物品",
|
||||
'spec_model': row.spec_model or "",
|
||||
'category': row.category or "",
|
||||
'material_type': row.material_type or "",
|
||||
|
||||
'inbound_date': date_str,
|
||||
'quantity': in_qty,
|
||||
'current_qty': current_qty,
|
||||
'source_info': row.source_info or "",
|
||||
'status': final_status,
|
||||
'source_type': row.source_type,
|
||||
'type_label': type_map.get(row.source_type, "未知类型"),
|
||||
'batch_number': row.batch_number or ""
|
||||
})
|
||||
|
||||
return {
|
||||
'items': items,
|
||||
'total': total,
|
||||
'pages': (total + per_page - 1) // per_page if per_page > 0 else 0,
|
||||
'current_page': page
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print("【InboundSummaryService Error】:", str(e))
|
||||
traceback.print_exc()
|
||||
raise e
|
||||
@ -32,6 +32,9 @@ class ProductInboundService:
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
# ============================================================
|
||||
# 2. 新增入库逻辑 (修改:精确到时间)
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def handle_inbound(data):
|
||||
from app.models.inbound.product import StockProduct
|
||||
@ -42,14 +45,21 @@ class ProductInboundService:
|
||||
material = MaterialBase.query.get(base_id)
|
||||
if not material: raise ValueError("物料不存在")
|
||||
|
||||
in_date_val = datetime.utcnow().date()
|
||||
# [核心修改] 处理 production_date,包含时分秒
|
||||
current_time = datetime.now()
|
||||
in_date_val = current_time
|
||||
|
||||
if data.get('in_date'):
|
||||
try:
|
||||
date_str = str(data['in_date'])
|
||||
if len(date_str) > 10: date_str = date_str[:10]
|
||||
in_date_val = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
if len(date_str) > 10:
|
||||
in_date_val = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
|
||||
else:
|
||||
d_temp = datetime.strptime(date_str, '%Y-%m-%d')
|
||||
in_date_val = datetime(d_temp.year, d_temp.month, d_temp.day,
|
||||
current_time.hour, current_time.minute, current_time.second)
|
||||
except:
|
||||
pass
|
||||
in_date_val = current_time
|
||||
|
||||
in_qty = float(data.get('in_quantity') or 0)
|
||||
|
||||
@ -65,7 +75,6 @@ class ProductInboundService:
|
||||
generated_sku = str(next_global_id).zfill(10)
|
||||
final_barcode = data.get('barcode') or generated_sku
|
||||
|
||||
# 处理三个图片/链接列表
|
||||
photo_list = data.get('product_photo', [])
|
||||
quality_list = data.get('quality_report_link', [])
|
||||
inspection_list = data.get('inspection_report_link', [])
|
||||
@ -78,7 +87,7 @@ class ProductInboundService:
|
||||
base_id=material.id,
|
||||
global_print_id=next_global_id,
|
||||
sku=generated_sku,
|
||||
production_date=in_date_val,
|
||||
production_date=in_date_val, # 存入 DateTime
|
||||
barcode=final_barcode,
|
||||
serial_number=data.get('serial_number'),
|
||||
|
||||
@ -100,7 +109,6 @@ class ProductInboundService:
|
||||
|
||||
quality_status=data.get('quality_status', '合格'),
|
||||
|
||||
# 存为 JSON
|
||||
product_photo=json.dumps(photo_list),
|
||||
quality_report_link=json.dumps(quality_list),
|
||||
inspection_report_link=json.dumps(inspection_list),
|
||||
@ -136,7 +144,6 @@ class ProductInboundService:
|
||||
for f in fields:
|
||||
if f in data: setattr(stock, f, data[f])
|
||||
|
||||
# 更新 JSON 字段
|
||||
if 'product_photo' in data:
|
||||
imgs = data['product_photo']
|
||||
if isinstance(imgs, list): stock.product_photo = json.dumps(imgs)
|
||||
@ -188,9 +195,6 @@ class ProductInboundService:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
# ============================================================
|
||||
# 获取出库流转历史 (与 Buy 逻辑一致,关联 TransOutbound 表)
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def get_outbound_history(stock_id):
|
||||
"""获取出库历史"""
|
||||
@ -203,7 +207,7 @@ class ProductInboundService:
|
||||
return []
|
||||
|
||||
# ============================================================
|
||||
# 获取列表 (包含状态筛选与零库存隐藏逻辑)
|
||||
# 获取列表 (修改:按时间倒序排序 + 展示只显示日期)
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def get_list(page, limit, keyword=None, statuses=None):
|
||||
@ -211,7 +215,6 @@ class ProductInboundService:
|
||||
try:
|
||||
query = db.session.query(StockProduct).outerjoin(MaterialBase, StockProduct.base_id == MaterialBase.id)
|
||||
|
||||
# 1. 关键词搜索
|
||||
if keyword:
|
||||
query = query.filter(or_(
|
||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||
@ -222,11 +225,9 @@ class ProductInboundService:
|
||||
StockProduct.sku.ilike(f'%{keyword}%')
|
||||
))
|
||||
|
||||
# 2. 状态筛选与零库存隐藏逻辑
|
||||
if not statuses:
|
||||
statuses = ['在库', '借库']
|
||||
|
||||
# 如果筛选包含'已出库',则显示所有数量;否则隐藏 stock_quantity <= 0 的记录
|
||||
if '已出库' in statuses:
|
||||
query = query.filter(StockProduct.status.in_(statuses))
|
||||
else:
|
||||
@ -237,7 +238,9 @@ class ProductInboundService:
|
||||
)
|
||||
)
|
||||
|
||||
pagination = query.order_by(StockProduct.id.desc()).paginate(page=page, per_page=limit, error_out=False)
|
||||
# [核心修改] 按照 production_date (入库日期) 倒序排序
|
||||
pagination = query.order_by(StockProduct.production_date.desc()).paginate(page=page, per_page=limit,
|
||||
error_out=False)
|
||||
|
||||
current_items = pagination.items
|
||||
|
||||
@ -252,20 +255,23 @@ class ProductInboundService:
|
||||
for item in current_items:
|
||||
d = item.to_dict()
|
||||
|
||||
# 直接使用当前行的库存,不再聚合
|
||||
# [核心修改] 格式化日期
|
||||
date_display = ''
|
||||
if item.production_date:
|
||||
try:
|
||||
date_display = item.production_date.strftime('%Y-%m-%d')
|
||||
except:
|
||||
date_display = str(item.production_date)[:10]
|
||||
d['inbound_date'] = date_display
|
||||
|
||||
d['qty_stock'] = float(item.stock_quantity or 0)
|
||||
d['qty_available'] = float(item.available_quantity or 0)
|
||||
|
||||
# 兼容前端字段 key
|
||||
d['sum_stock'] = d['qty_stock']
|
||||
d['sum_available'] = d['qty_available']
|
||||
|
||||
# 图片/链接解析
|
||||
d['product_photo'] = parse_img(item.product_photo)
|
||||
d['quality_report_link'] = parse_img(item.quality_report_link)
|
||||
d['inspection_report_link'] = parse_img(item.inspection_report_link)
|
||||
|
||||
# 打印ID
|
||||
d['global_print_id'] = item.global_print_id
|
||||
|
||||
items.append(d)
|
||||
|
||||
@ -41,7 +41,6 @@ class SemiInboundService:
|
||||
|
||||
@staticmethod
|
||||
def handle_inbound(data):
|
||||
# 局部导入 Model,解决循环引用
|
||||
from app.models.inbound.semi import StockSemi
|
||||
|
||||
try:
|
||||
@ -53,16 +52,21 @@ class SemiInboundService:
|
||||
if not material:
|
||||
raise ValueError(f"ID为 {base_id} 的基础物料不存在")
|
||||
|
||||
# 1. 处理入库日期
|
||||
in_date_val = datetime.utcnow().date()
|
||||
# [核心修改] 处理入库日期(production_date),包含时分秒
|
||||
current_time = datetime.now()
|
||||
in_date_val = current_time
|
||||
|
||||
if data.get('in_date'):
|
||||
try:
|
||||
date_str = str(data['in_date'])
|
||||
if len(date_str) > 10:
|
||||
date_str = date_str[:10]
|
||||
in_date_val = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
in_date_val = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
|
||||
else:
|
||||
d_temp = datetime.strptime(date_str, '%Y-%m-%d')
|
||||
in_date_val = datetime(d_temp.year, d_temp.month, d_temp.day,
|
||||
current_time.hour, current_time.minute, current_time.second)
|
||||
except ValueError:
|
||||
pass
|
||||
in_date_val = current_time
|
||||
|
||||
# 2. 处理生产时间
|
||||
p_start = None
|
||||
@ -102,18 +106,15 @@ class SemiInboundService:
|
||||
print("❌ 数据库序列 global_print_seq 不存在,请执行SQL创建!")
|
||||
raise e
|
||||
|
||||
# 5. 自动生成 SKU
|
||||
generated_sku = str(next_global_id).zfill(10)
|
||||
final_sku = data.get('sku')
|
||||
if not final_sku:
|
||||
final_sku = generated_sku
|
||||
|
||||
# 6. 条码逻辑处理
|
||||
final_barcode = data.get('barcode')
|
||||
if not final_barcode:
|
||||
final_barcode = final_sku
|
||||
|
||||
# 7. 图片列表转 JSON 字符串处理
|
||||
arrival_list = data.get('arrival_photo', [])
|
||||
quality_report_list = data.get('quality_report_link', [])
|
||||
|
||||
@ -125,7 +126,7 @@ class SemiInboundService:
|
||||
base_id=material.id,
|
||||
global_print_id=next_global_id,
|
||||
sku=final_sku,
|
||||
production_date=in_date_val,
|
||||
production_date=in_date_val, # 存入 DateTime
|
||||
|
||||
serial_number=data.get('serial_number'),
|
||||
batch_number=data.get('batch_number'),
|
||||
@ -151,7 +152,6 @@ class SemiInboundService:
|
||||
manual_cost=manual_cost,
|
||||
total_price=total_value,
|
||||
|
||||
# [核心修改] 将列表转为 JSON 字符串存储
|
||||
arrival_photo=json.dumps(arrival_list),
|
||||
quality_report_link=json.dumps(quality_report_list),
|
||||
|
||||
@ -174,8 +174,6 @@ class SemiInboundService:
|
||||
from app.models.inbound.semi import StockSemi
|
||||
|
||||
try:
|
||||
print(f"----- UPDATE SEMI DEBUG: ID={stock_id} -----")
|
||||
|
||||
stock = StockSemi.query.get(stock_id)
|
||||
if not stock:
|
||||
raise ValueError("记录不存在")
|
||||
@ -200,7 +198,6 @@ class SemiInboundService:
|
||||
if frontend_key in data:
|
||||
setattr(stock, db_attr, data[frontend_key])
|
||||
|
||||
# [核心修改] 图片字段更新 (List -> JSON String)
|
||||
if 'arrival_photo' in data:
|
||||
imgs = data['arrival_photo']
|
||||
if isinstance(imgs, list):
|
||||
@ -211,7 +208,6 @@ class SemiInboundService:
|
||||
if isinstance(imgs, list):
|
||||
stock.quality_report_link = json.dumps(imgs)
|
||||
|
||||
# 时间处理
|
||||
if 'production_start_time' in data:
|
||||
try:
|
||||
if data['production_start_time']:
|
||||
@ -232,7 +228,6 @@ class SemiInboundService:
|
||||
except:
|
||||
pass
|
||||
|
||||
# 更新 production_time_range 字符串
|
||||
if 'production_time_range' in data:
|
||||
raw_range = data['production_time_range']
|
||||
if isinstance(raw_range, list):
|
||||
@ -269,8 +264,6 @@ class SemiInboundService:
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"----- UPDATE SEMI FAILED: {str(e)} -----")
|
||||
traceback.print_exc()
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
@ -287,9 +280,6 @@ class SemiInboundService:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# [核心修改] 获取关联出库历史 (跟 Buy 保持一致)
|
||||
# ------------------------------------------------------------------
|
||||
@staticmethod
|
||||
def get_outbound_history(stock_id):
|
||||
"""获取出库历史"""
|
||||
@ -301,9 +291,9 @@ class SemiInboundService:
|
||||
except:
|
||||
return []
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# [核心修改] 列表查询:支持状态筛选、默认隐藏0库存、去除聚合
|
||||
# ------------------------------------------------------------------
|
||||
# ============================================================
|
||||
# 6. 获取列表 (修改:按时间倒序排序 + 展示只显示日期)
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def get_list(page, limit, keyword=None, statuses=None):
|
||||
from app.models.inbound.semi import StockSemi
|
||||
@ -324,11 +314,9 @@ class SemiInboundService:
|
||||
)
|
||||
)
|
||||
|
||||
# [新增] 状态筛选与零库存隐藏逻辑
|
||||
if not statuses:
|
||||
statuses = ['在库', '借库']
|
||||
|
||||
# 如果筛选包含'已出库',则显示所有数量;否则隐藏 stock_quantity <= 0 的记录
|
||||
if '已出库' in statuses:
|
||||
query = query.filter(StockSemi.status.in_(statuses))
|
||||
else:
|
||||
@ -339,7 +327,9 @@ class SemiInboundService:
|
||||
)
|
||||
)
|
||||
|
||||
pagination = query.order_by(StockSemi.id.desc()).paginate(page=page, per_page=limit, error_out=False)
|
||||
# [核心修改] 按照 production_date (入库日期) 倒序排序
|
||||
pagination = query.order_by(StockSemi.production_date.desc()).paginate(page=page, per_page=limit,
|
||||
error_out=False)
|
||||
|
||||
current_items = pagination.items
|
||||
|
||||
@ -354,19 +344,22 @@ class SemiInboundService:
|
||||
for item in current_items:
|
||||
d = item.to_dict()
|
||||
|
||||
# 直接使用当前行的库存,不再聚合
|
||||
# [核心修改] 格式化展示日期,覆盖 to_dict 的默认行为
|
||||
date_display = ''
|
||||
if item.production_date:
|
||||
try:
|
||||
date_display = item.production_date.strftime('%Y-%m-%d')
|
||||
except:
|
||||
date_display = str(item.production_date)[:10]
|
||||
d['inbound_date'] = date_display
|
||||
|
||||
d['qty_stock'] = float(item.stock_quantity or 0)
|
||||
d['qty_available'] = float(item.available_quantity or 0)
|
||||
|
||||
# 兼容前端字段名
|
||||
d['sum_stock'] = d['qty_stock']
|
||||
d['sum_available'] = d['qty_available']
|
||||
|
||||
# 图片解析
|
||||
d['arrival_photo'] = parse_img(item.arrival_photo)
|
||||
d['quality_report_link'] = parse_img(item.quality_report_link)
|
||||
|
||||
# 打印ID
|
||||
d['global_print_id'] = item.global_print_id
|
||||
|
||||
items.append(d)
|
||||
|
||||
18
inventory-web/src/api/inbound/inbound_summary.ts
Normal file
18
inventory-web/src/api/inbound/inbound_summary.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export interface InboundSummaryQuery {
|
||||
page: number
|
||||
per_page: number
|
||||
keyword?: string
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
source_type?: string
|
||||
}
|
||||
|
||||
export function getInboundSummaryList(params: InboundSummaryQuery) {
|
||||
return request({
|
||||
url: '/v1/inbound/summary/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
@ -67,6 +67,13 @@ const routes: Array<RouteRecordRaw> = [
|
||||
component: () => import('@/views/stock/inbound/product.vue'),
|
||||
meta: { title: '成品' }
|
||||
},
|
||||
// ★ [新增] 入库记录整合
|
||||
{
|
||||
path: 'summary',
|
||||
name: 'InventorySummary',
|
||||
component: () => import('@/views/stock/inbound/inbound_summary.vue'),
|
||||
meta: { title: '入库记录' }
|
||||
},
|
||||
{
|
||||
path: 'service',
|
||||
name: 'InventoryService',
|
||||
|
||||
167
inventory-web/src/views/stock/inbound/inbound_summary.vue
Normal file
167
inventory-web/src/views/stock/inbound/inbound_summary.vue
Normal file
@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="filter-container">
|
||||
<el-input
|
||||
v-model="listQuery.keyword"
|
||||
placeholder="SKU / 名称 / 规格 / 批次号 / 来源"
|
||||
style="width: 300px;"
|
||||
class="filter-item"
|
||||
clearable
|
||||
@keyup.enter="handleFilter"
|
||||
/>
|
||||
|
||||
<el-select v-model="listQuery.source_type" placeholder="全部来源" clearable class="filter-item" style="width: 140px; margin-left: 10px;">
|
||||
<el-option label="采购入库" value="buy" />
|
||||
<el-option label="半成品生产" value="semi" />
|
||||
<el-option label="成品完工" value="product" />
|
||||
</el-select>
|
||||
|
||||
<el-date-picker
|
||||
v-model="listQuery.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
class="filter-item"
|
||||
style="margin-left: 10px;"
|
||||
value-format="YYYY-MM-DD"
|
||||
@change="handleFilter"
|
||||
/>
|
||||
<el-button type="primary" class="filter-item" style="margin-left: 10px;" @click="handleFilter">查询</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="list"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%; margin-top: 20px;"
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
|
||||
>
|
||||
<el-table-column prop="sku" label="SKU" min-width="140" fixed sortable show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="name" label="物品名称" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span style="font-weight: 500;">{{ row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="spec_model" label="规格型号" min-width="140" show-overflow-tooltip />
|
||||
|
||||
<el-table-column label="分类" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.category }}</span>
|
||||
<span v-if="row.category && row.material_type"> / </span>
|
||||
<span>{{ row.material_type }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="type_label" label="入库来源" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getSourceTag(row.source_type)" effect="plain">
|
||||
{{ row.type_label }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="inbound_date" label="入库/生产日期" width="120" align="center" sortable />
|
||||
|
||||
<el-table-column label="入库数量" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<span style="font-weight: bold; color: #409EFF;">{{ row.quantity }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="batch_number" label="批次/序列号" min-width="140" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="source_info" label="供应商/负责人" min-width="140" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="status" label="当前状态" width="100" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="getStatusTag(row.status)">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
|
||||
<div style="margin-top: 20px; display: flex; justify-content: flex-end;">
|
||||
<el-pagination
|
||||
v-model:current-page="listQuery.page"
|
||||
v-model:page-size="listQuery.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100, 200]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleFilter"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { getInboundSummaryList } from '@/api/inbound/inbound_summary'
|
||||
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
const listQuery = reactive({
|
||||
page: 1,
|
||||
per_page: 20, // 默认每页20
|
||||
keyword: '',
|
||||
source_type: '',
|
||||
dateRange: null as any
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: listQuery.page,
|
||||
per_page: listQuery.per_page,
|
||||
keyword: listQuery.keyword,
|
||||
source_type: listQuery.source_type,
|
||||
start_date: listQuery.dateRange ? listQuery.dateRange[0] : null,
|
||||
end_date: listQuery.dateRange ? listQuery.dateRange[1] : null
|
||||
}
|
||||
|
||||
const res = await getInboundSummaryList(params)
|
||||
if (res.data) {
|
||||
list.value = res.data.items || []
|
||||
total.value = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取入库记录失败', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查询操作重置页码
|
||||
const handleFilter = () => {
|
||||
listQuery.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 来源类型的 Tag 颜色
|
||||
const getSourceTag = (type: string) => {
|
||||
if (type === 'buy') return 'success' // 绿色
|
||||
if (type === 'semi') return 'warning' // 橙色
|
||||
if (type === 'product') return 'primary' // 蓝色
|
||||
return 'info'
|
||||
}
|
||||
|
||||
// [新增] 状态的 Tag 颜色逻辑
|
||||
const getStatusTag = (status: string) => {
|
||||
if (!status) return 'info'
|
||||
if (status.includes('已出库')) return 'info' // 灰色
|
||||
if (status.includes('部分')) return 'warning' // 橙色
|
||||
if (status.includes('不合格') || status.includes('异常')) return 'danger' // 红色
|
||||
return 'success' // 默认(如:合格、在库)为绿色
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user