添加条形码内容

This commit is contained in:
dxc
2026-02-02 15:06:20 +08:00
parent a1133aac94
commit cf6a4a8957
11 changed files with 449 additions and 34 deletions

View File

@ -20,7 +20,9 @@ def create_app():
# 2. 注册蓝图 (Blueprints)
# =========================================================
# 注册入库聚合模块 (Inbound)
# -----------------------------------------------------
# 2.1 注册入库聚合模块 (Inbound)
# -----------------------------------------------------
try:
# 指向聚合文件: app/api/v1/inbound/__init__.py
# 该文件里应该包含了 buy, semi, base, product 的聚合逻辑
@ -29,9 +31,6 @@ def create_app():
# 注册父蓝图,路由前缀为 /api/v1/inbound
# 最终路由效果:
# /api/v1/inbound + /buy/list -> /api/v1/inbound/buy/list
# /api/v1/inbound + /semi/list -> /api/v1/inbound/semi/list
# /api/v1/inbound + /product/list -> /api/v1/inbound/product/list
# /api/v1/inbound + /base/search -> /api/v1/inbound/base/search
app.register_blueprint(inbound_bp, url_prefix='/api/v1/inbound')
print("✅ Inbound (Buy, Semi, Product, Base) 模块注册成功")
@ -39,6 +38,22 @@ def create_app():
except ImportError as e:
print(f"❌ 错误: Inbound 模块导入失败: {e}")
# -----------------------------------------------------
# 2.2 注册通用打印模块 (Common Print) - [新增]
# -----------------------------------------------------
try:
from app.api.v1.common.print import print_bp
# 注册打印蓝图
# 前端请求地址: /common/print/preview
# 配合 baseURL=/api/v1最终对应后端: /api/v1/common/print/preview
app.register_blueprint(print_bp, url_prefix='/api/v1/common/print')
print("✅ Print (Label Printing) 模块注册成功")
except ImportError as e:
print(f"❌ 错误: Print 模块导入失败: {e}")
# =========================================================
# 3. 预加载数据模型 (解决 relationship 找不到模型的问题)
# =========================================================
@ -50,7 +65,7 @@ def create_app():
from app.models.inbound.buy import StockBuy
# 3. 半成品入库
from app.models.inbound.semi import StockSemi
# 4. 成品入库 (新增)
# 4. 成品入库
from app.models.inbound.product import StockProduct
# 开发环境如果需要自动建表,可以取消注释

View File

@ -0,0 +1,27 @@
# app/api/v1/common/print.py
from flask import Blueprint, request, jsonify
from app.services.print.label_service import LabelPrintService
from app.models.inbound.buy import StockBuy
# 引入其他模型 StockSemi, StockProduct
import traceback
print_bp = Blueprint('print', __name__)
@print_bp.route('/preview', methods=['POST'])
def preview_label():
try:
data = request.get_json()
# 如果只传了ID和类型可以在这里查库补全数据也可以直接前端传全量数据
img_base64 = LabelPrintService.generate_preview_image(data)
return jsonify({"code": 200, "msg": "success", "data": img_base64})
except Exception as e:
return jsonify({"code": 500, "msg": str(e)}), 500
@print_bp.route('/execute', methods=['POST'])
def execute_print():
try:
data = request.get_json()
LabelPrintService.send_to_printer(data)
return jsonify({"code": 200, "msg": "指令已发送至打印机"})
except Exception as e:
return jsonify({"code": 500, "msg": str(e)}), 500

View File

