feat(borrow): 借库未归还直接报废功能(关联报废单流程)

后端:
- trans_service 新增 scrap_borrow 方法:未归还借用标记为 scrapped,
  扣减总库存 stock_quantity(可用已在借出时冻结),生成 TransScrap 报废记录
- transactions.py 新增 POST /borrow/scrap 接口(复用 op_return:operation 权限,库管/主管可操作)
- scrap.py query_records 支持 trans_borrow 来源的物料名解析 + 行级隔离

前端:
- records.vue 未归还记录新增「报废」按钮(库管/主管可见)
- 报废弹窗:勾选明细 + 填报废原因 + 二次确认
- 状态显示新增「已报废」标签;整单所有明细报废时标记为 scrapped
This commit is contained in:
yueli
2026-08-31 10:21:28 +08:00
parent 39f3f8af45
commit 36e9f500aa
4 changed files with 280 additions and 5 deletions

View File

@ -327,6 +327,86 @@ class TransService:
db.session.rollback()
raise e
@staticmethod
def scrap_borrow(record_ids, operator_name='System', reason=''):
"""
借库未归还直接报废(关联报废单流程)
适用场景:借出的物品确认丢失/损坏/无法归还,库管/主管手动报废。
- 该笔借用不再追讨归还,从借还记录中标记为已报废
- 生成一条 TransScrap 报废记录(关联 trans_borrow
- 总库存 stock_quantity 扣减(该物品确认损失);可用库存已在借出时冻结,无需重复扣
Args:
record_ids: TransBorrow 记录ID列表
operator_name: 操作人
reason: 报废原因
Returns:
{count: 报废条数}
"""
from app.models.transaction import TransScrap
if not record_ids:
raise ValueError('请选择要报废的借出记录')
if not reason:
raise ValueError('请填写报废原因')
model_map = {'stock_buy': StockBuy, 'stock_semi': StockSemi, 'stock_product': StockProduct}
created_records = []
scraped_count = 0
try:
for rid in record_ids:
record = TransBorrow.query.with_for_update().get(rid)
if not record:
continue
# 仅未归还的借用可报废
if record.is_returned:
continue
pending_qty = float(record.quantity) - float(record.returned_quantity or 0)
if pending_qty <= 0:
continue
# 1. 标记借用记录为已报废(不再追讨归还)
record.is_returned = True
record.status = 'scrapped'
record.return_time = datetime.now()
record.return_operator = operator_name
# 2. 扣减总库存(该物品确认损失);可用库存已在借出时冻结
ModelClass = model_map.get(record.source_table)
if ModelClass:
stock = ModelClass.query.with_for_update().get(record.stock_id)
if stock:
stock.stock_quantity = float(stock.stock_quantity or 0) - pending_qty
# 防止负库存
if stock.stock_quantity < 0:
stock.stock_quantity = 0
# 3. 创建报废记录
scrap_record = TransScrap(
sku=record.sku,
source_table='trans_borrow', # 标识来源为借出转报废
stock_id=record.id,
quantity=pending_qty,
reason=f"[借库报废] {reason}",
operator_name=operator_name,
approval_status='approved',
cost_at_scrap=0,
total_loss=0
)
db.session.add(scrap_record)
created_records.append(scrap_record)
scraped_count += 1
db.session.commit()
return {'count': scraped_count}
except Exception as e:
db.session.rollback()
raise e
@staticmethod
def get_records(page=1, limit=10, status='all', keyword=None, search_type='all'):
"""