Compare commits

2 Commits

10 changed files with 142 additions and 16 deletions

View File

@ -218,7 +218,13 @@ def submit():
if not location: if not location:
return jsonify({"code": 400, "msg": "入库失败:库位为必填项,不能为空!"}), 400 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({ return jsonify({
"code": 200, "code": 200,

View File

@ -145,7 +145,15 @@ def submit():
for field in list(data.keys()): for field in list(data.keys()):
perm_code = field_to_perm.get(field) perm_code = field_to_perm.get(field)
if perm_code and perm_code not in user_permissions: data.pop(field, None) 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()}) return jsonify({"code": 200, "msg": "入库成功", "data": new_stock.to_dict()})
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()

View File

@ -140,7 +140,15 @@ def submit():
for field in list(data.keys()): for field in list(data.keys()):
perm_code = field_to_perm.get(field) perm_code = field_to_perm.get(field)
if perm_code and perm_code not in user_permissions: data.pop(field, None) 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()}) return jsonify({"code": 200, "msg": "入库成功", "data": new_stock.to_dict()})
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()

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_
@ -16,6 +17,10 @@ class BuyInboundService:
# ============================================================ # ============================================================
@staticmethod @staticmethod
def _check_unique(base_id, serial_number, batch_number, exclude_id=None): def _check_unique(base_id, serial_number, batch_number, exclude_id=None):
"""
校验序列号/批号是否已存在
返回: None 表示校验通过str 类型的错误信息表示校验失败
"""
if serial_number: if serial_number:
query = StockBuy.query.filter(StockBuy.serial_number == serial_number) query = StockBuy.query.filter(StockBuy.serial_number == serial_number)
if exclude_id: if exclude_id:
@ -23,7 +28,7 @@ class BuyInboundService:
exists = query.first() exists = query.first()
if exists: if exists:
occupied_name = exists.base.name if exists.base else "未知物料" 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: if batch_number and base_id:
query = StockBuy.query.filter( query = StockBuy.query.filter(
@ -33,7 +38,9 @@ class BuyInboundService:
if exclude_id: if exclude_id:
query = query.filter(StockBuy.id != exclude_id) query = query.filter(StockBuy.id != exclude_id)
if query.first(): if query.first():
raise ValueError(f"该物料已存在批号【{batch_number}】,请勿重复录入,可直接在该批次下追加库存。") return f"该物料已存在批号【{batch_number}】,请勿重复录入,可直接在该批次下追加库存。"
return None
# ============================================================ # ============================================================
# 1. 基础物料搜索 # 1. 基础物料搜索
@ -59,9 +66,25 @@ class BuyInboundService:
items = [] items = []
for item in pagination.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({ items.append({
'id': item.id, 'id': item.id,
'company_name': item.company_name, # [新增] 'company_name': item.company_name,
'name': item.name, 'name': item.name,
'spec': item.spec_model, 'spec': item.spec_model,
'category': item.category, 'category': item.category,
@ -70,7 +93,8 @@ class BuyInboundService:
'brand': getattr(item, 'brand', ''), 'brand': getattr(item, 'brand', ''),
'manufacturer': getattr(item, 'manufacturer', ''), 'manufacturer': getattr(item, 'manufacturer', ''),
'pinyin': getattr(item, 'pinyin', ''), 'pinyin': getattr(item, 'pinyin', ''),
'status': '启用' 'status': '启用',
'history_location': last_location
}) })
return { return {
@ -113,11 +137,14 @@ class BuyInboundService:
if not has_report_file: if not has_report_file:
raise ValueError(f"物料【{material.name}】为强管控物料,必须提供检测报告文件或外部链接") raise ValueError(f"物料【{material.name}】为强管控物料,必须提供检测报告文件或外部链接")
BuyInboundService._check_unique( # 校验批号/序列号唯一性
unique_error = BuyInboundService._check_unique(
base_id=base_id, base_id=base_id,
serial_number=data.get('serial_number'), serial_number=data.get('serial_number'),
batch_number=data.get('batch_number') batch_number=data.get('batch_number')
) )
if unique_error:
return {'error': unique_error}
beijing_tz = timezone(timedelta(hours=8)) beijing_tz = timezone(timedelta(hours=8))
current_time = datetime.now(beijing_tz).replace(tzinfo=None) current_time = datetime.now(beijing_tz).replace(tzinfo=None)

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_
@ -21,7 +23,9 @@ class ProductInboundService:
exists = query.first() exists = query.first()
if exists: if exists:
occupied_name = exists.base.name if (hasattr(exists, 'base') and exists.base) else "未知物料" 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 @staticmethod
def search_base_material(keyword, page=1, limit=50): 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) pagination = query.paginate(page=page, per_page=limit, error_out=False)
results = [] results = []
for item in pagination.items: 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({ results.append({
'id': item.id, 'id': item.id,
'company_name': item.company_name, 'company_name': item.company_name,
@ -48,7 +75,8 @@ class ProductInboundService:
'category': item.category, 'category': item.category,
'unit': item.unit, 'unit': item.unit,
'type': item.material_type, 'type': item.material_type,
'status': '启用' 'status': '启用',
'history_location': last_location
}) })
return { return {
"items": results, "items": results,
@ -107,9 +135,12 @@ class ProductInboundService:
if not material.is_enabled: if not material.is_enabled:
raise ValueError(f"物料【{material.name}】已停用,无法办理新入库。") raise ValueError(f"物料【{material.name}】已停用,无法办理新入库。")
ProductInboundService._check_unique( # 校验序列号唯一性
unique_error = ProductInboundService._check_unique(
serial_number=data.get('serial_number') serial_number=data.get('serial_number')
) )
if unique_error:
return {'error': unique_error}
beijing_tz = timezone(timedelta(hours=8)) beijing_tz = timezone(timedelta(hours=8))
current_time = datetime.now(beijing_tz).replace(tzinfo=None) current_time = datetime.now(beijing_tz).replace(tzinfo=None)

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_
@ -21,7 +23,7 @@ class SemiInboundService:
exists = query.first() exists = query.first()
if exists: if exists:
occupied_name = exists.base.name if (hasattr(exists, 'base') and exists.base) else "未知物料" 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: if batch_number and base_id:
query = StockSemi.query.filter( query = StockSemi.query.filter(
@ -31,7 +33,9 @@ class SemiInboundService:
if exclude_id: if exclude_id:
query = query.filter(StockSemi.id != exclude_id) query = query.filter(StockSemi.id != exclude_id)
if query.first(): if query.first():
raise ValueError(f"该物料已存在批号【{batch_number}】,请勿重复建单,建议在原批次上追加库存。") return f"该物料已存在批号【{batch_number}】,请勿重复建单,建议在原批次上追加库存。"
return None
@staticmethod @staticmethod
def search_base_material(keyword, page=1, limit=50): 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) pagination = query.paginate(page=page, per_page=limit, error_out=False)
results = [] results = []
for item in pagination.items: 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({ results.append({
'id': item.id, 'id': item.id,
'company_name': item.company_name, 'company_name': item.company_name,
@ -58,7 +85,8 @@ class SemiInboundService:
'category': item.category, 'category': item.category,
'unit': item.unit, 'unit': item.unit,
'type': item.material_type, 'type': item.material_type,
'status': '启用' 'status': '启用',
'history_location': last_location
}) })
return {"items": results, "total": pagination.total, "page": page, "has_next": pagination.has_next} return {"items": results, "total": pagination.total, "page": page, "has_next": pagination.has_next}
except Exception as e: except Exception as e:
@ -114,11 +142,14 @@ class SemiInboundService:
if not material.is_enabled: if not material.is_enabled:
raise ValueError(f"物料【{material.name}】已停用,无法办理新入库。") raise ValueError(f"物料【{material.name}】已停用,无法办理新入库。")
SemiInboundService._check_unique( # 校验批号/序列号唯一性
unique_error = SemiInboundService._check_unique(
base_id=base_id, base_id=base_id,
serial_number=data.get('serial_number'), serial_number=data.get('serial_number'),
batch_number=data.get('batch_number') batch_number=data.get('batch_number')
) )
if unique_error:
return {'error': unique_error}
beijing_tz = timezone(timedelta(hours=8)) beijing_tz = timezone(timedelta(hours=8))
current_time = datetime.now(beijing_tz).replace(tzinfo=None) current_time = datetime.now(beijing_tz).replace(tzinfo=None)

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

@ -1146,6 +1146,11 @@ const onMaterialSelected = (val: number) => {
// 更新表单校验规则 // 更新表单校验规则
updateInspectionRules() updateInspectionRules()
checkHistoryAndSetMode(item.id) checkHistoryAndSetMode(item.id)
// 自动填充历史库位
if (item.history_location) {
form.warehouse_location = item.history_location
ElMessage.info(`已自动带入该物料的历史库位:【${item.history_location}】,请核对。`)
}
} }
} }

View File

@ -1038,6 +1038,11 @@ 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
// 自动填充历史库位
if (item.history_location) {
form.warehouse_location = item.history_location
ElMessage.info(`已自动带入该物料的历史库位:【${item.history_location}】,请核对。`)
}
} }
} }

View File

@ -1037,6 +1037,11 @@ 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)
// 自动填充历史库位
if (item.history_location) {
form.warehouse_location = item.history_location
ElMessage.info(`已自动带入该物料的历史库位:【${item.history_location}】,请核对。`)
}
} }
} }