2026-03-10 17:33:14 +08:00
|
|
|
|
from app.extensions import db, beijing_time # .material -> .base refactor checked
|
2026-02-06 11:28:48 +08:00
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
2026-03-13 09:59:01 +08:00
|
|
|
|
|
2026-09-11 13:28:43 +08:00
|
|
|
|
# 盘点模式
|
|
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-06 11:28:48 +08:00
|
|
|
|
class StocktakeDraft(db.Model):
|
2026-03-13 09:59:01 +08:00
|
|
|
|
"""
|
|
|
|
|
|
盘点草稿表
|
2026-03-13 10:45:37 +08:00
|
|
|
|
临时工作台,盘点时使用,会话结束后清空
|
2026-03-13 09:59:01 +08:00
|
|
|
|
"""
|
2026-02-06 11:28:48 +08:00
|
|
|
|
__tablename__ = 'stocktake_draft'
|
|
|
|
|
|
|
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
|
|
|
|
user_id = db.Column(db.String(100), default='admin')
|
2026-03-13 09:59:01 +08:00
|
|
|
|
# 关联的库存UUID (sku/barcode)
|
2026-02-06 11:28:48 +08:00
|
|
|
|
uuid = db.Column(db.String(100))
|
2026-03-13 09:59:01 +08:00
|
|
|
|
# 实际盘点数量
|
2026-02-06 14:30:14 +08:00
|
|
|
|
quantity = db.Column(db.Numeric(19, 4), default=1)
|
2026-03-10 17:33:14 +08:00
|
|
|
|
scan_time = db.Column(db.DateTime, default=beijing_time)
|
2026-03-13 10:45:37 +08:00
|
|
|
|
# 盘点会话标识 (用于区分不同批次的盘点)
|
2026-03-13 09:59:01 +08:00
|
|
|
|
session_id = db.Column(db.String(100))
|
2026-03-13 10:45:37 +08:00
|
|
|
|
# 关联的库存类型 (stock_buy/stock_semi/stock_product)
|
2026-03-13 09:59:01 +08:00
|
|
|
|
source_table = db.Column(db.String(50))
|
2026-03-13 10:45:37 +08:00
|
|
|
|
# 关联的库存ID
|
2026-03-13 09:59:01 +08:00
|
|
|
|
stock_id = db.Column(db.Integer)
|
2026-03-13 10:45:37 +08:00
|
|
|
|
# 账面库存数量 (记录盘点时的账面数量)
|
|
|
|
|
|
stock_qty = db.Column(db.Numeric(19, 4), default=0)
|
|
|
|
|
|
# 差异数量 (实盘 - 账面, 正=盘盈, 负=盘亏)
|
|
|
|
|
|
diff_qty = db.Column(db.Numeric(19, 4), default=0)
|
2026-03-19 10:56:40 +08:00
|
|
|
|
# 盘点备注(可选)
|
|
|
|
|
|
remark = db.Column(db.String(255), comment='盘点备注(可选)')
|
2026-09-11 11:33:04 +08:00
|
|
|
|
# 所属公司(多租户隔离键)。取自 material_base.company_name;
|
|
|
|
|
|
# 超管/跨域用户在未指定公司时为 NULL,表示不做公司过滤。
|
|
|
|
|
|
company_name = db.Column(db.String(255), index=True, comment='所属公司')
|
2026-03-13 09:59:01 +08:00
|
|
|
|
|
2026-02-06 11:28:48 +08:00
|
|
|
|
def to_dict(self):
|
|
|
|
|
|
return {
|
|
|
|
|
|
'id': self.id,
|
|
|
|
|
|
'user_id': self.user_id,
|
|
|
|
|
|
'uuid': self.uuid,
|
2026-02-06 14:30:14 +08:00
|
|
|
|
'quantity': float(self.quantity or 1),
|
2026-03-13 09:59:01 +08:00
|
|
|
|
'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,
|
2026-03-13 10:45:37 +08:00
|
|
|
|
'stock_id': self.stock_id,
|
|
|
|
|
|
'stock_qty': float(self.stock_qty or 0),
|
2026-03-19 10:56:40 +08:00
|
|
|
|
'diff_qty': float(self.diff_qty or 0),
|
2026-09-11 11:33:04 +08:00
|
|
|
|
'remark': self.remark,
|
|
|
|
|
|
'company_name': self.company_name
|
2026-03-19 10:56:40 +08:00
|
|
|
|
}
|