背景
为满足乐企直连纳税人多场景、规模化、合规化、自动化的电子发票开具需求,乐企数字开放平台提供全面数字化的电子发票(以下简称数字化电子发票)的版式文件及 XML 生成规范,乐企直连单位按照本规范在自有业务系统中生成数字化电子发票的版式及 XML 电子文件并进行交付。
目的
根据发票信息生成对应的xml、pdf、odf三种文件格式。
技术方案
- xml格式:使用jackson-dataformat-xml的相关注解可以轻松将实体类转xml。若不想定义诸多实体类,也可以通过StringBuilder逐个模块地拼接出xml字符串,根据需要决定是否需要美化xml。
- pdf格式:起初我们想通过美工绘制或者使用已开发票擦除黑色字体的发票信息作为发票模板,通过模板套打发票信息来生成pdf文件,但是发票高度会跟随明细行数目动态变化,因此模板套打方案需预先准备多套模板。随后我们决定使用itextpdf等依赖直接绘制pdf文件,但版面元素信息和发票信息在绘制时坐标很难精确。最后我们考虑先生成ofd,再通过工具将ofd转pdf,经测试后效果不错。
- ofd格式:因为发票版面除去高度可能会变化外,其余格式几乎是固定的,因此我们不考虑直接生成ofd,而且是通过解压已开发票的odf文件作为模板,然后使用代码替换其中的相关xml文件(详见下文),最后压缩还原为odf文件。
发票文件Xml格式的具体实现
实体类定义
实体对象转xml字符串
发票文件Ofd格式的具体实现
先准备发票odf的模板,这个可以从电子税局下载一个解压得到。解压后我们通过代码修改以下文件,最后压缩还原为odf文件就可以了。具体详细的ofd的文件解压详情可以去其他博客看看,这里不详述。
需要注意的是汉字和英文、数字等的字符宽度和间距不一样,因此要计算deltaX,另外汉字类多居左或居中、金额数组类多居右。具体样式细节参考乐企规范,以下代码可做参考:
/**
* 获取税号的DeltaX,税号的x偏移都是2.54
*
* @param text text
* @return DeltaX表达式
*/
public static String getDeltaX4TaxpayerNo(String text) {
if (StrUtil.isBlank(text) || text.length() == 1) {
return "";
}
return String.format("g %s 2.54", text.length() - 1);
}
/**
* 获取汉字的DeltaX
*
* @param text text
* @return DeltaX表达式
*/
public static String getDeltaX4Chinese(String text) {
if (StrUtil.isBlank(text) || text.length() == 1) {
return "";
}
return String.format("g %s 3.175", text.length() - 1);
}
/**
* 获取数字的DeltaX
*
* @param text text
* @return DeltaX表达式
*/
public static String getDeltaX4Number(String text) {
if (StrUtil.isBlank(text) || text.length() == 1) {
return "";
}
return String.format("g %s 1.5875", text.length() - 1);
}
/**
* 根据输入的文本获取DeltaX的复杂形式
*
* @param text 输入的文本
* @return 返回DeltaX的复杂形式
*/
public static String getDeltaX4Complex(String text) {
if (StrUtil.isBlank(text) || text.length() == 1) {
return "";
}
StringBuilder sb = new StringBuilder();
char[] charArray = text.toCharArray();
for (int currentIndex = 0; currentIndex < charArray.length - 1; ) {
String deltaX = getDeltaX(charArray[currentIndex]);
int repeatNum = 1;
for (int nextIndex = currentIndex + 1; nextIndex < charArray.length - 1; nextIndex++) {
String nextDeltaX = getDeltaX(charArray[nextIndex]);
if (deltaX.equals(nextDeltaX)) {
repeatNum++;
}
// 跳出循环条件
if (!deltaX.equals(nextDeltaX) || nextIndex == charArray.length - 2) {
break;
}
}
if (repeatNum <= 1) {
sb.append(deltaX).append(" ");
} else {
sb.append(String.format("g %s %s ", repeatNum, deltaX));
}
currentIndex = currentIndex + repeatNum;
}
return sb.toString();
}
/**
* 根据传入的字符返回对应的DeltaX值
*
* @param c 字符
* @return 如果字符是中文字符,返回"3.175";否则返回"1.5875"
*/
private static String getDeltaX(char c) {
return isChineseCharV2(c) ? "3.175" : "1.5875";
}
/**
* 判断一个字符是否为中文字符。
*
* @param c 待判断的字符
* @return 如果字符是中文字符,则返回true;否则返回false。
*/
private static boolean isChineseChar(char c) {
return (c >= '一' && c <= '龥') || ('(' == c || ')' == c);
}
/**
* 获取文本在给定边界宽度下的水平居中起始X坐标
*
* @param boundaryWidth 边界宽度,以字符串形式表示
* @param text 文本内容
* @return 水平居中起始X坐标,以字符串形式表示
*/
public static String getCenterStartX(String boundaryWidth, String text) {
return String.valueOf((Float.parseFloat(boundaryWidth) - Float.parseFloat(getTextWidth(text))) / 2);
}
/**
* 获取文本在给定边界宽度下的右对齐起始X坐标
*
* @param boundaryWidth 边界宽度,以字符串形式表示
* @param text 文本内容
* @return 右对齐起始X坐标,以字符串形式表示
*/
public static String getRightStartX(String boundaryWidth, String text) {
return String.valueOf(Float.parseFloat(boundaryWidth) - Float.parseFloat(getTextWidth(text)));
}
/**
* 获取字符串的宽度
*
* @param text 待获取宽度的字符串
* @return 返回字符串的宽度,以浮点数形式表示
*/
public static String getTextWidth(String text) {
char[] charArray = text.toCharArray();
float width = 0f;
for (char c : charArray) {
width += (float) Double.parseDouble(getDeltaX(c));
}
return String.valueOf(width);
}
/**
* 判断给定字符是否为中文字符
*
* @param c 待判断的字符
* @return 如果给定字符为中文字符,则返回true;否则返回false
*/
public static boolean isChineseCharV2(char c) {
Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);
return ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
|| ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION
|| ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS
|| ub == Character.UnicodeBlock.CJK_RADICALS_SUPPLEMENT
|| ub == Character.UnicodeBlock.KANGXI_RADICALS
|| ub == Character.UnicodeBlock.IDEOGRAPHIC_DESCRIPTION_CHARACTERS;
}
发票文件Pdf格式的具体实现
使用转换器直接将odf转换为pdf即可。
引入依赖:
<dependency>
<groupId>org.ofdrw</groupId>
<artifactId>ofdrw-converter</artifactId>
<version>2.3.1</version>
</dependency>
odfrw gitee地址:ofdrw: OFD Reader & Writer 开源的OFD处理库,支持文档生成、数字签名、文档保护、文档合并、转换等功能,文档格式遵循《GB/T 33190-2016 电子文件存储与交换格式版式文档》。 - Gitee.com 具体代码: