Files
KCGL/inventory-backend/app/models/inbound/stocktake.py
yueli 69cd23e30e fix(stocktake): 实盘 0 不再被显示成 1
draft/list 与 StocktakeDraft.to_dict 用 float(x or 1) 序列化实盘数,而 0 是 falsy —— 货架盘到 0 的物料会被报成 1。前端「重复扫码」提示读的正是这个值,库管看到“当前实盘为 1”会以为填错而改成 1,真实盘亏就这么被抹掉。改为显式 None 判断,保住合法的 0。
2026-10-03 11:00:38 +08:00

109 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from app.extensions import db, beijing_time # .material -> .base refactor checked
from datetime import datetime
# 盘点模式
STOCKTAKE_MODE_OPEN = 'open' # 明盘:可看到账面数
STOCKTAKE_MODE_BLIND = 'blind' # 盲盘:隐藏账面数与差异
# 盘点范围
STOCKTAKE_SCOPE_FULL = 'full' # 全仓盘点
STOCKTAKE_SCOPE_ACTIVE = 'active' # 活跃库位抽盘(范围过滤尚未实现,暂不可选)
# 会话状态
STOCKTAKE_STATUS_ACTIVE = 'active'
STOCKTAKE_STATUS_FINISHED = 'finished'
class StocktakeSession(db.Model):
"""
盘点会话表 —— 一次盘点的「主心骨」。
为什么需要独立一张表(而不是把配置冗余进 stocktake_draft):
1. 盲盘/明盘、全盘/抽盘都是**会话级**属性,塞进草稿表就要每行存一份,
改一次配置得 UPDATE 上万行;
2. stocktake_draft 没有任何「会话存在但还没扫码」的表示能力 ——
空会话没有草稿行,旧实现只能靠 max(scan_time) 去猜哪个会话是活跃的,
导致新开的空会话被其他 PDA 忽略、反而加入上一轮的旧会话;
3. 没有状态字段就无法把「已结束」的会话排除掉,generate-missing 跑完后
旧会话仍被当成活跃。
有了本表,活跃会话判定变成 company_name + status='active' 的精确查询。
"""
__tablename__ = 'stocktake_session'
session_id = db.Column(db.String(100), primary_key=True)
company_name = db.Column(db.String(255), nullable=False, index=True)
# open(明盘) / blind(盲盘) —— 决定 merged-list 是否下发账面数
mode = db.Column(db.String(20), nullable=False, default=STOCKTAKE_MODE_OPEN)
# full(全仓) / active(活跃库位抽盘)
scope_type = db.Column(db.String(20), nullable=False, default=STOCKTAKE_SCOPE_FULL)
# 范围明细,例: {"days":30,"top_n":50,"locations":[...],"uuids":[...]}
scope_config = db.Column(db.JSON, nullable=False, default=dict)
# active / finished
status = db.Column(db.String(20), nullable=False, default=STOCKTAKE_STATUS_ACTIVE, index=True)
created_by = db.Column(db.String(100))
created_at = db.Column(db.DateTime, default=beijing_time)
finished_at = db.Column(db.DateTime)
def to_dict(self):
return {
'session_id': self.session_id,
'company_name': self.company_name,
'mode': self.mode,
'scope_type': self.scope_type,
'scope_config': self.scope_config or {},
'status': self.status,
'created_by': self.created_by,
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None,
'finished_at': self.finished_at.strftime('%Y-%m-%d %H:%M:%S') if self.finished_at else None,
}
class StocktakeDraft(db.Model):
"""
盘点草稿表
临时工作台,盘点时使用,会话结束后清空
"""
__tablename__ = 'stocktake_draft'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.String(100), default='admin')
# 关联的库存UUID (sku/barcode)
uuid = db.Column(db.String(100))
# 实际盘点数量
quantity = db.Column(db.Numeric(19, 4), default=1)
scan_time = db.Column(db.DateTime, default=beijing_time)
# 盘点会话标识 (用于区分不同批次的盘点)
session_id = db.Column(db.String(100))
# 关联的库存类型 (stock_buy/stock_semi/stock_product)
source_table = db.Column(db.String(50))
# 关联的库存ID
stock_id = db.Column(db.Integer)
# 账面库存数量 (记录盘点时的账面数量)
stock_qty = db.Column(db.Numeric(19, 4), default=0)
# 差异数量 (实盘 - 账面, 正=盘盈, 负=盘亏)
diff_qty = db.Column(db.Numeric(19, 4), default=0)
# 盘点备注(可选)
remark = db.Column(db.String(255), comment='盘点备注(可选)')
# 所属公司(多租户隔离键)。取自 material_base.company_name;
# 超管/跨域用户在未指定公司时为 NULL,表示不做公司过滤。
company_name = db.Column(db.String(255), index=True, comment='所属公司')
def to_dict(self):
return {
'id': self.id,
'user_id': self.user_id,
'uuid': self.uuid,
# ★ 同 stock.py 的 /draft/list:0 是合法实盘值,`or 1` 会把它变成 1
'quantity': float(self.quantity) if self.quantity is not None else 0,
'scan_time': self.scan_time.strftime('%Y-%m-%d %H:%M:%S') if self.scan_time else None,
'session_id': self.session_id,
'source_table': self.source_table,
'stock_id': self.stock_id,
'stock_qty': float(self.stock_qty or 0),
'diff_qty': float(self.diff_qty or 0),
'remark': self.remark,
'company_name': self.company_name
}