维护三个基础物件入库时候与数据库不匹配问题
This commit is contained in:
@ -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. 半成品入库
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# ✅ 正确的引用方式
|
# ✅ 正确的引用方式
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user