Browse Source

financeInfo render

dengjia 1 month ago
parent
commit
0a52787413

+ 225 - 0
easier-report-biz/src/main/java/com/yaoyicloud/render/FinancialInfoRender.java

@@ -0,0 +1,225 @@
+package com.yaoyicloud.render;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.poi.xwpf.usermodel.XWPFParagraph;
+import org.apache.poi.xwpf.usermodel.XWPFRun;
+import org.apache.poi.xwpf.usermodel.XWPFTable;
+import org.apache.poi.xwpf.usermodel.XWPFTableCell;
+import org.apache.poi.xwpf.usermodel.XWPFTableRow;
+import com.deepoove.poi.XWPFTemplate;
+import com.deepoove.poi.config.Configure;
+import com.deepoove.poi.config.ConfigureBuilder;
+import com.deepoove.poi.exception.RenderException;
+import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
+import com.deepoove.poi.policy.RenderPolicy;
+import com.deepoove.poi.template.ElementTemplate;
+import com.deepoove.poi.template.run.RunTemplate;
+import com.deepoove.poi.util.TableTools;
+import com.google.protobuf.util.JsonFormat;
+import com.yaoyicloud.message.FxyProtos.FinancialInfo;
+import com.yaoyicloud.tools.Util;
+
+/**
+ * FinancialInfo渲染器
+ *
+ */
+public final class FinancialInfoRender extends AbstractRender {
+
+    public FinancialInfoRender(String cwd) {
+        super(cwd);
+    }
+
+    /**
+     * Docx 渲染
+     *
+     * @param info 数据
+     * @param templateFileContent 模板内容
+     * @return 本地文件目录
+     * @throws IOException
+     */
+    public String renderDocx(FinancialInfo info, byte[] templateFileContent) throws IOException {
+
+        // 不需要定制展示逻辑的时候,使用protobuf的转json方法
+        String jsonStr = JsonFormat.printer().print(info);
+
+        // 注: 报告模板的模板变量按照json序列化的结果命名
+        // 注: 目前的实现假设:一个session对应一个cwd目录
+        ConfigureBuilder builder = Configure.builder();
+        builder.bind("indicators", new LoopRowIncludeStatisticsTableRenderPolicy("values"));
+        builder.bind("financialDataSeq", new LoopColumnStaticTableRenderPolicy("[", "]", false, true, 2));
+        builder.bind("financialCheckDetails", new LoopRowCutAndMergeFirstColTableRenderPolicy());
+        // 注意使用了SpringEL之后,每个模板变量都要设置值,不然会报错
+        builder.useSpringEL();
+
+        this.docxResultPath = this.renderDocx(jsonStr, templateFileContent, builder,
+            Paths.get(cwd, UUID.randomUUID().toString() + ".docx").toString());
+        return this.docxResultPath;
+    }
+
+    /**
+     * 这些render policy类都应当是共享的 重要设计假设: data的类型cast都可以建立在json通用反序列化后的基本类型基础上。
+     */
+    public class LoopColumnStaticTableRenderPolicy implements RenderPolicy {
+
+        private String prefix;
+        private String suffix;
+        private boolean onSameLine;
+        private boolean reverse;
+        private int valColIndex;
+
+        public LoopColumnStaticTableRenderPolicy() {
+            this(false);
+        }
+
+        public LoopColumnStaticTableRenderPolicy(boolean onSameLine) {
+            this("[", "]", onSameLine);
+        }
+
+        public LoopColumnStaticTableRenderPolicy(String prefix, String suffix) {
+            this(prefix, suffix, false);
+        }
+
+        public LoopColumnStaticTableRenderPolicy(String prefix, String suffix, boolean onSameLine) {
+            this(prefix, suffix, onSameLine, false);
+        }
+
+        public LoopColumnStaticTableRenderPolicy(String prefix, String suffix, boolean onSameLine, boolean reverse) {
+            this(prefix, suffix, onSameLine, false, 1);
+        }
+
+        public LoopColumnStaticTableRenderPolicy(String prefix, String suffix, boolean onSameLine, boolean reverse,
+            int valRowIndex) {
+            this.prefix = prefix;
+            this.suffix = suffix;
+            this.onSameLine = onSameLine;
+            this.reverse = reverse;
+            this.valColIndex = valRowIndex;
+        }
+
+        @Override
+        public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
+            RunTemplate runTemplate = (RunTemplate) eleTemplate;
+            XWPFRun run = runTemplate.getRun();
+            try {
+                if (!TableTools.isInsideTable(run)) {
+                    throw new IllegalStateException(
+                        "The template tag " + runTemplate.getSource() + " must be inside a table");
+                }
+                XWPFTableCell tagCell = (XWPFTableCell) ((XWPFParagraph) run.getParent()).getBody();
+                XWPFTable table = tagCell.getTableRow().getTable();
+                run.setText("", 0);
+
+                int templateColIndex = getTemplateColIndex(tagCell);
+                // 模版变量列总是写在左边
+                int minIndex = templateColIndex;
+                int maxIndex = table.getRows().get(valColIndex).getTableCells().size() - 1;
+                int currIndex = reverse ? maxIndex : minIndex;
+                int indexDelta = reverse ? -1 : 1;
+
+                // 目前expression当作数据Map的key来对待,将来可以当作POI-TL变量一致化的处理
+                Map<Integer, String> idx2Expression = new HashMap<>();
+                for (int i = 0; i < table.getRows().size(); i++) {
+                    XWPFTableCell cell = table.getRows().get(i).getCell(templateColIndex);
+                    String text = cell.getText().trim();
+                    if (text.startsWith(prefix) && text.endsWith(suffix)) {
+                        idx2Expression.put(i, text.substring(1, text.length() - 1));
+                        cell.setText("");
+                    }
+                }
+
+                int rowSize = table.getRows().size();
+                @SuppressWarnings("unchecked")
+                List<Map<String, Object>> mpData = (List<Map<String, Object>>) data;
+
+                for (Map<String, Object> realData : mpData) {
+                    for (int i = 0; i < rowSize; i++) {
+                        if (!idx2Expression.containsKey(i)) {
+                            continue;
+                        }
+                        XWPFTableRow row = table.getRow(i);
+                        XWPFTableCell valueCell = row.getCell(currIndex);
+                        String valStr = realData.getOrDefault(idx2Expression.get(i), "-").toString();
+                        valueCell.setText(valStr);
+                    }
+                    currIndex += indexDelta;
+                }
+
+            } catch (Exception e) {
+                throw new RenderException("HackLoopTable for " + eleTemplate + "error: " + e.getMessage(), e);
+            }
+        }
+
+        private int getTemplateColIndex(XWPFTableCell tagCell) {
+            return onSameLine ? Util.getColIndexOfFirstRow(tagCell) : (Util.getColIndexOfFirstRow(tagCell) + 1);
+        }
+    }
+
+    public class LoopRowCutAndMergeFirstColTableRenderPolicy extends LoopRowTableRenderPolicy {
+
+        @Override
+        public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
+            RunTemplate runTemplate = (RunTemplate) eleTemplate;
+            XWPFRun run = runTemplate.getRun();
+            XWPFTable table = null;
+            try {
+                if (!TableTools.isInsideTable(run)) {
+                    throw new IllegalStateException(
+                        "The template tag " + runTemplate.getSource() + " must be inside a table");
+                }
+                // Reserve the first two rows
+                XWPFTableCell tagCell = (XWPFTableCell) ((XWPFParagraph) run.getParent()).getBody();
+                table = tagCell.getTableRow().getTable();
+                for (int i = table.getNumberOfRows() - 1; i > 1; i--) {
+                    table.removeRow(i);
+                }
+            } catch (Exception e) {
+                throw new RenderException(
+                    "LoopRowCutAndMergeFirstColTable for " + eleTemplate + " error: " + e.getMessage(), e);
+            }
+
+            // in case data not sorted by rank
+            @SuppressWarnings("unchecked")
+            List<Map<String, Object>> mpData = (List<Map<String, Object>>) data;
+            mpData.sort((a, b) -> Integer.valueOf(a.getOrDefault("rank", "0").toString())
+                - Integer.valueOf(b.getOrDefault("rank", "0").toString()));
+
+            super.render(eleTemplate, data, template);
+
+            try {
+                // merge the first column
+                Util.mergeFirstNColSimple(table, 1, 0);
+            } catch (Exception e) {
+                throw new RenderException(
+                    "LoopRowCutAndMergeFirstColTable for " + eleTemplate + " error: " + e.getMessage(), e);
+            }
+        }
+    }
+
+    public class LoopRowIncludeStatisticsTableRenderPolicy extends LoopRowTableRenderPolicy {
+
+        private String valueTag;
+
+        public LoopRowIncludeStatisticsTableRenderPolicy(String valueTag) {
+            this.valueTag = valueTag;
+        }
+
+        @Override
+        public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
+            @SuppressWarnings("unchecked")
+            List<Map<String, Object>> mpData = (List<Map<String, Object>>) data;
+            for (Map<String, Object> row : mpData) {
+                @SuppressWarnings("unchecked")
+                List<String> values = (List<String>) row.get(valueTag);
+                row.put("avg", values.stream().mapToLong(Long::valueOf).average().orElse(Double.NaN));
+            }
+
+            super.render(eleTemplate, data, template);
+        }
+    }
+}

+ 1 - 1
easier-report-biz/src/main/java/com/yaoyicloud/render/PublicRecordRender.java

@@ -21,7 +21,7 @@ import com.google.protobuf.util.JsonFormat;
 import com.yaoyicloud.message.FxyProtos.PublicRecord;
 
 /**
- * ServiceProviderInfo渲染器
+ * PublicRecord渲染器
  *
  */
 public final class PublicRecordRender extends AbstractRender {

+ 150 - 0
easier-report-biz/src/main/java/com/yaoyicloud/tools/Util.java

@@ -0,0 +1,150 @@
+package com.yaoyicloud.tools;
+
+import org.apache.poi.xwpf.usermodel.XWPFParagraph;
+import org.apache.poi.xwpf.usermodel.XWPFRun;
+import org.apache.poi.xwpf.usermodel.XWPFTable;
+import org.apache.poi.xwpf.usermodel.XWPFTableCell;
+import org.apache.poi.xwpf.usermodel.XWPFTableRow;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDecimalNumber;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTc;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.STVerticalJc;
+
+import com.deepoove.poi.util.TableTools;
+
+public final class Util {
+
+    /**
+     * 合并第n列
+     *
+     * @param table 表格对象
+     * @param sizeMap 任务汇总map
+     * @param count 合并前n列
+     */
+    public static void mergeFirstNCol(XWPFTable table, int n, int keySeq) {
+        if (table.getNumberOfRows() < 1) {
+            return;
+        }
+        int currentStart = 1;
+        for (int i = 1; i < table.getNumberOfRows() - 1; i++) { // 最后一行是合计
+            XWPFParagraph par = getParagraphForCell(table.getRow(i).getCell(keySeq));
+            String key = par.getRuns().get(0).getText(0);
+            for (int j = 0; j < n; j++) {
+                if (key.equals("")) {
+                    TableTools.mergeCellsVertically(table, j, currentStart, i);
+                } else {
+                    currentStart = i;
+                }
+                // 设置居中
+                verticalCentering(table.getRow(i).getCell(j));
+            }
+        }
+        int i = 0;
+        while (table.getRow(table.getNumberOfRows() - 1).getCell(i) != null) {
+            verticalCentering(table.getRow(table.getNumberOfRows() - 1).getCell(i));
+            i++;
+        }
+    }
+
+    public static void mergeFirstNColSimple(XWPFTable table, int n, int keySeq) {
+        if (table.getNumberOfRows() < 1) {
+            return;
+        }
+        int currentStart = 1;
+        String lastKey = "impossible_to_collide";
+        for (int i = 1; i < table.getNumberOfRows(); i++) {
+            XWPFParagraph par = getParagraphForCell(table.getRow(i).getCell(keySeq));
+            String key = par.getRuns().get(0).getText(0);
+            for (int j = 0; j < n; j++) {
+                if (lastKey.equals(key)) {
+                    // 清除被合并单元格内容
+                    getParagraphForCell(table.getRow(i).getCell(j)).getRuns().get(0).setText("");;
+                    TableTools.mergeCellsVertically(table, j, currentStart, i);
+                } else {
+                    currentStart = i;
+                }
+                // 设置居中
+                verticalCentering(table.getRow(i).getCell(j));
+            }
+            lastKey = key;
+        }
+    }
+
+    /**
+     * 垂直居中
+     *
+     * @param cell 单元格
+     */
+    public static void verticalCentering(XWPFTableCell cell) {
+        CTTc cttc = cell.getCTTc();
+        CTTcPr ctPr = cttc.addNewTcPr();
+        ctPr.addNewVAlign().setVal(STVerticalJc.CENTER);
+        // cttc.getPList().get(0).addNewPPr().addNewJc().setVal(STJc.CENTER);
+    }
+
+    /**
+     *
+     * @param cell
+     * @param val
+     */
+    public static void setText(XWPFTableCell cell, String val) {
+        cell.setText(val);
+        verticalCentering(cell);
+    }
+
+    /**
+     * 合计染色
+     *
+     * @param cell
+     */
+    public static XWPFTableCell colorSummary(XWPFTableCell cell, String value) {
+        XWPFParagraph par = getParagraphForCell(cell);
+        XWPFRun run = par.createRun();
+        run.setColor("8FAADC");
+        run.setText(value);
+        verticalCentering(cell);
+        return cell;
+    }
+
+    /**
+     *
+     * @param cell
+     * @param value
+     */
+    public static void replaceLastRowText(XWPFTableCell cell, String value) {
+        if (cell.getParagraph(cell.getCTTc().getPArray(0)) != null
+            && cell.getParagraph(cell.getCTTc().getPArray(0)).getRuns().size() > 0
+            && cell.getParagraph(cell.getCTTc().getPArray(0)).getRuns().get(0) != null) {
+            cell.getParagraph(cell.getCTTc().getPArray(0)).getRuns().get(0).setText(value, 0);
+        } else {
+            cell.setText(value);
+        }
+    }
+
+    private static XWPFParagraph getParagraphForCell(XWPFTableCell cell) {
+        CTTc ctTc = cell.getCTTc();
+        CTP ctP = (ctTc.sizeOfPArray() == 0) ? ctTc.addNewP() : ctTc.getPArray(0);
+        return new XWPFParagraph(ctP, cell);
+    }
+
+    public static int getColIndexOfFirstRow(XWPFTableCell cell) {
+        XWPFTableRow tableRow = cell.getTableRow();
+        int orginalCol = 0;
+        for (int i = 0; i < tableRow.getTableCells().size(); i++) {
+            XWPFTableCell current = tableRow.getCell(i);
+            int intValue = 1;
+            CTTcPr tcPr = current.getCTTc().getTcPr();
+            if (null != tcPr) {
+                CTDecimalNumber gridSpan = tcPr.getGridSpan();
+                if (null != gridSpan)
+                    intValue = gridSpan.getVal().intValue();
+            }
+            orginalCol += intValue;
+            if (current.getCTTc() == cell.getCTTc()) {
+                return orginalCol - intValue;
+            }
+        }
+        return -1;
+    }
+}

+ 76 - 0
easier-report-biz/src/main/proto/fxy.proto

@@ -18,6 +18,8 @@ message CheckItemDetail {
     optional int64 score = 5;      //
     optional string reviewResult = 6; // 复核结果
     optional int32 reviewScore = 7; // 复核评分
+    optional string category = 8;
+    optional int32 rank = 9;  // 展示排序
 }
 
 message CheckItemScore {
@@ -213,3 +215,77 @@ message PublicRecord {
     repeated AdministrativeSeriousIllegal severeViolations = 5; // 严重违法记录(3.5)
     optional CheckSummary publicRecordSummary = 6; // 公共记录评分及建议(3.6)
 }
+
+message FinancialData {
+    optional int32 year = 1; // 年份(如2022、2023、2024)
+    optional string donationIncome = 2; // 捐赠收入(单位:元)
+    optional string publicExpense = 3; // 公益事业支出(单位:元)
+    optional string totalAssets = 4; // 总资产(单位:元) 资产合计
+    optional string netAssets = 5; // 净资产(单位:元)
+    optional string totalIncome = 6; // 总收入(单位:元) 收入合计
+    optional string investmentIncome = 7; // 投资收益(单位:元)
+    optional string governmentGrants = 8; // 政府补助收入(单位:元)
+    optional string serviceIncome = 9; // 服务收入(单位:元)
+    optional string totalExpense = 10; // 总支出(单位:元)
+    optional string salaryExpense = 11; // 工资福利支出(单位:元)
+    optional string adminExpense = 12; // 行政办公支出(单位:元)
+    optional string activityCost = 13; // 业务活动成本(单位:元)
+    optional string managementExpense = 14; // 管理费用(单位:元)
+    optional string fundraisingExpense = 15; // 筹资费用(单位:元)
+    optional string lastYearFundBalance = 16; // 上年基金余额(单位:元)
+
+    optional string flowAssets = 17; // 流动资产
+    optional string flowLiabilities = 18; // 流动负债
+    optional string flowCapital = 19; // 营运资本
+    optional string fixedAsset = 20; // 固定资产
+    optional string inventory = 21; // 存货
+    optional string receivables = 22; // 应收账款
+    optional string liabTotal = 23; // 总负债
+    optional string las3yTotAmtLiaEquMap = 24; // 所有者权益
+    optional string operatingIncome = 25; // 营业收入
+    optional string mainBusInc = 26; // 主营业务收入
+    optional string mainBusProfit = 27; // 营业利润
+    optional string netProfit = 28; // 净利润
+    optional string las3yTotProfMap = 29; // 利润总额
+    optional string interestExpense = 30; // 利息支出
+
+    //捐赠项目成本
+    optional string donationProjectCost= 31;
+    //净资产合计
+    optional string totalNetAssets = 32;
+    //费用合计
+    optional string totalCost = 33;
+}
+
+message FinancialIndicator {
+    optional string category = 1; // 分类(如"运营能力"、"发展能力")
+    optional string indicatorName = 2; // 指标名称(如"公益支出比例")财务指标
+    optional string formula = 3; // 计算公式(如"公益支出/上年基金余额")
+
+    repeated string values = 4;  //按年取值
+}
+
+message FinancialInfo {
+    repeated FinancialData financialDataSeq = 1; // 重要财务数据(4.1)
+    repeated string years = 4;   //按年取值
+    optional string remark = 5; // 备注
+    repeated FinancialIndicator indicators = 9; // 财务指标(4.2)
+    repeated CheckItemDetail financialCheckDetails = 10;
+    optional CheckSummary financialSummary = 11; // 财务信息评分及建议(4.3)
+    repeated Attachment financialFiles = 12; // 没有财务解析时的临时方案
+
+    // 平台报告新加字段
+    optional string operatingRevenue = 13; // 最近一年营业收入
+    optional string neProfit = 14; // 最近一年净利润
+}
+
+message QuestionnaireItem {
+    optional int32 id = 1;
+    optional string question = 2;
+    optional string answer = 3;
+}
+
+message AntiBribery {
+    repeated QuestionnaireItem questionnaireItems = 1; // 反贿赂反腐败诚信保证问卷
+    optional CheckSummary antiBriberySummary = 2; // 反贿赂反腐败诚信保证评分及建议
+}

+ 43 - 0
easier-report-biz/src/test/java/com/yaoyicloud/render/test/TestFinancialInfoRender.java

@@ -0,0 +1,43 @@
+package com.yaoyicloud.render.test;
+
+import static org.junit.Assert.assertTrue;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import org.junit.Test;
+
+import com.yaoyicloud.message.FxyProtos.CheckItemDetail;
+import com.yaoyicloud.message.FxyProtos.CheckSummary;
+import com.yaoyicloud.message.FxyProtos.FinancialData;
+import com.yaoyicloud.message.FxyProtos.FinancialIndicator;
+import com.yaoyicloud.message.FxyProtos.FinancialInfo;
+import com.yaoyicloud.render.FinancialInfoRender;
+
+public class TestFinancialInfoRender {
+
+    @Test
+    public void testRenderDocx() throws IOException {
+
+        byte[] content = Files
+            .readAllBytes(Paths.get(getClass().getClassLoader().getResource("docx/finance.docx").getFile()));
+        FinancialInfoRender render = new FinancialInfoRender("../temp/");
+        String retPath = render.renderDocx(
+            FinancialInfo.newBuilder().setNeProfit("20").addYears("2024").addYears("2023").addYears("2022")
+                .setOperatingRevenue("999")
+                .setFinancialSummary(CheckSummary.newBuilder().setRiskSummary("high risk").setSuggestion("accept it"))
+                .addIndicators(
+                    FinancialIndicator.newBuilder().setCategory("cate1").setIndicatorName("name1").addValues("13")
+                        .addValues("14").addValues("15").setFormula("magic"))
+                .addFinancialDataSeq(FinancialData.newBuilder().setFixedAsset("1000").build())
+                .addFinancialCheckDetails(CheckItemDetail.newBuilder().setCategory("cate1").setName("n2")
+                    .setResult("r1").setScore(0).setReviewResult("rr1").setReviewScore(0).setRank(1))
+                .addFinancialCheckDetails(CheckItemDetail.newBuilder().setCategory("cate2").setName("n3")
+                    .setResult("r1").setScore(0).setReviewResult("rr1").setReviewScore(0).setRank(2))
+                .addFinancialCheckDetails(CheckItemDetail.newBuilder().setCategory("cate1").setName("n1")
+                    .setResult("r1").setScore(0).setReviewResult("rr1").setReviewScore(0).setRank(0))
+                .build(),
+            content);
+
+        assertTrue(retPath.length() > 0);
+    }
+}

BIN
easier-report-biz/src/test/resources/docx/finance.docx