盘库操作初设计
This commit is contained in:
@ -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')
|
||||
# ★ [新增] 挂载 stock 模块,路径前缀为 /stock
|
||||
# 最终访问路径例:/api/v1/inbound/stock/all
|
||||
inbound_bp.register_blueprint(stock_bp, url_prefix='/stock')
|
||||
@ -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__)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
|
||||
@ -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__)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@ -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__)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@ -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__)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
98
inventory-backend/app/api/v1/inbound/stock.py
Normal file
98
inventory-backend/app/api/v1/inbound/stock.py
Normal file
@ -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
|
||||
@ -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
|
||||
# 4. 出库记录 (如果有,BuyService 用到了 TransOutbound)
|
||||
try:
|
||||
from app.models.outbound import TransOutbound
|
||||
except ImportError:
|
||||
pass
|
||||
@ -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):
|
||||
|
||||
@ -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),
|
||||
|
||||
114
inventory-backend/app/services/print/network_print_service.py
Normal file
114
inventory-backend/app/services/print/network_print_service.py
Normal file
@ -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)
|
||||
Reference in New Issue
Block a user