增加入库记录页面,同时修正三组入库的时间问题

This commit is contained in:
dxc
2026-02-05 14:30:11 +08:00
parent 10e53cab23
commit 0bc47d306d
12 changed files with 511 additions and 85 deletions

View File

@ -40,7 +40,7 @@ class BuyInboundService:
return []
# ============================================================
# 2. 新增入库逻辑
# 2. 新增入库逻辑 (修改:精确到时间)
# ============================================================
@staticmethod
def handle_inbound(data):
@ -51,14 +51,24 @@ class BuyInboundService:
material = MaterialBase.query.get(base_id)
if not material: raise ValueError("物料不存在")
in_date_val = datetime.utcnow().date()
# [核心修改] 默认使用当前时间(含时分秒),不再截取 .date()
current_time = datetime.now()
in_date_val = current_time
if data.get('in_date'):
try:
date_str = str(data['in_date'])
if len(date_str) > 10: date_str = date_str[:10]
in_date_val = datetime.strptime(date_str, '%Y-%m-%d').date()
# 如果前端传了时分秒,尝试直接解析
if len(date_str) > 10:
in_date_val = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
else:
# 如果只传了日期,手动补上当前时间的时分秒,保证同日入库的排序正确
d_temp = datetime.strptime(date_str, '%Y-%m-%d')
in_date_val = datetime(d_temp.year, d_temp.month, d_temp.day,
current_time.hour, current_time.minute, current_time.second)
except:
pass
# 解析失败则使用当前时间作为兜底
in_date_val = current_time
in_qty = float(data.get('in_quantity') or 0)
u_price = float(data.get('unit_price') or 0)
@ -80,10 +90,10 @@ class BuyInboundService:
global_print_id=next_global_id,
sku=generated_sku,
barcode=final_barcode,
in_date=in_date_val,
in_date=in_date_val, # 存入 DateTime 对象
serial_number=data.get('serial_number'),
batch_number=data.get('batch_number'),
status=data.get('status', '在库'), # 默认在库
status=data.get('status', '在库'),
in_quantity=in_qty,
stock_quantity=in_qty,
available_quantity=in_qty,
@ -185,7 +195,7 @@ class BuyInboundService:
return []
# ============================================================
# 6. 获取列表 (核心逻辑修改)
# 6. 获取列表 (修改:按时间倒序排序 + 展示只显示日期)
# ============================================================
@staticmethod
def get_list(page, limit, keyword=None, statuses=None):
@ -196,7 +206,7 @@ class BuyInboundService:
try:
query = db.session.query(StockBuy).outerjoin(MaterialBase, StockBuy.base_id == MaterialBase.id)
# 1. 关键词搜索 (覆盖所有关键字段)
# 1. 关键词搜索
if keyword:
kw = f'%{keyword}%'
query = query.filter(
@ -210,24 +220,13 @@ class BuyInboundService:
)
)
# 2. 状态筛选与零库存隐藏逻辑
# 用户要求:
# - 默认显示:'在库', '借库'。
# - 零库存规则库存为0时不在页面显示除非筛选了'已出库')。
# 2. 状态筛选
if not statuses:
# 默认情况:只查 '在库' 和 '借库'
statuses = ['在库', '借库']
# 构建筛选条件
# 如果筛选条件中 包含 '已出库',则允许显示 stock_quantity >= 0 (即显示所有)
# 如果筛选条件中 不包含 '已出库',则强制要求 stock_quantity > 0 (隐藏零库存)
if '已出库' in statuses:
# 用户想看已出库的,直接按状态查,不做数量限制
query = query.filter(StockBuy.status.in_(statuses))
else:
# 用户不想看已出库的,按状态查 AND 数量必须 > 0
query = query.filter(
and_(
StockBuy.status.in_(statuses),
@ -235,7 +234,8 @@ class BuyInboundService:
)
)
pagination = query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit, error_out=False)
# [核心修改] 按照入库时间倒序排序 (从近到远)
pagination = query.order_by(StockBuy.in_date.desc()).paginate(page=page, per_page=limit, error_out=False)
current_items = pagination.items
def parse_img(json_str):
@ -247,10 +247,17 @@ class BuyInboundService:
items = []
for item in current_items:
# 获取单行数据,不再进行聚合计算
qty_stock = float(item.stock_quantity or 0)
qty_avail = float(item.available_quantity or 0)
# [核心修改] 格式化展示日期,去掉时分秒
date_display = ''
if item.in_date:
try:
date_display = item.in_date.strftime('%Y-%m-%d')
except:
date_display = str(item.in_date)[:10]
d = {
'id': item.id,
'base_id': item.base_id,
@ -261,7 +268,7 @@ class BuyInboundService:
'material_type': item.material.material_type if item.material else '',
'sku': item.sku,
'inbound_date': str(item.in_date) if item.in_date else '',
'inbound_date': date_display, # 前端展示用的日期字符串
'barcode': item.barcode,
'serial_number': item.serial_number,
'batch_number': item.batch_number,
@ -273,8 +280,6 @@ class BuyInboundService:
'qty_stock': qty_stock,
'qty_available': qty_avail,
# 解除挂钩:不再返回所有批次的总和,直接返回当前批次的数量
# 为了兼容前端字段名,这里直接用当前行数量填充
'sum_stock': qty_stock,
'sum_available': qty_avail,

View File

@ -0,0 +1,190 @@
from sqlalchemy import select, literal, union_all, desc, asc, func, or_, cast, String, Numeric, Date
from app.extensions import db
from app.models.inbound.buy import StockBuy
from app.models.inbound.semi import StockSemi
from app.models.inbound.product import StockProduct
from app.models.base import MaterialBase
import traceback
class InboundSummaryService:
@staticmethod
def get_list(page=1, per_page=10, keyword=None, start_date=None, end_date=None, source_type=None):
"""
聚合查询:
1. 联合 StockBuy, StockSemi, StockProduct 三张表
2. 关联 MaterialBase 获取名称规格
3. 计算动态状态 (库存耗尽显示已出库)
4. 排序:默认按入库日期倒序 (最近的在前)
"""
try:
# =========================================================
# 1. 构建三个子查询 (Subqueries)
# =========================================================
# --- A. 采购件 (StockBuy) ---
q_buy = db.session.query(
StockBuy.id.label('id'),
StockBuy.base_id.label('base_id'),
StockBuy.sku.label('sku'),
StockBuy.in_date.label('inbound_date'),
StockBuy.in_quantity.label('in_qty'),
StockBuy.stock_quantity.label('current_qty'),
cast(StockBuy.supplier_name, String).label('source_info'),
StockBuy.status.label('orig_status'),
cast(StockBuy.batch_number, String).label('batch_number'),
cast(literal('buy'), String).label('source_type')
)
# --- B. 半成品 (StockSemi) ---
q_semi = db.session.query(
StockSemi.id.label('id'),
StockSemi.base_id.label('base_id'),
StockSemi.sku.label('sku'),
StockSemi.production_date.label('inbound_date'),
StockSemi.in_quantity.label('in_qty'),
StockSemi.stock_quantity.label('current_qty'),
cast(StockSemi.production_manager, String).label('source_info'),
StockSemi.status.label('orig_status'),
cast(StockSemi.batch_number, String).label('batch_number'),
cast(literal('semi'), String).label('source_type')
)
# --- C. 成品 (StockProduct) ---
q_product = db.session.query(
StockProduct.id.label('id'),
StockProduct.base_id.label('base_id'),
StockProduct.sku.label('sku'),
StockProduct.production_date.label('inbound_date'),
StockProduct.in_quantity.label('in_qty'),
StockProduct.stock_quantity.label('current_qty'),
cast(StockProduct.production_manager, String).label('source_info'),
StockProduct.status.label('orig_status'),
cast(StockProduct.serial_number, String).label('batch_number'),
cast(literal('product'), String).label('source_type')
)
# =========================================================
# 2. 组合查询 (UNION ALL)
# =========================================================
combined_query = union_all(q_buy, q_semi, q_product)
cte = combined_query.subquery()
# =========================================================
# 3. 主查询:关联 MaterialBase
# =========================================================
query = db.session.query(
cte,
MaterialBase.name.label('material_name'),
MaterialBase.spec_model.label('spec_model'),
MaterialBase.category.label('category'),
MaterialBase.material_type.label('material_type')
).outerjoin(
MaterialBase, cte.c.base_id == MaterialBase.id
)
# =========================================================
# 4. 过滤条件
# =========================================================
if keyword:
rule = or_(
cte.c.sku.ilike(f'%{keyword}%'),
cte.c.source_info.ilike(f'%{keyword}%'),
cte.c.batch_number.ilike(f'%{keyword}%'),
MaterialBase.name.ilike(f'%{keyword}%'),
MaterialBase.spec_model.ilike(f'%{keyword}%')
)
query = query.filter(rule)
if start_date and end_date:
query = query.filter(cte.c.inbound_date.between(start_date, end_date))
if source_type:
query = query.filter(cte.c.source_type == source_type)
# =========================================================
# 5. 获取总数
# =========================================================
count_query = db.session.query(func.count()) \
.select_from(cte) \
.outerjoin(MaterialBase, cte.c.base_id == MaterialBase.id)
if keyword:
count_query = count_query.filter(rule)
if start_date and end_date:
count_query = count_query.filter(cte.c.inbound_date.between(start_date, end_date))
if source_type:
count_query = count_query.filter(cte.c.source_type == source_type)
total = count_query.scalar() or 0
# =========================================================
# 6. 排序与分页
# =========================================================
# ★★★ 修改处:优先按入库日期倒序排列 (最近的在前) ★★★
# 如果日期相同,再按 SKU 排序,保证分页稳定性
query = query.order_by(desc(cte.c.inbound_date), asc(cte.c.sku))
pagination = query.limit(per_page).offset((page - 1) * per_page).all()
# =========================================================
# 7. 数据格式化
# =========================================================
items = []
type_map = {
'buy': '采购入库',
'semi': '半成品生产',
'product': '成品完工'
}
for row in pagination:
date_str = ""
if row.inbound_date:
try:
date_str = row.inbound_date.strftime('%Y-%m-%d')
except Exception:
date_str = str(row.inbound_date)
in_qty = float(row.in_qty) if row.in_qty is not None else 0.0
current_qty = float(row.current_qty) if row.current_qty is not None else 0.0
# 状态逻辑
final_status = row.orig_status
if current_qty <= 0:
final_status = "已出库"
elif current_qty < in_qty:
final_status = "部分出库"
items.append({
'id': row.id,
'sku': row.sku or "",
'name': row.material_name or "未知物品",
'spec_model': row.spec_model or "",
'category': row.category or "",
'material_type': row.material_type or "",
'inbound_date': date_str,
'quantity': in_qty,
'current_qty': current_qty,
'source_info': row.source_info or "",
'status': final_status,
'source_type': row.source_type,
'type_label': type_map.get(row.source_type, "未知类型"),
'batch_number': row.batch_number or ""
})
return {
'items': items,
'total': total,
'pages': (total + per_page - 1) // per_page if per_page > 0 else 0,
'current_page': page
}
except Exception as e:
print("【InboundSummaryService Error】:", str(e))
traceback.print_exc()
raise e

View File

@ -32,6 +32,9 @@ class ProductInboundService:
traceback.print_exc()
return []
# ============================================================
# 2. 新增入库逻辑 (修改:精确到时间)
# ============================================================
@staticmethod
def handle_inbound(data):
from app.models.inbound.product import StockProduct
@ -42,14 +45,21 @@ class ProductInboundService:
material = MaterialBase.query.get(base_id)
if not material: raise ValueError("物料不存在")
in_date_val = datetime.utcnow().date()
# [核心修改] 处理 production_date包含时分秒
current_time = datetime.now()
in_date_val = current_time
if data.get('in_date'):
try:
date_str = str(data['in_date'])
if len(date_str) > 10: date_str = date_str[:10]
in_date_val = datetime.strptime(date_str, '%Y-%m-%d').date()
if len(date_str) > 10:
in_date_val = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
else:
d_temp = datetime.strptime(date_str, '%Y-%m-%d')
in_date_val = datetime(d_temp.year, d_temp.month, d_temp.day,
current_time.hour, current_time.minute, current_time.second)
except:
pass
in_date_val = current_time
in_qty = float(data.get('in_quantity') or 0)
@ -65,7 +75,6 @@ class ProductInboundService:
generated_sku = str(next_global_id).zfill(10)
final_barcode = data.get('barcode') or generated_sku
# 处理三个图片/链接列表
photo_list = data.get('product_photo', [])
quality_list = data.get('quality_report_link', [])
inspection_list = data.get('inspection_report_link', [])
@ -78,7 +87,7 @@ class ProductInboundService:
base_id=material.id,
global_print_id=next_global_id,
sku=generated_sku,
production_date=in_date_val,
production_date=in_date_val, # 存入 DateTime
barcode=final_barcode,
serial_number=data.get('serial_number'),
@ -100,7 +109,6 @@ class ProductInboundService:
quality_status=data.get('quality_status', '合格'),
# 存为 JSON
product_photo=json.dumps(photo_list),
quality_report_link=json.dumps(quality_list),
inspection_report_link=json.dumps(inspection_list),
@ -136,7 +144,6 @@ class ProductInboundService:
for f in fields:
if f in data: setattr(stock, f, data[f])
# 更新 JSON 字段
if 'product_photo' in data:
imgs = data['product_photo']
if isinstance(imgs, list): stock.product_photo = json.dumps(imgs)
@ -188,9 +195,6 @@ class ProductInboundService:
db.session.rollback()
raise e
# ============================================================
# 获取出库流转历史 (与 Buy 逻辑一致,关联 TransOutbound 表)
# ============================================================
@staticmethod
def get_outbound_history(stock_id):
"""获取出库历史"""
@ -203,7 +207,7 @@ class ProductInboundService:
return []
# ============================================================
# 获取列表 (包含状态筛选与零库存隐藏逻辑)
# 获取列表 (修改:按时间倒序排序 + 展示只显示日期)
# ============================================================
@staticmethod
def get_list(page, limit, keyword=None, statuses=None):
@ -211,7 +215,6 @@ class ProductInboundService:
try:
query = db.session.query(StockProduct).outerjoin(MaterialBase, StockProduct.base_id == MaterialBase.id)
# 1. 关键词搜索
if keyword:
query = query.filter(or_(
MaterialBase.name.ilike(f'%{keyword}%'),
@ -222,11 +225,9 @@ class ProductInboundService:
StockProduct.sku.ilike(f'%{keyword}%')
))
# 2. 状态筛选与零库存隐藏逻辑
if not statuses:
statuses = ['在库', '借库']
# 如果筛选包含'已出库',则显示所有数量;否则隐藏 stock_quantity <= 0 的记录
if '已出库' in statuses:
query = query.filter(StockProduct.status.in_(statuses))
else:
@ -237,7 +238,9 @@ class ProductInboundService:
)
)
pagination = query.order_by(StockProduct.id.desc()).paginate(page=page, per_page=limit, error_out=False)
# [核心修改] 按照 production_date (入库日期) 倒序排序
pagination = query.order_by(StockProduct.production_date.desc()).paginate(page=page, per_page=limit,
error_out=False)
current_items = pagination.items
@ -252,20 +255,23 @@ class ProductInboundService:
for item in current_items:
d = item.to_dict()
# 直接使用当前行的库存,不再聚合
# [核心修改] 格式化日期
date_display = ''
if item.production_date:
try:
date_display = item.production_date.strftime('%Y-%m-%d')
except:
date_display = str(item.production_date)[:10]
d['inbound_date'] = date_display
d['qty_stock'] = float(item.stock_quantity or 0)
d['qty_available'] = float(item.available_quantity or 0)
# 兼容前端字段 key
d['sum_stock'] = d['qty_stock']
d['sum_available'] = d['qty_available']
# 图片/链接解析
d['product_photo'] = parse_img(item.product_photo)
d['quality_report_link'] = parse_img(item.quality_report_link)
d['inspection_report_link'] = parse_img(item.inspection_report_link)
# 打印ID
d['global_print_id'] = item.global_print_id
items.append(d)

