einvoice-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -0
- package/bin/cli.js +52 -0
- package/index.js +23 -0
- package/lib/BaseInvoiceService.js +158 -0
- package/lib/ErrorHandler.js +98 -0
- package/lib/Invoice.js +108 -0
- package/lib/InvoiceValidator.js +422 -0
- package/lib/OfdInvoiceExtractor.js +170 -0
- package/lib/PDFTextPositionAnalyzer.js +366 -0
- package/lib/PdfFinancialInvoiceService.js +134 -0
- package/lib/PdfFullElectronicInvoiceService.js +325 -0
- package/lib/PdfInvoiceExtractor.js +124 -0
- package/lib/PdfRegularInvoiceService.js +786 -0
- package/lib/RegexPatterns.js +202 -0
- package/lib/StringUtils.js +70 -0
- package/lib/extractor.js +24 -0
- package/package.json +31 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 发票字段验证器
|
|
3
|
+
* 对识别的发票进行数据验证和修正
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class InvoiceValidator {
|
|
7
|
+
/**
|
|
8
|
+
* 验证发票对象的所有字段
|
|
9
|
+
* @param {Invoice} invoice - 发票对象
|
|
10
|
+
* @returns {object} {valid: boolean, errors: [], warnings: []}
|
|
11
|
+
*/
|
|
12
|
+
static validate(invoice) {
|
|
13
|
+
const errors = [];
|
|
14
|
+
const warnings = [];
|
|
15
|
+
|
|
16
|
+
// 验证标题
|
|
17
|
+
if (!invoice.title) {
|
|
18
|
+
warnings.push('缺少发票标题');
|
|
19
|
+
} else {
|
|
20
|
+
const titleValidation = this.validateTitle(invoice.title);
|
|
21
|
+
if (!titleValidation.valid) {
|
|
22
|
+
warnings.push(titleValidation.message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 验证发票类型
|
|
27
|
+
if (!invoice.type) {
|
|
28
|
+
warnings.push('缺少发票类型');
|
|
29
|
+
} else {
|
|
30
|
+
const typeValidation = this.validateInvoiceType(invoice.type);
|
|
31
|
+
if (!typeValidation.valid) {
|
|
32
|
+
warnings.push(typeValidation.message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 验证日期
|
|
37
|
+
if (invoice.date) {
|
|
38
|
+
const dateValidation = this.validateDate(invoice.date);
|
|
39
|
+
if (!dateValidation.valid) {
|
|
40
|
+
warnings.push(dateValidation.message);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
warnings.push('缺少开票日期');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 验证机器编号
|
|
47
|
+
if (invoice.machineNumber) {
|
|
48
|
+
const mnValidation = this.validateMachineNumber(invoice.machineNumber);
|
|
49
|
+
if (!mnValidation.valid) {
|
|
50
|
+
errors.push(mnValidation.message);
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
warnings.push('缺少机器编号');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 验证发票代码
|
|
57
|
+
if (invoice.code) {
|
|
58
|
+
const codeValidation = this.validateInvoiceCode(invoice.code);
|
|
59
|
+
if (!codeValidation.valid) {
|
|
60
|
+
errors.push(codeValidation.message);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
warnings.push('缺少发票代码');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 验证发票号码
|
|
67
|
+
if (invoice.number) {
|
|
68
|
+
const numberValidation = this.validateInvoiceNumber(invoice.number);
|
|
69
|
+
if (!numberValidation.valid) {
|
|
70
|
+
errors.push(numberValidation.message);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
warnings.push('缺少发票号码');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 验证金额
|
|
77
|
+
const amountValidation = this.validateAmounts(invoice);
|
|
78
|
+
if (!amountValidation.valid) {
|
|
79
|
+
errors.push(...amountValidation.errors);
|
|
80
|
+
warnings.push(...amountValidation.warnings);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 验证购销方信息
|
|
84
|
+
const partyValidation = this.validatePartyInfo(invoice);
|
|
85
|
+
if (!partyValidation.valid) {
|
|
86
|
+
warnings.push(...partyValidation.warnings);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 验证校验码
|
|
90
|
+
if (invoice.checksum) {
|
|
91
|
+
const checksumValidation = this.validateChecksum(invoice.checksum);
|
|
92
|
+
if (!checksumValidation.valid) {
|
|
93
|
+
warnings.push(checksumValidation.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
valid: errors.length === 0,
|
|
99
|
+
errors,
|
|
100
|
+
warnings,
|
|
101
|
+
suggestions: this.generateSuggestions(invoice, errors, warnings)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 验证发票标题格式
|
|
107
|
+
*/
|
|
108
|
+
static validateTitle(title) {
|
|
109
|
+
const validTitles = ['电子普通发票', '电子专用发票', '普通发票', '专用发票', '通行费'];
|
|
110
|
+
const normalized = title.trim();
|
|
111
|
+
|
|
112
|
+
for (const validTitle of validTitles) {
|
|
113
|
+
if (normalized.includes(validTitle)) {
|
|
114
|
+
return { valid: true };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
valid: false,
|
|
120
|
+
message: `发票标题格式异常: ${title}`
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 验证发票类型
|
|
126
|
+
*/
|
|
127
|
+
static validateInvoiceType(type) {
|
|
128
|
+
const validTypes = ['普通发票', '专用发票', '通行费', '电子发票', '财政票据'];
|
|
129
|
+
if (validTypes.includes(type)) {
|
|
130
|
+
return { valid: true };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
valid: false,
|
|
135
|
+
message: `发票类型无效: ${type}`
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 验证日期格式
|
|
141
|
+
*/
|
|
142
|
+
static validateDate(date) {
|
|
143
|
+
const dateRegex = /(\d{4})年(\d{2})月(\d{2})日/;
|
|
144
|
+
const match = date.match(dateRegex);
|
|
145
|
+
|
|
146
|
+
if (!match) {
|
|
147
|
+
return {
|
|
148
|
+
valid: false,
|
|
149
|
+
message: `日期格式异常: ${date}`
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const [, year, month, day] = match.map(x => parseInt(x));
|
|
154
|
+
|
|
155
|
+
// 检查日期合理性
|
|
156
|
+
if (month < 1 || month > 12) {
|
|
157
|
+
return {
|
|
158
|
+
valid: false,
|
|
159
|
+
message: `月份无效: ${month}`
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (day < 1 || day > 31) {
|
|
164
|
+
return {
|
|
165
|
+
valid: false,
|
|
166
|
+
message: `日期无效: ${day}`
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 检查年份(发票通常不会太旧或太新)
|
|
171
|
+
const currentYear = new Date().getFullYear();
|
|
172
|
+
if (year < 2000 || year > currentYear + 1) {
|
|
173
|
+
return {
|
|
174
|
+
valid: false,
|
|
175
|
+
message: `年份异常: ${year}`
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { valid: true };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 验证机器编号(12位数字)
|
|
184
|
+
*/
|
|
185
|
+
static validateMachineNumber(machineNumber) {
|
|
186
|
+
if (!/^\d{12}$/.test(machineNumber)) {
|
|
187
|
+
return {
|
|
188
|
+
valid: false,
|
|
189
|
+
message: `机器编号格式错误(应为12位数字): ${machineNumber}`
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return { valid: true };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 验证发票代码(12位数字)
|
|
197
|
+
*/
|
|
198
|
+
static validateInvoiceCode(code) {
|
|
199
|
+
if (!/^\d{12}$/.test(code)) {
|
|
200
|
+
return {
|
|
201
|
+
valid: false,
|
|
202
|
+
message: `发票代码格式错误(应为12位数字): ${code}`
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return { valid: true };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 验证发票号码
|
|
210
|
+
*/
|
|
211
|
+
static validateInvoiceNumber(number) {
|
|
212
|
+
// 普通发票号码: 1开头的9-10位数字
|
|
213
|
+
if (!/^\d{8,10}$/.test(number)) {
|
|
214
|
+
return {
|
|
215
|
+
valid: false,
|
|
216
|
+
message: `发票号码格式错误: ${number}`
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return { valid: true };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 验证金额信息
|
|
224
|
+
*/
|
|
225
|
+
static validateAmounts(invoice) {
|
|
226
|
+
const errors = [];
|
|
227
|
+
const warnings = [];
|
|
228
|
+
|
|
229
|
+
// 检查是否有金额
|
|
230
|
+
if (!invoice.amount && !invoice.totalAmount) {
|
|
231
|
+
warnings.push('缺少金额信息');
|
|
232
|
+
return { valid: true, errors, warnings };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 验证金额格式
|
|
236
|
+
const amount = this.parseAmount(invoice.amount);
|
|
237
|
+
const taxAmount = this.parseAmount(invoice.taxAmount);
|
|
238
|
+
const totalAmount = this.parseAmount(invoice.totalAmount);
|
|
239
|
+
|
|
240
|
+
if (amount !== null && isNaN(amount)) {
|
|
241
|
+
errors.push(`金额格式错误: ${invoice.amount}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (taxAmount !== null && isNaN(taxAmount)) {
|
|
245
|
+
warnings.push(`税额格式错误: ${invoice.taxAmount}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (totalAmount !== null && isNaN(totalAmount)) {
|
|
249
|
+
warnings.push(`价税合计格式错误: ${invoice.totalAmount}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 验证金额逻辑关系
|
|
253
|
+
if (amount !== null && taxAmount !== null && totalAmount !== null) {
|
|
254
|
+
const calculated = parseFloat((amount + taxAmount).toFixed(2));
|
|
255
|
+
const actual = parseFloat(totalAmount);
|
|
256
|
+
|
|
257
|
+
// 允许 0.01 的误差
|
|
258
|
+
if (Math.abs(calculated - actual) > 0.01) {
|
|
259
|
+
warnings.push(
|
|
260
|
+
`金额不匹配: ${amount} + ${taxAmount} = ${calculated}, 而总额为 ${actual}`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 金额合理性检查
|
|
266
|
+
if (amount !== null && (amount < 0 || amount > 99999999)) {
|
|
267
|
+
warnings.push(`金额异常(超出合理范围): ${amount}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
valid: errors.length === 0,
|
|
272
|
+
errors,
|
|
273
|
+
warnings
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 验证购销方信息
|
|
279
|
+
*/
|
|
280
|
+
static validatePartyInfo(invoice) {
|
|
281
|
+
const warnings = [];
|
|
282
|
+
|
|
283
|
+
// 检查购买方信息
|
|
284
|
+
if (!invoice.buyerName) {
|
|
285
|
+
warnings.push('缺少购买方名称');
|
|
286
|
+
}
|
|
287
|
+
if (!invoice.buyerCode) {
|
|
288
|
+
warnings.push('缺少购买方税号');
|
|
289
|
+
} else if (!/^[0-9A-Z]{18}$/.test(invoice.buyerCode.trim())) {
|
|
290
|
+
warnings.push(`购买方税号格式异常: ${invoice.buyerCode}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 检查销售方信息
|
|
294
|
+
if (!invoice.sellerName) {
|
|
295
|
+
warnings.push('缺少销售方名称');
|
|
296
|
+
}
|
|
297
|
+
if (!invoice.sellerCode) {
|
|
298
|
+
warnings.push('缺少销售方税号');
|
|
299
|
+
} else if (!/^[0-9A-Z]{18}$/.test(invoice.sellerCode.trim())) {
|
|
300
|
+
warnings.push(`销售方税号格式异常: ${invoice.sellerCode}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
valid: warnings.length === 0,
|
|
305
|
+
warnings
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 验证校验码格式
|
|
311
|
+
*/
|
|
312
|
+
static validateChecksum(checksum) {
|
|
313
|
+
// 校验码通常是 20 位数字或特殊格式
|
|
314
|
+
if (!/^\d{20}/.test(checksum)) {
|
|
315
|
+
return {
|
|
316
|
+
valid: false,
|
|
317
|
+
message: `校验码格式异常: ${checksum}`
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return { valid: true };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 解析金额字符串
|
|
325
|
+
* @param {string|number} amount - 金额字符串或数字
|
|
326
|
+
* @returns {number} 解析后的数字,或 NaN
|
|
327
|
+
*/
|
|
328
|
+
static parseAmount(amount) {
|
|
329
|
+
if (!amount) return null;
|
|
330
|
+
if (typeof amount === 'number') return amount;
|
|
331
|
+
|
|
332
|
+
// 提取数字部分
|
|
333
|
+
const match = String(amount).match(/[\d.]+/);
|
|
334
|
+
if (match) {
|
|
335
|
+
const parsed = parseFloat(match[0]);
|
|
336
|
+
return isNaN(parsed) ? NaN : parsed;
|
|
337
|
+
}
|
|
338
|
+
return NaN;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* 生成改进建议
|
|
343
|
+
*/
|
|
344
|
+
static generateSuggestions(invoice, errors, warnings) {
|
|
345
|
+
const suggestions = [];
|
|
346
|
+
|
|
347
|
+
// 根据错误类型提供建议
|
|
348
|
+
if (errors.some(e => e.includes('机器编号'))) {
|
|
349
|
+
suggestions.push('检查 PDF 的机器编号字段是否清晰');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (warnings.some(w => w.includes('购买方'))) {
|
|
353
|
+
suggestions.push('使用坐标定位功能重新提取购买方信息');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (warnings.some(w => w.includes('金额不匹配'))) {
|
|
357
|
+
suggestions.push('验证金额提取的准确性,可能因文本识别错误导致');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (warnings.length > 5) {
|
|
361
|
+
suggestions.push('建议手动审查完整的 PDF 文件');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return suggestions;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* 修正常见的识别错误
|
|
369
|
+
* @param {Invoice} invoice - 发票对象
|
|
370
|
+
* @returns {Invoice} 修正后的发票对象
|
|
371
|
+
*/
|
|
372
|
+
static correctCommonErrors(invoice) {
|
|
373
|
+
// 修正数字的常见误识别
|
|
374
|
+
if (invoice.machineNumber) {
|
|
375
|
+
invoice.machineNumber = this.correctNumberString(invoice.machineNumber, 12);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (invoice.code) {
|
|
379
|
+
invoice.code = this.correctNumberString(invoice.code, 12);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (invoice.number) {
|
|
383
|
+
invoice.number = this.correctNumberString(invoice.number, 10);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 修正金额
|
|
387
|
+
if (invoice.amount) {
|
|
388
|
+
invoice.amount = this.correctAmountString(invoice.amount);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (invoice.taxAmount) {
|
|
392
|
+
invoice.taxAmount = this.correctAmountString(invoice.taxAmount);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (invoice.totalAmount) {
|
|
396
|
+
invoice.totalAmount = this.correctAmountString(invoice.totalAmount);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return invoice;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* 修正数字字符串(移除非数字)
|
|
404
|
+
*/
|
|
405
|
+
static correctNumberString(str, expectedLength) {
|
|
406
|
+
if (!str) return null;
|
|
407
|
+
const cleaned = String(str).replace(/[^\d]/g, '');
|
|
408
|
+
return cleaned.length === expectedLength ? cleaned : str;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 修正金额字符串
|
|
413
|
+
*/
|
|
414
|
+
static correctAmountString(str) {
|
|
415
|
+
if (!str) return null;
|
|
416
|
+
const cleaned = String(str).replace(/[^\d.]/g, '');
|
|
417
|
+
const match = cleaned.match(/^(\d+\.?\d*)$/);
|
|
418
|
+
return match ? match[1] : str;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
module.exports = InvoiceValidator;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const JSZip = require('jszip');
|
|
3
|
+
const { parseStringPromise } = require('xml2js');
|
|
4
|
+
const { Invoice, Detail } = require('./Invoice');
|
|
5
|
+
const ErrorHandler = require('./ErrorHandler');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* OFD 发票提取器
|
|
9
|
+
*/
|
|
10
|
+
class OfdInvoiceExtractor {
|
|
11
|
+
static async extract(filePath) {
|
|
12
|
+
try {
|
|
13
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
14
|
+
const zip = await JSZip.loadAsync(fileBuffer);
|
|
15
|
+
|
|
16
|
+
// 读取 XML 文件
|
|
17
|
+
const xmlFile = zip.file('Doc_0/Attachs/original_invoice.xml');
|
|
18
|
+
if (!xmlFile) {
|
|
19
|
+
throw new Error('Missing original_invoice.xml');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const contentFile = zip.file('Doc_0/Pages/Page_0/Content.xml');
|
|
23
|
+
const xmlContent = await xmlFile.async('string');
|
|
24
|
+
const contentXml = contentFile ? await contentFile.async('string') : '';
|
|
25
|
+
|
|
26
|
+
// 解析 XML
|
|
27
|
+
const parsed = await parseStringPromise(xmlContent);
|
|
28
|
+
const root = parsed.invoice || parsed.Invoice || {};
|
|
29
|
+
|
|
30
|
+
// 创建发票对象
|
|
31
|
+
const invoice = new Invoice();
|
|
32
|
+
|
|
33
|
+
// 提取基础信息
|
|
34
|
+
this.extractFromXml(invoice, root);
|
|
35
|
+
|
|
36
|
+
// 从 content.xml 中提取标题
|
|
37
|
+
if (contentXml) {
|
|
38
|
+
const titleMatch = contentXml.match(/<ofd:TextCode[^>]*>([^<]*发票[^<]*)<\/ofd:TextCode>/);
|
|
39
|
+
if (titleMatch) {
|
|
40
|
+
invoice.title = titleMatch[1];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const amountStringMatch = contentXml.match(/圆整<\/ofd:TextCode>([^<]*)/);
|
|
44
|
+
if (amountStringMatch) {
|
|
45
|
+
invoice.totalAmountString = amountStringMatch[1].substring(0, 2);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return invoice;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return ErrorHandler.createErrorInvoice(error, 'ofd');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static extractFromXml(invoice, root) {
|
|
56
|
+
// 基础信息
|
|
57
|
+
invoice.machineNumber = this.getElementValue(root, 'MachineNo');
|
|
58
|
+
invoice.code = this.getElementValue(root, 'InvoiceCode');
|
|
59
|
+
invoice.number = this.getElementValue(root, 'InvoiceNo');
|
|
60
|
+
invoice.date = this.getElementValue(root, 'IssueDate');
|
|
61
|
+
invoice.checksum = this.getElementValue(root, 'InvoiceCheckCode');
|
|
62
|
+
|
|
63
|
+
// 金额
|
|
64
|
+
const amount = this.getElementValue(root, 'TaxExclusiveTotalAmount');
|
|
65
|
+
if (amount) invoice.amount = amount;
|
|
66
|
+
|
|
67
|
+
const taxTotalStr = this.getElementValue(root, 'TaxTotalAmount');
|
|
68
|
+
if (taxTotalStr) {
|
|
69
|
+
const taxAmount = StringUtils.extractNumber(taxTotalStr);
|
|
70
|
+
if (taxAmount) invoice.taxAmount = taxAmount;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const totalAmount = this.getElementValue(root, 'TaxInclusiveTotalAmount');
|
|
74
|
+
if (totalAmount) invoice.totalAmount = totalAmount;
|
|
75
|
+
|
|
76
|
+
// 人员信息
|
|
77
|
+
invoice.payee = this.getElementValue(root, 'Payee');
|
|
78
|
+
invoice.reviewer = this.getElementValue(root, 'Checker');
|
|
79
|
+
invoice.drawer = this.getElementValue(root, 'InvoiceClerk');
|
|
80
|
+
invoice.password = this.getElementValue(root, 'TaxControlCode');
|
|
81
|
+
|
|
82
|
+
// 发票类型
|
|
83
|
+
const title = this.getElementValue(root, 'InvoiceTitle') || invoice.title;
|
|
84
|
+
if (title) {
|
|
85
|
+
invoice.title = title;
|
|
86
|
+
if (title.includes('专用发票')) {
|
|
87
|
+
invoice.type = '专用发票';
|
|
88
|
+
} else if (title.includes('通行费')) {
|
|
89
|
+
invoice.type = '通行费';
|
|
90
|
+
} else {
|
|
91
|
+
invoice.type = '普通发票';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 购方信息
|
|
96
|
+
const buyer = this.getElement(root, 'Buyer');
|
|
97
|
+
if (buyer) {
|
|
98
|
+
invoice.buyerName = this.getElementValue(buyer, 'BuyerName');
|
|
99
|
+
invoice.buyerCode = this.getElementValue(buyer, 'BuyerTaxID');
|
|
100
|
+
invoice.buyerAddress = this.getElementValue(buyer, 'BuyerAddrTel');
|
|
101
|
+
invoice.buyerAccount = this.getElementValue(buyer, 'BuyerFinancialAccount');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 销方信息
|
|
105
|
+
const seller = this.getElement(root, 'Seller');
|
|
106
|
+
if (seller) {
|
|
107
|
+
invoice.sellerName = this.getElementValue(seller, 'SellerName');
|
|
108
|
+
invoice.sellerCode = this.getElementValue(seller, 'SellerTaxID');
|
|
109
|
+
invoice.sellerAddress = this.getElementValue(seller, 'SellerAddrTel');
|
|
110
|
+
invoice.sellerAccount = this.getElementValue(seller, 'SellerFinancialAccount');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 明细
|
|
114
|
+
const goodsInfos = this.getElement(root, 'GoodsInfos');
|
|
115
|
+
if (goodsInfos) {
|
|
116
|
+
const details = [];
|
|
117
|
+
const items = Array.isArray(goodsInfos['GoodsInfo']) ? goodsInfos['GoodsInfo'] : [goodsInfos['GoodsInfo']];
|
|
118
|
+
|
|
119
|
+
if (items) {
|
|
120
|
+
for (const item of items) {
|
|
121
|
+
const detail = new Detail();
|
|
122
|
+
detail.name = this.getElementValue(item, 'Item');
|
|
123
|
+
detail.amount = this.getElementValue(item, 'Amount');
|
|
124
|
+
detail.count = this.getElementValue(item, 'Quantity');
|
|
125
|
+
detail.price = this.getElementValue(item, 'Price');
|
|
126
|
+
detail.unit = this.getElementValue(item, 'MeasurementDimension');
|
|
127
|
+
detail.model = this.getElementValue(item, 'Specification');
|
|
128
|
+
|
|
129
|
+
const taxAmountStr = this.getElementValue(item, 'TaxAmount');
|
|
130
|
+
if (taxAmountStr) {
|
|
131
|
+
const taxAmount = StringUtils.extractNumber(taxAmountStr);
|
|
132
|
+
if (taxAmount) detail.taxAmount = taxAmount;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const taxRateStr = this.getElementValue(item, 'TaxScheme');
|
|
136
|
+
if (taxRateStr) {
|
|
137
|
+
const rateNum = parseInt(taxRateStr.replace('%', ''));
|
|
138
|
+
detail.taxRate = (rateNum / 100).toString();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
details.push(detail);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
invoice.details = details;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 获取 XML element
|
|
151
|
+
*/
|
|
152
|
+
static getElement(obj, key) {
|
|
153
|
+
if (!obj || !key) return null;
|
|
154
|
+
return obj[key] ? (Array.isArray(obj[key]) ? obj[key][0] : obj[key]) : null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 获取 XML element 的文本值
|
|
159
|
+
*/
|
|
160
|
+
static getElementValue(obj, key) {
|
|
161
|
+
if (!obj || !key) return null;
|
|
162
|
+
const elem = this.getElement(obj, key);
|
|
163
|
+
if (!elem) return null;
|
|
164
|
+
if (typeof elem === 'string') return elem;
|
|
165
|
+
if (elem._) return elem._;
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = OfdInvoiceExtractor;
|