From 273f20f5c3a49468bda08dd0c2521fc8dbd9c7b6 Mon Sep 17 00:00:00 2001 From: dxc Date: Thu, 5 Feb 2026 11:08:29 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=87=E8=B4=AD=E4=BB=B6=E5=85=A5=E5=BA=93?= =?UTF-8?q?=E4=B8=8E=E5=87=BA=E5=BA=93=E7=9B=B8=E5=85=B3=E8=81=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/services/inbound/buy_service.py | 89 +++++++++++++------ inventory-web/vite.config.ts | 2 +- 2 files changed, 65 insertions(+), 26 deletions(-) diff --git a/inventory-backend/app/services/inbound/buy_service.py b/inventory-backend/app/services/inbound/buy_service.py index 34e4cec..3c5913e 100644 --- a/inventory-backend/app/services/inbound/buy_service.py +++ b/inventory-backend/app/services/inbound/buy_service.py @@ -1,7 +1,10 @@ -# app/services/inbound/buy_service.py +# 文件路径: inventory-backend/app/services/inbound/buy_service.py + from app.extensions import db from app.models.inbound.buy import StockBuy from app.models.base import MaterialBase +# 引入出库记录模型,用于查询流转历史 +from app.models.outbound import TransOutbound from datetime import datetime from sqlalchemy import or_, func, text import traceback @@ -9,6 +12,10 @@ import json class BuyInboundService: + + # ============================================================ + # 1. 基础物料搜索 (供下拉框使用) + # ============================================================ @staticmethod def search_base_material(keyword): """ @@ -45,6 +52,9 @@ class BuyInboundService: traceback.print_exc() return [] + # ============================================================ + # 2. 新增入库逻辑 + # ============================================================ @staticmethod def handle_inbound(data): """ @@ -73,32 +83,23 @@ class BuyInboundService: in_qty = float(data.get('in_quantity') or 0) u_price = float(data.get('unit_price') or 0) - # ------------------------------------------------------------------ # 1. 获取全局打印流水号 (跨表唯一) - # ------------------------------------------------------------------ seq_sql = text("SELECT nextval('global_print_seq')") result = db.session.execute(seq_sql) next_global_id = result.scalar() - # ------------------------------------------------------------------ # 2. 自动生成 SKU (格式: 00000001) - # ------------------------------------------------------------------ generated_sku = str(next_global_id).zfill(10) - # ------------------------------------------------------------------ # 3. 条码逻辑处理 - # ------------------------------------------------------------------ final_barcode = data.get('barcode') if not final_barcode: final_barcode = generated_sku - # ------------------------------------------------------------------ # 4. 图片列表转 JSON 字符串处理 - # ------------------------------------------------------------------ arrival_list = data.get('arrival_photo', []) report_list = data.get('inspection_report', []) - # 确保是列表类型,防止前端传错导致报错 if not isinstance(arrival_list, list): arrival_list = [] if not isinstance(report_list, list): report_list = [] @@ -127,7 +128,7 @@ class BuyInboundService: original_link=data.get('source_link'), detail_link=data.get('detail_link'), - # [核心修改] 将列表转为 JSON 字符串存储 + # 将列表转为 JSON 字符串存储 arrival_photo=json.dumps(arrival_list), inspection_report=json.dumps(report_list) ) @@ -142,6 +143,9 @@ class BuyInboundService: db.session.rollback() raise e + # ============================================================ + # 3. 更新入库逻辑 + # ============================================================ @staticmethod def update_inbound(stock_id, data): """ @@ -149,7 +153,6 @@ class BuyInboundService: """ try: print(f"----- UPDATE DEBUG: ID={stock_id} -----") - stock = StockBuy.query.get(stock_id) if not stock: raise ValueError("记录不存在") @@ -176,7 +179,6 @@ class BuyInboundService: 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): @@ -197,6 +199,7 @@ class BuyInboundService: if new_qty != old_qty: diff = new_qty - old_qty stock.in_quantity = new_qty + # 注意:手动修改入库量时,同步调整总库存和可用库存 stock.stock_quantity = float(stock.stock_quantity) + diff stock.available_quantity = float(stock.available_quantity) + diff qty_changed = True @@ -221,6 +224,9 @@ class BuyInboundService: traceback.print_exc() raise e + # ============================================================ + # 4. 删除逻辑 + # ============================================================ @staticmethod def delete_inbound(stock_id): """ @@ -237,13 +243,32 @@ class BuyInboundService: db.session.rollback() raise e + # ============================================================ + # 5. [新增] 获取出库流转历史 (挂钩出库记录) + # ============================================================ @staticmethod - def get_list(page, limit, keyword=None): + def get_outbound_history(stock_id): """ - 获取分页列表 + 查询该入库单对应的所有出库记录 """ try: - # 1. 查询分页数据 + records = TransOutbound.query.filter_by( + source_table='stock_buy', + stock_id=stock_id + ).order_by(TransOutbound.outbound_time.desc()).all() + + return [r.to_dict() for r in records] + except Exception as e: + traceback.print_exc() + return [] + + # ============================================================ + # 6. 获取列表 (含动态状态计算) + # ============================================================ + @staticmethod + def get_list(page, limit, keyword=None): + try: + # 1. 联表查询:StockBuy join MaterialBase query = db.session.query(StockBuy).outerjoin(MaterialBase, StockBuy.base_id == MaterialBase.id) if keyword: @@ -253,18 +278,17 @@ class BuyInboundService: MaterialBase.spec_model.ilike(f'%{keyword}%'), StockBuy.batch_number.ilike(f'%{keyword}%'), StockBuy.serial_number.ilike(f'%{keyword}%'), - StockBuy.sku.ilike(f'%{keyword}%') + StockBuy.sku.ilike(f'%{keyword}%'), + StockBuy.supplier_name.ilike(f'%{keyword}%') ) ) pagination = query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit, error_out=False) - # --------------------------------------------------------------------- - # 计算总库存 (聚合) - # --------------------------------------------------------------------- current_items = pagination.items base_ids = list(set([item.base_id for item in current_items if item.base_id])) + # 2. 聚合统计 (计算该种物料的总库存) stock_map = {} if base_ids: aggregates = db.session.query( @@ -284,7 +308,6 @@ class BuyInboundService: if not json_str: return [] try: - # 兼容旧数据:如果不是 JSON 格式(比如是单个 URL),则包装成 list if not json_str.startswith('['): return [json_str] return json.loads(json_str) @@ -293,6 +316,20 @@ class BuyInboundService: items = [] for item in current_items: + # ------------------------------------------------------------- + # [核心逻辑] 动态计算状态 + # ------------------------------------------------------------- + qty_in = float(item.in_quantity or 0) + qty_avail = float(item.available_quantity or 0) + + # 默认使用数据库字段 + current_status = item.status + + # 如果有入库量,但可用量为0,说明已经全部出库 + if qty_in > 0 and qty_avail <= 0: + current_status = '出库' + + # 获取聚合数据 mat_name = item.material.name if item.material else '未知物料' mat_spec = item.material.spec_model if item.material else '' mat_cat = item.material.category if item.material else '' @@ -315,12 +352,15 @@ class BuyInboundService: 'barcode': item.barcode, 'serial_number': item.serial_number, 'batch_number': item.batch_number, - 'status': item.status, + + # 使用动态计算的状态 + 'status': current_status, + 'inspection_status': item.inspection_status, - 'qty_inbound': float(item.in_quantity or 0), + 'qty_inbound': qty_in, 'qty_stock': float(item.stock_quantity or 0), - 'qty_available': float(item.available_quantity or 0), + 'qty_available': qty_avail, 'sum_stock': stats['total_stock'], 'sum_available': stats['total_avail'], @@ -336,7 +376,6 @@ class BuyInboundService: 'source_link': item.original_link, 'detail_link': item.detail_link, - # [核心修改] 解析 JSON 字符串为数组返回给前端 'arrival_photo': parse_img_list(item.arrival_photo), 'inspection_report': parse_img_list(item.inspection_report), diff --git a/inventory-web/vite.config.ts b/inventory-web/vite.config.ts index 3bc881e..76052f0 100644 --- a/inventory-web/vite.config.ts +++ b/inventory-web/vite.config.ts @@ -17,7 +17,7 @@ export default defineConfig({ // 允许局域网访问前端页面 host: '0.0.0.0', port: 5173, - https: false, // ★ [新增] 强制开启 HTTPS,否则浏览器会拦截摄像头 + https: true, // ★ [新增] 强制开启 HTTPS,否则浏览器会拦截摄像头 proxy: { // 拦截所有以 /api 开头的请求 '/api': {