diff --git a/inventory-backend/app/api/v1/inbound/buy.py b/inventory-backend/app/api/v1/inbound/buy.py index 118e868..20afbd2 100644 --- a/inventory-backend/app/api/v1/inbound/buy.py +++ b/inventory-backend/app/api/v1/inbound/buy.py @@ -218,7 +218,13 @@ def submit(): if not location: return jsonify({"code": 400, "msg": "入库失败:库位为必填项,不能为空!"}), 400 - new_stock = BuyInboundService.handle_inbound(data) + result = BuyInboundService.handle_inbound(data) + + # 检查是否返回了业务校验错误 + if isinstance(result, dict) and result.get('error'): + return jsonify({"code": 400, "msg": result['error']}), 400 + + new_stock = result return jsonify({ "code": 200, diff --git a/inventory-backend/app/api/v1/inbound/product.py b/inventory-backend/app/api/v1/inbound/product.py index 1c70632..11e7a00 100644 --- a/inventory-backend/app/api/v1/inbound/product.py +++ b/inventory-backend/app/api/v1/inbound/product.py @@ -145,7 +145,15 @@ def submit(): for field in list(data.keys()): perm_code = field_to_perm.get(field) if perm_code and perm_code not in user_permissions: data.pop(field, None) - new_stock = ProductInboundService.handle_inbound(data) + + result = ProductInboundService.handle_inbound(data) + + # 检查是否返回了业务校验错误 + if isinstance(result, dict) and result.get('error'): + return jsonify({"code": 400, "msg": result['error']}), 400 + + new_stock = result + return jsonify({"code": 200, "msg": "入库成功", "data": new_stock.to_dict()}) except Exception as e: traceback.print_exc() diff --git a/inventory-backend/app/api/v1/inbound/semi.py b/inventory-backend/app/api/v1/inbound/semi.py index 8f6b6f0..0edc9c4 100644 --- a/inventory-backend/app/api/v1/inbound/semi.py +++ b/inventory-backend/app/api/v1/inbound/semi.py @@ -140,7 +140,15 @@ def submit(): for field in list(data.keys()): perm_code = field_to_perm.get(field) if perm_code and perm_code not in user_permissions: data.pop(field, None) - new_stock = SemiInboundService.handle_inbound(data) + + result = SemiInboundService.handle_inbound(data) + + # 检查是否返回了业务校验错误 + if isinstance(result, dict) and result.get('error'): + return jsonify({"code": 400, "msg": result['error']}), 400 + + new_stock = result + return jsonify({"code": 200, "msg": "入库成功", "data": new_stock.to_dict()}) except Exception as e: traceback.print_exc() diff --git a/inventory-backend/app/services/inbound/buy_service.py b/inventory-backend/app/services/inbound/buy_service.py index f3d3d38..9caa4e2 100644 --- a/inventory-backend/app/services/inbound/buy_service.py +++ b/inventory-backend/app/services/inbound/buy_service.py @@ -1,6 +1,7 @@ # inventory-backend/app/services/inbound/buy_service.py from app.extensions import db from app.models.inbound.buy import StockBuy +from app.models.inbound.product import StockProduct from app.models.base import MaterialBase from datetime import datetime, timedelta, timezone from sqlalchemy import or_, func, text, and_ @@ -16,6 +17,10 @@ class BuyInboundService: # ============================================================ @staticmethod def _check_unique(base_id, serial_number, batch_number, exclude_id=None): + """ + 校验序列号/批号是否已存在 + 返回: None 表示校验通过,str 类型的错误信息表示校验失败 + """ if serial_number: query = StockBuy.query.filter(StockBuy.serial_number == serial_number) if exclude_id: @@ -23,7 +28,7 @@ class BuyInboundService: exists = query.first() if exists: occupied_name = exists.base.name if exists.base else "未知物料" - raise ValueError(f"序列号【{serial_number}】已存在!被物料 [{occupied_name}] 占用,请核查。") + return f"序列号【{serial_number}】已存在!被物料 [{occupied_name}] 占用,请核查。" if batch_number and base_id: query = StockBuy.query.filter( @@ -33,7 +38,9 @@ class BuyInboundService: if exclude_id: query = query.filter(StockBuy.id != exclude_id) if query.first(): - raise ValueError(f"该物料已存在批号【{batch_number}】,请勿重复录入,可直接在该批次下追加库存。") + return f"该物料已存在批号【{batch_number}】,请勿重复录入,可直接在该批次下追加库存。" + + return None # ============================================================ # 1. 基础物料搜索 @@ -59,9 +66,25 @@ class BuyInboundService: items = [] for item in pagination.items: + # 查询最近一次入库的库位(采购入库 + 成品入库) + last_location = '' + # 查采购入库 + last_buy = StockBuy.query.filter( + StockBuy.base_id == item.id + ).order_by(StockBuy.in_date.desc()).first() + if last_buy and last_buy.warehouse_location: + last_location = last_buy.warehouse_location + else: + # 查成品入库 + last_product = StockProduct.query.filter( + StockProduct.base_id == item.id + ).order_by(StockProduct.in_date.desc()).first() + if last_product and last_product.warehouse_location: + last_location = last_product.warehouse_location + items.append({ 'id': item.id, - 'company_name': item.company_name, # [新增] + 'company_name': item.company_name, 'name': item.name, 'spec': item.spec_model, 'category': item.category, @@ -70,7 +93,8 @@ class BuyInboundService: 'brand': getattr(item, 'brand', ''), 'manufacturer': getattr(item, 'manufacturer', ''), 'pinyin': getattr(item, 'pinyin', ''), - 'status': '启用' + 'status': '启用', + 'history_location': last_location }) return { @@ -113,11 +137,14 @@ class BuyInboundService: if not has_report_file: raise ValueError(f"物料【{material.name}】为强管控物料,必须提供检测报告文件或外部链接") - BuyInboundService._check_unique( + # 校验批号/序列号唯一性 + unique_error = BuyInboundService._check_unique( base_id=base_id, serial_number=data.get('serial_number'), batch_number=data.get('batch_number') ) + if unique_error: + return {'error': unique_error} beijing_tz = timezone(timedelta(hours=8)) current_time = datetime.now(beijing_tz).replace(tzinfo=None) diff --git a/inventory-backend/app/services/inbound/product_service.py b/inventory-backend/app/services/inbound/product_service.py index a67c654..44ac4f2 100644 --- a/inventory-backend/app/services/inbound/product_service.py +++ b/inventory-backend/app/services/inbound/product_service.py @@ -1,6 +1,8 @@ # app/services/inbound/product_service.py from app.extensions import db from app.models.base import MaterialBase +from app.models.inbound.buy import StockBuy +from app.models.inbound.semi import StockSemi from app.models.outbound import TransOutbound from datetime import datetime, timedelta, timezone from sqlalchemy import or_, func, text, and_ @@ -21,7 +23,9 @@ class ProductInboundService: exists = query.first() if exists: occupied_name = exists.base.name if (hasattr(exists, 'base') and exists.base) else "未知物料" - raise ValueError(f"序列号【{serial_number}】已存在!被成品 [{occupied_name}] 占用,请核查。") + return f"序列号【{serial_number}】已存在!被成品 [{occupied_name}] 占用,请核查。" + + return None @staticmethod def search_base_material(keyword, page=1, limit=50): @@ -40,6 +44,29 @@ class ProductInboundService: pagination = query.paginate(page=page, per_page=limit, error_out=False) results = [] for item in pagination.items: + # 查询最近一次入库的库位(成品入库 + 采购入库 + 半成品入库) + last_location = '' + # 查成品入库 + last_product = StockProduct.query.filter( + StockProduct.base_id == item.id + ).order_by(StockProduct.in_date.desc()).first() + if last_product and last_product.warehouse_location: + last_location = last_product.warehouse_location + else: + # 查采购入库 + last_buy = StockBuy.query.filter( + StockBuy.base_id == item.id + ).order_by(StockBuy.in_date.desc()).first() + if last_buy and last_buy.warehouse_location: + last_location = last_buy.warehouse_location + else: + # 查半成品入库 + last_semi = StockSemi.query.filter( + StockSemi.base_id == item.id + ).order_by(StockSemi.in_date.desc()).first() + if last_semi and last_semi.warehouse_location: + last_location = last_semi.warehouse_location + results.append({ 'id': item.id, 'company_name': item.company_name, @@ -48,7 +75,8 @@ class ProductInboundService: 'category': item.category, 'unit': item.unit, 'type': item.material_type, - 'status': '启用' + 'status': '启用', + 'history_location': last_location }) return { "items": results, @@ -107,9 +135,12 @@ class ProductInboundService: if not material.is_enabled: raise ValueError(f"物料【{material.name}】已停用,无法办理新入库。") - ProductInboundService._check_unique( + # 校验序列号唯一性 + unique_error = ProductInboundService._check_unique( serial_number=data.get('serial_number') ) + if unique_error: + return {'error': unique_error} beijing_tz = timezone(timedelta(hours=8)) current_time = datetime.now(beijing_tz).replace(tzinfo=None) diff --git a/inventory-backend/app/services/inbound/semi_service.py b/inventory-backend/app/services/inbound/semi_service.py index 4e8cd7f..4f16926 100644 --- a/inventory-backend/app/services/inbound/semi_service.py +++ b/inventory-backend/app/services/inbound/semi_service.py @@ -1,6 +1,8 @@ # app/services/inbound/semi_service.py from app.extensions import db from app.models.base import MaterialBase +from app.models.inbound.buy import StockBuy +from app.models.inbound.product import StockProduct from app.models.outbound import TransOutbound from datetime import datetime, timedelta, timezone from sqlalchemy import or_, func, text, and_ @@ -21,7 +23,7 @@ class SemiInboundService: exists = query.first() if exists: occupied_name = exists.base.name if (hasattr(exists, 'base') and exists.base) else "未知物料" - raise ValueError(f"序列号【{serial_number}】已存在!被半成品 [{occupied_name}] 占用,请核查。") + return f"序列号【{serial_number}】已存在!被半成品 [{occupied_name}] 占用,请核查。" if batch_number and base_id: query = StockSemi.query.filter( @@ -31,7 +33,9 @@ class SemiInboundService: if exclude_id: query = query.filter(StockSemi.id != exclude_id) if query.first(): - raise ValueError(f"该物料已存在批号【{batch_number}】,请勿重复建单,建议在原批次上追加库存。") + return f"该物料已存在批号【{batch_number}】,请勿重复建单,建议在原批次上追加库存。" + + return None @staticmethod def search_base_material(keyword, page=1, limit=50): @@ -50,6 +54,29 @@ class SemiInboundService: pagination = query.paginate(page=page, per_page=limit, error_out=False) results = [] for item in pagination.items: + # 查询最近一次入库的库位(半成品入库 + 采购入库 + 成品入库) + last_location = '' + # 查半成品入库 + last_semi = StockSemi.query.filter( + StockSemi.base_id == item.id + ).order_by(StockSemi.in_date.desc()).first() + if last_semi and last_semi.warehouse_location: + last_location = last_semi.warehouse_location + else: + # 查采购入库 + last_buy = StockBuy.query.filter( + StockBuy.base_id == item.id + ).order_by(StockBuy.in_date.desc()).first() + if last_buy and last_buy.warehouse_location: + last_location = last_buy.warehouse_location + else: + # 查成品入库 + last_product = StockProduct.query.filter( + StockProduct.base_id == item.id + ).order_by(StockProduct.in_date.desc()).first() + if last_product and last_product.warehouse_location: + last_location = last_product.warehouse_location + results.append({ 'id': item.id, 'company_name': item.company_name, @@ -58,7 +85,8 @@ class SemiInboundService: 'category': item.category, 'unit': item.unit, 'type': item.material_type, - 'status': '启用' + 'status': '启用', + 'history_location': last_location }) return {"items": results, "total": pagination.total, "page": page, "has_next": pagination.has_next} except Exception as e: @@ -114,11 +142,14 @@ class SemiInboundService: if not material.is_enabled: raise ValueError(f"物料【{material.name}】已停用,无法办理新入库。") - SemiInboundService._check_unique( + # 校验批号/序列号唯一性 + unique_error = SemiInboundService._check_unique( base_id=base_id, serial_number=data.get('serial_number'), batch_number=data.get('batch_number') ) + if unique_error: + return {'error': unique_error} beijing_tz = timezone(timedelta(hours=8)) current_time = datetime.now(beijing_tz).replace(tzinfo=None) diff --git a/inventory-web/src/views/stock/inbound/buy.vue b/inventory-web/src/views/stock/inbound/buy.vue index a83212e..239a1e3 100644 --- a/inventory-web/src/views/stock/inbound/buy.vue +++ b/inventory-web/src/views/stock/inbound/buy.vue @@ -1146,6 +1146,11 @@ const onMaterialSelected = (val: number) => { // 更新表单校验规则 updateInspectionRules() checkHistoryAndSetMode(item.id) + // 自动填充历史库位 + if (item.history_location) { + form.warehouse_location = item.history_location + ElMessage.info(`已自动带入该物料的历史库位:【${item.history_location}】,请核对。`) + } } } diff --git a/inventory-web/src/views/stock/inbound/product.vue b/inventory-web/src/views/stock/inbound/product.vue index 1f28615..90e6550 100644 --- a/inventory-web/src/views/stock/inbound/product.vue +++ b/inventory-web/src/views/stock/inbound/product.vue @@ -1038,6 +1038,11 @@ const onMaterialSelected = (val: number) => { form.material_type = item.type form.category = item.category form.unit = item.unit + // 自动填充历史库位 + if (item.history_location) { + form.warehouse_location = item.history_location + ElMessage.info(`已自动带入该物料的历史库位:【${item.history_location}】,请核对。`) + } } } diff --git a/inventory-web/src/views/stock/inbound/semi.vue b/inventory-web/src/views/stock/inbound/semi.vue index 1327765..161863a 100644 --- a/inventory-web/src/views/stock/inbound/semi.vue +++ b/inventory-web/src/views/stock/inbound/semi.vue @@ -1037,6 +1037,11 @@ const onMaterialSelected = (val: number) => { form.unit = item.unit form.material_type = item.type checkHistoryAndSetMode(item.id) + // 自动填充历史库位 + if (item.history_location) { + form.warehouse_location = item.history_location + ElMessage.info(`已自动带入该物料的历史库位:【${item.history_location}】,请核对。`) + } } }