Compare commits

4 Commits

12 changed files with 229 additions and 10 deletions

View File

@ -390,3 +390,21 @@ def get_location_suggestions():
return jsonify({"code": 400, "msg": "base_id required"}), 400 return jsonify({"code": 400, "msg": "base_id required"}), 400
data = BuyInboundService.get_history_locations(base_id) data = BuyInboundService.get_history_locations(base_id)
return jsonify({"code": 200, "msg": "success", "data": data}) 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}})

View File

@ -245,3 +245,21 @@ def calculate_bom_cost():
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
return jsonify({"code": 500, "msg": str(e)}), 500 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}})

View File

@ -240,3 +240,21 @@ def calculate_bom_cost():
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
return jsonify({"code": 500, "msg": str(e)}), 500 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}})

View File

@ -18,11 +18,15 @@ def build_tree(nodes, parent_id=None):
children = build_tree(nodes, node.id) children = build_tree(nodes, node.id)
node_dict = node.to_dict() node_dict = node.to_dict()
if children: if children:
node_dict['children'] = children # 子节点按 name 升序排序
children_sorted = sorted(children, key=lambda x: x.get('name', ''))
node_dict['children'] = children_sorted
else: else:
node_dict['children'] = [] node_dict['children'] = []
tree.append(node_dict) 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']) @warehouse_bp.route('/tree', methods=['GET'])
@ -31,8 +35,8 @@ def get_tree():
获取库位树形结构 获取库位树形结构
""" """
try: try:
# 查询所有库位 # 查询所有库位,按 name 升序排序
all_locations = SysWarehouseLocation.query.order_by(SysWarehouseLocation.level, SysWarehouseLocation.id).all() all_locations = SysWarehouseLocation.query.order_by(SysWarehouseLocation.name.asc()).all()
# 构建树形结构 # 构建树形结构
tree_data = build_tree(all_locations, parent_id=None) tree_data = build_tree(all_locations, parent_id=None)

View File

@ -1,6 +1,7 @@
# inventory-backend/app/services/inbound/buy_service.py # inventory-backend/app/services/inbound/buy_service.py
from app.extensions import db from app.extensions import db
from app.models.inbound.buy import StockBuy from app.models.inbound.buy import StockBuy
from app.models.inbound.product import StockProduct
from app.models.base import MaterialBase from app.models.base import MaterialBase
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from sqlalchemy import or_, func, text, and_ from sqlalchemy import or_, func, text, and_
@ -525,3 +526,42 @@ class BuyInboundService:
def get_history_locations(base_id): def get_history_locations(base_id):
return [r[0] for r in return [r[0] for r in
db.session.query(StockBuy.warehouse_location).filter(StockBuy.base_id == base_id).distinct().all()] 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.production_date.desc()).first()
# 3. 查询半成品入库最新记录
last_semi = StockSemi.query.filter(
StockSemi.base_id == base_id
).order_by(StockSemi.production_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.production_date, last_product.warehouse_location))
if last_semi and last_semi.warehouse_location:
candidates.append((last_semi.production_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 ""

View File

@ -1,6 +1,8 @@
# app/services/inbound/product_service.py # app/services/inbound/product_service.py
from app.extensions import db from app.extensions import db
from app.models.base import MaterialBase 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 app.models.outbound import TransOutbound
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from sqlalchemy import or_, func, text, and_ from sqlalchemy import or_, func, text, and_
@ -576,3 +578,42 @@ class ProductInboundService:
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
raise e 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.production_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.production_date.desc()).first()
# 比较三个表中的最新入库时间,返回最新的库位
candidates = []
if last_product and last_product.warehouse_location:
candidates.append((last_product.production_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.production_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 ""

View File

@ -1,6 +1,8 @@
# app/services/inbound/semi_service.py # app/services/inbound/semi_service.py
from app.extensions import db from app.extensions import db
from app.models.base import MaterialBase 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 app.models.outbound import TransOutbound
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from sqlalchemy import or_, func, text, and_ from sqlalchemy import or_, func, text, and_
@ -653,3 +655,42 @@ class SemiInboundService:
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
raise e 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.production_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.production_date.desc()).first()
# 比较三个表中的最新入库时间,返回最新的库位
candidates = []
if last_semi and last_semi.warehouse_location:
candidates.append((last_semi.production_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.production_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 ""

View File

@ -176,7 +176,7 @@ const handleLogout = () => {
<footer v-if="!isLoginPage" class="app-footer"> <footer v-if="!isLoginPage" class="app-footer">
<span class="version-tag"> <span class="version-tag">
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon> <el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
当前版本:V3.10(4.7部署 当前版本:V3.11(4.8部署
</span> </span>
</footer> </footer>

View File

@ -317,7 +317,10 @@ const selectedIds = ref<number[]>([])
const isBulkDeleteMode = ref(false) const isBulkDeleteMode = ref(false)
const handleTreeCheck = (data: any, checked: any) => { 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 = () => { const cancelBatchMode = () => {

View File

@ -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 {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 {ElMessage, ElMessageBox, ElLoading} from 'element-plus'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import request from '@/utils/request'
import { import {
getBuyList, getBuyList,
createBuyInbound, 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) const item = materialOptions.value.find(i => i.id === val)
if (item) { if (item) {
form.company_name = item.company_name form.company_name = item.company_name
@ -1146,6 +1147,17 @@ const onMaterialSelected = (val: number) => {
// 更新表单校验规则 // 更新表单校验规则
updateInspectionRules() updateInspectionRules()
checkHistoryAndSetMode(item.id) checkHistoryAndSetMode(item.id)
// 获取该物料历史入库库位(新增独立接口)
try {
const res = await request.get('/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)
}
} }
} }

View File

@ -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 { Plus, Setting, Refresh, Search, Box, House, Link, InfoFilled, Printer, Camera, Picture } from '@element-plus/icons-vue'
import { ElMessage, ElLoading } from 'element-plus' import { ElMessage, ElLoading } from 'element-plus'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import request from '@/utils/request'
import { import {
getProductList, getProductList,
createProductInbound, 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) const item = materialOptions.value.find(i => i.id === val)
if (item) { if (item) {
form.company_name = item.company_name // [新增] form.company_name = item.company_name // [新增]
@ -1038,6 +1039,17 @@ const onMaterialSelected = (val: number) => {
form.material_type = item.type form.material_type = item.type
form.category = item.category form.category = item.category
form.unit = item.unit form.unit = item.unit
// 获取该物料历史入库库位(新增独立接口)
try {
const res = await request.get('/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)
}
} }
} }

View File

@ -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 {Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link, Printer, Camera, Picture} from '@element-plus/icons-vue'
import {ElMessage, ElLoading} from 'element-plus' import {ElMessage, ElLoading} from 'element-plus'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import request from '@/utils/request'
import { import {
getSemiList, getSemiList,
createSemiInbound, 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) const item = materialOptions.value.find(i => i.id === val)
if (item) { if (item) {
form.company_name = item.company_name // [新增] form.company_name = item.company_name // [新增]
@ -1037,6 +1038,17 @@ const onMaterialSelected = (val: number) => {
form.unit = item.unit form.unit = item.unit
form.material_type = item.type form.material_type = item.type
checkHistoryAndSetMode(item.id) checkHistoryAndSetMode(item.id)
// 获取该物料历史入库库位(新增独立接口)
try {
const res = await request.get('/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)
}
} }
} }