From 1000deda2f9acd93b364dc812f399ace75aa98c6 Mon Sep 17 00:00:00 2001 From: yueli Date: Mon, 31 Aug 2026 13:54:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(purchase):=20=E9=87=87=E8=B4=AD=E5=8D=95?= =?UTF-8?q?=E5=8F=B7=E6=94=B9=E4=B8=BA=20=E6=89=B9=E6=AC=A1-=E6=89=B9?= =?UTF-8?q?=E5=86=85=E5=BA=8F=E5=8F=B7=20=E6=A0=BC=E5=BC=8F=EF=BC=8C?= =?UTF-8?q?=E6=98=BE=E5=BC=8F=E4=BD=93=E7=8E=B0=E5=90=8C=E6=89=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按用户原始逻辑还原并扩展: - 单号格式: PUR-日期-时间-批次-批内序号 例: PUR-20260831-1021-0001-0001(今日第1次采购的第1条) - generate_request_no 支持 batch_seq 参数生成批次+批内序号 - 新增 GET /purchase/next-batch-seq 返回今日下一个批次号 - 前端提交时获取批次号,同批所有行共享,循环提交生成连续批内序号 - 批次识别: 单号去掉末尾 -批内序号 即为批次前缀 - 保留旧格式兼容(无 batch_seq 时回退 PUR-日期-时间-流水) --- inventory-backend/app/api/v1/purchase.py | 12 ++++ .../app/services/purchase_service.py | 55 +++++++++++++++++-- inventory-web/src/api/purchase.ts | 8 +++ inventory-web/src/views/purchase/index.vue | 13 ++++- 4 files changed, 80 insertions(+), 8 deletions(-) diff --git a/inventory-backend/app/api/v1/purchase.py b/inventory-backend/app/api/v1/purchase.py index b15507e..7f2478e 100644 --- a/inventory-backend/app/api/v1/purchase.py +++ b/inventory-backend/app/api/v1/purchase.py @@ -237,6 +237,18 @@ def approve_purchase_request(purchase_id): # 5. 获取可选审批人列表 # GET /api/v1/purchase/approvers # -------------------------------------------------------- +@purchase_bp.route('/next-batch-seq', methods=['GET']) +@jwt_required() +def get_next_batch_seq(): + """获取今日下一个采购批次号(前端提交一批时调用,同批所有行共享)""" + try: + next_seq = PurchaseService.get_next_batch_seq() + return jsonify({'code': 200, 'msg': '获取成功', 'data': {'batch_seq': next_seq}}), 200 + except Exception as e: + traceback.print_exc() + return jsonify({'code': 500, 'msg': str(e)}), 500 + + @purchase_bp.route('/batch-approve', methods=['POST']) @jwt_required() @permission_required('inbound_purchase:operation') diff --git a/inventory-backend/app/services/purchase_service.py b/inventory-backend/app/services/purchase_service.py index adf9eaf..46fe694 100644 --- a/inventory-backend/app/services/purchase_service.py +++ b/inventory-backend/app/services/purchase_service.py @@ -9,16 +9,58 @@ from app.models.base import MaterialBase class PurchaseService: @staticmethod - def generate_request_no(): - """生成采购单号: PUR-yyyyMMdd-HHmm-当日流水(4位)""" + def generate_request_no(batch_seq: int = None): + """ + 生成采购单号: PUR-yyyyMMdd-HHmm-批次-批内序号 + + Args: + batch_seq: 今日第几次采购批次(前端提交一批时传入,同一批共享) + None 时回退为旧格式 PUR-日期-时间-流水 + + 返回: + - 有 batch_seq: PUR-20260831-1021-0001-0001(批次号-批内序号) + - 无 batch_seq: PUR-20260831-1021-0001(旧格式兼容) + """ + beijing_tz = timezone(timedelta(hours=8)) + now = datetime.now(beijing_tz) + date_str = now.strftime('%Y%m%d') + time_str = now.strftime('%H%M') + + if batch_seq is not None: + # 批次前缀: PUR-日期-时间-批次 + batch_prefix = f"PUR-{date_str}-{time_str}-{batch_seq:04d}" + # 批内序号: 该批次前缀下的记录数 + 1 + item_count = db.session.query(func.count(func.distinct(PurchaseRequest.request_no))) \ + .filter(PurchaseRequest.request_no.like(f"{batch_prefix}-%")).scalar() + return f"{batch_prefix}-{(item_count + 1):04d}" + + # 旧格式兼容: PUR-日期-时间-流水 + prefix = f"PUR-{date_str}-{time_str}-" + existing_count = db.session.query(func.count(func.distinct(PurchaseRequest.request_no))) \ + .filter(PurchaseRequest.request_no.like(f"{prefix}%")).scalar() + return f"{prefix}{(existing_count + 1):04d}" + + @staticmethod + def get_next_batch_seq(): + """ + 返回今日下一个采购批次号(今日第几次) + 统计今日已有的不同批次(单号第4段)数量,+1 + """ beijing_tz = timezone(timedelta(hours=8)) now = datetime.now(beijing_tz) date_str = now.strftime('%Y%m%d') time_str = now.strftime('%H%M') prefix = f"PUR-{date_str}-{time_str}-" - existing_count = db.session.query(func.count(func.distinct(PurchaseRequest.request_no))) \ - .filter(PurchaseRequest.request_no.like(f"{prefix}%")).scalar() - return f"{prefix}{(existing_count + 1):04d}" + + # 查询今日所有单号,提取第4段(批次号)去重 + rows = db.session.query(PurchaseRequest.request_no) \ + .filter(PurchaseRequest.request_no.like(f"{prefix}%")).all() + batch_seqs = set() + for (rn,) in rows: + parts = rn.split('-') + if len(parts) >= 4: + batch_seqs.add(parts[3]) + return len(batch_seqs) + 1 @staticmethod def auto_fill_from_material(keyword: str): @@ -47,7 +89,8 @@ class PurchaseService: data 包含: name, spec_model, quantity, purchase_date, supplier_link, remark, images, unit_price, total_price, approver_id, base_id (可选) """ - request_no = PurchaseService.generate_request_no() + batch_seq = data.get('batch_seq') + request_no = PurchaseService.generate_request_no(batch_seq=batch_seq) purchase_date = data.get('purchase_date') if isinstance(purchase_date, str): diff --git a/inventory-web/src/api/purchase.ts b/inventory-web/src/api/purchase.ts index c626819..ddff0f3 100644 --- a/inventory-web/src/api/purchase.ts +++ b/inventory-web/src/api/purchase.ts @@ -109,6 +109,14 @@ export function getPurchaseByBatch(batchKey: string) { }) } +// 获取今日下一个采购批次号(提交一批时调用,同批所有行共享) +export function getNextBatchSeq() { + return request({ + url: '/purchase/next-batch-seq', + method: 'get' + }) +} + // 获取可选审批人列表 export function getPurchaseApprovers() { return request({ diff --git a/inventory-web/src/views/purchase/index.vue b/inventory-web/src/views/purchase/index.vue index 41f301e..4da8dcf 100644 --- a/inventory-web/src/views/purchase/index.vue +++ b/inventory-web/src/views/purchase/index.vue @@ -362,7 +362,7 @@ import { searchMaterialPurchase } from '@/api/purchase' import { getPurchaseList, createPurchase, getPurchaseDetail, approvePurchase, batchApprovePurchase, getPurchaseApprovers, autoFillPurchase, - getPurchaseByBatch + getPurchaseByBatch, getNextBatchSeq } from '@/api/purchase' import { uploadFile, deleteFile } from '@/api/common/upload' import type { FormInstance } from 'element-plus' @@ -951,12 +951,21 @@ const submitForm = async () => { } } - // 3. 逐行提交(每行携带公共信息) + // 3. 逐行提交(每行携带公共信息 + 同一批次号) submitLoading.value = true let successCount = 0 + // ★ 获取今日批次号:同一批所有行共享,单号体现 PUR-日期-时间-批次-批内序号 + let batchSeq: number | undefined + try { + const seqRes: any = await getNextBatchSeq() + batchSeq = seqRes.data?.batch_seq + } catch (e) { + // 获取批次号失败不阻断,回退旧格式 + } try { for (const row of validItems) { const payload: any = { + batch_seq: batchSeq, name: row.name, spec_model: row.spec_model, quantity: row.quantity,