2026-03-10 17:33:14 +08:00
|
|
|
|
from app.extensions import db, beijing_time
|
2026-02-04 13:30:07 +08:00
|
|
|
|
from datetime import datetime
|
2026-02-28 11:43:10 +08:00
|
|
|
|
from sqlalchemy import func
|
2026-09-04 16:12:36 +08:00
|
|
|
|
from functools import lru_cache
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@lru_cache(maxsize=512)
|
|
|
|
|
|
def _borrow_user_name(uid):
|
|
|
|
|
|
"""按用户 ID 取用户名(带缓存,避免列表页 N+1)"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.models.system import SysUser
|
|
|
|
|
|
u = SysUser.query.get(uid)
|
|
|
|
|
|
return u.username if u else None
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _display_borrow_operator(op):
|
|
|
|
|
|
"""归还人展示映射:历史存的是数字 user id → 映射为用户名;新数据存姓名则原样返回"""
|
|
|
|
|
|
if op and str(op).strip().isdigit():
|
|
|
|
|
|
mapped = _borrow_user_name(int(op))
|
|
|
|
|
|
return mapped if mapped else op
|
|
|
|
|
|
return op
|
2026-02-04 13:30:07 +08:00
|
|
|
|
|
2026-02-04 14:29:59 +08:00
|
|
|
|
|
2026-02-04 13:30:07 +08:00
|
|
|
|
class TransBorrow(db.Model):
|
|
|
|
|
|
__tablename__ = 'trans_borrow'
|
2026-02-04 14:29:59 +08:00
|
|
|
|
|
2026-02-04 13:30:07 +08:00
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
2026-02-06 17:11:47 +08:00
|
|
|
|
borrow_no = db.Column(db.String(100))
|
|
|
|
|
|
sku = db.Column(db.String(100))
|
2026-02-04 13:30:07 +08:00
|
|
|
|
source_table = db.Column(db.String(50))
|
|
|
|
|
|
stock_id = db.Column(db.Integer)
|
2026-02-06 17:11:47 +08:00
|
|
|
|
barcode = db.Column(db.String(100))
|
2026-02-04 13:30:07 +08:00
|
|
|
|
quantity = db.Column(db.Numeric(19, 4))
|
2026-03-18 10:41:19 +08:00
|
|
|
|
returned_quantity = db.Column(db.Numeric(19, 4), default=0)
|
2026-02-06 17:11:47 +08:00
|
|
|
|
borrower_name = db.Column(db.String(100))
|
2026-03-10 17:33:14 +08:00
|
|
|
|
borrow_time = db.Column(db.DateTime, default=beijing_time)
|
2026-02-09 11:59:35 +08:00
|
|
|
|
borrow_signature = db.Column(db.Text)
|
2026-02-04 13:30:07 +08:00
|
|
|
|
expected_return_time = db.Column(db.DateTime)
|
2026-02-06 17:11:47 +08:00
|
|
|
|
is_returned = db.Column(db.Boolean, default=False)
|
|
|
|
|
|
return_time = db.Column(db.DateTime)
|
2026-02-09 11:59:35 +08:00
|
|
|
|
return_operator = db.Column(db.String(100))
|
|
|
|
|
|
return_signature = db.Column(db.Text)
|
|
|
|
|
|
return_location = db.Column(db.String(100))
|
2026-09-04 11:42:44 +08:00
|
|
|
|
# [新增] 借出时的库位快照(从源库存记录带出,便于历史追溯)
|
|
|
|
|
|
location = db.Column(db.String(100))
|
2026-02-04 14:29:59 +08:00
|
|
|
|
status = db.Column(db.String(20), default='borrowed')
|
2026-02-06 17:11:47 +08:00
|
|
|
|
remark = db.Column(db.Text)
|
2026-02-04 13:30:07 +08:00
|
|
|
|
|
|
|
|
|
|
def to_dict(self):
|
2026-03-18 10:41:19 +08:00
|
|
|
|
returned_qty = float(self.returned_quantity) if self.returned_quantity is not None else 0
|
|
|
|
|
|
total_qty = float(self.quantity) if self.quantity is not None else 0
|
|
|
|
|
|
pending_qty = total_qty - returned_qty
|
2026-02-04 13:30:07 +08:00
|
|
|
|
return {
|
|
|
|
|
|
'id': self.id,
|
2026-02-06 17:11:47 +08:00
|
|
|
|
'borrow_no': self.borrow_no,
|
2026-02-04 13:30:07 +08:00
|
|
|
|
'sku': self.sku,
|
2026-02-09 11:59:35 +08:00
|
|
|
|
'source_table': self.source_table,
|
|
|
|
|
|
'stock_id': self.stock_id,
|
2026-02-06 17:11:47 +08:00
|
|
|
|
'barcode': self.barcode,
|
2026-03-18 10:41:19 +08:00
|
|
|
|
'quantity': total_qty,
|
|
|
|
|
|
'returned_quantity': returned_qty,
|
|
|
|
|
|
'pending_quantity': pending_qty,
|
2026-02-04 13:30:07 +08:00
|
|
|
|
'borrower_name': self.borrower_name,
|
2026-02-09 11:59:35 +08:00
|
|
|
|
'borrow_time': self.borrow_time.strftime('%Y-%m-%d %H:%M') if self.borrow_time else None,
|
2026-02-06 17:11:47 +08:00
|
|
|
|
'borrow_signature': self.borrow_signature,
|
2026-02-09 11:59:35 +08:00
|
|
|
|
'expected_return_time': self.expected_return_time.strftime('%Y-%m-%d %H:%M') if self.expected_return_time else None,
|
2026-02-06 17:11:47 +08:00
|
|
|
|
'is_returned': self.is_returned,
|
2026-02-09 11:59:35 +08:00
|
|
|
|
'return_time': self.return_time.strftime('%Y-%m-%d %H:%M') if self.return_time else None,
|
2026-09-04 16:12:36 +08:00
|
|
|
|
'return_operator': _display_borrow_operator(self.return_operator),
|
2026-02-06 17:11:47 +08:00
|
|
|
|
'return_signature': self.return_signature,
|
|
|
|
|
|
'return_location': self.return_location,
|
2026-09-04 11:42:44 +08:00
|
|
|
|
'location': self.location or '',
|
2026-02-06 17:11:47 +08:00
|
|
|
|
'status': self.status,
|
2026-02-09 11:59:35 +08:00
|
|
|
|
'remark': self.remark,
|
2026-02-09 11:44:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 11:43:10 +08:00
|
|
|
|
@classmethod
|
|
|
|
|
|
def get_borrowed_quantity(cls, source_table, stock_id):
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取指定库存记录(source_table 和 stock_id)的借出未还数量总和。
|
|
|
|
|
|
返回浮点数,若无借出记录则返回 0.0。
|
|
|
|
|
|
"""
|
|
|
|
|
|
result = db.session.query(func.sum(cls.quantity)).filter(
|
|
|
|
|
|
cls.source_table == source_table,
|
|
|
|
|
|
cls.stock_id == stock_id,
|
|
|
|
|
|
cls.is_returned == False
|
|
|
|
|
|
).scalar()
|
|
|
|
|
|
return float(result) if result is not None else 0.0
|
|
|
|
|
|
|
2026-02-09 11:44:24 +08:00
|
|
|
|
|
|
|
|
|
|
class TransRepair(db.Model):
|
|
|
|
|
|
__tablename__ = 'trans_repair'
|
|
|
|
|
|
|
2026-04-08 18:23:22 +08:00
|
|
|
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
|
|
|
|
|
|
|
# 维修单号 (新增)
|
|
|
|
|
|
repair_no = db.Column(db.String(50), nullable=True, unique=True)
|
|
|
|
|
|
|
|
|
|
|
|
# 关联基础信息 (新增)
|
|
|
|
|
|
base_id = db.Column(db.Integer, db.ForeignKey('material_base.id'), nullable=True)
|
|
|
|
|
|
|
|
|
|
|
|
# SKU 保留
|
2026-02-09 11:44:24 +08:00
|
|
|
|
sku = db.Column(db.String(100))
|
2026-04-08 18:23:22 +08:00
|
|
|
|
|
2026-04-08 19:36:14 +08:00
|
|
|
|
# 物料名称 (独立录入时使用,非关联base_id)
|
|
|
|
|
|
material_name = db.Column(db.String(200))
|
|
|
|
|
|
|
2026-04-08 18:23:22 +08:00
|
|
|
|
# 序列号SN (新增,用于单台追溯)
|
|
|
|
|
|
serial_number = db.Column(db.String(100), nullable=True)
|
|
|
|
|
|
|
|
|
|
|
|
# 来源追溯 (兼容旧数据)
|
2026-02-09 11:44:24 +08:00
|
|
|
|
source_table = db.Column(db.String(50))
|
|
|
|
|
|
stock_id = db.Column(db.Integer)
|
2026-02-09 11:59:35 +08:00
|
|
|
|
is_self_made = db.Column(db.Boolean, default=False)
|
|
|
|
|
|
related_product_id = db.Column(db.Integer)
|
2026-04-08 18:23:22 +08:00
|
|
|
|
|
|
|
|
|
|
# 入库/接收时间
|
|
|
|
|
|
arrival_date = db.Column(db.Date)
|
|
|
|
|
|
|
|
|
|
|
|
# 维修状态 (新增)
|
|
|
|
|
|
repair_status = db.Column(db.String(50), default='待检测')
|
|
|
|
|
|
|
|
|
|
|
|
# 客户反馈
|
2026-02-09 11:59:35 +08:00
|
|
|
|
fault_description = db.Column(db.Text)
|
2026-04-08 18:23:22 +08:00
|
|
|
|
|
|
|
|
|
|
# 预计修复时间
|
|
|
|
|
|
expected_repair_time = db.Column(db.String(100))
|
|
|
|
|
|
|
|
|
|
|
|
# 维修日志/结果
|
2026-02-09 11:59:35 +08:00
|
|
|
|
repair_result = db.Column(db.Text)
|
2026-04-08 18:23:22 +08:00
|
|
|
|
|
|
|
|
|
|
# 维修人
|
|
|
|
|
|
repair_manager = db.Column(db.String(100))
|
|
|
|
|
|
|
|
|
|
|
|
# 出库交付时间
|
|
|
|
|
|
shipping_date = db.Column(db.Date)
|
|
|
|
|
|
|
|
|
|
|
|
# 客户名/来源
|
|
|
|
|
|
related_contract_id = db.Column(db.String(100))
|
|
|
|
|
|
|
2026-04-08 19:36:14 +08:00
|
|
|
|
# 客户名称 (新增)
|
|
|
|
|
|
customer_name = db.Column(db.String(100))
|
|
|
|
|
|
|
|
|
|
|
|
# 客户所在地 (新增)
|
|
|
|
|
|
customer_location = db.Column(db.String(255))
|
|
|
|
|
|
|
2026-04-08 18:23:22 +08:00
|
|
|
|
# 成本与售价
|
2026-02-09 11:59:35 +08:00
|
|
|
|
cost_price = db.Column(db.Numeric(19, 4))
|
|
|
|
|
|
sale_price = db.Column(db.Numeric(19, 4))
|
|
|
|
|
|
|
2026-04-08 18:23:22 +08:00
|
|
|
|
# 数据隔离 (新增)
|
|
|
|
|
|
company_id = db.Column(db.Integer, nullable=True)
|
|
|
|
|
|
|
|
|
|
|
|
# 关联关系
|
|
|
|
|
|
base = db.relationship('MaterialBase', backref='repairs')
|
|
|
|
|
|
|
2026-02-09 11:44:24 +08:00
|
|
|
|
def to_dict(self):
|
|
|
|
|
|
return {
|
|
|
|
|
|
'id': self.id,
|
2026-04-08 18:23:22 +08:00
|
|
|
|
'repair_no': self.repair_no,
|
|
|
|
|
|
'base_id': self.base_id,
|
2026-02-09 11:44:24 +08:00
|
|
|
|
'sku': self.sku,
|
2026-04-08 19:36:14 +08:00
|
|
|
|
'material_name': self.material_name,
|
2026-04-08 18:23:22 +08:00
|
|
|
|
'serial_number': self.serial_number,
|
2026-02-09 11:44:24 +08:00
|
|
|
|
'source_table': self.source_table,
|
|
|
|
|
|
'stock_id': self.stock_id,
|
2026-02-09 11:59:35 +08:00
|
|
|
|
'arrival_date': self.arrival_date.strftime('%Y-%m-%d') if self.arrival_date else None,
|
2026-04-08 18:23:22 +08:00
|
|
|
|
'repair_status': self.repair_status,
|
2026-02-09 11:59:35 +08:00
|
|
|
|
'expected_repair_time': self.expected_repair_time,
|
|
|
|
|
|
'shipping_date': self.shipping_date.strftime('%Y-%m-%d') if self.shipping_date else None,
|
|
|
|
|
|
'is_self_made': self.is_self_made,
|
|
|
|
|
|
'related_product_id': self.related_product_id,
|
|
|
|
|
|
'related_contract_id': self.related_contract_id,
|
2026-04-08 19:36:14 +08:00
|
|
|
|
'customer_name': self.customer_name,
|
|
|
|
|
|
'customer_location': self.customer_location,
|
2026-02-09 11:59:35 +08:00
|
|
|
|
'repair_manager': self.repair_manager,
|
2026-02-09 11:44:24 +08:00
|
|
|
|
'fault_description': self.fault_description,
|
|
|
|
|
|
'repair_result': self.repair_result,
|
|
|
|
|
|
'cost_price': float(self.cost_price) if self.cost_price is not None else None,
|
|
|
|
|
|
'sale_price': float(self.sale_price) if self.sale_price is not None else None,
|
2026-04-08 18:23:22 +08:00
|
|
|
|
'company_id': self.company_id,
|
2026-02-09 11:44:24 +08:00
|
|
|
|
}
|
2026-02-09 11:50:03 +08:00
|
|
|
|
|
2026-02-09 11:59:35 +08:00
|
|
|
|
|
2026-02-09 11:50:03 +08:00
|
|
|
|
class TransScrap(db.Model):
|
|
|
|
|
|
__tablename__ = 'trans_scrap'
|
|
|
|
|
|
|
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
|
|
|
|
sku = db.Column(db.String(100))
|
|
|
|
|
|
source_table = db.Column(db.String(50))
|
|
|
|
|
|
stock_id = db.Column(db.Integer)
|
|
|
|
|
|
quantity = db.Column(db.Numeric(19, 4))
|
|
|
|
|
|
reason = db.Column(db.Text)
|
|
|
|
|
|
operator_name = db.Column(db.String(100))
|
2026-03-10 17:33:14 +08:00
|
|
|
|
operation_time = db.Column(db.DateTime, default=beijing_time)
|
2026-02-09 11:50:03 +08:00
|
|
|
|
approver_name = db.Column(db.String(100))
|
|
|
|
|
|
approval_status = db.Column(db.String(20), default='pending')
|
2026-02-09 11:59:35 +08:00
|
|
|
|
cost_at_scrap = db.Column(db.Numeric(19, 4))
|
|
|
|
|
|
total_loss = db.Column(db.Numeric(19, 4))
|
2026-09-10 10:14:14 +08:00
|
|
|
|
# ★ 关联报废申请单号(审批流写台账用;DDL 见 db_migrations/add_scrap_approval.sql)
|
|
|
|
|
|
scrap_request_no = db.Column(db.String(100), index=True)
|
2026-02-09 11:59:35 +08:00
|
|
|
|
|
2026-02-09 11:50:03 +08:00
|
|
|
|
def to_dict(self):
|
|
|
|
|
|
return {
|
|
|
|
|
|
'id': self.id,
|
|
|
|
|
|
'sku': self.sku,
|
|
|
|
|
|
'source_table': self.source_table,
|
|
|
|
|
|
'stock_id': self.stock_id,
|
|
|
|
|
|
'quantity': float(self.quantity) if self.quantity is not None else None,
|
|
|
|
|
|
'reason': self.reason,
|
|
|
|
|
|
'operator_name': self.operator_name,
|
2026-02-09 11:59:35 +08:00
|
|
|
|
'operation_time': self.operation_time.strftime('%Y-%m-%d %H:%M:%S') if self.operation_time else None,
|
2026-02-09 11:50:03 +08:00
|
|
|
|
'approver_name': self.approver_name,
|
|
|
|
|
|
'approval_status': self.approval_status,
|
|
|
|
|
|
'cost_at_scrap': float(self.cost_at_scrap) if self.cost_at_scrap is not None else None,
|
|
|
|
|
|
'total_loss': float(self.total_loss) if self.total_loss is not None else None,
|
|
|
|
|
|
}
|
2026-09-16 15:45:11 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 原单退回(逆向物流)
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 设计要点
|
|
|
|
|
|
# --------
|
|
|
|
|
|
# 1. **流水不覆盖**:每一次退回写一条 trans_return,而不是在原记录上累加覆盖。
|
|
|
|
|
|
# 这是刻意与 trans_borrow 划清界限 —— 后者在部分归还时会把
|
|
|
|
|
|
# return_time / return_operator / return_signature 逐次覆盖,导致
|
|
|
|
|
|
# 「谁在什么时候还了多少」永久丢失。退回流水不能再犯同样的错。
|
|
|
|
|
|
#
|
|
|
|
|
|
# 2. **不良品不入库存表**:库存表的 status 是**行级**属性,而质量是**件级**
|
|
|
|
|
|
# 属性。把坏件加回原行,会让一行同时含良品与坏件 —— 只能整行打不良,
|
|
|
|
|
|
# 而实测 stock_buy 单行最大 4789 件(中位 8 件),整行打不良等于凭空
|
|
|
|
|
|
# 损失大量良品。故坏件全程存放在独立的 trans_defective_goods 台账里,
|
|
|
|
|
|
# 只有修好回库那一刻才回到原库存行。
|
|
|
|
|
|
#
|
|
|
|
|
|
# 3. **与维修模块解耦**:trans_repair 是 SN 单台粒度、且没有任何数量列,
|
|
|
|
|
|
# 承载不了「一批坏件」(实测 50.6% 的出库是多件,中位 2、最大 186)。
|
|
|
|
|
|
# 故坏件台账独立建表,不复用 trans_repair。
|
|
|
|
|
|
|
|
|
|
|
|
RETURN_TYPE_GOOD = '良品'
|
|
|
|
|
|
RETURN_TYPE_DEFECTIVE = '不良品'
|
|
|
|
|
|
|
|
|
|
|
|
VALID_RETURN_TYPES = (RETURN_TYPE_GOOD, RETURN_TYPE_DEFECTIVE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TransReturn(db.Model):
|
|
|
|
|
|
"""
|
|
|
|
|
|
原单退回流水。
|
|
|
|
|
|
|
|
|
|
|
|
一行 = 一次退回动作(不做覆盖式更新)。累计退回量另存于
|
|
|
|
|
|
trans_outbound.returned_quantity,本表负责回答「每一次是谁、何时、
|
|
|
|
|
|
退了多少、良品还是不良品」。
|
|
|
|
|
|
"""
|
|
|
|
|
|
__tablename__ = 'trans_return'
|
|
|
|
|
|
|
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
|
|
|
|
|
|
|
|
|
|
outbound_id = db.Column(db.Integer, nullable=False, index=True) # 原出库明细 trans_outbound.id
|
|
|
|
|
|
stock_id = db.Column(db.Integer) # 原库存行 id(快照)
|
|
|
|
|
|
source_table = db.Column(db.String(50)) # 原库存表名(快照)
|
|
|
|
|
|
sku = db.Column(db.String(100)) # 冗余,避免列表页联表
|
|
|
|
|
|
|
|
|
|
|
|
return_qty = db.Column(db.Numeric(19, 4), nullable=False, default=0)
|
|
|
|
|
|
return_type = db.Column(db.String(20), nullable=False) # '良品' | '不良品'
|
|
|
|
|
|
reason = db.Column(db.Text)
|
|
|
|
|
|
operator = db.Column(db.String(100))
|
|
|
|
|
|
return_time = db.Column(db.DateTime, default=beijing_time)
|
|
|
|
|
|
|
|
|
|
|
|
def to_dict(self):
|
|
|
|
|
|
return {
|
|
|
|
|
|
'id': self.id,
|
|
|
|
|
|
'outbound_id': self.outbound_id,
|
|
|
|
|
|
'stock_id': self.stock_id,
|
|
|
|
|
|
'source_table': self.source_table,
|
|
|
|
|
|
'sku': self.sku,
|
|
|
|
|
|
'return_qty': float(self.return_qty) if self.return_qty is not None else 0,
|
|
|
|
|
|
'return_type': self.return_type,
|
|
|
|
|
|
'reason': self.reason,
|
|
|
|
|
|
'operator': self.operator,
|
|
|
|
|
|
'return_time': self.return_time.strftime('%Y-%m-%d %H:%M:%S') if self.return_time else None,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 状态机(三期升级):
|
|
|
|
|
|
# 待处理 ──┬─→ 处理中 ──┬─→ 已回库(全部回库,无报废)
|
|
|
|
|
|
# │ ├─→ 已报废(全部报废,无回库)
|
|
|
|
|
|
# │ └─→ 已闭环(回库与报废混合,在管量归零)
|
|
|
|
|
|
# └─(一次处置即为终态时,直接跳到对应的终态)
|
|
|
|
|
|
#
|
|
|
|
|
|
# ★ 三种终态而非单一「已闭环」,是为了让工作台一眼看出这批坏件的**去向**:
|
|
|
|
|
|
# 修好回到库存了、还是被销毁了。混合处置无法用单一去向描述,才归到「已闭环」。
|
|
|
|
|
|
DEFECTIVE_STATUS_PENDING = '待处理' # 刚退回,尚未做任何处置
|
|
|
|
|
|
DEFECTIVE_STATUS_IN_PROGRESS = '处理中' # 已部分回库/部分报废,仍有在管量
|
|
|
|
|
|
DEFECTIVE_STATUS_RESTOCKED = '已回库' # 全部回库,无报废
|
|
|
|
|
|
DEFECTIVE_STATUS_SCRAPPED = '已报废' # 全部报废,无回库
|
|
|
|
|
|
DEFECTIVE_STATUS_CLOSED = '已闭环' # 回库与报废混合,在管量归零
|
|
|
|
|
|
|
|
|
|
|
|
VALID_DEFECTIVE_STATUSES = (
|
|
|
|
|
|
DEFECTIVE_STATUS_PENDING,
|
|
|
|
|
|
DEFECTIVE_STATUS_IN_PROGRESS,
|
|
|
|
|
|
DEFECTIVE_STATUS_RESTOCKED,
|
|
|
|
|
|
DEFECTIVE_STATUS_SCRAPPED,
|
|
|
|
|
|
DEFECTIVE_STATUS_CLOSED,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ★ 可继续处置的状态白名单(Fail-Closed:未列出的一律拒绝回库/报废)。
|
|
|
|
|
|
# 三种终态都不可再动:已回库→再回库就是凭空多一份库存;已报废→实物已销毁;
|
|
|
|
|
|
# 已闭环→在管量已归零。
|
|
|
|
|
|
OPEN_DEFECTIVE_STATUSES = (
|
|
|
|
|
|
DEFECTIVE_STATUS_PENDING,
|
|
|
|
|
|
DEFECTIVE_STATUS_IN_PROGRESS,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 语义别名:回库与报废的准入白名单是同一组「未结案」状态
|
|
|
|
|
|
RESTOCKABLE_DEFECTIVE_STATUSES = OPEN_DEFECTIVE_STATUSES
|
|
|
|
|
|
SCRAPPABLE_DEFECTIVE_STATUSES = OPEN_DEFECTIVE_STATUSES
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def defective_close_status(restocked_qty, scrapped_qty):
|
|
|
|
|
|
"""
|
|
|
|
|
|
在管量归零时,由累计去向推导终态。
|
|
|
|
|
|
|
|
|
|
|
|
★ 为什么需要它:一次坏件批次可能既回库了一部分、又报废了剩余部分。
|
|
|
|
|
|
此时 remaining_qty 归零,但既不是「已回库」也不是「已报废」——
|
|
|
|
|
|
按最后一次动作定状态会产生误导(最后报废 ≠ 整批报废)。
|
|
|
|
|
|
故由累计量推导,语义稳定且与动作顺序无关。
|
|
|
|
|
|
"""
|
|
|
|
|
|
restocked = float(restocked_qty or 0)
|
|
|
|
|
|
scrapped = float(scrapped_qty or 0)
|
|
|
|
|
|
if restocked > 0 and scrapped > 0:
|
|
|
|
|
|
return DEFECTIVE_STATUS_CLOSED
|
|
|
|
|
|
if scrapped > 0:
|
|
|
|
|
|
return DEFECTIVE_STATUS_SCRAPPED
|
|
|
|
|
|
return DEFECTIVE_STATUS_RESTOCKED
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TransDefectiveGoods(db.Model):
|
|
|
|
|
|
"""
|
|
|
|
|
|
不良品在管台账。
|
|
|
|
|
|
|
|
|
|
|
|
一行 = 一批同源坏件。支持**部分回库**:remaining_qty 随每次回库递减,
|
|
|
|
|
|
归零才算整批回库完成。
|
|
|
|
|
|
|
|
|
|
|
|
本表与库存表解耦:坏件在管期间不占用任何库存行的数量,也不改其 status。
|
|
|
|
|
|
回库时才按 source_table + stock_id 回到原库存行。
|
|
|
|
|
|
"""
|
|
|
|
|
|
__tablename__ = 'trans_defective_goods'
|
|
|
|
|
|
|
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
|
|
|
|
|
|
|
|
|
|
# --- 来源追溯 ---
|
|
|
|
|
|
return_id = db.Column(db.Integer, index=True) # trans_return.id
|
|
|
|
|
|
outbound_id = db.Column(db.Integer, index=True) # trans_outbound.id
|
|
|
|
|
|
|
|
|
|
|
|
# --- 回库目标(原库存行)---
|
|
|
|
|
|
source_table = db.Column(db.String(50), nullable=False)
|
|
|
|
|
|
stock_id = db.Column(db.Integer, nullable=False)
|
|
|
|
|
|
|
|
|
|
|
|
# --- 物料快照(回库目标行可能被删,此处保留可读信息)---
|
|
|
|
|
|
base_id = db.Column(db.Integer)
|
|
|
|
|
|
sku = db.Column(db.String(100))
|
|
|
|
|
|
material_name = db.Column(db.String(200))
|
|
|
|
|
|
spec_model = db.Column(db.String(255))
|
|
|
|
|
|
|
|
|
|
|
|
# --- 数量 ---
|
|
|
|
|
|
# ★ 不变式:restocked_qty + scrapped_qty + remaining_qty = quantity
|
|
|
|
|
|
# 三个去向列相互独立,不可互推 —— 二期曾用 quantity - remaining_qty
|
|
|
|
|
|
# 反推回库量,三期加入报废出口后该反推即失效。
|
|
|
|
|
|
quantity = db.Column(db.Numeric(19, 4), nullable=False, default=0) # 进入在管时的原始数量
|
|
|
|
|
|
remaining_qty = db.Column(db.Numeric(19, 4), nullable=False, default=0) # 仍在管数量
|
|
|
|
|
|
restocked_qty = db.Column(db.Numeric(19, 4), nullable=False, default=0) # [三期] 累计已回库
|
|
|
|
|
|
scrapped_qty = db.Column(db.Numeric(19, 4), nullable=False, default=0) # [三期] 累计已报废
|
|
|
|
|
|
|
|
|
|
|
|
# --- 状态与归属 ---
|
|
|
|
|
|
status = db.Column(db.String(20), nullable=False, default=DEFECTIVE_STATUS_PENDING)
|
|
|
|
|
|
company_name = db.Column(db.String(255), index=True) # 行级隔离:本表承载实物,按库存表口径存公司
|
|
|
|
|
|
reason = db.Column(db.Text)
|
|
|
|
|
|
operator = db.Column(db.String(100))
|
|
|
|
|
|
remark = db.Column(db.Text)
|
|
|
|
|
|
|
|
|
|
|
|
created_at = db.Column(db.DateTime, default=beijing_time)
|
|
|
|
|
|
updated_at = db.Column(db.DateTime, default=beijing_time, onupdate=beijing_time)
|
|
|
|
|
|
|
|
|
|
|
|
def to_dict(self):
|
|
|
|
|
|
qty = float(self.quantity) if self.quantity is not None else 0
|
|
|
|
|
|
remain = float(self.remaining_qty) if self.remaining_qty is not None else 0
|
|
|
|
|
|
restocked = float(self.restocked_qty) if self.restocked_qty is not None else 0
|
|
|
|
|
|
scrapped = float(self.scrapped_qty) if self.scrapped_qty is not None else 0
|
|
|
|
|
|
return {
|
|
|
|
|
|
'id': self.id,
|
|
|
|
|
|
'return_id': self.return_id,
|
|
|
|
|
|
'outbound_id': self.outbound_id,
|
|
|
|
|
|
'source_table': self.source_table,
|
|
|
|
|
|
'stock_id': self.stock_id,
|
|
|
|
|
|
'base_id': self.base_id,
|
|
|
|
|
|
'sku': self.sku,
|
|
|
|
|
|
'material_name': self.material_name,
|
|
|
|
|
|
'spec_model': self.spec_model,
|
|
|
|
|
|
'quantity': qty,
|
|
|
|
|
|
'remaining_qty': remain,
|
|
|
|
|
|
# ★ 三个去向列各自独立取值。改造前 restocked_qty 由
|
|
|
|
|
|
# quantity - remaining_qty 反推,三期加入报废出口后会算错。
|
|
|
|
|
|
'restocked_qty': restocked,
|
|
|
|
|
|
'scrapped_qty': scrapped,
|
|
|
|
|
|
'status': self.status,
|
|
|
|
|
|
'company_name': self.company_name,
|
|
|
|
|
|
'reason': self.reason,
|
|
|
|
|
|
'operator': self.operator,
|
|
|
|
|
|
'remark': self.remark,
|
|
|
|
|
|
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None,
|
|
|
|
|
|
'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S') if self.updated_at else None,
|
|
|
|
|
|
}
|