让成交卡等报价文件真正发出后再出现,并收紧 PDF 顶部留白,避免抬头被裁或空一截。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+154
-8
@@ -191,6 +191,11 @@ public final class QuoteTemplateExcelSupport {
|
||||
writeFeeRows(wb, mapping, facts, yellow);
|
||||
// 后台常未配 fee_rows:按「空运费」等名称写入金额列,SUM 才能加算
|
||||
writeFeeAmountsByName(wb, facts, yellow);
|
||||
// 模板「预估总价」是 SUM。POI 写完数字不会重算,缓存仍是 0。
|
||||
// 企微/WPS 预览常不重算,销售会看到汇总为 0。这里把公式缓存刷成当前值。
|
||||
refreshFormulas(wb);
|
||||
// 转 PDF 前把纸张改成 A4 居中,避免顶部裁字、整页偏左。
|
||||
applyQuotePrintLayout(wb);
|
||||
Files.createDirectories(targetXlsx.getParent());
|
||||
try (OutputStream out = Files.newOutputStream(targetXlsx)) {
|
||||
wb.write(out);
|
||||
@@ -252,7 +257,62 @@ public final class QuoteTemplateExcelSupport {
|
||||
if (cell == null) {
|
||||
cell = row.createCell(col);
|
||||
}
|
||||
cell.setCellValue(value == null ? "" : value);
|
||||
writeNumericOrText(cell, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 金额/数量/单价写成数字,SUM 才能加算;名称等仍写文本。
|
||||
*/
|
||||
private static void writeNumericOrText(Cell cell, String value) {
|
||||
if (value == null) {
|
||||
cell.setCellValue("");
|
||||
return;
|
||||
}
|
||||
String norm = stripMoneyPrefix(value);
|
||||
if (StringUtils.isBlank(norm) || "-".equals(norm)) {
|
||||
cell.setCellValue(value);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
cell.setCellValue(Double.parseDouble(norm));
|
||||
} catch (NumberFormatException e) {
|
||||
cell.setCellValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回填后重算公式,把 SUM 等缓存从 0 刷成真实合计。
|
||||
* 失败不抛:出单仍要发出,只记日志。
|
||||
*/
|
||||
static void refreshFormulas(Workbook wb) {
|
||||
if (wb == null) {
|
||||
return;
|
||||
}
|
||||
FormulaEvaluator ev = wb.getCreationHelper().createFormulaEvaluator();
|
||||
for (int s = 0; s < wb.getNumberOfSheets(); s++) {
|
||||
Sheet sheet = wb.getSheetAt(s);
|
||||
if (sheet == null) {
|
||||
continue;
|
||||
}
|
||||
for (Row row : sheet) {
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
for (Cell cell : row) {
|
||||
if (cell == null || cell.getCellType() != CellType.FORMULA) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
ev.evaluateFormulaCell(cell);
|
||||
} catch (Exception e) {
|
||||
log.warn("报价公式未重算 {}!{}: {}",
|
||||
sheet.getSheetName(),
|
||||
new CellReference(cell).formatAsString(),
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void clearCell(Workbook wb, String cellRef) {
|
||||
@@ -493,6 +553,23 @@ public final class QuoteTemplateExcelSupport {
|
||||
return fuzzy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 费用明细表头原字,回填系统字段时不得覆盖。
|
||||
*/
|
||||
static boolean isFeeTableHeader(String text) {
|
||||
if (StringUtils.isBlank(text)) {
|
||||
return false;
|
||||
}
|
||||
String t = text.trim();
|
||||
return "项目".equals(t) || "费用名称".equals(t) || "费用名".equals(t)
|
||||
|| "单位".equals(t) || "單位".equals(t)
|
||||
|| "币别".equals(t) || "币種".equals(t) || "币种".equals(t)
|
||||
|| "单价".equals(t) || "單價".equals(t)
|
||||
|| "数量".equals(t) || "數量".equals(t)
|
||||
|| "金额".equals(t) || "金額".equals(t)
|
||||
|| "备注".equals(t) || "備註".equals(t);
|
||||
}
|
||||
|
||||
private static String readCellText(Cell cell) {
|
||||
if (cell == null) {
|
||||
return "";
|
||||
@@ -585,6 +662,10 @@ public final class QuoteTemplateExcelSupport {
|
||||
String text = value == null ? "" : value;
|
||||
// 标签格「报价编号:」只换冒号后的值,避免整格被工单号顶掉看起来像残缺
|
||||
String existing = readCellText(cell);
|
||||
// 「币别」右侧常被误映到「单价」表头,不能把 CNY 盖掉单价二字
|
||||
if (isFeeTableHeader(existing)) {
|
||||
return;
|
||||
}
|
||||
int colon = Math.max(existing.indexOf(':'), existing.indexOf(':'));
|
||||
if (colon >= 0 && colon == existing.length() - 1) {
|
||||
text = existing + text;
|
||||
@@ -599,13 +680,78 @@ public final class QuoteTemplateExcelSupport {
|
||||
copy.setDataFormat(wb.createDataFormat().getFormat("@"));
|
||||
cell.setCellStyle(copy);
|
||||
}
|
||||
// 工单号、日期略加宽,避免列太窄只露出半截
|
||||
if (dest[1] >= 0 && dest[1] < 20 && text.length() >= 8) {
|
||||
org.apache.poi.ss.usermodel.Sheet sh = sheet;
|
||||
int col = dest[1];
|
||||
double width = sh.getColumnWidth(col) / 256.0;
|
||||
if (width > 0 && width < text.length() + 2) {
|
||||
sh.setColumnWidth(col, (int) Math.min(40, text.length() + 4) * 256);
|
||||
// 不再临时加宽列:会把打印缩放打乱,转 PDF 后整页偏左。
|
||||
}
|
||||
|
||||
/**
|
||||
* 报价单打印版心:A4、水平居中、按页宽缩放。
|
||||
* <p>
|
||||
* 空运模板常是 62% 缩放且不居中。LibreOffice 按这个出 PDF,
|
||||
* 公司名会被裁掉上半截,正文整页靠左。出单时统一改过来。
|
||||
* 只改打印属性,不改单元格里的业务数字。
|
||||
*/
|
||||
static void applyQuotePrintLayout(Workbook wb) {
|
||||
if (wb == null) {
|
||||
return;
|
||||
}
|
||||
for (int s = 0; s < wb.getNumberOfSheets(); s++) {
|
||||
Sheet sheet = wb.getSheetAt(s);
|
||||
if (sheet == null) {
|
||||
continue;
|
||||
}
|
||||
sheet.setMargin(Sheet.LeftMargin, 0.5);
|
||||
sheet.setMargin(Sheet.RightMargin, 0.5);
|
||||
// 上边距适中:太小企微会裁公司名,太大纸顶空一截
|
||||
sheet.setMargin(Sheet.TopMargin, 0.75);
|
||||
sheet.setMargin(Sheet.BottomMargin, 0.6);
|
||||
sheet.setMargin(Sheet.HeaderMargin, 0.1);
|
||||
sheet.setMargin(Sheet.FooterMargin, 0.2);
|
||||
sheet.setHorizontallyCenter(true);
|
||||
sheet.setVerticallyCenter(false);
|
||||
sheet.setAutobreaks(true);
|
||||
sheet.setFitToPage(true);
|
||||
PrintSetup ps = sheet.getPrintSetup();
|
||||
ps.setPaperSize(PrintSetup.A4_PAPERSIZE);
|
||||
ps.setLandscape(false);
|
||||
ps.setFitWidth((short) 1);
|
||||
ps.setFitHeight((short) 0);
|
||||
ensureHeaderRowHeight(sheet);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 抬头合并格(公司中英文)行高太矮时加高。
|
||||
* 现网空运模板 A1:H2 合计只有 42 磅,两行字会裁顶。
|
||||
*/
|
||||
static void ensureHeaderRowHeight(Sheet sheet) {
|
||||
if (sheet == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
|
||||
org.apache.poi.ss.util.CellRangeAddress range = sheet.getMergedRegion(i);
|
||||
if (range.getFirstRow() != 0 || range.getLastRow() < 1) {
|
||||
continue;
|
||||
}
|
||||
Row first = sheet.getRow(range.getFirstRow());
|
||||
Cell cell = first == null ? null : first.getCell(range.getFirstColumn());
|
||||
String text = readCellText(cell);
|
||||
if (!text.contains("公司") && text.toLowerCase().indexOf("company") < 0) {
|
||||
continue;
|
||||
}
|
||||
// 模板末尾常带空行,字会被顶到合并格上沿,转 PDF 再被裁一刀
|
||||
if (cell != null && StringUtils.isNotBlank(text)) {
|
||||
cell.setCellValue(text.replaceAll("[\\r\\n]+$", ""));
|
||||
}
|
||||
int rows = range.getLastRow() - range.getFirstRow() + 1;
|
||||
float each = Math.max(32f, 64f / rows);
|
||||
for (int r = range.getFirstRow(); r <= range.getLastRow(); r++) {
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null) {
|
||||
row = sheet.createRow(r);
|
||||
}
|
||||
if (row.getHeightInPoints() < each) {
|
||||
row.setHeightInPoints(each);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+91
@@ -2,6 +2,8 @@ package org.jeecg.modules.inquiry.template;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.apache.poi.ss.usermodel.PrintSetup;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
@@ -12,6 +14,8 @@ import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.poi.ss.usermodel.CellType;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@@ -153,4 +157,91 @@ public class QuoteTemplateFillTest {
|
||||
Files.deleteIfExists(out);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 空运模板「预估总价」是 SUM。回填后必须把公式缓存刷成合计,
|
||||
* 否则企微/WPS 预览仍显示模板里的 0。
|
||||
*/
|
||||
@Test
|
||||
public void refreshSumCacheAfterWritingFeeAmount() throws Exception {
|
||||
Path src = Files.createTempFile("quote-sum-src", ".xlsx");
|
||||
Path out = Files.createTempFile("quote-sum-out", ".xlsx");
|
||||
try (Workbook wb = new XSSFWorkbook()) {
|
||||
Sheet sheet = wb.createSheet("Sheet2");
|
||||
org.apache.poi.ss.usermodel.Row head = sheet.createRow(15);
|
||||
head.createCell(1).setCellValue("费用名称");
|
||||
head.createCell(2).setCellValue("单位");
|
||||
head.createCell(3).setCellValue("币别");
|
||||
head.createCell(4).setCellValue("单价");
|
||||
head.createCell(5).setCellValue("数量");
|
||||
head.createCell(6).setCellValue("金额");
|
||||
head.createCell(7).setCellValue("备注");
|
||||
sheet.createRow(16).createCell(1).setCellValue("空运费");
|
||||
org.apache.poi.ss.usermodel.Row total = sheet.createRow(22);
|
||||
total.createCell(0).setCellValue("预估总价");
|
||||
total.createCell(6).setCellFormula("SUM(G17:G22)");
|
||||
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||
wb.write(os);
|
||||
}
|
||||
}
|
||||
JSONObject mapping = new JSONObject();
|
||||
JSONObject fields = new JSONObject();
|
||||
// 测服空运模板常把「币别」右侧误映到单价表头,回填时不能盖掉「单价」
|
||||
fields.put("currency", "Sheet2!E16");
|
||||
mapping.put("fields", fields);
|
||||
JSONObject facts = new JSONObject(true);
|
||||
facts.put("currency", "CNY");
|
||||
facts.put("total_amount", "14669.60");
|
||||
facts.put("fee_rows", QuoteTemplateExcelSupport.parseFeeRows(
|
||||
"[{\"name\":\"空运费\",\"amount\":\"14669.60\",\"currency\":\"CNY\","
|
||||
+ "\"unit\":\"KG\",\"quantity\":\"1333.6\",\"unit_price\":\"11.0\"}]"));
|
||||
QuoteTemplateExcelSupport.fillWorkbook(src, out, mapping, facts);
|
||||
try (Workbook formulas = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||
org.apache.poi.ss.usermodel.Cell sum = formulas.getSheetAt(0).getRow(22).getCell(6);
|
||||
assertEquals(CellType.FORMULA, sum.getCellType());
|
||||
assertEquals("SUM(G17:G22)", sum.getCellFormula());
|
||||
// 公式格的数字缓存 = 企微预览看到的值;没刷缓存会是 0
|
||||
assertEquals(14669.60, sum.getNumericCellValue(), 0.01);
|
||||
assertEquals("单价", formulas.getSheetAt(0).getRow(15).getCell(4).getStringCellValue());
|
||||
} finally {
|
||||
Files.deleteIfExists(src);
|
||||
Files.deleteIfExists(out);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 出单后必须 A4 水平居中,抬头行高够放下中英文公司名。
|
||||
* 否则转 PDF 会顶部裁字、整页偏左。
|
||||
*/
|
||||
@Test
|
||||
public void applyPrintLayoutCentersA4AndRaisesHeader() throws Exception {
|
||||
Path src = Files.createTempFile("quote-layout-src", ".xlsx");
|
||||
Path out = Files.createTempFile("quote-layout-out", ".xlsx");
|
||||
try (Workbook wb = new XSSFWorkbook()) {
|
||||
Sheet sheet = wb.createSheet("Sheet2");
|
||||
sheet.setHorizontallyCenter(false);
|
||||
sheet.getPrintSetup().setScale((short) 62);
|
||||
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(0, 1, 0, 7));
|
||||
Row r0 = sheet.createRow(0);
|
||||
r0.setHeightInPoints(21f);
|
||||
r0.createCell(0).setCellValue("深圳市友通达国际货运代理有限公司\nShenzhen Youtongda International Freight Forwarding Co.LTD");
|
||||
sheet.createRow(1).setHeightInPoints(21f);
|
||||
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||
wb.write(os);
|
||||
}
|
||||
}
|
||||
QuoteTemplateExcelSupport.fillWorkbook(src, out, new JSONObject(), new JSONObject());
|
||||
try (Workbook filled = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||
Sheet sheet = filled.getSheetAt(0);
|
||||
assertTrue(sheet.getHorizontallyCenter());
|
||||
assertEquals(PrintSetup.A4_PAPERSIZE, sheet.getPrintSetup().getPaperSize());
|
||||
assertEquals(1, sheet.getPrintSetup().getFitWidth());
|
||||
assertEquals(0.75, sheet.getMargin(Sheet.TopMargin), 0.01);
|
||||
assertTrue(sheet.getRow(0).getHeightInPoints() >= 32f);
|
||||
assertTrue(sheet.getRow(1).getHeightInPoints() >= 32f);
|
||||
} finally {
|
||||
Files.deleteIfExists(src);
|
||||
Files.deleteIfExists(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user