云
4 天以前 f02d0df2f1dfc7a8b775cf50d45375e57628a5ef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
package cn.iocoder.yudao.module.crm.service.contract;
 
import cn.hutool.core.convert.Convert;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractRespVO;
import org.springframework.core.io.ClassPathResource;
import org.w3c.dom.*;
 
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.zip.*;
 
/** 在销售合同模板中填写合同信息,保留模板的其他内容及格式。 */
public final class CrmContractDocumentRenderer {
    private static final String W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
    private CrmContractDocumentRenderer() { }
 
    public static byte[] render(CrmContractRespVO contract) throws IOException {
        if (contract == null || contract.getProducts() == null || contract.getProducts().isEmpty()) {
            throw new IllegalArgumentException("合同不存在或没有产品明细,无法生成合同文件");
        }
        try (var input = new ClassPathResource("templates/contract/sale-contract.docx").getInputStream();
             var zip = new ZipInputStream(input); var output = new ByteArrayOutputStream();
             var result = new ZipOutputStream(output)) {
            ZipEntry entry;
            while ((entry = zip.getNextEntry()) != null) {
                byte[] content = zip.readAllBytes();
                if ("word/document.xml".equals(entry.getName())) content = fillDocument(content, contract);
                result.putNextEntry(new ZipEntry(entry.getName()));
                result.write(content);
                result.closeEntry();
            }
            result.finish();
            return output.toByteArray();
        }
    }
 
    private static byte[] fillDocument(byte[] xml, CrmContractRespVO contract) throws IOException {
        try {
            var factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
            factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
            var document = factory.newDocumentBuilder().parse(new ByteArrayInputStream(xml));
            var paragraphs = document.getElementsByTagNameNS(W, "p");
            boolean numberFilled = false, dateFilled = false, buyerFilled = false;
            for (int i = 0; i < paragraphs.getLength(); i++) {
                Node p = paragraphs.item(i);
                String value = p.getTextContent();
                if (value.contains("合同编号:SHHT20260102902-F")) {
                    setParagraphText(p, "合同编号:" + text(contract.getNo()));
                    alignRight(p);
                    numberFilled = true;
                } else if (value.trim().equals("签订日期:")) {
                    setParagraphText(p, "签订日期:" + date(contract.getOrderDate()));
                    alignRight(p);
                    dateFilled = true;
                } else if (value.contains("买方(甲方):") && value.contains("签订地点:江苏南通")) {
                    fillBuyer(p, text(contract.getCustomerName()));
                    buyerFilled = true;
                } else if (value.equals("乙方(盖章):")) {
                    // 原模板的空白买方签章框误写为乙方;同时处理 DrawingML 和 VML 两个兼容分支。
                    setParagraphText(p, "甲方(盖章):" + text(contract.getCustomerName()));
                }
            }
            if (!numberFilled || !dateFilled || !buyerFilled) throw new IOException("销售合同模板表头不完整");
 
            Node table = document.getElementsByTagNameNS(W, "tbl").item(0);
            List<Node> rows = children(table, "tr");
            if (rows.size() != 4 || children(rows.get(1), "tc").size() != 7
                    || !rows.get(3).getTextContent().contains("合计(含13%增值税)")) {
                throw new IOException("销售合同模板产品表格不完整");
            }
            Node rowTemplate = rows.get(1);
            BigDecimal productTotal = BigDecimal.ZERO;
            int index = 0;
            for (var product : contract.getProducts()) {
                Node row = rowTemplate.cloneNode(true);
                BigDecimal price = Objects.requireNonNull(product.getContractPrice(), "合同单价不能为空");
                BigDecimal count = Objects.requireNonNull(product.getCount(), "合同数量不能为空");
                BigDecimal amount = product.getTotalPrice() != null ? product.getTotalPrice()
                        : price.multiply(count).setScale(2, RoundingMode.HALF_UP);
                productTotal = productTotal.add(amount);
                String name = text(product.getItemName());
                if (name.isBlank()) name = text(product.getItemCode());
                List<Node> cells = children(row, "tc");
                String[] values = {String.valueOf(++index), name, text(product.getItemSpecification()),
                        text(product.getItemUnitName()), count.stripTrailingZeros().toPlainString(), money(price), money(amount)};
                for (int i = 0; i < values.length; i++) {
                    Node cell = cells.get(i);
                    Node properties = firstChild(cell, "tcPr");
                    Node noWrap = firstChild(properties, "noWrap");
                    if (noWrap != null) properties.removeChild(noWrap);
                    setParagraphText(firstChild(cell, "p"), values[i]);
                }
                Node rowProperties = firstChild(row, "trPr");
                if (firstChild(rowProperties, "cantSplit") == null) {
                    rowProperties.appendChild(document.createElementNS(W, "w:cantSplit"));
                }
                rowTemplate.getParentNode().insertBefore(row, rowTemplate);
            }
            rowTemplate.getParentNode().removeChild(rowTemplate);
            BigDecimal total = Objects.requireNonNull(contract.getTotalPrice(), "合同总金额不能为空");
            BigDecimal discount = productTotal.subtract(total);
            String summary = "合计(含13%增值税)  人民币:" + money(total) + "元  大写:" + Convert.digitToChinese(total);
            if (discount.signum() != 0) summary += "(明细合计 " + money(productTotal) + " 元,与合同金额的差额 " + money(discount) + " 元)";
            Node totalCell = children(rows.get(3), "tc").get(0);
            Node totalProperties = firstChild(totalCell, "tcPr");
            Node noWrap = firstChild(totalProperties, "noWrap");
            if (noWrap != null) totalProperties.removeChild(noWrap);
            setParagraphText(firstChild(totalCell, "p"), summary);
            var transformerFactory = TransformerFactory.newInstance();
            transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
            transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
            var output = new ByteArrayOutputStream();
            transformerFactory.newTransformer().transform(new DOMSource(document), new StreamResult(output));
            return output.toByteArray();
        } catch (IOException exception) { throw exception;
        } catch (Exception exception) { throw new IOException("生成合同 Word 文件失败", exception); }
    }
 
