Quellcode durchsuchen

打印功能和出库单管理页面

day01-init vor 5 Monaten
Ursprung
Commit
3aec7bbc4c

+ 15 - 0
src/main/java/com/jeesite/modules/erp/dao/MesProductOutDao.java

@@ -0,0 +1,15 @@
+package com.jeesite.modules.erp.dao;
+
+import com.jeesite.common.dao.CrudDao;
+import com.jeesite.common.mybatis.annotation.MyBatisDao;
+import com.jeesite.modules.erp.entity.MesProductOut;
+
+/**
+ * 产品出库明细DAO接口
+ * @author mes
+ * @version 2026-05-15
+ */
+@MyBatisDao
+public interface MesProductOutDao extends CrudDao<MesProductOut> {
+
+}

+ 101 - 0
src/main/java/com/jeesite/modules/erp/entity/MesProductOut.java

@@ -0,0 +1,101 @@
+package com.jeesite.modules.erp.entity;
+
+import java.util.Date;
+
+import javax.validation.constraints.Size;
+
+import com.jeesite.common.entity.DataEntity;
+import com.jeesite.common.mybatis.annotation.Column;
+import com.jeesite.common.mybatis.annotation.Table;
+import com.jeesite.common.mybatis.mapper.query.QueryType;
+
+/**
+ * 产品出库明细Entity
+ * @author mes
+ * @version 2026-05-15
+ */
+@Table(name = "mes_product_out", alias = "a", label = "产品出库明细信息", columns = {
+		@Column(name = "id", attrName = "id", label = "编号", isPK = true),
+		@Column(name = "out_order_no", attrName = "outOrderNo", label = "出库单", queryType = QueryType.LIKE),
+		@Column(name = "sn", attrName = "sn", label = "工件码", queryType = QueryType.LIKE),
+		@Column(name = "status", attrName = "status", label = "状态", isUpdate = false, isQuery = true),
+		@Column(name = "remark", attrName = "remark", label = "备注", queryType = QueryType.LIKE),
+		@Column(name = "create_by", attrName = "createBy", label = "创建者", isUpdate = false, isQuery = false),
+		@Column(name = "create_date", attrName = "createDate", label = "创建时间", isUpdate = false, isUpdateForce = true),
+		@Column(name = "update_by", attrName = "updateBy", label = "更新者", isQuery = false),
+		@Column(name = "update_date", attrName = "updateDate", label = "更新时间", isQuery = false, isUpdateForce = true),
+}, orderBy = "a.update_date DESC")
+public class MesProductOut extends DataEntity<MesProductOut> {
+
+	private static final long serialVersionUID = 1L;
+	private String outOrderNo;
+	private String sn;
+	private String remark;
+
+	public MesProductOut() {
+		this(null);
+	}
+
+	public MesProductOut(String id) {
+		super(id);
+	}
+
+	@Size(min = 0, max = 100, message = "出库单长度不能超过 100 个字符")
+	public String getOutOrderNo() {
+		return outOrderNo;
+	}
+
+	public void setOutOrderNo(String outOrderNo) {
+		this.outOrderNo = outOrderNo;
+	}
+
+	@Size(min = 0, max = 255, message = "工件码长度不能超过 255 个字符")
+	public String getSn() {
+		return sn;
+	}
+
+	public void setSn(String sn) {
+		this.sn = sn;
+	}
+
+	@Size(min = 0, max = 500, message = "备注长度不能超过 500 个字符")
+	public String getRemark() {
+		return remark;
+	}
+
+	public void setRemark(String remark) {
+		this.remark = remark;
+	}
+
+	public Date getCreateDate_gte() {
+		return sqlMap.getWhere().getValue("create_date", QueryType.GTE);
+	}
+
+	public void setCreateDate_gte(Date createDate) {
+		sqlMap.getWhere().and("create_date", QueryType.GTE, createDate);
+	}
+
+	public Date getCreateDate_lte() {
+		return sqlMap.getWhere().getValue("create_date", QueryType.LTE);
+	}
+
+	public void setCreateDate_lte(Date createDate) {
+		sqlMap.getWhere().and("create_date", QueryType.LTE, createDate);
+	}
+
+	public Date getUpdateDate_gte() {
+		return sqlMap.getWhere().getValue("update_date", QueryType.GTE);
+	}
+
+	public void setUpdateDate_gte(Date updateDate) {
+		sqlMap.getWhere().and("update_date", QueryType.GTE, updateDate);
+	}
+
+	public Date getUpdateDate_lte() {
+		return sqlMap.getWhere().getValue("update_date", QueryType.LTE);
+	}
+
+	public void setUpdateDate_lte(Date updateDate) {
+		sqlMap.getWhere().and("update_date", QueryType.LTE, updateDate);
+	}
+}

+ 53 - 0
src/main/java/com/jeesite/modules/erp/service/MesProductOutService.java

@@ -0,0 +1,53 @@
+package com.jeesite.modules.erp.service;
+
+import java.util.List;
+
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.service.CrudService;
+import com.jeesite.modules.erp.dao.MesProductOutDao;
+import com.jeesite.modules.erp.entity.MesProductOut;
+
+/**
+ * 产品出库明细Service
+ * @author mes
+ * @version 2026-05-15
+ */
+@Service
+public class MesProductOutService extends CrudService<MesProductOutDao, MesProductOut> {
+
+	@Override
+	public MesProductOut get(MesProductOut mesProductOut) {
+		return super.get(mesProductOut);
+	}
+
+	@Override
+	public Page<MesProductOut> findPage(MesProductOut mesProductOut) {
+		return super.findPage(mesProductOut);
+	}
+
+	@Override
+	public List<MesProductOut> findList(MesProductOut mesProductOut) {
+		return super.findList(mesProductOut);
+	}
+
+	@Override
+	@Transactional
+	public void save(MesProductOut mesProductOut) {
+		super.save(mesProductOut);
+	}
+
+	@Override
+	@Transactional
+	public void updateStatus(MesProductOut mesProductOut) {
+		super.updateStatus(mesProductOut);
+	}
+
+	@Override
+	@Transactional
+	public void delete(MesProductOut mesProductOut) {
+		super.delete(mesProductOut);
+	}
+}

+ 47 - 0
src/main/java/com/jeesite/modules/erp/web/MesMaterialOrderController.java

@@ -6,6 +6,7 @@ import javax.servlet.http.HttpServletResponse;
 
 import com.jeesite.modules.erp.entity.MesMaterialErp;
 import com.jeesite.modules.erp.service.MesMaterialErpService;
+import com.jeesite.modules.mes.util.PrintUtil;
 import org.apache.shiro.authz.annotation.RequiresPermissions;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
@@ -15,12 +16,15 @@ import org.springframework.web.bind.annotation.ModelAttribute;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.util.StringUtils;
 
 import com.jeesite.common.config.Global;
 import com.jeesite.common.entity.Page;
 import com.jeesite.common.web.BaseController;
 import com.jeesite.modules.erp.entity.MesMaterialOrder;
 import com.jeesite.modules.erp.service.MesMaterialOrderService;
+import java.util.ArrayList;
+import java.util.Collections;
 
 /**
  * 订单领料表Controller
@@ -107,4 +111,47 @@ public class MesMaterialOrderController extends BaseController {
 		return renderResult(Global.TRUE, text("删除订单领料表成功!"));
 	}
 
+	/**
+	 * 批量打印领料单
+	 */
+	@RequiresPermissions("erp:mesMaterialOrder:view")
+	@PostMapping(value = "print")
+	@ResponseBody
+	public String print(String ids) {
+		if (!StringUtils.hasText(ids)) {
+			return renderResult(Global.FALSE, text("请先选择要打印的数据"));
+		}
+		String[] idArr = ids.split(",");
+		int printCount = 0;
+		List<PrintUtil.MaterialOrderPrintTask> tasks = new ArrayList<>();
+		for (String id : idArr) {
+			if (!StringUtils.hasText(id)) {
+				continue;
+			}
+			MesMaterialOrder order = mesMaterialOrderService.get(id, false);
+			if (order == null || !StringUtils.hasText(order.getRequisitionNo())) {
+				continue;
+			}
+			MesMaterialErp query = new MesMaterialErp();
+			query.setRequisitionNo(order.getRequisitionNo());
+			List<MesMaterialErp> itemList = mesMaterialErpService.findList(query);
+			if (itemList == null || itemList.isEmpty()) {
+				continue;
+			}
+			// 一物料一张纸:每张含生产订单编号、领料单编号及该物料一行明细
+			for (MesMaterialErp item : itemList) {
+				if (item == null) {
+					continue;
+				}
+				tasks.add(new PrintUtil.MaterialOrderPrintTask(order, Collections.singletonList(item)));
+				printCount++;
+			}
+		}
+		if (printCount == 0) {
+			return renderResult(Global.FALSE, text("未找到可打印数据,请检查所选记录"));
+		}
+		PrintUtil.printMaterialOrders(tasks);
+		return renderResult(Global.TRUE, text("已汇总并提交打印任务,共 {0} 张", String.valueOf(printCount)));
+	}
+
 }

+ 94 - 0
src/main/java/com/jeesite/modules/erp/web/MesProductOutController.java

@@ -0,0 +1,94 @@
+package com.jeesite.modules.erp.web;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import com.jeesite.common.config.Global;
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.erp.entity.MesProductOut;
+import com.jeesite.modules.erp.service.MesProductOutService;
+
+/**
+ * 产品出库明细 Controller(ERP 菜单入口)
+ * @author mes
+ * @version 2026-05-15
+ */
+@Controller
+@RequestMapping(value = "${adminPath}/erp/mesProductOut")
+public class MesProductOutController extends BaseController {
+
+	@Autowired
+	private MesProductOutService mesProductOutService;
+
+	@ModelAttribute
+	public MesProductOut get(String id, boolean isNewRecord) {
+		return mesProductOutService.get(id, isNewRecord);
+	}
+
+	@RequiresPermissions("erp:mesProductOut:view")
+	@RequestMapping(value = { "list", "" })
+	public String list(MesProductOut mesProductOut, Model model) {
+		model.addAttribute("mesProductOut", mesProductOut);
+		return "modules/erp/mesProductOutList";
+	}
+
+	@RequiresPermissions("erp:mesProductOut:view")
+	@RequestMapping(value = "listData")
+	@ResponseBody
+	public Page<MesProductOut> listData(MesProductOut mesProductOut, HttpServletRequest request, HttpServletResponse response) {
+		mesProductOut.setPage(new Page<>(request, response));
+		return mesProductOutService.findPage(mesProductOut);
+	}
+
+	@RequiresPermissions("erp:mesProductOut:view")
+	@RequestMapping(value = "form")
+	public String form(MesProductOut mesProductOut, Model model) {
+		model.addAttribute("mesProductOut", mesProductOut);
+		return "modules/erp/mesProductOutForm";
+	}
+
+	@RequiresPermissions("erp:mesProductOut:edit")
+	@PostMapping(value = "save")
+	@ResponseBody
+	public String save(@Validated MesProductOut mesProductOut) {
+		mesProductOutService.save(mesProductOut);
+		return renderResult(Global.TRUE, text("保存产品出库明细成功!"));
+	}
+
+	@RequiresPermissions("erp:mesProductOut:edit")
+	@RequestMapping(value = "disable")
+	@ResponseBody
+	public String disable(MesProductOut mesProductOut) {
+		mesProductOut.setStatus(MesProductOut.STATUS_DISABLE);
+		mesProductOutService.updateStatus(mesProductOut);
+		return renderResult(Global.TRUE, text("停用成功"));
+	}
+
+	@RequiresPermissions("erp:mesProductOut:edit")
+	@RequestMapping(value = "enable")
+	@ResponseBody
+	public String enable(MesProductOut mesProductOut) {
+		mesProductOut.setStatus(MesProductOut.STATUS_NORMAL);
+		mesProductOutService.updateStatus(mesProductOut);
+		return renderResult(Global.TRUE, text("启用成功"));
+	}
+
+	@RequiresPermissions("erp:mesProductOut:edit")
+	@RequestMapping(value = "delete")
+	@ResponseBody
+	public String delete(MesProductOut mesProductOut) {
+		mesProductOutService.delete(mesProductOut);
+		return renderResult(Global.TRUE, text("删除产品出库明细成功!"));
+	}
+}

+ 445 - 0
src/main/java/com/jeesite/modules/mes/util/PrintUtil.java

@@ -0,0 +1,445 @@
+package com.jeesite.modules.mes.util;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.EncodeHintType;
+import com.google.zxing.MultiFormatWriter;
+import com.google.zxing.WriterException;
+import com.google.zxing.client.j2se.MatrixToImageWriter;
+import com.google.zxing.common.BitMatrix;
+import com.jeesite.modules.erp.entity.MesMaterialErp;
+import com.jeesite.modules.erp.entity.MesMaterialOrder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.print.PrintService;
+import javax.print.PrintServiceLookup;
+import javax.print.attribute.HashPrintRequestAttributeSet;
+import javax.print.attribute.PrintRequestAttributeSet;
+import javax.print.attribute.standard.MediaPrintableArea;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.awt.print.PageFormat;
+import java.awt.print.Printable;
+import java.awt.print.PrinterJob;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class PrintUtil {
+    public static final Logger log =  LoggerFactory.getLogger(PrintUtil.class);
+    private static final String DEFAULT_PRINTER_NAME = "QR-888";
+    /** 物理纸 100×80mm 时,单条物料仅在上方 100×40mm 可视区域内排版,下半张留白 */
+    private static final double SINGLE_MATERIAL_ZONE_HEIGHT_MM = 40;
+
+    public static void printLabel(String barCode){
+        log.info("sn:"+barCode);
+        // 获取所有打印服务(打印机)
+        PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
+        if (services.length == 0) {
+            log.info("No printers found.");
+            return;
+        }
+
+        String printerName = "QR-888";
+        PrintService selectedService = Arrays.stream(services).filter(s -> s.getName().equals(printerName)).findFirst().orElse(null);
+        if (selectedService == null) { // 没有找到指定名称的打印机,则退出程序或使用默认打印机等处理方式。
+            log.info(printerName+":Printer not found.");
+            return;
+        }
+
+        try{
+            PrinterJob job = PrinterJob.getPrinterJob(); // 使用默认的PrinterJob对象,然后设置服务。
+            job.setPrintService(selectedService); // 设置选定的打印机服务。
+            job.setPrintable(new Printable() { // 设置打印内容。
+                public int print(Graphics g, PageFormat pf, int page) {
+                    if (page > 0) { // 只打印一页。
+                        return Printable.NO_SUCH_PAGE;
+                    } else { // 绘制内容。
+                        int fontSize = Integer.valueOf("14");
+                        int labelx = Integer.valueOf("45");
+                        int labely = Integer.valueOf("35");
+                        int labelLineSpace = Integer.valueOf("10");
+                        g.setFont(new Font("Serif", Font.PLAIN, fontSize)); // 设置字体和大小
+                        String line1 = barCode;
+                        g.drawString(line1, labelx, labely); // 打印第一行文本
+//                        g.drawString(sn, labelx, labely + labelLineSpace); // 打印第二行文本
+//                        String line3 = param1 + "    " + param2;
+//                        g.drawString(line3, labelx, labely + 2*labelLineSpace);
+                        try{
+                            g.drawImage(PrintUtil.generateQRCodeImage(line1,120,120),labelx+35,labely + labelLineSpace,35,35,null);
+                        }catch (Exception e){
+                            e.printStackTrace();
+                        }
+
+
+                        return Printable.PAGE_EXISTS;
+                    }
+
+                }
+            });
+
+
+            int x = 100;
+            int y = 30;
+            // 设置打印参数
+            PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet();
+
+            // 设置纸张大小 , 实际测试没有这个功能, 还是要标签打印机设置
+            MediaPrintableArea area = new MediaPrintableArea(0, 0, x, y, MediaPrintableArea.MM);
+            aset.add(area);
+
+            job.print(aset);
+            System.out.println("Printer finish.");
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+    }
+
+    public static BufferedImage generateQRCodeImage(String text, int width, int height) throws WriterException, IOException {
+        Map<EncodeHintType, Object> hints = new HashMap<>();
+        hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
+        BitMatrix bitMatrix = new MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints);
+        return MatrixToImageWriter.toBufferedImage(bitMatrix);
+    }
+
+    /**
+     * 打印领料单模板(订单号+领料单号+物料明细)
+     */
+    public static void printMaterialOrder(MesMaterialOrder order, List<MesMaterialErp> items) {
+        List<MaterialOrderPrintTask> tasks = new ArrayList<>();
+        tasks.add(new MaterialOrderPrintTask(order, items));
+        printMaterialOrders(tasks);
+    }
+
+    /**
+     * 批量打印:一次提交到打印机(多页打印)
+     */
+    public static void printMaterialOrders(List<MaterialOrderPrintTask> tasks) {
+        if (tasks == null || tasks.isEmpty()) {
+            return;
+        }
+        List<MaterialOrderPrintTask> validTasks = new ArrayList<>();
+        for (MaterialOrderPrintTask task : tasks) {
+            if (task != null && task.getOrder() != null) {
+                validTasks.add(task);
+            }
+        }
+        if (validTasks.isEmpty()) {
+            return;
+        }
+
+        PrintService selectedService = findPrinter(DEFAULT_PRINTER_NAME);
+        if (selectedService == null) {
+            return;
+        }
+        try {
+            PrinterJob job = PrinterJob.getPrinterJob();
+            job.setPrintService(selectedService);
+            // 纸张: 100mm(宽) x 80mm(高)
+            final double paperWidthPt = mmToPoint(100);
+            final double paperHeightPt = mmToPoint(80);
+            final double marginPt = mmToPoint(2); // 2mm边距
+            PageFormat pageFormat = job.defaultPage();
+            java.awt.print.Paper paper = new java.awt.print.Paper();
+            paper.setSize(paperWidthPt, paperHeightPt);
+            paper.setImageableArea(marginPt, marginPt, paperWidthPt - marginPt * 2, paperHeightPt - marginPt * 2);
+            pageFormat.setPaper(paper);
+
+            job.setPrintable(new Printable() {
+                @Override
+                public int print(Graphics g, PageFormat pf, int page) {
+                    if (page < 0 || page >= validTasks.size()) {
+                        return Printable.NO_SUCH_PAGE;
+                    }
+                    MaterialOrderPrintTask task = validTasks.get(page);
+                    renderMaterialOrderPage((Graphics2D) g, pf, task.getOrder(), task.getItems());
+                    return Printable.PAGE_EXISTS;
+                }
+            }, pageFormat);
+
+            PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet();
+            aset.add(new MediaPrintableArea(0, 0, 100, 80, MediaPrintableArea.MM));
+            job.print(aset);
+            log.info("Batch print material order finish, count={}", validTasks.size());
+        } catch (Exception e) {
+            log.error("Batch print material order failed", e);
+        }
+    }
+
+    private static void renderMaterialOrderPage(Graphics2D g2, PageFormat pf, MesMaterialOrder order, List<MesMaterialErp> printItems) {
+        int itemCount = printItems == null ? 0 : printItems.size();
+        int numRows = Math.max(1, itemCount);
+        if (numRows <= 1) {
+            renderMaterialOrderPageTopZone40mm(g2, pf, order, printItems);
+        } else {
+            renderMaterialOrderPageMultiRow(g2, pf, order, printItems, itemCount, numRows);
+        }
+    }
+
+    /**
+     * 单物料:在可打印区顶部 40mm 高度内排版(等效 100×40mm 标签区),物理纸仍为 100×80mm 时下半部分留白。
+     */
+    private static void renderMaterialOrderPageTopZone40mm(Graphics2D g2, PageFormat pf, MesMaterialOrder order, List<MesMaterialErp> printItems) {
+        final String orderId = safe(order.getOrderId());
+        final String requisitionNo = safe(order.getRequisitionNo());
+        g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+        g2.setFont(new Font("SimSun", Font.PLAIN, 10));
+
+        int left = (int) pf.getImageableX();
+        int top = (int) pf.getImageableY();
+        int contentW = (int) pf.getImageableWidth();
+        int imageableBottom = (int) (pf.getImageableY() + pf.getImageableHeight());
+        int zoneLimitY = Math.min(top + (int) mmToPoint(SINGLE_MATERIAL_ZONE_HEIGHT_MM), imageableBottom);
+
+        // 左右略缩进,整体视觉上更靠纸面中间
+        int hPad = (int) mmToPoint(4);
+        int drawLeft = left + hPad;
+        int drawW = Math.max(1, contentW - 2 * hPad);
+        // 上缘略下移,在 40mm 区内略偏中
+        int contentTop = top + (int) mmToPoint(2);
+
+        int qrSize = (int) mmToPoint(18);
+        int qrGap = (int) mmToPoint(2);
+        // 生产订单、领料单两格同宽(左侧至二维码前整块)
+        int labelBoxW = Math.max(1, drawW - qrSize - qrGap);
+
+        int boxH = (int) mmToPoint(8.5);
+        int gapY = (int) mmToPoint(2.5);
+        int innerPadX = (int) mmToPoint(2);
+        int leftTextPadX = innerPadX + (int) mmToPoint(0.8);
+        int textBaseInBox = (int) mmToPoint(6.2);
+
+        g2.drawRect(drawLeft, contentTop, labelBoxW, boxH);
+        drawStringFitCell(g2, "生产订单编号: " + orderId, drawLeft, labelBoxW, leftTextPadX, innerPadX,
+                contentTop + textBaseInBox, 10, 6);
+
+        g2.drawRect(drawLeft, contentTop + boxH + gapY, labelBoxW, boxH);
+        drawStringFitCell(g2, "领料单编号: " + requisitionNo, drawLeft, labelBoxW, leftTextPadX, innerPadX,
+                contentTop + boxH + gapY + textBaseInBox, 10, 6);
+
+        int qrX = drawLeft + drawW - qrSize;
+        int qrY = contentTop;
+        g2.drawRect(qrX, qrY, qrSize, qrSize);
+        if (!requisitionNo.isEmpty()) {
+            try {
+                int qrPad = (int) mmToPoint(1.2);
+                g2.drawImage(generateQRCodeImage(requisitionNo, 200, 200), qrX + qrPad, qrY + qrPad, qrSize - qrPad * 2, qrSize - qrPad * 2, null);
+            } catch (Exception ex) {
+                log.error("Generate QR code failed, requisitionNo={}", requisitionNo, ex);
+            }
+        }
+
+        int headBandBottom = contentTop + boxH + gapY + boxH;
+        int qrBottom = qrY + qrSize;
+        int tableY = Math.max(headBandBottom, qrBottom) + (int) mmToPoint(1.5);
+        int headerH = (int) mmToPoint(7);
+        int rowGap = (int) mmToPoint(1);
+        // 行高:表头+数据行底部不超出 40mm 区下沿
+        int maxRowH = zoneLimitY - tableY - headerH - rowGap;
+        if (maxRowH < 1) {
+            maxRowH = 1;
+        }
+        int rowH = Math.min((int) mmToPoint(9), maxRowH);
+
+        // c1 物料编号、c2 名称、c3 数量;右侧余宽为规格(收窄规格、加宽编号与名称)
+        int c1 = (int) mmToPoint(30);
+        int c2 = (int) mmToPoint(28);
+        int c3 = (int) mmToPoint(12);
+
+        g2.drawRect(drawLeft, tableY, drawW, headerH);
+        g2.drawLine(drawLeft + c1, tableY, drawLeft + c1, tableY + headerH);
+        g2.drawLine(drawLeft + c1 + c2, tableY, drawLeft + c1 + c2, tableY + headerH);
+        g2.drawLine(drawLeft + c1 + c2 + c3, tableY, drawLeft + c1 + c2 + c3, tableY + headerH);
+        int headerTextY = tableY + (int) mmToPoint(5.2);
+        g2.drawString("物料编号", drawLeft + leftTextPadX, headerTextY);
+        g2.drawString("名称", drawLeft + c1 + innerPadX, headerTextY);
+        g2.drawString("数量", drawLeft + c1 + c2 + innerPadX, headerTextY);
+        g2.drawString("规格", drawLeft + c1 + c2 + c3 + innerPadX, headerTextY);
+
+        int rowY = tableY + headerH + rowGap;
+        MesMaterialErp item = (printItems != null && !printItems.isEmpty()) ? printItems.get(0) : null;
+        g2.drawRect(drawLeft, rowY, drawW, rowH);
+        g2.drawLine(drawLeft + c1, rowY, drawLeft + c1, rowY + rowH);
+        g2.drawLine(drawLeft + c1 + c2, rowY, drawLeft + c1 + c2, rowY + rowH);
+        g2.drawLine(drawLeft + c1 + c2 + c3, rowY, drawLeft + c1 + c2 + c3, rowY + rowH);
+        if (item != null) {
+            int textBaseline = rowY + Math.max((int) mmToPoint(3), (rowH * 5) / 8);
+            drawStringFitCell(g2, item.getMaterialCode(), drawLeft, c1, leftTextPadX, innerPadX, textBaseline, 10, 5);
+            drawStringFitCell(g2, item.getMaterialName(), drawLeft + c1, c2, innerPadX, innerPadX, textBaseline, 10, 5);
+            g2.setFont(new Font("SimSun", Font.PLAIN, 10));
+            g2.drawString(clip(safe(item.getMaterialNumber()), 8), drawLeft + c1 + c2 + innerPadX, textBaseline);
+            g2.drawString(clip(safe(item.getMateriarSize()), 14), drawLeft + c1 + c2 + c3 + innerPadX, textBaseline);
+        }
+    }
+
+    /** 多条物料:沿用原全高布局,占满可打印区域 */
+    private static void renderMaterialOrderPageMultiRow(Graphics2D g2, PageFormat pf, MesMaterialOrder order, List<MesMaterialErp> printItems,
+                                                        int itemCount, int numRows) {
+        final String orderId = safe(order.getOrderId());
+        final String requisitionNo = safe(order.getRequisitionNo());
+        g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+        g2.setFont(new Font("SimSun", Font.PLAIN, 8));
+
+        int left = (int) pf.getImageableX();
+        int top = (int) pf.getImageableY();
+        int contentW = (int) pf.getImageableWidth();
+
+        int qrSize = (int) mmToPoint(26);
+        int qrGap = (int) mmToPoint(2);
+        int labelBoxW = Math.max(1, contentW - qrSize - qrGap);
+
+        int boxH = (int) mmToPoint(11);
+        int gapY = (int) mmToPoint(4);
+        int innerPadX = (int) mmToPoint(2);
+        int leftTextPadX = innerPadX + (int) mmToPoint(1);
+        int textBase = (int) mmToPoint(7.5);
+
+        g2.drawRect(left, top, labelBoxW, boxH);
+        drawStringFitCell(g2, "生产订单编号: " + orderId, left, labelBoxW, leftTextPadX, innerPadX, top + textBase, 8, 5);
+
+        g2.drawRect(left, top + boxH + gapY, labelBoxW, boxH);
+        drawStringFitCell(g2, "领料单编号: " + requisitionNo, left, labelBoxW, leftTextPadX, innerPadX, top + boxH + gapY + textBase, 8, 5);
+
+        int qrX = left + contentW - qrSize;
+        int qrY = top;
+        g2.drawRect(qrX, qrY, qrSize, qrSize);
+        if (!requisitionNo.isEmpty()) {
+            try {
+                int qrPad = (int) mmToPoint(1.8);
+                g2.drawImage(generateQRCodeImage(requisitionNo, 200, 200), qrX + qrPad, qrY + qrPad, qrSize - qrPad * 2, qrSize - qrPad * 2, null);
+            } catch (Exception ex) {
+                log.error("Generate QR code failed, requisitionNo={}", requisitionNo, ex);
+            }
+        }
+
+        int tableY = top + boxH * 2 + gapY + (int) mmToPoint(4);
+        int headerH = (int) mmToPoint(9);
+        int preferredRowH = (int) mmToPoint(14);
+        int c1 = (int) mmToPoint(30);
+        int c2 = (int) mmToPoint(28);
+        int c3 = (int) mmToPoint(12);
+
+        g2.drawRect(left, tableY, contentW, headerH);
+        g2.drawLine(left + c1, tableY, left + c1, tableY + headerH);
+        g2.drawLine(left + c1 + c2, tableY, left + c1 + c2, tableY + headerH);
+        g2.drawLine(left + c1 + c2 + c3, tableY, left + c1 + c2 + c3, tableY + headerH);
+        g2.drawString("物料编号", left + leftTextPadX, tableY + (int) mmToPoint(6));
+        g2.drawString("名称", left + c1 + innerPadX, tableY + (int) mmToPoint(6));
+        g2.drawString("数量", left + c1 + c2 + innerPadX, tableY + (int) mmToPoint(6));
+        g2.drawString("规格", left + c1 + c2 + c3 + innerPadX, tableY + (int) mmToPoint(6));
+
+        int rowY = tableY + headerH + (int) mmToPoint(3);
+        int imageableBottom = (int) (pf.getImageableY() + pf.getImageableHeight());
+        int bottomPad = (int) mmToPoint(1);
+        int availableForRows = Math.max((int) mmToPoint(5), imageableBottom - bottomPad - rowY);
+        int rowH = Math.min(preferredRowH, availableForRows / numRows);
+        if (rowH < 1) {
+            rowH = 1;
+        }
+
+        for (int i = 0; i < numRows; i++) {
+            MesMaterialErp item = i < itemCount ? printItems.get(i) : null;
+            g2.drawRect(left, rowY + i * rowH, contentW, rowH);
+            g2.drawLine(left + c1, rowY + i * rowH, left + c1, rowY + (i + 1) * rowH);
+            g2.drawLine(left + c1 + c2, rowY + i * rowH, left + c1 + c2, rowY + (i + 1) * rowH);
+            g2.drawLine(left + c1 + c2 + c3, rowY + i * rowH, left + c1 + c2 + c3, rowY + (i + 1) * rowH);
+            if (item != null) {
+                int textBaseline = rowY + i * rowH + Math.max((int) mmToPoint(3), (rowH * 5) / 8);
+                drawStringFitCell(g2, item.getMaterialCode(), left, c1, leftTextPadX, innerPadX, textBaseline, 8, 5);
+                drawStringFitCell(g2, item.getMaterialName(), left + c1, c2, innerPadX, innerPadX, textBaseline, 8, 5);
+                g2.setFont(new Font("SimSun", Font.PLAIN, 8));
+                g2.drawString(clip(safe(item.getMaterialNumber()), 6), left + c1 + c2 + innerPadX, textBaseline);
+                g2.drawString(clip(safe(item.getMateriarSize()), 12), left + c1 + c2 + c3 + innerPadX, textBaseline);
+            }
+        }
+    }
+
+    private static PrintService findPrinter(String printerName) {
+        PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
+        if (services.length == 0) {
+            log.info("No printers found.");
+            return null;
+        }
+        PrintService selectedService = Arrays.stream(services)
+                .filter(s -> s.getName().equals(printerName))
+                .findFirst()
+                .orElse(null);
+        if (selectedService == null) {
+            log.info("{}:Printer not found.", printerName);
+        }
+        return selectedService;
+    }
+
+    private static String safe(String value) {
+        return value == null ? "" : value;
+    }
+
+    private static String clip(String value, int maxLen) {
+        if (value == null || value.length() <= maxLen) {
+            return safe(value);
+        }
+        return value.substring(0, maxLen);
+    }
+
+    /**
+     * 在单元格宽度内绘制文本:从 max 字号递减直至能放下全文;仍放不下则在最小字号下按宽度截断。
+     */
+    private static void drawStringFitCell(Graphics2D g2, String text, int cellX, int cellWidth, int padLeft, int padRight,
+                                          int baselineY, int maxFontSize, int minFontSize) {
+        String s = safe(text);
+        int avail = cellWidth - padLeft - padRight;
+        if (avail < 1) {
+            avail = 1;
+        }
+        Font old = g2.getFont();
+        int size = maxFontSize;
+        g2.setFont(new Font("SimSun", Font.PLAIN, size));
+        FontMetrics fm = g2.getFontMetrics();
+        while (size > minFontSize && fm.stringWidth(s) > avail) {
+            size--;
+            g2.setFont(new Font("SimSun", Font.PLAIN, size));
+            fm = g2.getFontMetrics();
+        }
+        String draw = s;
+        if (fm.stringWidth(draw) > avail) {
+            int lo = 0;
+            int hi = draw.length();
+            while (lo < hi) {
+                int mid = (lo + hi + 1) / 2;
+                if (fm.stringWidth(draw.substring(0, mid)) <= avail) {
+                    lo = mid;
+                } else {
+                    hi = mid - 1;
+                }
+            }
+            draw = lo > 0 ? draw.substring(0, lo) : "";
+        }
+        g2.drawString(draw, cellX + padLeft, baselineY);
+        g2.setFont(old);
+    }
+
+    private static double mmToPoint(double mm) {
+        return mm * 72d / 25.4d;
+    }
+
+    public static class MaterialOrderPrintTask {
+        private final MesMaterialOrder order;
+        private final List<MesMaterialErp> items;
+
+        public MaterialOrderPrintTask(MesMaterialOrder order, List<MesMaterialErp> items) {
+            this.order = order;
+            this.items = items;
+        }
+
+        public MesMaterialOrder getOrder() {
+            return order;
+        }
+
+        public List<MesMaterialErp> getItems() {
+            return items;
+        }
+    }
+}

