diff --git a/inventory-backend/app/api/v1/scrap.py b/inventory-backend/app/api/v1/scrap.py index 5971239..6d8ba3f 100644 --- a/inventory-backend/app/api/v1/scrap.py +++ b/inventory-backend/app/api/v1/scrap.py @@ -356,7 +356,30 @@ class ScrapService: ).join(MaterialBase, TransRepair.base_id == MaterialBase.id).filter( MaterialBase.company_name == company_limit ) - all_matches = buy_subq.union(semi_subq, product_subq, repair_subq).subquery() + # ★ 借库转报废:stock_id 指向 trans_borrow.id,通过 borrow 关联源库存表再关联 MaterialBase + from app.models.transaction import TransBorrow + borrow_stock_buy = db.session.query(TransScrap.id).join( + TransBorrow, db.and_(TransScrap.stock_id == TransBorrow.id, + TransScrap.source_table == 'trans_borrow') + ).join(StockBuy, TransBorrow.stock_id == StockBuy.id).join( + MaterialBase, StockBuy.base_id == MaterialBase.id + ).filter(MaterialBase.company_name == company_limit) + borrow_stock_semi = db.session.query(TransScrap.id).join( + TransBorrow, db.and_(TransScrap.stock_id == TransBorrow.id, + TransScrap.source_table == 'trans_borrow') + ).join(StockSemi, TransBorrow.stock_id == StockSemi.id).join( + MaterialBase, StockSemi.base_id == MaterialBase.id + ).filter(MaterialBase.company_name == company_limit) + borrow_stock_product = db.session.query(TransScrap.id).join( + TransBorrow, db.and_(TransScrap.stock_id == TransBorrow.id, + TransScrap.source_table == 'trans_borrow') + ).join(StockProduct, TransBorrow.stock_id == StockProduct.id).join( + MaterialBase, StockProduct.base_id == MaterialBase.id + ).filter(MaterialBase.company_name == company_limit) + all_matches = buy_subq.union( + semi_subq, product_subq, repair_subq, + borrow_stock_buy, borrow_stock_semi, borrow_stock_product + ).subquery() query = query.filter(TransScrap.id.in_(all_matches)) # 按时间倒序 @@ -390,7 +413,24 @@ class ScrapService: material_name = '' spec_model = '' - if r.source_table == 'trans_repair': + if r.source_table == 'trans_borrow': + # 借库转报废:stock_id 指向 trans_borrow.id,通过源库存表取物料 + from app.models.transaction import TransBorrow + borrow = TransBorrow.query.get(r.stock_id) + if borrow: + bstock_model = None + if borrow.source_table == 'stock_buy': + bstock_model = StockBuy.query.get(borrow.stock_id) + elif borrow.source_table == 'stock_semi': + bstock_model = StockSemi.query.get(borrow.stock_id) + elif borrow.source_table == 'stock_product': + bstock_model = StockProduct.query.get(borrow.stock_id) + if bstock_model and hasattr(bstock_model, 'base_id') and bstock_model.base_id: + base = MaterialBase.query.get(bstock_model.base_id) + if base: + material_name = base.name or '' + spec_model = base.spec_model or '' + elif r.source_table == 'trans_repair': # 维修单 repair = TransRepair.query.get(r.stock_id) if repair: diff --git a/inventory-backend/app/api/v1/transactions.py b/inventory-backend/app/api/v1/transactions.py index ae3f0d5..20b0166 100644 --- a/inventory-backend/app/api/v1/transactions.py +++ b/inventory-backend/app/api/v1/transactions.py @@ -110,6 +110,38 @@ def submit_return(): return jsonify({'code': 400, 'msg': str(e)}), 400 +# --- 借库报废(未归还直接报废,关联报废单流程)--- +@trans_bp.route('/borrow/scrap', methods=['POST']) +@jwt_required() +@permission_required('op_return:operation') # 复用归还权限:能归还的库管即可报废 +@audit_log( + module='借库管理', + action='借库报废', + get_target_name_fn=lambda: request.get_json().get('reason') if request.get_json() else None +) +def scrap_borrow(): + """ + 借库未归还直接报废(库管/主管操作) + 请求体: { "record_ids": [1, 2, 3], "reason": "物品丢失" } + """ + data = request.get_json() or {} + record_ids = data.get('record_ids', []) + reason = data.get('reason', '') + + if not record_ids: + return jsonify({'code': 400, 'msg': '请选择要报废的借出记录'}), 400 + + user = get_jwt_identity() or 'Unknown' + try: + result = TransService.scrap_borrow(record_ids, operator_name=user, reason=reason) + return jsonify({'code': 200, 'msg': f'已报废 {result["count"]} 条借出记录', 'data': result}) + except ValueError as e: + return jsonify({'code': 400, 'msg': str(e)}), 400 + except Exception as e: + traceback.print_exc() + return jsonify({'code': 500, 'msg': f'报废失败: {str(e)}'}), 500 + + # --- 记录列表 --- @trans_bp.route('/records', methods=['GET']) @jwt_required() diff --git a/inventory-backend/app/services/trans_service.py b/inventory-backend/app/services/trans_service.py index f0b0c80..94725f4 100644 --- a/inventory-backend/app/services/trans_service.py +++ b/inventory-backend/app/services/trans_service.py @@ -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'): """ diff --git a/inventory-web/src/views/transaction/records.vue b/inventory-web/src/views/transaction/records.vue index 238cb8f..ad11712 100644 --- a/inventory-web/src/views/transaction/records.vue +++ b/inventory-web/src/views/transaction/records.vue @@ -105,7 +105,8 @@ @@ -126,6 +127,17 @@ + + + + + + + + + +
+
单号: {{ scrapBorrowNo }}
+
请勾选要报废的未归还明细:
+
+ + + + + + + + + + + + + + + +