@ -1,11 +1,13 @@
# app/api/v1/inbound/buy.py
from flask import Blueprint, request, jsonify
from app.services.inbound.buy_service import BuyInboundService
import traceback
inbound_buy_bp = Blueprint('inbound_buy', __name__)
# ------------------------------------------------------------------
# 0. 基础物料搜索 (新增)
# 0. 基础物料搜索
# ------------------------------------------------------------------
@inbound_buy_bp.route('/search-base', methods=['GET'])
def search_base():
@ -25,6 +27,7 @@ def search_base():
traceback.print_exc()
return jsonify({"code": 500, "msg": str(e)}), 500
# ------------------------------------------------------------------
# 1. 获取列表
# ------------------------------------------------------------------
@ -38,8 +41,9 @@ def get_list():
except Exception as e:
return jsonify({"code": 500, "msg": str(e)}), 500
# ------------------------------------------------------------------
# 2. 新增入库
# 2. 新增入库 (修改:返回创建的对象数据)
# ------------------------------------------------------------------
@inbound_buy_bp.route('/submit', methods=['POST'])
def submit():
@ -47,12 +51,21 @@ def submit():
data = request.get_json()
if not data:
return jsonify({"code": 400, "msg": "No data"}), 400
BuyInboundService.handle_inbound(data)
return jsonify({"code": 200, "msg": "入库成功"})
# 调用 Service 处理入库,获取新创建的对象
new_stock = BuyInboundService.handle_inbound(data)
# 返回成功信息以及新创建的数据包含生成的ID和SKU供前端打印使用
return jsonify({
"code": 200,
"msg": "入库成功",
"data": new_stock.to_dict()
})
except Exception as e:
traceback.print_exc()
return jsonify({"code": 500, "msg": str(e)}), 500
# ------------------------------------------------------------------
# 3. 更新入库
# ------------------------------------------------------------------
@ -65,6 +78,7 @@ def update_buy(id):
except Exception as e:
return jsonify({"code": 500, "msg": str(e)}), 500
# ------------------------------------------------------------------
# 4. 删除
# ------------------------------------------------------------------

View File

@ -42,7 +42,8 @@ class StockBuy(db.Model):
detail_link = db.Column(db.Text)
arrival_photo = db.Column(db.Text)
# 注意SQL 中没有 remark 字段,这里已移除
# [新增] 全局打印流水号 (用于跨表连续编号,对应 Sequence: global_print_seq)
global_print_id = db.Column(db.Integer)
# 关系定义
material = db.relationship('MaterialBase', back_populates='stock_buys')
@ -83,5 +84,9 @@ class StockBuy(db.Model):
'purchaser_email': self.buyer_email,
'source_link': self.original_link,
'detail_link': self.detail_link,
'arrival_photo': self.arrival_photo
'arrival_photo': self.arrival_photo,
# [新增] 返回全局打印ID及其格式化字符串
'global_print_id': self.global_print_id,
'global_print_id_str': f"{self.global_print_id:08d}" if self.global_print_id else ""
}

View File

@ -1,11 +1,9 @@
# app/services/inbound/buy_service.py
from app.extensions import db
from app.models.inbound.buy import StockBuy
# ==============================================================================
# ✅ 修复点:基础物料模型实际位于 app/models/base.py
# ==============================================================================
from app.models.base import MaterialBase
from datetime import datetime
from sqlalchemy import or_, func
from sqlalchemy import or_, func, text
import traceback
@ -71,13 +69,36 @@ class BuyInboundService:
in_qty = float(data.get('in_quantity') or 0)
u_price = float(data.get('unit_price') or 0)
# ------------------------------------------------------------------
# 1. 获取全局打印流水号 (跨表唯一)
# ------------------------------------------------------------------
seq_sql = text("SELECT nextval('global_print_seq')")
result = db.session.execute(seq_sql)
next_global_id = result.scalar()
# ------------------------------------------------------------------
# 2. 自动生成 SKU (格式: 00000001)
# ------------------------------------------------------------------
generated_sku = f"{next_global_id:08d}"
# ------------------------------------------------------------------
# 3. 条码逻辑处理 (核心修改)
# 如果前端没传条码(barcode),则默认使用系统生成的 SKU 作为条码
# 这样保证了条码生成依据是 "自动填写的 SKU"
# ------------------------------------------------------------------
final_barcode = data.get('barcode')
if not final_barcode:
final_barcode = generated_sku
new_stock = StockBuy(
base_id=material.id,
sku=data.get('sku'),
global_print_id=next_global_id,
sku=generated_sku, # 自动生成的SKU
barcode=final_barcode, # 如果未输入则存入SKU值
in_date=in_date_val,
serial_number=data.get('serial_number'),
batch_number=data.get('batch_number'),
barcode=data.get('barcode'),
status='在库',
in_quantity=in_qty,
stock_quantity=in_qty,
@ -98,6 +119,8 @@ class BuyInboundService:
db.session.add(new_stock)
db.session.commit()
# 返回创建的对象实例
return new_stock
except Exception as e:
@ -264,7 +287,10 @@ class BuyInboundService:
'purchaser_email': item.buyer_email,
'source_link': item.original_link,
'detail_link': item.detail_link,
'arrival_photo': item.arrival_photo
'arrival_photo': item.arrival_photo,
'global_print_id': item.global_print_id,
'global_print_id_str': f"{item.global_print_id:08d}" if item.global_print_id else ""
}
items.append(d)