+ 5 - 0
src/main/resources/mappings/modules/erp/MesProductOutDao.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeesite.modules.erp.dao.MesProductOutDao">
+
+</mapper>

+ 18 - 1
src/main/resources/views/modules/erp/mesMaterialOrderList.html

@@ -6,6 +6,7 @@
 				<i class="fa icon-notebook"></i> ${text('订单领料表管理')}
 			</div>
 			<div class="box-tools pull-right">
+				<a href="#" class="btn btn-default" id="btnPrint" title="${text('打印')}"><i class="fa fa-print"></i> ${text('打印')}</a>
 				<a href="#" class="btn btn-default" id="btnSearch" title="${text('查询')}"><i class="fa fa-filter"></i> ${text('查询')}</a>
 				<% if(hasPermi('erp:mesMaterialOrder:edit')){ %>
 					<a href="${ctx}/erp/mesMaterialOrder/form" class="btn btn-default btnTool" title="${text('新增订单领料表')}"><i class="fa fa-plus"></i> ${text('新增')}</a>
@@ -62,8 +63,11 @@
 <% } %>
 <script>
 // 初始化DataGrid对象
-$('#dataGrid').dataGrid({
+var dataGrid = $('#dataGrid');
+dataGrid.dataGrid({
 	searchForm: $("#searchForm"),
+	showCheckbox: true,
+	multiboxonly: false,
 	columnModel: [
 
 		{header:'${text("领料单号")}', name:'requisitionNo', index:'a.requisition_no', width:150, align:"center", formatter: function(val, obj, row, act){
@@ -92,4 +96,17 @@ $('#dataGrid').dataGrid({
 
 	}
 });
+$('#btnPrint').on('click', function (e) {
+	e.preventDefault();
+	var ids = dataGrid.dataGrid('getSelectRows');
+	if (!ids || ids.length === 0) {
+		js.showMessage('${text("请先勾选要打印的数据")}');
+		return;
+	}
+	js.ajaxSubmit('${ctx}/erp/mesMaterialOrder/print', {
+		ids: ids.join(',')
+	}, function(data){
+		js.showMessage(data.message || '${text("打印请求已提交")}');
+	});
+});
 </script>

+ 82 - 0
src/main/resources/views/modules/erp/mesProductOutForm.html

@@ -0,0 +1,82 @@
+<% layout('/layouts/default.html', {title: '产品出库明细', libs: ['validate']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header with-border">
+			<div class="box-title">
+				<i class="fa icon-note"></i> ${text(mesProductOut.isNewRecord ? '新增产品出库明细' : '编辑产品出库明细')}
+			</div>
+			<div class="box-tools pull-right">
+				<button type="button" class="btn btn-box-tool" data-widget="collapse"><i class="fa fa-minus"></i></button>
+			</div>
+		</div>
+		<#form:form id="inputForm" model="${mesProductOut}" action="${ctx}/erp/mesProductOut/save" method="post" class="form-horizontal">
+			<div class="box-body">
+				<div class="form-unit">${text('基本信息')}</div>
+				<#form:hidden path="id"/>
+				<div class="row">
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4" title="">
+								<span class="required hide">*</span> ${text('出库单')}:<i class="fa icon-question hide"></i></label>
+							<div class="col-sm-8">
+								<#form:input path="outOrderNo" maxlength="100" class="form-control"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4" title="">
+								<span class="required hide">*</span> ${text('工件码')}:<i class="fa icon-question hide"></i></label>
+							<div class="col-sm-8">
+								<#form:input path="sn" maxlength="255" class="form-control"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4" title="">
+								<span class="required hide">*</span> ${text('状态')}:<i class="fa icon-question hide"></i></label>
+							<div class="col-sm-8">
+								<#form:radiobuttons path="status" dictType="sys_search_status" class="required"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-12">
+						<div class="form-group">
+							<label class="control-label col-sm-2" title="">
+								<span class="required hide">*</span> ${text('备注')}:<i class="fa icon-question hide"></i></label>
+							<div class="col-sm-10">
+								<#form:textarea path="remark" rows="3" maxlength="500" class="form-control"/>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
+			<div class="box-footer">
+				<div class="row">
+					<div class="col-sm-offset-2 col-sm-10">
+						<% if (hasPermi('erp:mesProductOut:edit')){ %>
+							<button type="submit" class="btn btn-sm btn-primary" id="btnSubmit"><i class="fa fa-check"></i> ${text('保 存')}</button>&nbsp;
+						<% } %>
+						<button type="button" class="btn btn-sm btn-default" id="btnCancel" onclick="js.closeCurrentTabPage()"><i class="fa fa-reply-all"></i> ${text('关 闭')}</button>
+					</div>
+				</div>
+			</div>
+		</#form:form>
+	</div>
+</div>
+<% } %>
+<script>
+$("#inputForm").validate({
+	submitHandler: function(form){
+		js.ajaxSubmitForm($(form), function(data){
+			js.showMessage(data.message);
+			if(data.result == Global.TRUE){
+				js.closeCurrentTabPage(function(contentWindow){
+					contentWindow.page();
+				});
+			}
+		}, "json");
+    }
+});
+</script>

+ 97 - 0
src/main/resources/views/modules/erp/mesProductOutList.html

@@ -0,0 +1,97 @@
+<% layout('/layouts/default.html', {title: '产品出库明细', libs: ['dataGrid']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header">
+			<div class="box-title">
+				<i class="fa icon-notebook"></i> ${text('产品出库明细')}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="#" class="btn btn-default" id="btnSearch" title="${text('查询')}"><i class="fa fa-filter"></i> ${text('查询')}</a>
+				<% if(hasPermi('erp:mesProductOut:edit')){ %>
+					<a href="${ctx}/erp/mesProductOut/form" class="btn btn-default btnTool" title="${text('新增产品出库明细')}"><i class="fa fa-plus"></i> ${text('新增')}</a>
+				<% } %>
+				<a href="#" class="btn btn-default" id="btnSetting" title="${text('设置')}"><i class="fa fa-navicon"></i></a>
+			</div>
+		</div>
+		<div class="box-body">
+			<#form:form id="searchForm" model="${mesProductOut}" action="${ctx}/erp/mesProductOut/listData" method="post" class="form-inline hide"
+					data-page-no="${parameter.pageNo}" data-page-size="${parameter.pageSize}" data-order-by="${parameter.orderBy}">
+				<div class="form-group">
+					<label class="control-label">${text('出库单')}:</label>
+					<div class="control-inline">
+						<#form:input path="outOrderNo" maxlength="100" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('工件码')}:</label>
+					<div class="control-inline">
+						<#form:input path="sn" maxlength="255" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('备注')}:</label>
+					<div class="control-inline">
+						<#form:input path="remark" maxlength="500" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('状态')}:</label>
+					<div class="control-inline width-120">
+						<#form:select path="status" dictType="sys_search_status" blankOption="true" class="form-control isQuick"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('创建时间')}:</label>
+					<div class="control-inline">
+						<#form:input path="createDate_gte" readonly="true" maxlength="20" class="form-control laydate width-datetime"
+							dataFormat="datetime" data-type="datetime" data-format="yyyy-MM-dd HH:mm" data-done="createDate_lte.click()"/>
+						&nbsp;-&nbsp;
+						<#form:input path="createDate_lte" readonly="true" maxlength="20" class="form-control laydate width-datetime"
+							dataFormat="datetime" data-type="datetime" data-format="yyyy-MM-dd HH:mm"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<button type="submit" class="btn btn-primary btn-sm">${text('查询')}</button>
+					<button type="reset" class="btn btn-default btn-sm">${text('重置')}</button>
+				</div>
+			</#form:form>
+			<table id="dataGrid"></table>
+			<div id="dataGridPage"></div>
+		</div>
+	</div>
+</div>
+<% } %>
+<script>
+$('#dataGrid').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("出库单")}', name:'outOrderNo', index:'a.out_order_no', width:140, align:"center", frozen:true, formatter: function(val, obj, row, act){
+			return '<a href="${ctx}/erp/mesProductOut/form?id='+row.id+'" class="btnList" data-title="${text("编辑产品出库明细")}">'+(val||row.id)+'</a>';
+		}},
+		{header:'${text("工件码")}', name:'sn', index:'a.sn', width:160, align:"center"},
+		{header:'${text("状态")}', name:'status', index:'a.status', width:90, align:"center", formatter: function(val, obj, row, act){
+			return js.getDictLabel(${@DictUtils.getDictListJson('sys_search_status')}, val, '${text("未知")}', true);
+		}},
+		{header:'${text("备注")}', name:'remark', index:'a.remark', width:180, align:"left"},
+		{header:'${text("创建者")}', name:'createBy', index:'a.create_by', width:100, align:"center"},
+		{header:'${text("创建时间")}', name:'createDate', index:'a.create_date', width:150, align:"center"},
+		{header:'${text("更新者")}', name:'updateBy', index:'a.update_by', width:100, align:"center"},
+		{header:'${text("更新时间")}', name:'updateDate', index:'a.update_date', width:150, align:"center"},
+		{header:'${text("操作")}', name:'actions', width:130, formatter: function(val, obj, row, act){
+			var actions = [];
+			//<% if(hasPermi('erp:mesProductOut:edit')){ %>
+				actions.push('<a href="${ctx}/erp/mesProductOut/form?id='+row.id+'" class="btnList" title="${text("编辑")}"><i class="fa fa-pencil"></i></a>&nbsp;');
+				if (row.status == Global.STATUS_NORMAL){
+					actions.push('<a href="${ctx}/erp/mesProductOut/disable?id='+row.id+'" class="btnList" title="${text("停用")}" data-confirm="${text("确认要停用吗?")}"><i class="glyphicon glyphicon-ban-circle"></i></a>&nbsp;');
+				} else if (row.status == Global.STATUS_DISABLE){
+					actions.push('<a href="${ctx}/erp/mesProductOut/enable?id='+row.id+'" class="btnList" title="${text("启用")}" data-confirm="${text("确认要启用吗?")}"><i class="glyphicon glyphicon-ok-circle"></i></a>&nbsp;');
+				}
+				actions.push('<a href="${ctx}/erp/mesProductOut/delete?id='+row.id+'" class="btnList" title="${text("删除")}" data-confirm="${text("确认要删除吗?")}"><i class="fa fa-trash-o"></i></a>&nbsp;');
+			//<% } %>
+			return actions.join('');
+		}}
+	],
+	ajaxSuccess: function(data){
+	}
+});
+</script>