修复发票PDF单页模板版式
This commit is contained in:
@@ -6,8 +6,10 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
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;
|
||||
@@ -24,11 +26,15 @@ public class ReservationInvoiceExcelTemplateRenderer {
|
||||
|
||||
private static final String TEMPLATE_PATH = "templates/reservation-invoice/proforma-invoice-v1.xlsx";
|
||||
private static final DateTimeFormatter DISPLAY_DATE_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy");
|
||||
private static final int CHARGE_START_ROW_INDEX = 22;
|
||||
private static final String SHEET_NAME = "Proforma Invoice";
|
||||
private static final int CHARGE_START_ROW_INDEX = 25;
|
||||
private static final int DEFAULT_CHARGE_ROW_COUNT = 10;
|
||||
private static final int SUBTOTAL_ROW_INDEX = 32;
|
||||
private static final int VAT_ROW_INDEX = 33;
|
||||
private static final int TOTAL_ROW_INDEX = 34;
|
||||
private static final int SUBTOTAL_ROW_INDEX = 35;
|
||||
private static final int VAT_ROW_INDEX = 36;
|
||||
private static final int TOTAL_ROW_INDEX = 37;
|
||||
private static final int PRINT_LAST_ROW_INDEX = 51;
|
||||
private static final int PRINT_LAST_COLUMN_INDEX = 7;
|
||||
private static final short PRINT_SCALE = 55;
|
||||
|
||||
/**
|
||||
* 使用受控 Excel 模板渲染 Invoice,并返回 xlsx 字节。
|
||||
@@ -39,6 +45,7 @@ public class ReservationInvoiceExcelTemplateRenderer {
|
||||
fillHeader(sheet, request);
|
||||
fillChargeRows(sheet, request.invoicePayload().charges());
|
||||
fillTotalFormulas(sheet);
|
||||
applyPrintSettings(workbook, sheet);
|
||||
workbook.setForceFormulaRecalculation(true);
|
||||
workbook.write(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
@@ -67,21 +74,22 @@ public class ReservationInvoiceExcelTemplateRenderer {
|
||||
var booking = payload.booking();
|
||||
var firstCharge = payload.charges().get(0);
|
||||
|
||||
setText(sheet, "G5", "Date : " + DISPLAY_DATE_FORMATTER.format(document.invoiceDate()));
|
||||
setText(sheet, "B6", recipient.attention());
|
||||
setText(sheet, "B7", recipient.company());
|
||||
setText(sheet, "B8", recipient.address());
|
||||
setText(sheet, "B12", recipient.telephone());
|
||||
setText(sheet, "B13", recipient.email());
|
||||
setText(sheet, "B14", DISPLAY_DATE_FORMATTER.format(document.bookingDate()));
|
||||
setText(sheet, "G17", "Due Date : " + DISPLAY_DATE_FORMATTER.format(document.dueDate()));
|
||||
setText(sheet, "B18", booking.groupName());
|
||||
setText(sheet, "B19", DISPLAY_DATE_FORMATTER.format(booking.arrivalDate()));
|
||||
setText(sheet, "B20", DISPLAY_DATE_FORMATTER.format(booking.departureDate()));
|
||||
setText(sheet, "G18", "Room Rate : " + formatDecimal(firstCharge.rate())
|
||||
setText(sheet, "H5", "Date : " + DISPLAY_DATE_FORMATTER.format(document.invoiceDate()));
|
||||
setText(sheet, "C6", recipient.attention());
|
||||
setText(sheet, "C7", recipient.company());
|
||||
fillAddress(sheet, recipient.address());
|
||||
setText(sheet, "C12", recipient.telephone());
|
||||
setText(sheet, "C13", recipient.email());
|
||||
setText(sheet, "C14", DISPLAY_DATE_FORMATTER.format(document.bookingDate()));
|
||||
setText(sheet, "H19", "Due Date : " + DISPLAY_DATE_FORMATTER.format(document.dueDate()));
|
||||
setText(sheet, "D20", booking.groupName());
|
||||
setText(sheet, "D22", DISPLAY_DATE_FORMATTER.format(booking.arrivalDate()));
|
||||
setText(sheet, "D23", DISPLAY_DATE_FORMATTER.format(booking.departureDate()));
|
||||
setText(sheet, "H20", "Room Rate : " + formatDecimal(firstCharge.rate())
|
||||
+ " /rm/n" + roomRateNoteSuffix(booking.roomRateNote()));
|
||||
setText(sheet, "G19", "No.of room(s) : " + formatDecimal(totalQuantity(payload.charges())));
|
||||
setText(sheet, "G20", "No.of Night(s) : " + formatDecimal(firstCharge.nights()));
|
||||
setText(sheet, "H21", "Extra bed : " + formatDecimal(booking.extraBedRate()));
|
||||
setText(sheet, "H22", "No.of room (s) : " + formatDecimal(totalQuantity(payload.charges())));
|
||||
setText(sheet, "H23", "No.of Night (s) : " + formatDecimal(firstCharge.nights()) + " NIGHTS");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,11 +101,12 @@ public class ReservationInvoiceExcelTemplateRenderer {
|
||||
ReservationInvoiceChargeRequest charge = charges.get(index);
|
||||
int rowIndex = CHARGE_START_ROW_INDEX + index;
|
||||
Row row = row(sheet, rowIndex);
|
||||
setCellText(row, 0, charge.description() + "\n" + charge.roomType());
|
||||
setCellNumber(row, 3, charge.quantity());
|
||||
setCellNumber(row, 4, charge.rate());
|
||||
setCellNumber(row, 5, charge.nights());
|
||||
setCellFormula(row, 6, "D" + (rowIndex + 1) + "*E" + (rowIndex + 1) + "*F" + (rowIndex + 1));
|
||||
setCellText(row, 1, charge.description());
|
||||
setCellText(row, 3, charge.roomType());
|
||||
setCellNumber(row, 4, charge.quantity());
|
||||
setCellNumber(row, 5, charge.rate());
|
||||
setCellNumber(row, 6, charge.nights());
|
||||
setCellFormula(row, 7, "E" + (rowIndex + 1) + "*F" + (rowIndex + 1) + "*G" + (rowIndex + 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,9 +117,23 @@ public class ReservationInvoiceExcelTemplateRenderer {
|
||||
Row subtotalRow = row(sheet, SUBTOTAL_ROW_INDEX);
|
||||
Row vatRow = row(sheet, VAT_ROW_INDEX);
|
||||
Row totalRow = row(sheet, TOTAL_ROW_INDEX);
|
||||
setCellFormula(subtotalRow, 6, "SUM(G23:G32)/1.07");
|
||||
setCellFormula(vatRow, 6, "SUM(G23:G32)-G33");
|
||||
setCellFormula(totalRow, 6, "SUM(G23:G32)");
|
||||
setCellFormula(subtotalRow, 7, "SUM(H26:H35)/1.07");
|
||||
setCellFormula(vatRow, 7, "SUM(H26:H35)-H36");
|
||||
setCellFormula(totalRow, 7, "SUM(H26:H35)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 固定 PDF 输出版式,避免模板被编辑后再次变成多页或 Letter 纸张。
|
||||
*/
|
||||
private void applyPrintSettings(Workbook workbook, Sheet sheet) {
|
||||
int sheetIndex = workbook.getSheetIndex(sheet);
|
||||
workbook.setSheetName(sheetIndex, SHEET_NAME);
|
||||
workbook.setPrintArea(sheetIndex, 0, PRINT_LAST_COLUMN_INDEX, 0, PRINT_LAST_ROW_INDEX);
|
||||
PrintSetup printSetup = sheet.getPrintSetup();
|
||||
printSetup.setPaperSize(PrintSetup.A4_PAPERSIZE);
|
||||
printSetup.setLandscape(false);
|
||||
printSetup.setScale(PRINT_SCALE);
|
||||
sheet.setAutobreaks(false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,13 +142,73 @@ public class ReservationInvoiceExcelTemplateRenderer {
|
||||
private void clearChargeRows(Sheet sheet) {
|
||||
for (int index = 0; index < DEFAULT_CHARGE_ROW_COUNT; index++) {
|
||||
Row row = row(sheet, CHARGE_START_ROW_INDEX + index);
|
||||
for (int cellIndex = 0; cellIndex <= 6; cellIndex++) {
|
||||
for (int cellIndex = 1; cellIndex <= 7; cellIndex++) {
|
||||
Cell cell = cell(row, cellIndex);
|
||||
cell.setBlank();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 地址字段最多拆成三行展示,兼顾模板空间和用户手填长地址。
|
||||
*/
|
||||
private void fillAddress(Sheet sheet, String address) {
|
||||
List<String> addressLines = splitAddressLines(address);
|
||||
setText(sheet, "C8", addressLines.get(0));
|
||||
setText(sheet, "C9", addressLines.get(1));
|
||||
setText(sheet, "C10", addressLines.get(2));
|
||||
}
|
||||
|
||||
/**
|
||||
* 优先按换行拆分;无换行时按逗号做温和换行,避免单行地址在 PDF 中过长。
|
||||
*/
|
||||
private List<String> splitAddressLines(String address) {
|
||||
if (address == null || address.isBlank()) {
|
||||
return List.of("", "", "");
|
||||
}
|
||||
String normalized = address.trim();
|
||||
String[] hardLines = normalized.split("\\R");
|
||||
if (hardLines.length > 1) {
|
||||
return firstThreeLines(List.of(hardLines));
|
||||
}
|
||||
List<String> lines = new ArrayList<>();
|
||||
StringBuilder current = new StringBuilder();
|
||||
for (String part : normalized.split(",")) {
|
||||
String segment = part.trim();
|
||||
if (segment.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String candidate = current.length() == 0 ? segment : current + ", " + segment;
|
||||
if (candidate.length() > 58 && lines.size() < 2 && current.length() > 0) {
|
||||
lines.add(current.toString());
|
||||
current = new StringBuilder(segment);
|
||||
} else {
|
||||
current = new StringBuilder(candidate);
|
||||
}
|
||||
}
|
||||
if (current.length() > 0) {
|
||||
lines.add(current.toString());
|
||||
}
|
||||
return firstThreeLines(lines);
|
||||
}
|
||||
|
||||
private List<String> firstThreeLines(List<String> lines) {
|
||||
List<String> result = new ArrayList<>(3);
|
||||
for (int index = 0; index < lines.size(); index++) {
|
||||
String line = lines.get(index);
|
||||
if (result.size() == 2) {
|
||||
String remaining = String.join(", ", lines.subList(index, lines.size()));
|
||||
result.add(remaining);
|
||||
break;
|
||||
}
|
||||
result.add(line == null ? "" : line.trim());
|
||||
}
|
||||
while (result.size() < 3) {
|
||||
result.add("");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Row row(Sheet sheet, int rowIndex) {
|
||||
Row row = sheet.getRow(rowIndex);
|
||||
return row == null ? sheet.createRow(rowIndex) : row;
|
||||
|
||||
Reference in New Issue
Block a user