From 0bc47d306d4db5b8c78c90cae83780626027c51f Mon Sep 17 00:00:00 2001 From: dxc Date: Thu, 5 Feb 2026 14:30:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=85=A5=E5=BA=93=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E9=A1=B5=E9=9D=A2=EF=BC=8C=E5=90=8C=E6=97=B6=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E4=B8=89=E7=BB=84=E5=85=A5=E5=BA=93=E7=9A=84=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/api/v1/inbound/__init__.py | 7 +- .../app/api/v1/inbound/inbound_summary.py | 35 ++++ inventory-backend/app/models/inbound/buy.py | 2 +- .../app/models/inbound/product.py | 2 +- inventory-backend/app/models/inbound/semi.py | 2 +- .../app/services/inbound/buy_service.py | 57 +++--- .../inbound/inbound_summary_service.py | 190 ++++++++++++++++++ .../app/services/inbound/product_service.py | 50 +++-- .../app/services/inbound/semi_service.py | 59 +++--- .../src/api/inbound/inbound_summary.ts | 18 ++ inventory-web/src/router/index.ts | 7 + .../views/stock/inbound/inbound_summary.vue | 167 +++++++++++++++ 12 files changed, 511 insertions(+), 85 deletions(-) create mode 100644 inventory-backend/app/api/v1/inbound/inbound_summary.py create mode 100644 inventory-backend/app/services/inbound/inbound_summary_service.py create mode 100644 inventory-web/src/api/inbound/inbound_summary.ts create mode 100644 inventory-web/src/views/stock/inbound/inbound_summary.vue diff --git a/inventory-backend/app/api/v1/inbound/__init__.py b/inventory-backend/app/api/v1/inbound/__init__.py index c14f385..9c413ea 100644 --- a/inventory-backend/app/api/v1/inbound/__init__.py +++ b/inventory-backend/app/api/v1/inbound/__init__.py @@ -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__) @@ -11,4 +13,7 @@ inbound_bp.register_blueprint(inbound_buy_bp, url_prefix='/buy') 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') \ No newline at end of file +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') \ No newline at end of file diff --git a/inventory-backend/app/api/v1/inbound/inbound_summary.py b/inventory-backend/app/api/v1/inbound/inbound_summary.py new file mode 100644 index 0000000..a4e0932 --- /dev/null +++ b/inventory-backend/app/api/v1/inbound/inbound_summary.py @@ -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 \ No newline at end of file diff --git a/inventory-backend/app/models/inbound/buy.py b/inventory-backend/app/models/inbound/buy.py index 7492879..0ee8a48 100644 --- a/inventory-backend/app/models/inbound/buy.py +++ b/inventory-backend/app/models/inbound/buy.py @@ -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)) diff --git a/inventory-backend/app/models/inbound/product.py b/inventory-backend/app/models/inbound/product.py index 30bf40a..39ca826 100644 --- a/inventory-backend/app/models/inbound/product.py +++ b/inventory-backend/app/models/inbound/product.py @@ -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)) diff --git a/inventory-backend/app/models/inbound/semi.py b/inventory-backend/app/models/inbound/semi.py index d750a8b..a47a2bd 100644 --- a/inventory-backend/app/models/inbound/semi.py +++ b/inventory-backend/app/models/inbound/semi.py @@ -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)) diff --git a/inventory-backend/app/services/inbound/buy_service.py b/inventory-backend/app/services/inbound/buy_service.py index 4c8ef53..3f41e34 100644 --- a/inventory-backend/app/services/inbound/buy_service.py +++ b/inventory-backend/app/services/inbound/buy_service.py @@ -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, diff --git a/inventory-backend/app/services/inbound/inbound_summary_service.py b/inventory-backend/app/services/inbound/inbound_summary_service.py new file mode 100644 index 0000000..41820c4 --- /dev/null +++ b/inventory-backend/app/services/inbound/inbound_summary_service.py @@ -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 \ No newline at end of file diff --git a/inventory-backend/app/services/inbound/product_service.py b/inventory-backend/app/services/inbound/product_service.py index 507e507..1033578 100644 --- a/inventory-backend/app/services/inbound/product_service.py +++ b/inventory-backend/app/services/inbound/product_service.py @@ -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) diff --git a/inventory-backend/app/services/inbound/semi_service.py b/inventory-backend/app/services/inbound/semi_service.py index fd1a93c..6515e6e 100644 --- a/inventory-backend/app/services/inbound/semi_service.py +++ b/inventory-backend/app/services/inbound/semi_service.py @@ -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) diff --git a/inventory-web/src/api/inbound/inbound_summary.ts b/inventory-web/src/api/inbound/inbound_summary.ts new file mode 100644 index 0000000..30f93f1 --- /dev/null +++ b/inventory-web/src/api/inbound/inbound_summary.ts @@ -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 + }) +} \ No newline at end of file diff --git a/inventory-web/src/router/index.ts b/inventory-web/src/router/index.ts index 6a23fe8..328ed94 100644 --- a/inventory-web/src/router/index.ts +++ b/inventory-web/src/router/index.ts @@ -67,6 +67,13 @@ const routes: Array = [ 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', diff --git a/inventory-web/src/views/stock/inbound/inbound_summary.vue b/inventory-web/src/views/stock/inbound/inbound_summary.vue new file mode 100644 index 0000000..707c60e --- /dev/null +++ b/inventory-web/src/views/stock/inbound/inbound_summary.vue @@ -0,0 +1,167 @@ + + + \ No newline at end of file