diff --git a/inventory-backend/app/api/v1/inbound/buy.py b/inventory-backend/app/api/v1/inbound/buy.py index 118e868..761278a 100644 --- a/inventory-backend/app/api/v1/inbound/buy.py +++ b/inventory-backend/app/api/v1/inbound/buy.py @@ -390,3 +390,21 @@ def get_location_suggestions(): return jsonify({"code": 400, "msg": "base_id required"}), 400 data = BuyInboundService.get_history_locations(base_id) return jsonify({"code": 200, "msg": "success", "data": data}) + + +# ------------------------------------------------------------------ +# 11. 获取最近一次入库的库位(跨表查询) +# ------------------------------------------------------------------ +@inbound_buy_bp.route('/last-location', methods=['GET']) +@permission_required('inbound_buy') +def get_last_location(): + """ + 获取指定物料最近一次入库的库位 + 查询顺序:采购入库 -> 成品入库 -> 半成品入库,返回最新入库的库位 + """ + base_id = request.args.get('base_id', type=int) + if not base_id: + return jsonify({"code": 400, "msg": "base_id required"}), 400 + + location = BuyInboundService.get_last_location_by_base_id(base_id) + return jsonify({"code": 200, "msg": "success", "data": {"location": location}}) diff --git a/inventory-backend/app/api/v1/inbound/product.py b/inventory-backend/app/api/v1/inbound/product.py index 1c70632..86f59d4 100644 --- a/inventory-backend/app/api/v1/inbound/product.py +++ b/inventory-backend/app/api/v1/inbound/product.py @@ -245,3 +245,21 @@ def calculate_bom_cost(): except Exception as e: traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 + + +# ------------------------------------------------------------------ +# 获取最近一次入库的库位(跨表查询) +# ------------------------------------------------------------------ +@inbound_product_bp.route('/last-location', methods=['GET']) +@permission_required('inbound_product') +def get_last_location(): + """ + 获取指定物料最近一次入库的库位 + 查询顺序:成品入库 -> 采购入库 -> 半成品入库,返回最新入库的库位 + """ + base_id = request.args.get('base_id', type=int) + if not base_id: + return jsonify({"code": 400, "msg": "base_id required"}), 400 + + location = ProductInboundService.get_last_location_by_base_id(base_id) + return jsonify({"code": 200, "msg": "success", "data": {"location": location}}) diff --git a/inventory-backend/app/api/v1/inbound/semi.py b/inventory-backend/app/api/v1/inbound/semi.py index 8f6b6f0..6d1b42b 100644 --- a/inventory-backend/app/api/v1/inbound/semi.py +++ b/inventory-backend/app/api/v1/inbound/semi.py @@ -240,3 +240,21 @@ def calculate_bom_cost(): except Exception as e: traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 + + +# ------------------------------------------------------------------ +# 获取最近一次入库的库位(跨表查询) +# ------------------------------------------------------------------ +@inbound_semi_bp.route('/last-location', methods=['GET']) +@permission_required('inbound_semi') +def get_last_location(): + """ + 获取指定物料最近一次入库的库位 + 查询顺序:半成品入库 -> 采购入库 -> 成品入库,返回最新入库的库位 + """ + base_id = request.args.get('base_id', type=int) + if not base_id: + return jsonify({"code": 400, "msg": "base_id required"}), 400 + + location = SemiInboundService.get_last_location_by_base_id(base_id) + return jsonify({"code": 200, "msg": "success", "data": {"location": location}}) diff --git a/inventory-backend/app/api/v1/warehouse.py b/inventory-backend/app/api/v1/warehouse.py index c027a15..f002e2e 100644 --- a/inventory-backend/app/api/v1/warehouse.py +++ b/inventory-backend/app/api/v1/warehouse.py @@ -18,11 +18,15 @@ def build_tree(nodes, parent_id=None): children = build_tree(nodes, node.id) node_dict = node.to_dict() if children: - node_dict['children'] = children + # 子节点按 name 升序排序 + children_sorted = sorted(children, key=lambda x: x.get('name', '')) + node_dict['children'] = children_sorted else: node_dict['children'] = [] tree.append(node_dict) - return tree + # 当前层级按 name 升序排序 + tree_sorted = sorted(tree, key=lambda x: x.get('name', '')) + return tree_sorted @warehouse_bp.route('/tree', methods=['GET']) @@ -31,9 +35,9 @@ def get_tree(): 获取库位树形结构 """ try: - # 查询所有库位 - all_locations = SysWarehouseLocation.query.order_by(SysWarehouseLocation.level, SysWarehouseLocation.id).all() - + # 查询所有库位,按 name 升序排序 + all_locations = SysWarehouseLocation.query.order_by(SysWarehouseLocation.name.asc()).all() + # 构建树形结构 tree_data = build_tree(all_locations, parent_id=None) diff --git a/inventory-backend/app/services/inbound/buy_service.py b/inventory-backend/app/services/inbound/buy_service.py index f3d3d38..4d89de5 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_ @@ -525,3 +526,42 @@ class BuyInboundService: def get_history_locations(base_id): return [r[0] for r in db.session.query(StockBuy.warehouse_location).filter(StockBuy.base_id == base_id).distinct().all()] + + @staticmethod + def get_last_location_by_base_id(base_id): + """ + 获取指定物料最近一次入库的库位(跨表查询) + 查询顺序:采购入库 -> 成品入库 -> 半成品入库,返回最新入库的库位 + """ + from app.models.inbound.semi import StockSemi + + # 1. 查询采购入库最新记录 + last_buy = StockBuy.query.filter( + StockBuy.base_id == base_id + ).order_by(StockBuy.in_date.desc()).first() + + # 2. 查询成品入库最新记录 + last_product = StockProduct.query.filter( + StockProduct.base_id == base_id + ).order_by(StockProduct.in_date.desc()).first() + + # 3. 查询半成品入库最新记录 + last_semi = StockSemi.query.filter( + StockSemi.base_id == base_id + ).order_by(StockSemi.in_date.desc()).first() + + # 比较三个表中的最新入库时间,返回最新的库位 + candidates = [] + if last_buy and last_buy.warehouse_location: + candidates.append((last_buy.in_date, last_buy.warehouse_location)) + if last_product and last_product.warehouse_location: + candidates.append((last_product.in_date, last_product.warehouse_location)) + if last_semi and last_semi.warehouse_location: + candidates.append((last_semi.in_date, last_semi.warehouse_location)) + + if not candidates: + return "" + + # 按时间倒序排序,返回最新的库位 + candidates.sort(key=lambda x: x[0] if x[0] else datetime.min, reverse=True) + return candidates[0][1] if candidates[0][1] else "" diff --git a/inventory-backend/app/services/inbound/product_service.py b/inventory-backend/app/services/inbound/product_service.py index a67c654..0dce41f 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_ @@ -576,3 +578,42 @@ class ProductInboundService: except Exception as e: traceback.print_exc() raise e + + @staticmethod + def get_last_location_by_base_id(base_id): + """ + 获取指定物料最近一次入库的库位(跨表查询) + 查询顺序:成品入库 -> 采购入库 -> 半成品入库,返回最新入库的库位 + """ + from app.models.inbound.product import StockProduct + + # 1. 查询成品入库最新记录 + last_product = StockProduct.query.filter( + StockProduct.base_id == base_id + ).order_by(StockProduct.in_date.desc()).first() + + # 2. 查询采购入库最新记录 + last_buy = StockBuy.query.filter( + StockBuy.base_id == base_id + ).order_by(StockBuy.in_date.desc()).first() + + # 3. 查询半成品入库最新记录 + last_semi = StockSemi.query.filter( + StockSemi.base_id == base_id + ).order_by(StockSemi.in_date.desc()).first() + + # 比较三个表中的最新入库时间,返回最新的库位 + candidates = [] + if last_product and last_product.warehouse_location: + candidates.append((last_product.in_date, last_product.warehouse_location)) + if last_buy and last_buy.warehouse_location: + candidates.append((last_buy.in_date, last_buy.warehouse_location)) + if last_semi and last_semi.warehouse_location: + candidates.append((last_semi.in_date, last_semi.warehouse_location)) + + if not candidates: + return "" + + # 按时间倒序排序,返回最新的库位 + candidates.sort(key=lambda x: x[0] if x[0] else datetime.min, reverse=True) + return candidates[0][1] if candidates[0][1] else "" diff --git a/inventory-backend/app/services/inbound/semi_service.py b/inventory-backend/app/services/inbound/semi_service.py index 4e8cd7f..fd81c79 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_ @@ -653,3 +655,42 @@ class SemiInboundService: except Exception as e: traceback.print_exc() raise e + + @staticmethod + def get_last_location_by_base_id(base_id): + """ + 获取指定物料最近一次入库的库位(跨表查询) + 查询顺序:半成品入库 -> 采购入库 -> 成品入库,返回最新入库的库位 + """ + from app.models.inbound.semi import StockSemi + + # 1. 查询半成品入库最新记录 + last_semi = StockSemi.query.filter( + StockSemi.base_id == base_id + ).order_by(StockSemi.in_date.desc()).first() + + # 2. 查询采购入库最新记录 + last_buy = StockBuy.query.filter( + StockBuy.base_id == base_id + ).order_by(StockBuy.in_date.desc()).first() + + # 3. 查询成品入库最新记录 + last_product = StockProduct.query.filter( + StockProduct.base_id == base_id + ).order_by(StockProduct.in_date.desc()).first() + + # 比较三个表中的最新入库时间,返回最新的库位 + candidates = [] + if last_semi and last_semi.warehouse_location: + candidates.append((last_semi.in_date, last_semi.warehouse_location)) + if last_buy and last_buy.warehouse_location: + candidates.append((last_buy.in_date, last_buy.warehouse_location)) + if last_product and last_product.warehouse_location: + candidates.append((last_product.in_date, last_product.warehouse_location)) + + if not candidates: + return "" + + # 按时间倒序排序,返回最新的库位 + candidates.sort(key=lambda x: x[0] if x[0] else datetime.min, reverse=True) + return candidates[0][1] if candidates[0][1] else "" diff --git a/inventory-web/src/views/dashboard/index.vue b/inventory-web/src/views/dashboard/index.vue index 92fa157..e8f21e5 100644 --- a/inventory-web/src/views/dashboard/index.vue +++ b/inventory-web/src/views/dashboard/index.vue @@ -317,7 +317,10 @@ const selectedIds = ref([]) const isBulkDeleteMode = ref(false) const handleTreeCheck = (data: any, checked: any) => { - selectedIds.value = checked.checkedKeys + // 使用 getCheckedKeys(true) 只获取叶子节点,防止 el-tree 自动连带选中父节点导致误删 + if (treeRef.value) { + selectedIds.value = treeRef.value.getCheckedKeys(true) as number[] + } } const cancelBatchMode = () => { diff --git a/inventory-web/src/views/stock/inbound/buy.vue b/inventory-web/src/views/stock/inbound/buy.vue index a83212e..0b91ba5 100644 --- a/inventory-web/src/views/stock/inbound/buy.vue +++ b/inventory-web/src/views/stock/inbound/buy.vue @@ -669,6 +669,7 @@ import {ref, reactive, onMounted, watch, computed} from 'vue' import {Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link, Printer, Camera, Delete, Picture} from '@element-plus/icons-vue' import {ElMessage, ElMessageBox, ElLoading} from 'element-plus' import dayjs from 'dayjs' +import request from '@/utils/request' import { getBuyList, createBuyInbound, @@ -1132,7 +1133,7 @@ const loadMoreMaterials = async () => { } } -const onMaterialSelected = (val: number) => { +const onMaterialSelected = async (val: number) => { const item = materialOptions.value.find(i => i.id === val) if (item) { form.company_name = item.company_name @@ -1146,6 +1147,17 @@ const onMaterialSelected = (val: number) => { // 更新表单校验规则 updateInspectionRules() checkHistoryAndSetMode(item.id) + + // 获取该物料历史入库库位(新增独立接口) + try { + const res = await request.get('/api/v1/inbound/buy/last-location', { params: { base_id: val } }) + if (res.code === 200 && res.data.location) { + form.warehouse_location = res.data.location + ElMessage.info(`已自动带入该物料历史库位:【${res.data.location}】,请核对。`) + } + } catch (e) { + console.error('获取历史库位失败', e) + } } } diff --git a/inventory-web/src/views/stock/inbound/product.vue b/inventory-web/src/views/stock/inbound/product.vue index 1f28615..ab6c116 100644 --- a/inventory-web/src/views/stock/inbound/product.vue +++ b/inventory-web/src/views/stock/inbound/product.vue @@ -559,6 +559,7 @@ import { ref, reactive, onMounted, watch, computed } from 'vue' import { Plus, Setting, Refresh, Search, Box, House, Link, InfoFilled, Printer, Camera, Picture } from '@element-plus/icons-vue' import { ElMessage, ElLoading } from 'element-plus' import dayjs from 'dayjs' +import request from '@/utils/request' import { getProductList, createProductInbound, @@ -1029,7 +1030,7 @@ const loadMoreMaterials = async () => { } } -const onMaterialSelected = (val: number) => { +const onMaterialSelected = async (val: number) => { const item = materialOptions.value.find(i => i.id === val) if (item) { form.company_name = item.company_name // [新增] @@ -1038,6 +1039,17 @@ const onMaterialSelected = (val: number) => { form.material_type = item.type form.category = item.category form.unit = item.unit + + // 获取该物料历史入库库位(新增独立接口) + try { + const res = await request.get('/api/v1/inbound/product/last-location', { params: { base_id: val } }) + if (res.code === 200 && res.data.location) { + form.warehouse_location = res.data.location + ElMessage.info(`已自动带入该物料历史库位:【${res.data.location}】,请核对。`) + } + } catch (e) { + console.error('获取历史库位失败', e) + } } } diff --git a/inventory-web/src/views/stock/inbound/semi.vue b/inventory-web/src/views/stock/inbound/semi.vue index 1327765..8483c1c 100644 --- a/inventory-web/src/views/stock/inbound/semi.vue +++ b/inventory-web/src/views/stock/inbound/semi.vue @@ -616,6 +616,7 @@ import {ref, reactive, onMounted, watch, computed} from 'vue' import {Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link, Printer, Camera, Picture} from '@element-plus/icons-vue' import {ElMessage, ElLoading} from 'element-plus' import dayjs from 'dayjs' +import request from '@/utils/request' import { getSemiList, createSemiInbound, @@ -1027,7 +1028,7 @@ const loadMoreMaterials = async () => { } } -const onMaterialSelected = (val: number) => { +const onMaterialSelected = async (val: number) => { const item = materialOptions.value.find(i => i.id === val) if (item) { form.company_name = item.company_name // [新增] @@ -1037,6 +1038,17 @@ const onMaterialSelected = (val: number) => { form.unit = item.unit form.material_type = item.type checkHistoryAndSetMode(item.id) + + // 获取该物料历史入库库位(新增独立接口) + try { + const res = await request.get('/api/v1/inbound/semi/last-location', { params: { base_id: val } }) + if (res.code === 200 && res.data.location) { + form.warehouse_location = res.data.location + ElMessage.info(`已自动带入该物料历史库位:【${res.data.location}】,请核对。`) + } + } catch (e) { + console.error('获取历史库位失败', e) + } } }