View File

@ -41,7 +41,6 @@ class SemiInboundService:
@staticmethod
def handle_inbound(data):
# 局部导入 Model解决循环引用
from app.models.inbound.semi import StockSemi
try:
@ -53,16 +52,21 @@ class SemiInboundService:
if not material:
raise ValueError(f"ID为 {base_id} 的基础物料不存在")
# 1. 处理入库日期
in_date_val = datetime.utcnow().date()
# [核心修改] 处理入库日期(production_date),包含时分秒
current_time = datetime.now()
in_date_val = current_time
if data.get('in_date'):
try:
date_str = str(data['in_date'])
if len(date_str) > 10:
date_str = date_str[:10]
in_date_val = datetime.strptime(date_str, '%Y-%m-%d').date()
in_date_val = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
else:
d_temp = datetime.strptime(date_str, '%Y-%m-%d')
in_date_val = datetime(d_temp.year, d_temp.month, d_temp.day,
current_time.hour, current_time.minute, current_time.second)
except ValueError:
pass
in_date_val = current_time
# 2. 处理生产时间
p_start = None
@ -102,18 +106,15 @@ class SemiInboundService:
print("❌ 数据库序列 global_print_seq 不存在请执行SQL创建")
raise e
# 5. 自动生成 SKU
generated_sku = str(next_global_id).zfill(10)
final_sku = data.get('sku')
if not final_sku:
final_sku = generated_sku
# 6. 条码逻辑处理
final_barcode = data.get('barcode')
if not final_barcode:
final_barcode = final_sku
# 7. 图片列表转 JSON 字符串处理
arrival_list = data.get('arrival_photo', [])
quality_report_list = data.get('quality_report_link', [])
@ -125,7 +126,7 @@ class SemiInboundService:
base_id=material.id,
global_print_id=next_global_id,
sku=final_sku,
production_date=in_date_val,
production_date=in_date_val, # 存入 DateTime
serial_number=data.get('serial_number'),
batch_number=data.get('batch_number'),
@ -151,7 +152,6 @@ class SemiInboundService:
manual_cost=manual_cost,
total_price=total_value,
# [核心修改] 将列表转为 JSON 字符串存储
arrival_photo=json.dumps(arrival_list),
quality_report_link=json.dumps(quality_report_list),
@ -174,8 +174,6 @@ class SemiInboundService:
from app.models.inbound.semi import StockSemi
try:
print(f"----- UPDATE SEMI DEBUG: ID={stock_id} -----")
stock = StockSemi.query.get(stock_id)
if not stock:
raise ValueError("记录不存在")
@ -200,7 +198,6 @@ class SemiInboundService:
if frontend_key in data:
setattr(stock, db_attr, data[frontend_key])
# [核心修改] 图片字段更新 (List -> JSON String)
if 'arrival_photo' in data:
imgs = data['arrival_photo']
if isinstance(imgs, list):
@ -211,7 +208,6 @@ class SemiInboundService:
if isinstance(imgs, list):
stock.quality_report_link = json.dumps(imgs)
# 时间处理
if 'production_start_time' in data:
try:
if data['production_start_time']:
@ -232,7 +228,6 @@ class SemiInboundService:
except:
pass
# 更新 production_time_range 字符串
if 'production_time_range' in data:
raw_range = data['production_time_range']
if isinstance(raw_range, list):
@ -269,8 +264,6 @@ class SemiInboundService:
except Exception as e:
db.session.rollback()
print(f"----- UPDATE SEMI FAILED: {str(e)} -----")
traceback.print_exc()
raise e
@staticmethod
@ -287,9 +280,6 @@ class SemiInboundService:
db.session.rollback()
raise e
# ------------------------------------------------------------------
# [核心修改] 获取关联出库历史 (跟 Buy 保持一致)
# ------------------------------------------------------------------
@staticmethod
def get_outbound_history(stock_id):
"""获取出库历史"""
@ -301,9 +291,9 @@ class SemiInboundService:
except:
return []
# ------------------------------------------------------------------
# [核心修改] 列表查询支持状态筛选、默认隐藏0库存、去除聚合
# ------------------------------------------------------------------
# ============================================================
# 6. 获取列表 (修改:按时间倒序排序 + 展示只显示日期)
# ============================================================
@staticmethod
def get_list(page, limit, keyword=None, statuses=None):
from app.models.inbound.semi import StockSemi
@ -324,11 +314,9 @@ class SemiInboundService:
)
)
# [新增] 状态筛选与零库存隐藏逻辑
if not statuses:
statuses = ['在库', '借库']
# 如果筛选包含'已出库',则显示所有数量;否则隐藏 stock_quantity <= 0 的记录
if '已出库' in statuses:
query = query.filter(StockSemi.status.in_(statuses))
else:
@ -339,7 +327,9 @@ class SemiInboundService:
)
)
pagination = query.order_by(StockSemi.id.desc()).paginate(page=page, per_page=limit, error_out=False)
# [核心修改] 按照 production_date (入库日期) 倒序排序
pagination = query.order_by(StockSemi.production_date.desc()).paginate(page=page, per_page=limit,
error_out=False)
current_items = pagination.items
@ -354,19 +344,22 @@ class SemiInboundService:
for item in current_items:
d = item.to_dict()
# 直接使用当前行的库存,不再聚合
# [核心修改] 格式化展示日期,覆盖 to_dict 的默认行为
date_display = ''
if item.production_date:
try:
date_display = item.production_date.strftime('%Y-%m-%d')
except:
date_display = str(item.production_date)[:10]
d['inbound_date'] = date_display
d['qty_stock'] = float(item.stock_quantity or 0)
d['qty_available'] = float(item.available_quantity or 0)
# 兼容前端字段名
d['sum_stock'] = d['qty_stock']
d['sum_available'] = d['qty_available']
# 图片解析
d['arrival_photo'] = parse_img(item.arrival_photo)
d['quality_report_link'] = parse_img(item.quality_report_link)
# 打印ID
d['global_print_id'] = item.global_print_id
items.append(d)