diff --git a/inventory-backend/app/api/v1/transactions.py b/inventory-backend/app/api/v1/transactions.py index f473167..74828da 100644 --- a/inventory-backend/app/api/v1/transactions.py +++ b/inventory-backend/app/api/v1/transactions.py @@ -246,21 +246,32 @@ def get_records(): from app.utils.advanced_filter import parse_advanced_filters advanced_filters = parse_advanced_filters(request.args.get('advancedFilters', '')) - # ★ 数据权限:普通用户只看“借用人=本人姓名(不含账号前缀)”的借还记录;管理者看全部 + # ★ 数据权限:普通用户只看与自己有关的记录(借用人 / 当前持有人 / 待我接收的 + # 转交),管理者看全部。两个口径都要传下去: + # viewer_user_id —— 精确锚点,覆盖转交接收人(此前只按姓名过滤, + # 接收人在自己的列表里看不到东西) + # borrower_name —— 姓名口径,兼容只有姓名、没有 ID 的历史行 borrower_name = None + viewer_user_id = None + current_user_id = None + _identity = get_jwt_identity() + if _identity: + current_user_id = int(_identity) # 供「是否待我接收」判定,与可见性无关 if not is_privileged_viewer(): - _identity = get_jwt_identity() if _identity: from app.models.system import SysUser _u = SysUser.query.get(int(_identity)) - _uname = _u.username if _u else '' - borrower_name = _uname.split('/')[0].strip() if _uname else None + if _u: + viewer_user_id = _u.id + _uname = _u.username or '' + borrower_name = _uname.split('/')[0].strip() if _uname else None res = TransService.get_records( page=page, limit=10, status=status, keyword=keyword, search_type=search_type, borrower_name=borrower_name, start_date=start_date, end_date=end_date, - advanced_filters=advanced_filters, + advanced_filters=advanced_filters, viewer_user_id=viewer_user_id, + current_user_id=current_user_id, ) # ★ service 层异常时:code==500 的字典(带 traceback),需要直通到前端,便于排查 @@ -602,7 +613,7 @@ def get_borrow_user_options(): }) -# --- 执行借库转交 --- +# --- 发起借库转交(双向握手第一步)--- @trans_bp.route('/borrow//transfer', methods=['POST']) @jwt_required() # ★ 幂等锁置于 permission_required 内层:prevent_double_submit 依赖 @@ -611,20 +622,25 @@ def get_borrow_user_options(): @permission_required('borrow_transfer') def transfer_borrow(borrow_id): """ - 借库转交:把一张借出单的持有权从当前持有人**整单**转给另一人。 + 发起借库转交:把一张借出单的持有权**整单**转给另一人,等待对方确认。 请求体: { - "transfer_qty": 10, # 必填,一期必须严格等于待还量 + "transfer_qty": 10, # 可选,传入时须等于整单待还量(一致性校验) "to_user_id": 12, # 必需,接收人ID(唯一身份锚点) "to_user_name": "张三", # 可选,仅作兼容;落库姓名以 to_user_id 反查为准 "remark": "..." # 可选 } + ★ 双向握手:本接口**只落一条 PENDING 流水,不改主表 current_holder**。 + 东西还没到接收人手上,责任仍归原持有人 —— 接收人在自己的列表里确认 + (POST /borrow/transfer//accept)后才真正转移。 + + ★ 覆盖范围是**整张单**(borrow_no)的全部未还明细,不是传入的这一行, + 避免同一张单出现两个持有人。 + ★ 严禁触碰库存:转交是纯持有权变更,实物不出入库, stock_buy / stock_semi / stock_product 的任何字段都不会被修改。 - - ★ 一期仅支持整单全量转交:部分转交会被拒绝(详见 service 层说明)。 """ try: data = request.get_json() or {} @@ -639,7 +655,7 @@ def transfer_borrow(borrow_id): return jsonify({ 'code': 200, - 'msg': '转交成功', + 'msg': f'已发起转交,等待【{transfer.to_user_name}】确认接收', 'data': transfer.to_dict(), }), 200 @@ -650,6 +666,65 @@ def transfer_borrow(borrow_id): return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500 +# --- 确认接收转交(双向握手第二步)--- +@trans_bp.route('/borrow/transfer//accept', methods=['POST']) +@jwt_required() +@prevent_double_submit(lock_timeout=5) +def accept_borrow_transfer(transfer_id): + """ + 接收人确认接收转交 —— 责任正式转移。 + + ★ 权限:**不加 permission_required**。这不是库管职权,而是员工对自己名下 + 资产的确认动作;service 层强校验当前登录人 == to_user_id 本人。 + + ★ 副作用:该单号下全部未还明细的 current_holder 一并改为接收人。 + 转交是整单行为,不允许单内出现两个持有人。 + """ + try: + transfer, affected = TransService.accept_transfer( + transfer_id=transfer_id, + user_id=get_jwt_identity(), + ) + return jsonify({ + 'code': 200, + 'msg': f'已接收,{affected} 项资产的持有权已转移到您名下', + 'data': transfer.to_dict(), + }), 200 + 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('/borrow/transfer//reject', methods=['POST']) +@jwt_required() +@prevent_double_submit(lock_timeout=5) +def reject_borrow_transfer(transfer_id): + """ + 接收人拒绝转交 —— 主表不动,责任仍在原持有人。 + 权限同 accept:仅 to_user_id 本人。 + """ + try: + data = request.get_json() or {} + transfer = TransService.reject_transfer( + transfer_id=transfer_id, + user_id=get_jwt_identity(), + reason=data.get('reason'), + ) + return jsonify({ + 'code': 200, + 'msg': '已拒绝该转交', + 'data': transfer.to_dict(), + }), 200 + 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('/borrow//history', methods=['GET']) @jwt_required() diff --git a/inventory-backend/app/services/trans_service.py b/inventory-backend/app/services/trans_service.py index f09720c..9fa6579 100644 --- a/inventory-backend/app/services/trans_service.py +++ b/inventory-backend/app/services/trans_service.py @@ -1,7 +1,10 @@ import uuid # .material -> .base refactor checked from datetime import datetime from app.extensions import db, beijing_time -from app.models.transaction import TransBorrow, TransBorrowTransfer, TransBorrowReturn +from app.models.transaction import ( + TransBorrow, TransBorrowTransfer, TransBorrowReturn, + TRANSFER_STATUS_PENDING, TRANSFER_STATUS_ACCEPTED, TRANSFER_STATUS_REJECTED, +) from app.models.inbound.buy import StockBuy from app.models.inbound.semi import StockSemi from app.models.inbound.product import StockProduct @@ -509,156 +512,213 @@ class TransService: raise e # ========================================================================== - # 借库转交(一期) + # 借库转交(一期 + 双向握手) + # + # 状态机: + # PENDING ──accept──> ACCEPTED (主表 current_holder 正式转移) + # └───reject──> REJECTED (主表不动,责任仍在原持有人) + # + # ★ 覆盖范围是**整张单**(borrow_no),不是单行明细 —— 原实现收明细行 ID + # 只改一行,一张 2 明细的单转交后一半归新接收人、一半仍是原借用人, + # 前端按单号聚合便同时显示两个名字(实测 BOR-20260917-0001)。 # ========================================================================== @staticmethod - def transfer_borrow(borrow_id, to_user_id, transfer_qty, operator_name='System', remark=None): + def _load_slip_for_update(borrow_no): """ - 把一张借出单的**持有权**从当前持有人整单转给另一人。 + 按单号锁定整单并返回全部明细行(含已归还的)。 - 与 execute_dispatch / process_return 的根本区别 - ------------------------------------------------ - 转交是**纯持有权变更**:实物不出入库,库存账目分毫不动。 - 本方法全程不触碰 stock_buy / stock_semi / stock_product 的任何字段 - (available_quantity 与 stock_quantity 都不动)。 - 理由见 restore_then_deduct 的 deduct_stock 说明:借出期间 available - 已冻结、stock 仍含借出未还量。转交若去动库存,会同时破坏两个既有假设 —— - 「归还只加 available」会算多,而「借库转报废在确认损失时扣 stock」 - (scrap_sources.BorrowScrapAdapter)会重复扣减。 + ★ 按 id 升序加锁:并发下所有事务以相同顺序取行锁,避免与归还/转交 + 交叉加锁造成死锁(与 execute_dispatch 的 items.sort 同一考虑)。 + """ + return (TransBorrow.query + .filter(TransBorrow.borrow_no == borrow_no) + .order_by(TransBorrow.id.asc()) + .with_for_update() + .all()) - 为什么一期只允许整单全量转交 - ---------------------------- - trans_borrow 是**单行**模型,只能存一个 current_holder_id。 - 若允许部分转交(借 10 个转 5 个出去),这一行的 current_holder 就必须 - 同时表示两个人,语义直接撕裂,且归还时无法判定该由谁还。 - 故 transfer_qty 必须严格等于待还量(quantity - returned_quantity)。 + @staticmethod + def transfer_borrow(borrow_id, to_user_id, transfer_qty=None, operator_name='System', remark=None): + """ + 发起转交(双向握手第一步):只落一条 PENDING 流水,**不动物权**。 - ★ 未来若需部分转交:应改为按数量**拆行**(新建一条 trans_borrow 承接 - 转出量、原行扣减),而不是在本行上加字段打补丁 —— 单行模型无论如何 - 扩展都无法同时表达两个持有人。 + 东西还没到接收人手上,责任仍由原持有人承担 —— 这是与旧实现最本质的 + 区别:旧实现「发起即生效」,接收人在毫不知情的情况下被强塞了资产责任。 参数 ---- - borrow_id : trans_borrow.id - to_user_id : 接收人(转交后的 current_holder)ID - transfer_qty : 转交数量,一期必须等于待还量 - operator_name: 执行转交操作的库管姓名 + borrow_id : 该单**任一明细行**的 ID,仅用于解析单据身份(borrow_no) + to_user_id : 接收人ID + transfer_qty : 可选。传入时须等于整单待还量,仅作一致性校验 + operator_name: 发起操作的库管 remark : 转交备注 - 返回 - ---- - TransBorrowTransfer 实例(已 commit) - - 异常 - ---- - ValueError: 任何校验不通过(调用方整单回滚,不会留下半转状态) + 返回已 commit 的 TransBorrowTransfer + 异常 ValueError """ from app.models.system import SysUser if to_user_id is None: raise ValueError("缺少接收人 to_user_id") - if transfer_qty is None: - raise ValueError("缺少转交数量 transfer_qty") - try: - transfer_qty = float(transfer_qty) - except (TypeError, ValueError): - raise ValueError("转交数量格式无效,应为数字") - - # ================================================================== - # ★ 防线1:锁行 —— 并发下防止同一张单被同时转给两个人 - # (后到的事务会阻塞在此,拿到锁后读到已推进的 current_holder_id, - # 从而在下方「接收人 == 当前持有人」校验处被拒绝) - # ================================================================== - record = TransBorrow.query.with_for_update().get(borrow_id) - if not record: - raise ValueError("借出记录不存在") - - # --- 1. 状态准入 --- - total_qty = float(record.quantity or 0) - returned_qty = float(record.returned_quantity or 0) - pending_qty = total_qty - returned_qty - - # ★ 顺序有意义:报废流程(scrap_sources)会同时置 is_returned=True 与 - # status='scrapped',若先判 is_returned 会让报废单收到「已归还」的 - # 误导性提示。故先判报废,给出准确原因。 - if record.status == 'scrapped': - raise ValueError("该借出记录已转入报废流程,不可再转交") - if record.is_returned or pending_qty <= 0: - raise ValueError("该借出记录已全部归还,无可转交的实物") - - # --- 2. 数量校验:一期必须整单全量转交 --- - if transfer_qty <= 0: - raise ValueError("转交数量必须大于0") - if transfer_qty > pending_qty: - raise ValueError( - f"转交数量({transfer_qty})不能大于待还数量({pending_qty})" - ) - # 浮点容差:quantity/returned_quantity 是 numeric(19,4),差值应精确, - # 但仍用容差比较,避免二进制浮点表示误差造成误拒。 - if abs(transfer_qty - pending_qty) > 1e-6: - raise ValueError( - f"目前仅支持整单全部转交:本单待还 {pending_qty}," - f"本次仅转交 {transfer_qty}。部分转交会导致当前持有人语义撕裂," - f"请整单转交,或先办理部分归还后再转交。" - ) - - # --- 3. 转出方必须已锚定 --- - # 历史行(迁移前无法用姓名唯一映射到 sys_user 的)holder 为 NULL, - # 此时「从谁转出」无从确定,Fail-Closed 拒绝。 - if record.current_holder_id is None: - raise ValueError( - "该借出记录的当前持有人未锚定(历史数据),无法转交,请先办理归还" - ) - - # --- 4. 接收人校验 --- try: to_user_id = int(to_user_id) except (TypeError, ValueError): - # 不直接 int() 抛裸异常:原生报错信息(invalid literal for int()...) - # 会原样透给前端,对库管毫无指导意义。 raise ValueError("接收人 to_user_id 格式无效,应为数字ID") + + anchor = TransBorrow.query.get(borrow_id) + if not anchor: + raise ValueError("借出记录不存在") + borrow_no = anchor.borrow_no + if not borrow_no: + raise ValueError("该借出记录缺少单号,无法转交") + + rows = TransService._load_slip_for_update(borrow_no) + open_rows = [r for r in rows if not r.is_returned] + + # --- 1. 状态准入 --- + if not open_rows: + raise ValueError("该借用单已全部归还,无可转交的实物") + if any(r.status == 'scrapped' for r in open_rows): + raise ValueError("该借用单已转入报废流程,不可转交") + + # --- 2. 数量:整单全量,不接受部分转交 --- + total_pending = sum( + float(r.quantity or 0) - float(r.returned_quantity or 0) for r in open_rows + ) + if total_pending <= 0: + raise ValueError("该借用单待还数量为 0,无可转交的实物") + if transfer_qty is not None: + try: + transfer_qty = float(transfer_qty) + except (TypeError, ValueError): + raise ValueError("转交数量格式无效,应为数字") + if abs(transfer_qty - total_pending) > 1e-6: + raise ValueError( + f"目前仅支持整单全部转交:本单待还 {total_pending}," + f"本次仅转交 {transfer_qty}。部分转交会让同一张单出现两个持有人," + f"请整单转交,或先办理部分归还后再转交。" + ) + + # --- 3. 转出方 = 所选明细当前的持有人 --- + # 正常单据内各明细持有人一致;历史遗留的「单内撕裂」以所选明细为准, + # accept 时会把该单未还明细**整体归一**到接收人名下(见 accept_transfer)。 + if anchor.current_holder_id is None: + raise ValueError("该借出记录的当前持有人未锚定(历史数据),无法转交,请先办理归还") + from_id = int(anchor.current_holder_id) + from_name = anchor.current_holder_name or user_display_name(SysUser.query.get(from_id)) + + # --- 4. 接收人校验 --- to_user = SysUser.query.get(to_user_id) if not to_user: raise ValueError(f"接收人不存在(ID:{to_user_id})") to_user_name = user_display_name(to_user) - if to_user_id == int(record.current_holder_id): + if to_user_id == from_id: raise ValueError(f"接收人与当前持有人同为【{to_user_name}】,无需转交") - # --- 5. 行级公司隔离(Fail-Closed)--- - _assert_borrow_company_visible(record) + # --- 5. 同一单号只允许一条待接收流水(否则两个接收人争抢同一批实物)--- + pending = TransBorrowTransfer.query.filter( + TransBorrowTransfer.borrow_no == borrow_no, + TransBorrowTransfer.status == TRANSFER_STATUS_PENDING, + ).first() + if pending: + raise ValueError( + f"该借用单已有一条待接收的转交(接收人:" + f"{pending.to_user_name or pending.to_user_id}),请等待对方处理" + ) + + # --- 6. 行级公司隔离(Fail-Closed)--- + _assert_borrow_company_visible(anchor) # ================================================================== - # ★ 防线2:以下只写台账,绝不触碰任何库存字段 + # ★ 只写台账,主表 current_holder **保持不变** —— 双向握手的关键。 + # 库存字段更是一律不碰(转交是纯持有权变更,实物不出入库)。 # ================================================================== - from_name = record.current_holder_name or user_display_name( - SysUser.query.get(record.current_holder_id) - ) - from_id = record.current_holder_id - transfer = TransBorrowTransfer( - borrow_id=record.id, + borrow_id=anchor.id, + borrow_no=borrow_no, + status=TRANSFER_STATUS_PENDING, from_user_id=from_id, from_user_name=from_name, to_user_id=to_user_id, to_user_name=to_user_name, - transfer_qty=transfer_qty, + transfer_qty=total_pending, transfer_time=beijing_time(), operator_name=operator_name, remark=remark, ) db.session.add(transfer) - - # 推进当前持有人。borrower_id(初始借用人)保持不动 —— 它回答的是 - # 「这单最初谁借的」,不应被转交改写。 - record.current_holder_id = to_user_id - record.current_holder_name = to_user_name - try: db.session.commit() except Exception as e: db.session.rollback() raise e + return transfer + @staticmethod + def accept_transfer(transfer_id, user_id): + """ + 接收转交(双向握手第二步):流水置 ACCEPTED,并**正式转移持有权**。 + + 覆盖范围 = 该单号下**全部未还明细**。正常单据各明细持有人一致,整批转移 + 天然无歧义;若遇历史遗留的「单内撕裂」,此处的整批归一同时把它修复 —— + 一张单本就只应有一个持有人。 + + 权限:仅 to_user_id 本人(这是员工对自己名下资产的确认,不是库管权限)。 + 返回 (transfer, 本次转移的明细行数) + """ + transfer = TransBorrowTransfer.query.with_for_update().get(transfer_id) + if not transfer: + raise ValueError("转交记录不存在") + if transfer.status != TRANSFER_STATUS_PENDING: + raise ValueError(f"该转交已【{transfer.to_dict()['status_text']}】,无法重复处理") + if transfer.to_user_id is None or int(transfer.to_user_id) != int(user_id): + raise ValueError("只有该转交的接收人本人可以确认接收") + if not transfer.borrow_no: + raise ValueError("该转交记录缺少单号(历史数据),无法确认接收") + + rows = TransService._load_slip_for_update(transfer.borrow_no) + open_rows = [r for r in rows if not r.is_returned] + if not open_rows: + raise ValueError("该借用单已全部归还,无需接收") + + to_name = transfer.to_user_name + if not to_name: + from app.models.system import SysUser + to_name = user_display_name(SysUser.query.get(transfer.to_user_id)) + + for r in open_rows: + r.current_holder_id = int(transfer.to_user_id) + r.current_holder_name = to_name + + transfer.status = TRANSFER_STATUS_ACCEPTED + try: + db.session.commit() + except Exception as e: + db.session.rollback() + raise e + return transfer, len(open_rows) + + @staticmethod + def reject_transfer(transfer_id, user_id, reason=None): + """ + 拒绝转交:流水置 REJECTED,**主表不动** —— 责任仍在原持有人。 + 权限:仅 to_user_id 本人。 + """ + transfer = TransBorrowTransfer.query.with_for_update().get(transfer_id) + if not transfer: + raise ValueError("转交记录不存在") + if transfer.status != TRANSFER_STATUS_PENDING: + raise ValueError(f"该转交已【{transfer.to_dict()['status_text']}】,无法重复处理") + if transfer.to_user_id is None or int(transfer.to_user_id) != int(user_id): + raise ValueError("只有该转交的接收人本人可以拒绝") + + transfer.status = TRANSFER_STATUS_REJECTED + if reason: + transfer.remark = (f"{transfer.remark}\n" if transfer.remark else "") + f"[拒绝原因] {reason}" + try: + db.session.commit() + except Exception as e: + db.session.rollback() + raise e return transfer @staticmethod @@ -861,7 +921,7 @@ class TransService: @staticmethod def get_records(page=1, limit=10, status='all', keyword=None, search_type='all', borrower_name=None, start_date=None, end_date=None, - advanced_filters=None): + advanced_filters=None, viewer_user_id=None, current_user_id=None): """ 获取借还记录列表(按单号 borrow_no 维度分页,避免明细撑爆 pageSize) @@ -1106,21 +1166,54 @@ class TransService: # ==================================================================== borrow_no_q = db.session.query(order_subq.c.borrow_no) - # ★ 数据权限:普通用户只看“借用人=本人姓名(不含账号前缀)”的借还记录; - # 兼容库里存成“姓名/xiaolongxia”全名(姓名 + '/' 前缀)的情况 - if borrower_name: + # ==================================================================== + # ★ 数据权限:普通用户能看到「与自己有关」的借还记录,三种关系任一成立: + # ① 我是借用人 + # ② 我是**当前持有人** —— 转交接收后东西在我手上,此前只比对 + # borrower_name,接收人在自己的列表里根本看不到该单 + # ③ 有一条**待我接收**的转交(PENDING)—— 东西还在对方手上、 + # 主表尚未转移,②匹配不到,必须单独并入,否则接收人看不到 + # 待办、无从确认 + # ID 与姓名双口径并存:新数据有 ID 锚点,历史行只有姓名。 + # ==================================================================== + if borrower_name or viewer_user_id: + own_conds = [] + if borrower_name: + # 兼容库里存成「姓名/xiaolongxia」全名的情况 + own_conds.append(TransBorrow.borrower_name == borrower_name) + own_conds.append(TransBorrow.borrower_name.like(f"{borrower_name}/%")) + own_conds.append(TransBorrow.current_holder_name == borrower_name) + own_conds.append(TransBorrow.current_holder_name.like(f"{borrower_name}/%")) + if viewer_user_id: + own_conds.append(TransBorrow.borrower_id == viewer_user_id) + own_conds.append(TransBorrow.current_holder_id == viewer_user_id) + own_borrow_nos_subq = ( db.session.query(TransBorrow.borrow_no) - .filter(or_( - TransBorrow.borrower_name == borrower_name, - TransBorrow.borrower_name.like(f"{borrower_name}/%") - )) + .filter(or_(*own_conds)) .distinct() .subquery() ) - borrow_no_q = borrow_no_q.filter( - order_subq.c.borrow_no.in_(own_borrow_nos_subq) - ) + + if viewer_user_id: + pending_to_me_subq = ( + db.session.query(TransBorrowTransfer.borrow_no) + .filter( + TransBorrowTransfer.to_user_id == viewer_user_id, + TransBorrowTransfer.status == TRANSFER_STATUS_PENDING, + TransBorrowTransfer.borrow_no.isnot(None), + ) + .distinct() + .subquery() + ) + borrow_no_q = borrow_no_q.filter(or_( + order_subq.c.borrow_no.in_(own_borrow_nos_subq), + order_subq.c.borrow_no.in_(pending_to_me_subq), + )) + else: + borrow_no_q = borrow_no_q.filter( + order_subq.c.borrow_no.in_(own_borrow_nos_subq) + ) # 关键词过滤 if keyword_borrow_nos_subq is not None: @@ -1331,6 +1424,33 @@ class TransService: key=lambda d: (_order_idx.get(d.get('borrow_no'), len(_order_idx)), d.get('id') or 0) ) + # ==================================================================== + # ★ 附加「待接收的转交」:前端据此渲染【接收转交】【拒绝】按钮与 + # 「转交待确认」状态。批量查一次,避免逐单 N+1。 + # 应用层保证同一单号最多一条 PENDING,故 borrow_no 可直接作键。 + # ==================================================================== + if items_with_names: + _bnos = [d.get('borrow_no') for d in items_with_names if d.get('borrow_no')] + _pending = TransBorrowTransfer.query.filter( + TransBorrowTransfer.borrow_no.in_(_bnos), + TransBorrowTransfer.status == TRANSFER_STATUS_PENDING, + ).all() if _bnos else [] + _pending_map = {t.borrow_no: t.to_dict() for t in _pending} + for d in items_with_names: + _pt = _pending_map.get(d.get('borrow_no')) + if _pt is not None: + # ★ is_mine 由后端判定:前端 localStorage 里只有 username + # 没有 user_id,靠姓名比对既有歧义又不可靠。 + # (viewer_user_id 对管理者为 None,故另取 current_user_id) + _pt['is_mine'] = ( + current_user_id is not None + and _pt.get('to_user_id') is not None + and int(_pt['to_user_id']) == int(current_user_id) + ) + d['pending_transfer'] = _pt + else: + _pending_map = {} + return { 'items': items_with_names, 'total': total_orders,