维护三个基础物件入库时候与数据库不匹配问题

This commit is contained in:
dxc
2026-01-30 11:50:35 +08:00
parent 482c5a2cb2
commit 30181fd21b
6 changed files with 57 additions and 53 deletions

View File

@ -45,7 +45,7 @@ def create_app():
with app.app_context(): with app.app_context():
try: try:
# 1. 基础物料 # 1. 基础物料
from app.models.material import MaterialBase from app.models.base import MaterialBase
# 2. 采购入库 # 2. 采购入库
from app.models.inbound.buy import StockBuy from app.models.inbound.buy import StockBuy
# 3. 半成品入库 # 3. 半成品入库

View File

@ -1,7 +1,7 @@
# 文件路径: app/services/inbound/base_service.py # 文件路径: app/services/inbound/base_service.py
from app.extensions import db from app.extensions import db
from app.models.material import MaterialBase from app.models.base import MaterialBase
# ============================================================================== # ==============================================================================
# ✅ 正确的引用方式 # ✅ 正确的引用方式

View File

@ -1,6 +1,9 @@
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.material import MaterialBase # ==============================================================================
# ✅ 修复点:基础物料模型实际位于 app/models/base.py
# ==============================================================================
from app.models.base import MaterialBase
from datetime import datetime from datetime import datetime
from sqlalchemy import or_, func from sqlalchemy import or_, func
import traceback import traceback
@ -9,16 +12,23 @@ import traceback
class BuyInboundService: class BuyInboundService:
@staticmethod @staticmethod
def search_base_material(keyword): def search_base_material(keyword):
"""
搜索基础物料
如果 keyword 为空,返回最新的 20 条记录
"""
try: try:
if not keyword: query = MaterialBase.query.filter(MaterialBase.is_enabled == True)
return []
query = MaterialBase.query.filter( if keyword:
MaterialBase.is_enabled == True, query = query.filter(
or_( or_(
MaterialBase.name.ilike(f'%{keyword}%'), MaterialBase.name.ilike(f'%{keyword}%'),
MaterialBase.spec_model.ilike(f'%{keyword}%') MaterialBase.spec_model.ilike(f'%{keyword}%')
) )
).limit(20) )
# 无论是否有关键词,都按 ID 倒序排列,取前 20 条
query = query.order_by(MaterialBase.id.desc()).limit(20)
results = [] results = []
for item in query.all(): for item in query.all():
@ -50,10 +60,11 @@ class BuyInboundService:
in_date_val = datetime.utcnow().date() in_date_val = datetime.utcnow().date()
if data.get('in_date'): if data.get('in_date'):
try: try:
if len(str(data['in_date'])) > 10: # 兼容 YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD
in_date_val = datetime.strptime(str(data['in_date'])[:10], '%Y-%m-%d').date() date_str = str(data['in_date'])
else: if len(date_str) > 10:
in_date_val = datetime.strptime(str(data['in_date']), '%Y-%m-%d').date() date_str = date_str[:10]
in_date_val = datetime.strptime(date_str, '%Y-%m-%d').date()
except ValueError: except ValueError:
pass pass
@ -82,8 +93,7 @@ class BuyInboundService:
buyer_email=data.get('purchaser_email'), buyer_email=data.get('purchaser_email'),
original_link=data.get('source_link'), original_link=data.get('source_link'),
detail_link=data.get('detail_link'), detail_link=data.get('detail_link'),
arrival_photo=data.get('arrival_photo'), arrival_photo=data.get('arrival_photo')
remark=data.get('remark')
) )
db.session.add(new_stock) db.session.add(new_stock)
@ -98,7 +108,6 @@ class BuyInboundService:
def update_inbound(stock_id, data): def update_inbound(stock_id, data):
try: try:
print(f"----- UPDATE DEBUG: ID={stock_id} -----") print(f"----- UPDATE DEBUG: ID={stock_id} -----")
print(f"Payload: {data}")
stock = StockBuy.query.get(stock_id) stock = StockBuy.query.get(stock_id)
if not stock: if not stock:
@ -115,7 +124,6 @@ class BuyInboundService:
'supplier_name': 'supplier_name', 'supplier_name': 'supplier_name',
'detail_link': 'detail_link', 'detail_link': 'detail_link',
'arrival_photo': 'arrival_photo', 'arrival_photo': 'arrival_photo',
'remark': 'remark',
'currency': 'currency', 'currency': 'currency',
'exchange_rate': 'exchange_rate', 'exchange_rate': 'exchange_rate',
'purchaser': 'buyer_name', 'purchaser': 'buyer_name',
@ -193,16 +201,13 @@ class BuyInboundService:
pagination = query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit, error_out=False) pagination = query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit, error_out=False)
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# 新增逻辑:计算总库存 # 计算总库存 (聚合)
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# 2. 提取当前页所有涉及的 base_id
current_items = pagination.items current_items = pagination.items
base_ids = list(set([item.base_id for item in current_items if item.base_id])) base_ids = list(set([item.base_id for item in current_items if item.base_id]))
# 3. 聚合查询:一次性查出这些 base_id 对应的 stock_quantity 和 available_quantity 总和
stock_map = {} stock_map = {}
if base_ids: if base_ids:
# SELECT base_id, SUM(stock_quantity), SUM(available_quantity) FROM stock_buy WHERE base_id IN (...) GROUP BY base_id
aggregates = db.session.query( aggregates = db.session.query(
StockBuy.base_id, StockBuy.base_id,
func.sum(StockBuy.stock_quantity).label('total_stock'), func.sum(StockBuy.stock_quantity).label('total_stock'),
@ -214,7 +219,6 @@ class BuyInboundService:
'total_stock': float(agg.total_stock or 0), 'total_stock': float(agg.total_stock or 0),
'total_avail': float(agg.total_avail or 0) 'total_avail': float(agg.total_avail or 0)
} }
# ---------------------------------------------------------------------
items = [] items = []
for item in current_items: for item in current_items:
@ -224,7 +228,6 @@ class BuyInboundService:
mat_unit = item.material.unit if item.material else '' mat_unit = item.material.unit if item.material else ''
mat_type = item.material.material_type if item.material else '' mat_type = item.material.material_type if item.material else ''
# 获取该物料的统计数据
stats = stock_map.get(item.base_id, {'total_stock': 0, 'total_avail': 0}) stats = stock_map.get(item.base_id, {'total_stock': 0, 'total_avail': 0})
d = { d = {
@ -244,12 +247,10 @@ class BuyInboundService:
'status': item.status, 'status': item.status,
'inspection_status': item.inspection_status, 'inspection_status': item.inspection_status,
# --- 原始批次数据 (用于编辑) ---
'qty_inbound': float(item.in_quantity or 0), 'qty_inbound': float(item.in_quantity or 0),
'qty_stock': float(item.stock_quantity or 0), 'qty_stock': float(item.stock_quantity or 0),
'qty_available': float(item.available_quantity or 0), 'qty_available': float(item.available_quantity or 0),
# --- [新增] 聚合统计数据 (用于列表显示) ---
'sum_stock': stats['total_stock'], 'sum_stock': stats['total_stock'],
'sum_available': stats['total_avail'], 'sum_available': stats['total_avail'],
@ -263,8 +264,7 @@ class BuyInboundService:
'purchaser_email': item.buyer_email, 'purchaser_email': item.buyer_email,
'source_link': item.original_link, 'source_link': item.original_link,
'detail_link': item.detail_link, 'detail_link': item.detail_link,
'arrival_photo': item.arrival_photo, 'arrival_photo': item.arrival_photo
'remark': item.remark
} }
items.append(d) items.append(d)