    private static void fillBuyer(Node paragraph, String buyer) {
        var nodes = ((Element) paragraph).getElementsByTagNameNS(W, "t");
        boolean afterColon = false, inserted = false;
        for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);
            String value = node.getTextContent();
            if (value.contains("签订地点:")) break;
            if (afterColon) {
                node.setTextContent(inserted ? "" : buyer + "  ");
                inserted = true;
            } else if (value.equals(":")) afterColon = true;
        }
        if (!inserted) throw new IllegalStateException("合同模板缺少买方填写位置");
    }
 
    private static void alignRight(Node paragraph) {
        Node properties = firstChild(paragraph, "pPr");
        Node indent = firstChild(properties, "ind");
        if (indent != null) properties.removeChild(indent);
        Node alignment = firstChild(properties, "jc");
        if (alignment == null) {
            alignment = paragraph.getOwnerDocument().createElementNS(W, "w:jc");
            properties.appendChild(alignment);
        }
        ((Element) alignment).setAttributeNS(W, "w:val", "right");
    }
 
    private static void setParagraphText(Node paragraph, String value) {
        var nodes = ((Element) paragraph).getElementsByTagNameNS(W, "t");
        if (nodes.getLength() == 0) {
            var document = paragraph.getOwnerDocument();
            Node run = document.createElementNS(W, "w:r");
            Node text = document.createElementNS(W, "w:t");
            run.appendChild(text);
            paragraph.appendChild(run);
            text.setTextContent(value);
        } else {
            nodes.item(0).setTextContent(value);
            for (int i = 1; i < nodes.getLength(); i++) nodes.item(i).setTextContent("");
        }
    }
 
    private static List<Node> children(Node parent, String localName) {
        List<Node> result = new ArrayList<>();
        if (parent == null) return result;
        for (Node node = parent.getFirstChild(); node != null; node = node.getNextSibling()) {
            if (W.equals(node.getNamespaceURI()) && localName.equals(node.getLocalName())) result.add(node);
        }
        return result;
    }
    private static Node firstChild(Node parent, String localName) {
        List<Node> result = children(parent, localName);
        return result.isEmpty() ? null : result.get(0);
    }
    private static String money(BigDecimal value) { return value.setScale(2, RoundingMode.HALF_UP).toPlainString(); }
    private static String date(java.time.LocalDateTime value) { return value == null ? "____________" : value.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")); }
    private static String text(String value) { return value == null ? "" : value; }
}