View File

@ -0,0 +1,198 @@
import socket
import base64
import os
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
# 引入条形码生成库
try:
import barcode
from barcode.writer import ImageWriter
except ImportError:
print("❌ 警告: 未安装 python-barcode 库,无法生成真实条形码。请执行: pip install python-barcode")
class LabelPrintService:
PRINTER_IP = "192.168.9.205"
PRINTER_PORT = 9100
# ================= 1. 尺寸与分辨率配置 (300 DPI) =================
DOTS_PER_MM = 12 # 300 DPI
LABEL_WIDTH_MM = 40
LABEL_HEIGHT_MM = 30
# 画布像素: 40mm -> 480px, 30mm -> 360px
LABEL_WIDTH = int(LABEL_WIDTH_MM * DOTS_PER_MM)
LABEL_HEIGHT = int(LABEL_HEIGHT_MM * DOTS_PER_MM)
# 顶部留白: 10mm (120px) - 内容从这里开始
TOP_MARGIN_MM = 5
TOP_MARGIN_PX = int(TOP_MARGIN_MM * DOTS_PER_MM)
@staticmethod
def _get_font(size):
"""获取字体"""
font_names = ["simhei.ttf", "msyh.ttf", "SimHei.ttf", "arial.ttf"]
base_dirs = [os.getcwd(), os.path.dirname(__file__)]
for d in base_dirs:
for name in font_names:
path = os.path.join(d, name)
if os.path.exists(path):
return ImageFont.truetype(path, size)
return ImageFont.load_default()
@staticmethod
def _generate_barcode_image(content, width_px, height_px):
"""
生成真实的条形码图片
"""
try:
# 使用 Code128 编码(通用性最强)
code128 = barcode.get('code128', content, writer=ImageWriter())
# 渲染到内存
buffer = BytesIO()
# write_text=False: 不在条码下方生成自带的数字(我们自己画)
code128.write(buffer, options={"write_text": False, "module_height": 10.0, "quiet_zone": 1.0})
# 打开生成的条码图
buffer.seek(0)
bc_img = Image.open(buffer)
# 强制调整大小以适应我们的布局
return bc_img.resize((width_px, height_px), Image.Resampling.LANCZOS)
except Exception as e:
print(f"条形码生成失败: {e}")
# 生成失败时返回一个黑色方块占位
return Image.new('RGB', (width_px, height_px), color='black')
@staticmethod
def _create_image_object(data):
"""
[绘图层] 生成标签图片
"""
# 1. 创建画布 (白底)
img = Image.new('RGB', (LabelPrintService.LABEL_WIDTH, LabelPrintService.LABEL_HEIGHT), color='white')
d = ImageDraw.Draw(img)
# 2. 统一字体配置 (字号 30)
UNIFIED_FONT_SIZE = 30
font = LabelPrintService._get_font(UNIFIED_FONT_SIZE)
# 3. 提取条码内容 (优先使用 SKU)
# 逻辑:使用自动生成的 SKU (如 00000001) 作为条码内容
sku_code = data.get('sku')
if not sku_code:
# 兜底逻辑:如果没有 SKU尝试用 serial_number 或 global_id
sku_code = data.get('serial_number') or str(data.get('global_print_id', '00000000')).zfill(8)
# ==================== 绘制布局 ====================
# X 轴偏移 (5mm)
GLOBAL_OFFSET_X = int(5 * LabelPrintService.DOTS_PER_MM)
# Y 轴起始: 10mm 处
CURRENT_Y = LabelPrintService.TOP_MARGIN_PX
# --- A. 绘制真实条形码 ---
bc_w = int(28 * LabelPrintService.DOTS_PER_MM) # 条码宽约 28mm
bc_h = int(6 * LabelPrintService.DOTS_PER_MM) # 条码高约 6mm
# 生成并粘贴条码
bc_img = LabelPrintService._generate_barcode_image(sku_code, bc_w, bc_h)
# 粘贴位置: (X, Y)
img.paste(bc_img, (GLOBAL_OFFSET_X - 5, CURRENT_Y))
# 更新 Y 坐标 (条码高度 + 间距)
CURRENT_Y += bc_h + 5
# --- B. 绘制文字 (统一加粗) ---
# 准备数据
name = str(data.get('material_name', '') or '-')
if len(name) > 8: name = name[:7] + ".."
spec = str(data.get('spec_model', '') or '-')
if len(spec) > 11: spec = spec[:10] + ".."
loc = str(data.get('warehouse_loc', '') or '-')
cat = str(data.get('category', '') or '')
typ = str(data.get('material_type', '') or '')
attr = f"{cat}/{typ}"
if len(attr) > 11: attr = attr[:10] + ".."
# 底部显示的文字SKU 编号
sku_str = f"NO.{sku_code}"
lines = [
f"名: {name}",
f"规: {spec}",
f"库: {loc}",
f"属: {attr}",
f"{sku_str}"
]
line_height = 36 # 行高 (字号30 + 6间距)
for line in lines:
# 边界检查
if CURRENT_Y + UNIFIED_FONT_SIZE > LabelPrintService.LABEL_HEIGHT:
break
# 绘制文字 (stroke_width=1 实现加粗效果)
d.text((GLOBAL_OFFSET_X, CURRENT_Y), line, font=font, fill='black', stroke_width=1, stroke_fill='black')
CURRENT_Y += line_height
return img
@staticmethod
def generate_preview_image(data):
img = LabelPrintService._create_image_object(data)
output_buffer = BytesIO()
img.save(output_buffer, format='JPEG')
base64_str = base64.b64encode(output_buffer.getvalue()).decode('utf-8')
return f"data:image/jpeg;base64,{base64_str}"
@staticmethod
def send_to_printer(data):
ip = LabelPrintService.PRINTER_IP
port = LabelPrintService.PRINTER_PORT
try:
# 1. 获取图像 (RGB)
img_rgb = LabelPrintService._create_image_object(data)
# 2. 图像转二值化 (白=1, 黑=0 的逻辑,不反转)
img_gray = img_rgb.convert('L')
img_bw = img_gray.convert('1', dither=Image.Dither.NONE)
# 获取位图数据
bitmap_data = img_bw.tobytes()
width_bytes = (img_bw.width + 7) // 8
height_dots = img_bw.height
# 3. TSPL 指令
header = (
f"SIZE {LabelPrintService.LABEL_WIDTH_MM} mm, {LabelPrintService.LABEL_HEIGHT_MM} mm\r\n"
"GAP 2 mm, 0 mm\r\n"
"CLS\r\n"
"DIRECTION 1\r\n"
"REFERENCE 0, 0\r\n"
).encode('gbk')
bitmap_cmd = f"BITMAP 0,0,{width_bytes},{height_dots},0,".encode('gbk')
footer = b"\r\nPRINT 1,1\r\n"
# 4. 发送
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
s.connect((ip, port))
s.sendall(header + bitmap_cmd + bitmap_data + footer)
s.close()
return True
except Exception as e:
print(f"❌ 打印异常: {e}")
raise Exception(f"打印机连接失败: {str(e)}")