View File

@ -1,5 +1,5 @@
from app.extensions import db from app.extensions import db
from app.models.material import MaterialBase from app.models.base import MaterialBase
# 引用新的 product 路径 # 引用新的 product 路径
from app.models.inbound.product import StockProduct from app.models.inbound.product import StockProduct
from datetime import datetime from datetime import datetime

View File

@ -1,5 +1,5 @@
from app.extensions import db from app.extensions import db
from app.models.material import MaterialBase from app.models.base import MaterialBase
from app.models.inbound.semi import StockSemi from app.models.inbound.semi import StockSemi
from datetime import datetime from datetime import datetime
from sqlalchemy import or_, func from sqlalchemy import or_, func

View File

@ -161,6 +161,7 @@
<span class="opt-name">{{ item.name }}</span> <span class="opt-name">{{ item.name }}</span>
<span class="opt-spec">{{ item.spec }}</span> <span class="opt-spec">{{ item.spec }}</span>
<el-tag v-if="item.isHistory" size="small" type="info" effect="plain">历史</el-tag> <el-tag v-if="item.isHistory" size="small" type="info" effect="plain">历史</el-tag>
<el-tag v-else size="small" type="success" effect="plain">系统</el-tag>
</div> </div>
</el-option> </el-option>
</el-select> </el-select>
@ -168,7 +169,7 @@
</el-col> </el-col>
<el-col :span="14" style="display: flex; align-items: center;"> <el-col :span="14" style="display: flex; align-items: center;">
<span class="search-tip"> <span class="search-tip">
<el-icon><InfoFilled /></el-icon> 点击可查看最近使用的物料输入关键词从云端搜索 <el-icon><InfoFilled /></el-icon> 未输入时展示最新物料输入关键词进行精确搜索
</span> </span>
</el-col> </el-col>
</el-row> </el-row>
@ -524,9 +525,8 @@ const saveMaterialHistory = (item: any) => {
try { try {
const existing = localStorage.getItem(key) const existing = localStorage.getItem(key)
let list = existing ? JSON.parse(existing) : [] let list = existing ? JSON.parse(existing) : []
// 必须保存完整对象,因为下拉框需要显示 name, spec 等
list = list.filter((i: any) => i.id !== item.id) list = list.filter((i: any) => i.id !== item.id)
list.unshift({ ...item, isHistory: true }) // 标记为历史 list.unshift({ ...item, isHistory: true })
if (list.length > 10) list = list.slice(0, 10) if (list.length > 10) list = list.slice(0, 10)
localStorage.setItem(key, JSON.stringify(list)) localStorage.setItem(key, JSON.stringify(list))
} catch (e) {} } catch (e) {}
@ -563,7 +563,7 @@ const mixedSearch = (queryString: string, tableField: string, storageKey: string
// 合并去重 // 合并去重
const map = new Map() const map = new Map()
historyList.forEach(i => map.set(i.value, i)) historyList.forEach(i => map.set(i.value, i))
tableList.forEach(i => map.set(i.value, i)) // 表格数据优先虽然value一样没区别 tableList.forEach(i => map.set(i.value, i))
const allList = Array.from(map.values()) const allList = Array.from(map.values())
const results = queryString ? allList.filter(createFilter(queryString)) : allList const results = queryString ? allList.filter(createFilter(queryString)) : allList
@ -594,42 +594,46 @@ const querySearchCurrency = (queryString: string, cb: any) => {
} }
// ------------------------------------ // ------------------------------------
// 物料搜索逻辑 (支持历史回显 + API) // 物料搜索逻辑 (优化:支持空查询加载默认值)
// ------------------------------------ // ------------------------------------
// 下拉框展开时触发
const handleMaterialDropdownVisible = (visible: boolean) => { const handleMaterialDropdownVisible = (visible: boolean) => {
if (visible) { if (visible) {
// 只有当列表为空时即没有进行API搜索时才填充历史记录 // 【修改点】: 展开时,如果列表为空,直接调用 API 加载前 20 条数据
// 这样不会覆盖用户正在搜的结果 // 同时也会把历史记录合并进去(在 handleSearchMaterial 内部处理或分开展示)
// 这里简单处理:直接调用 handleSearchMaterial('') 让后端返回默认数据
if (materialOptions.value.length === 0) { if (materialOptions.value.length === 0) {
materialOptions.value = getMaterialHistory() handleSearchMaterial('')
} }
} }
} }
const handleSearchMaterial = async (query: string) => { const handleSearchMaterial = async (query: string) => {
if (query) {
searchLoading.value = true searchLoading.value = true
try { try {
// 即使 query 为空,后端现在也会返回数据
const res: any = await searchMaterialBase(query) const res: any = await searchMaterialBase(query)
// 给 API 返回的数据加个标记,区分历史
const apiResults = (res.data || []).map((i: any) => ({ ...i, isHistory: false })) const apiResults = (res.data || []).map((i: any) => ({ ...i, isHistory: false }))
// 如果是空搜索,我们可以把本地历史记录插在前面,做个合并
if (!query) {
const history = getMaterialHistory()
// 简单去重:如果 API 返回的 ID 在历史记录里有,就跳过 API 的那个(优先显示历史标记)
const historyIds = new Set(history.map((h: any) => h.id))
const filteredApi = apiResults.filter((apiItem: any) => !historyIds.has(apiItem.id))
materialOptions.value = [...history, ...filteredApi]
} else {
materialOptions.value = apiResults materialOptions.value = apiResults
}
} finally { } finally {
searchLoading.value = false searchLoading.value = false
} }
} else {
// 搜索词清空时,恢复历史记录
materialOptions.value = getMaterialHistory()
}
} }
// 选中物料后 -> 存入历史 -> 填充表单 // 选中物料后 -> 存入历史 -> 填充表单
const onMaterialSelected = (val: number) => { const onMaterialSelected = (val: number) => {
const item = materialOptions.value.find(i => i.id === val) const item = materialOptions.value.find(i => i.id === val)
if (item) { if (item) {
// 存入历史
saveMaterialHistory(item) saveMaterialHistory(item)
form.material_name = item.name form.material_name = item.name
@ -832,7 +836,7 @@ const submitForm = async () => {
ElMessage.success('更新成功') ElMessage.success('更新成功')
} }
// 核心修改:提交成功时,保存供应商等信息到历史记录 // 保存供应商等信息到历史记录
saveToHistory(HISTORY_KEYS.SUPPLIER, form.supplier_name) saveToHistory(HISTORY_KEYS.SUPPLIER, form.supplier_name)
saveToHistory(HISTORY_KEYS.PURCHASER, form.purchaser) saveToHistory(HISTORY_KEYS.PURCHASER, form.purchaser)
saveToHistory(HISTORY_KEYS.EMAIL, form.purchaser_email) saveToHistory(HISTORY_KEYS.EMAIL, form.purchaser_email)