From e027ebd4a9c9eed9f5f13fa0d62b46a8e3d7ff2b Mon Sep 17 00:00:00 2001 From: dxc Date: Fri, 6 Feb 2026 10:16:37 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9B=98=E5=BA=93=E6=93=8D=E4=BD=9C=E5=88=9D?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/api/v1/inbound/__init__.py | 11 +- inventory-backend/app/api/v1/inbound/base.py | 2 +- inventory-backend/app/api/v1/inbound/buy.py | 2 +- .../app/api/v1/inbound/product.py | 2 +- inventory-backend/app/api/v1/inbound/semi.py | 2 +- inventory-backend/app/api/v1/inbound/stock.py | 98 +++++ inventory-backend/app/models/__init__.py | 18 +- inventory-backend/app/models/inbound/buy.py | 5 +- .../app/services/inbound/buy_service.py | 36 +- .../services/print/network_print_service.py | 114 +++++ inventory-web/src/api/inbound/stock.ts | 31 ++ .../src/components/QrScanner/index.vue | 219 ++++++++++ inventory-web/src/router/index.ts | 24 +- .../src/views/outbound/Selection.vue | 302 ++++++++++++++ .../src/views/stock/stocktake/index.vue | 391 ++++++++++++++++++ 15 files changed, 1227 insertions(+), 30 deletions(-) create mode 100644 inventory-backend/app/api/v1/inbound/stock.py create mode 100644 inventory-backend/app/services/print/network_print_service.py create mode 100644 inventory-web/src/api/inbound/stock.ts create mode 100644 inventory-web/src/components/QrScanner/index.vue create mode 100644 inventory-web/src/views/outbound/Selection.vue create mode 100644 inventory-web/src/views/stock/stocktake/index.vue diff --git a/inventory-backend/app/api/v1/inbound/__init__.py b/inventory-backend/app/api/v1/inbound/__init__.py index 9c413ea..fd9c495 100644 --- a/inventory-backend/app/api/v1/inbound/__init__.py +++ b/inventory-backend/app/api/v1/inbound/__init__.py @@ -2,18 +2,19 @@ from flask import Blueprint from .buy import inbound_buy_bp from .semi import inbound_semi_bp from .base import inbound_base_bp -# 导入 product from .product import inbound_product_bp -# ★ [新增] 导入 summary from .inbound_summary import bp as inbound_summary_bp +# ★ [新增] 导入 stock 模块 +from .stock import bp as stock_bp inbound_bp = Blueprint('inbound', __name__) inbound_bp.register_blueprint(inbound_buy_bp, url_prefix='/buy') inbound_bp.register_blueprint(inbound_semi_bp, url_prefix='/semi') inbound_bp.register_blueprint(inbound_base_bp, url_prefix='/base') -# 挂载 product,前缀改为 /product inbound_bp.register_blueprint(inbound_product_bp, url_prefix='/product') +inbound_bp.register_blueprint(inbound_summary_bp, url_prefix='/summary') -# ★ [新增] 挂载 summary, url 变成 /api/v1/inbound/summary/list -inbound_bp.register_blueprint(inbound_summary_bp, url_prefix='/summary') \ No newline at end of file +# ★ [新增] 挂载 stock 模块,路径前缀为 /stock +# 最终访问路径例:/api/v1/inbound/stock/all +inbound_bp.register_blueprint(stock_bp, url_prefix='/stock') \ No newline at end of file diff --git a/inventory-backend/app/api/v1/inbound/base.py b/inventory-backend/app/api/v1/inbound/base.py index eaf4b34..cc3264c 100644 --- a/inventory-backend/app/api/v1/inbound/base.py +++ b/inventory-backend/app/api/v1/inbound/base.py @@ -3,7 +3,7 @@ from flask import Blueprint, request, jsonify from app.services.inbound.base_service import MaterialBaseService import traceback -inbound_base_bp = Blueprint('inbound_base', __name__) +inbound_base_bp = Blueprint('stock_base', __name__) # ============================================================================== diff --git a/inventory-backend/app/api/v1/inbound/buy.py b/inventory-backend/app/api/v1/inbound/buy.py index 21c6059..948d998 100644 --- a/inventory-backend/app/api/v1/inbound/buy.py +++ b/inventory-backend/app/api/v1/inbound/buy.py @@ -3,7 +3,7 @@ from flask import Blueprint, request, jsonify from app.services.inbound.buy_service import BuyInboundService import traceback -inbound_buy_bp = Blueprint('inbound_buy', __name__) +inbound_buy_bp = Blueprint('stock_buy', __name__) # ------------------------------------------------------------------ diff --git a/inventory-backend/app/api/v1/inbound/product.py b/inventory-backend/app/api/v1/inbound/product.py index 0536660..ffae290 100644 --- a/inventory-backend/app/api/v1/inbound/product.py +++ b/inventory-backend/app/api/v1/inbound/product.py @@ -3,7 +3,7 @@ from flask import Blueprint, request, jsonify from app.services.inbound.product_service import ProductInboundService import traceback -inbound_product_bp = Blueprint('inbound_product', __name__) +inbound_product_bp = Blueprint('stock_product', __name__) # ------------------------------------------------------------------ diff --git a/inventory-backend/app/api/v1/inbound/semi.py b/inventory-backend/app/api/v1/inbound/semi.py index 33f2bb9..9875eb6 100644 --- a/inventory-backend/app/api/v1/inbound/semi.py +++ b/inventory-backend/app/api/v1/inbound/semi.py @@ -4,7 +4,7 @@ from app.services.inbound.semi_service import SemiInboundService import traceback # 定义蓝图 -inbound_semi_bp = Blueprint('inbound_semi', __name__) +inbound_semi_bp = Blueprint('stock_semi', __name__) # ------------------------------------------------------------------ diff --git a/inventory-backend/app/api/v1/inbound/stock.py b/inventory-backend/app/api/v1/inbound/stock.py new file mode 100644 index 0000000..b190e5e --- /dev/null +++ b/inventory-backend/app/api/v1/inbound/stock.py @@ -0,0 +1,98 @@ +from flask import Blueprint, jsonify, request +# ★ [核心修复] 导入正确的模型类名 StockBuy (替换原来的 InboundBuy) +from app.models.inbound.buy import StockBuy + +# 尝试导入半成品和成品模型 (根据你的命名习惯修正为 StockSemi/StockProduct) +# 使用 try-except 防止如果其他文件还没改名导致再次报错 +try: + from app.models.inbound.semi import StockSemi +except ImportError: + StockSemi = None + +try: + from app.models.inbound.product import StockProduct +except ImportError: + StockProduct = None + +from app.services.print.network_print_service import NetworkPrintService + +bp = Blueprint('stock_ops', __name__) + + +@bp.route('/all', methods=['GET']) +def get_all_stock(): + """ + 获取所有在库物品(采购件+半成品+成品) + 用于:盘点初始化、出库选单列表 + """ + try: + # 1. 获取采购件 + # ★ [核心修复] 使用 StockBuy 查询,并将状态条件改为 '在库' (匹配你的 Model 定义) + materials = [] + if StockBuy: + materials = StockBuy.query.filter(StockBuy.status == '在库').all() + + # 2. 获取半成品 + semis = [] + if StockSemi: + try: + # 假设半成品也使用 '在库' 状态 + semis = StockSemi.query.filter(StockSemi.status == '在库').all() + except Exception: + semis = [] + + # 3. 获取成品 + products = [] + if StockProduct: + try: + products = StockProduct.query.filter(StockProduct.status == '在库').all() + except Exception: + products = [] + + return jsonify({ + "materials": [item.to_dict() for item in materials], + "semis": [item.to_dict() for item in semis], + "products": [item.to_dict() for item in products] + }), 200 + except Exception as e: + print(f"Error in get_all_stock: {e}") # 输出错误日志以便调试 + return jsonify({"message": f"查询库存失败: {str(e)}"}), 500 + + +@bp.route('/print/selection', methods=['POST']) +def print_selection(): + """打印出库选单""" + try: + data = request.json + items = data.get('items', []) + + if not items: + return jsonify({"message": "未选择任何物品"}), 400 + + printer = NetworkPrintService() # 默认连接 192.168.9.205 + success, msg = printer.print_outbound_selection(items) + + if success: + return jsonify({"message": "打印指令已发送"}), 200 + else: + return jsonify({"message": msg}), 500 + except Exception as e: + return jsonify({"message": f"打印服务错误: {str(e)}"}), 500 + + +@bp.route('/print/stocktake', methods=['POST']) +def print_stocktake(): + """打印盘点报告""" + try: + data = request.json + # data 结构: { total, scanned, missing, missing_items: [] } + + printer = NetworkPrintService() + success, msg = printer.print_stocktake_report(data) + + if success: + return jsonify({"message": "盘点报告已发送"}), 200 + else: + return jsonify({"message": msg}), 500 + except Exception as e: + return jsonify({"message": f"打印服务错误: {str(e)}"}), 500 \ No newline at end of file diff --git a/inventory-backend/app/models/__init__.py b/inventory-backend/app/models/__init__.py index 028c3a9..744660e 100644 --- a/inventory-backend/app/models/__init__.py +++ b/inventory-backend/app/models/__init__.py @@ -1,13 +1,19 @@ # app/models/__init__.py -# 1. 基础物料 +# 1. 基础物料 (必须先加载,因为 buy 依赖它) from app.models.base import MaterialBase -# 2. 采购入库 (指向新路径) +# 2. 采购入库 (现在的类名是 StockBuy) from app.models.inbound.buy import StockBuy -# 3. 半成品入库 (指向新路径) -from app.models.inbound.semi import StockSemi +# 3. 半成品入库 (如果有) +try: + from app.models.inbound.semi import StockSemi +except ImportError: + pass -# 如果有其他模型 (比如 sys_user 等),保留它们 -# from app.models.sys_user import SysUser \ No newline at end of file +# 4. 出库记录 (如果有,BuyService 用到了 TransOutbound) +try: + from app.models.outbound import TransOutbound +except ImportError: + pass \ No newline at end of file diff --git a/inventory-backend/app/models/inbound/buy.py b/inventory-backend/app/models/inbound/buy.py index 0ee8a48..cbbeb85 100644 --- a/inventory-backend/app/models/inbound/buy.py +++ b/inventory-backend/app/models/inbound/buy.py @@ -1,8 +1,6 @@ -# app/models/inbound/buy.py from app.extensions import db import json - class StockBuy(db.Model): """ 采购入库库存表 @@ -21,7 +19,7 @@ class StockBuy(db.Model): batch_number = db.Column(db.String(100)) # 状态 - status = db.Column(db.String(50)) + status = db.Column(db.String(50), default='在库') inspection_status = db.Column(db.String(50)) warehouse_location = db.Column(db.String(100)) @@ -51,6 +49,7 @@ class StockBuy(db.Model): global_print_id = db.Column(db.Integer) # 关系定义 + # 注意:这里使用字符串 'MaterialBase' 引用,避免了直接 import 导致的潜在循环依赖 material = db.relationship('MaterialBase', back_populates='stock_buys') def to_dict(self): diff --git a/inventory-backend/app/services/inbound/buy_service.py b/inventory-backend/app/services/inbound/buy_service.py index e4b67f0..0fd7dbd 100644 --- a/inventory-backend/app/services/inbound/buy_service.py +++ b/inventory-backend/app/services/inbound/buy_service.py @@ -1,9 +1,15 @@ -# app/services/inbound/buy_service.py from app.extensions import db +# 引用新的模型类 StockBuy from app.models.inbound.buy import StockBuy from app.models.base import MaterialBase -from app.models.outbound import TransOutbound -from datetime import datetime, timedelta, timezone # [修改] 引入 timezone + +# 尝试导入出库模型,如果不存在则忽略(防止报错影响入库功能) +try: + from app.models.outbound import TransOutbound +except ImportError: + TransOutbound = None + +from datetime import datetime, timedelta, timezone from sqlalchemy import or_, func, text, and_ import traceback import json @@ -76,11 +82,22 @@ class BuyInboundService: in_qty = float(data.get('in_quantity') or 0) u_price = float(data.get('unit_price') or 0) - seq_sql = text("SELECT nextval('global_print_seq')") - result = db.session.execute(seq_sql) - next_global_id = result.scalar() + # [核心逻辑] 获取全局打印流水号 + try: + seq_sql = text("SELECT nextval('global_print_seq')") + result = db.session.execute(seq_sql) + next_global_id = result.scalar() + except Exception: + # 如果序列不存在,回退处理(或在数据库创建序列) + print("Warning: Sequence global_print_seq not found.") + next_global_id = None + + # SKU 生成逻辑:如果没有 ID,用临时随机数或空;通常应该依赖 next_global_id + if next_global_id: + generated_sku = str(next_global_id).zfill(10) + else: + generated_sku = datetime.now().strftime('%Y%m%d%H%M%S') # 降级方案 - generated_sku = str(next_global_id).zfill(10) final_barcode = data.get('barcode') or generated_sku arrival_list = data.get('arrival_photo', []) @@ -151,6 +168,7 @@ class BuyInboundService: if 'in_quantity' in data: new_qty = float(data['in_quantity']) + # 计算差值,同步更新库存量和可用量 diff = new_qty - float(stock.in_quantity) if diff != 0: stock.in_quantity = new_qty @@ -189,6 +207,8 @@ class BuyInboundService: @staticmethod def get_outbound_history(stock_id): """获取出库历史""" + if not TransOutbound: + return [] try: records = TransOutbound.query.filter_by( source_table='stock_buy', stock_id=stock_id @@ -228,8 +248,10 @@ class BuyInboundService: statuses = ['在库', '借库'] if '已出库' in statuses: + # 如果明确查已出库,可以包含库存为0的 query = query.filter(StockBuy.status.in_(statuses)) else: + # 默认查在库,必须保证库存 > 0 query = query.filter( and_( StockBuy.status.in_(statuses), diff --git a/inventory-backend/app/services/print/network_print_service.py b/inventory-backend/app/services/print/network_print_service.py new file mode 100644 index 0000000..d5c0d3e --- /dev/null +++ b/inventory-backend/app/services/print/network_print_service.py @@ -0,0 +1,114 @@ +import socket +import datetime + + +class NetworkPrintService: + def __init__(self, ip='192.168.9.205', port=9100): + """ + 初始化网络打印机服务 + :param ip: 打印机IP,默认 192.168.9.205 + :param port: 端口,默认 9100 + """ + self.ip = ip + self.port = port + + def _send_to_printer(self, content): + """底层发送方法""" + try: + # 建立 Socket 连接 + with socket.socket(socket.socket.AF_INET, socket.socket.SOCK_STREAM) as s: + s.settimeout(5) # 设置5秒超时 + s.connect((self.ip, self.port)) + + # 发送内容,使用 GB18030 编码以支持中文 + s.sendall(content.encode('gb18030')) + + # 发送切纸指令 (ESC/POS: GS V m) + # 十六进制: 1D 56 42 00 + s.sendall(b'\x1d\x56\x42\x00') + + return True, "打印成功" + except Exception as e: + print(f"[NetworkPrint Error] {str(e)}") + return False, f"打印失败: {str(e)}" + + def print_outbound_selection(self, items): + """ + 打印出库选单 (拣货单) + :param items: 选中的物品列表 + """ + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + lines = [] + lines.append("\n") + lines.append("********************************") + lines.append(" 出库拣货确认单 ") + lines.append("********************************") + lines.append(f"打印时间: {timestamp}") + lines.append(f"待出库总数: {len(items)} 件") + lines.append("--------------------------------") + lines.append(f"{'名称':<14}{'规格/批号':<10}") + lines.append("--------------------------------") + + for item in items: + # 获取名称,优先取 material_name, 其次 product_name + name = item.get('material_name') or item.get('product_name') or "未知物品" + if len(name) > 14: name = name[:13] + "." # 名称过长截断 + + standard = item.get('standard', '') + batch = item.get('batch_no', '') + uuid = item.get('uuid', '')[-6:] # 只显示UUID后6位 + + lines.append(f"{name:<14} {standard}") + lines.append(f"批号: {batch} | 尾号: {uuid}") + lines.append("- - - - - - - - - - - - - - - -") + + lines.append("\n") + lines.append("库管员签字: ______________") + lines.append("领料人签字: ______________") + lines.append("\n\n\n") # 走纸 + + content = "\n".join(lines) + return self._send_to_printer(content) + + def print_stocktake_report(self, data): + """ + 打印盘点统计报告 + :param data: 包含 total, scanned, missing, missing_items + """ + total = data.get('total', 0) + scanned = data.get('scanned', 0) + missing = data.get('missing', 0) + missing_items = data.get('missing_items', []) + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + lines = [] + lines.append("\n") + lines.append("================================") + lines.append(" 库存盘点统计报告 ") + lines.append("================================") + lines.append(f"盘点时间: {timestamp}") + lines.append(f"应盘总数: {total}") + lines.append(f"实盘(已扫): {scanned}") + lines.append(f"差异(未扫): {missing}") + lines.append("--------------------------------") + + if missing == 0: + lines.append("【结果】: 账实相符,库存完美!") + else: + lines.append("【差异明细 (未扫码物品)】:") + for item in missing_items: + name = item.get('material_name') or item.get('product_name') or "未知" + batch = item.get('batch_no', '-') + # 兼容不同模型的字段 + code = item.get('uuid', item.get('bar_code', 'N/A'))[-6:] + + lines.append(f"[ ] {name}") + lines.append(f" 批:{batch} 码:{code}") + + lines.append("\n") + lines.append("监盘人: ______________") + lines.append("\n\n\n") + + content = "\n".join(lines) + return self._send_to_printer(content) \ No newline at end of file diff --git a/inventory-web/src/api/inbound/stock.ts b/inventory-web/src/api/inbound/stock.ts new file mode 100644 index 0000000..f44ee35 --- /dev/null +++ b/inventory-web/src/api/inbound/stock.ts @@ -0,0 +1,31 @@ +import request from '@/utils/request' + +// 获取全量库存 +// 修改前: url: '/api/v1/inbound/stock/all' +// 修改后: url: '/v1/inbound/stock/all' +export function getAllStock() { + return request({ + url: '/v1/inbound/stock/all', + method: 'get' + }) +} + +// 打印出库选单 +// 修改后: 去掉开头的 /api +export function printSelectionList(items: any[]) { + return request({ + url: '/v1/inbound/stock/print/selection', + method: 'post', + data: { items } + }) +} + +// 打印盘点报告 +// 修改后: 去掉开头的 /api +export function printStocktakeReport(data: any) { + return request({ + url: '/v1/inbound/stock/print/stocktake', + method: 'post', + data + }) +} \ No newline at end of file diff --git a/inventory-web/src/components/QrScanner/index.vue b/inventory-web/src/components/QrScanner/index.vue new file mode 100644 index 0000000..263dbf4 --- /dev/null +++ b/inventory-web/src/components/QrScanner/index.vue @@ -0,0 +1,219 @@ + + + + + \ No newline at end of file diff --git a/inventory-web/src/router/index.ts b/inventory-web/src/router/index.ts index 328ed94..d8303e2 100644 --- a/inventory-web/src/router/index.ts +++ b/inventory-web/src/router/index.ts @@ -46,7 +46,7 @@ const routes: Array = [ { path: '/inventory', component: Layout, - meta: { title: '入库管理', icon: 'Shop' }, // 修改标题以区分出库 + meta: { title: '入库管理', icon: 'Shop' }, redirect: '/inventory/buy', children: [ { @@ -67,7 +67,7 @@ const routes: Array = [ component: () => import('@/views/stock/inbound/product.vue'), meta: { title: '成品' } }, - // ★ [新增] 入库记录整合 + // [原有] 入库记录整合 { path: 'summary', name: 'InventorySummary', @@ -79,17 +79,31 @@ const routes: Array = [ name: 'InventoryService', component: () => import('@/views/stock/inbound/service.vue'), meta: { title: '服务权益' } + }, + // ★ [新增] 库存盘点页面 (查库/消除) + { + path: 'stocktake', + name: 'InventoryStocktake', + component: () => import('@/views/stock/stocktake/index.vue'), + meta: { title: '库存盘点' } } ] }, - // 5. ★ [新增] 出库管理 + // 5. 出库管理 { - path: '/outbound', // 注意:这里使用了和你提供的文件路径一致的顶级路径 + path: '/outbound', component: Layout, - meta: { title: '出库管理', icon: 'Van' }, // 推荐使用 Van 图标 + meta: { title: '出库管理', icon: 'Van' }, redirect: '/outbound/index', children: [ + // ★ [新增] 出库选单打印页面 + { + path: 'selection', + name: 'OutboundSelection', + component: () => import('@/views/outbound/Selection.vue'), + meta: { title: '出库选单' } + }, { path: 'create', name: 'OutboundCreate', diff --git a/inventory-web/src/views/outbound/Selection.vue b/inventory-web/src/views/outbound/Selection.vue new file mode 100644 index 0000000..d1ddcf6 --- /dev/null +++ b/inventory-web/src/views/outbound/Selection.vue @@ -0,0 +1,302 @@ + + + + + \ No newline at end of file diff --git a/inventory-web/src/views/stock/stocktake/index.vue b/inventory-web/src/views/stock/stocktake/index.vue new file mode 100644 index 0000000..af780d4 --- /dev/null +++ b/inventory-web/src/views/stock/stocktake/index.vue @@ -0,0 +1,391 @@ + + + + + \ No newline at end of file