n8n-nodes-contract-lite 1.0.0 → 2.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/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/src/ContractGenerator.node.d.ts +6 -0
- package/dist/src/ContractGenerator.node.js +97 -0
- package/dist/src/DocumentEngine.d.ts +10 -0
- package/dist/src/DocumentEngine.js +131 -0
- package/dist/src/Roboto-Regular.ttf +0 -0
- package/dist/src/types.d.ts +11 -0
- package/dist/src/types.js +2 -0
- package/index.ts +1 -1
- package/package.json +5 -4
- package/scripts/copy-fonts.js +16 -0
- package/src/ContractGenerator.node.ts +103 -0
- package/src/DocumentEngine.ts +141 -0
- package/src/Roboto-Regular.ttf +0 -0
- package/src/types.ts +8 -0
- package/src/ContractGenerator.ts +0 -90
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { ContractGenerator } from './src/ContractGenerator';
|
|
1
|
+
import { ContractGenerator } from './src/ContractGenerator.node';
|
|
2
2
|
export { ContractGenerator };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ContractGenerator = void 0;
|
|
4
|
-
const
|
|
5
|
-
Object.defineProperty(exports, "ContractGenerator", { enumerable: true, get: function () { return
|
|
4
|
+
const ContractGenerator_node_1 = require("./src/ContractGenerator.node");
|
|
5
|
+
Object.defineProperty(exports, "ContractGenerator", { enumerable: true, get: function () { return ContractGenerator_node_1.ContractGenerator; } });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class ContractGenerator implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
5
|
+
private static getTitle;
|
|
6
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ContractGenerator = void 0;
|
|
4
|
+
const DocumentEngine_1 = require("./DocumentEngine");
|
|
5
|
+
class ContractGenerator {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.description = {
|
|
8
|
+
displayName: 'Document Generator Pro (RU)',
|
|
9
|
+
name: 'documentGeneratorPro',
|
|
10
|
+
icon: 'fa:file-signature',
|
|
11
|
+
group: ['transform'],
|
|
12
|
+
version: 1,
|
|
13
|
+
description: 'Премиальный генератор документов с защитой',
|
|
14
|
+
defaults: { name: 'Генератор документов' },
|
|
15
|
+
inputs: ['main'],
|
|
16
|
+
outputs: ['main'],
|
|
17
|
+
properties: [
|
|
18
|
+
{
|
|
19
|
+
displayName: 'Тип шаблона',
|
|
20
|
+
name: 'template',
|
|
21
|
+
type: 'options',
|
|
22
|
+
options: [
|
|
23
|
+
{ name: 'Счет (Invoice)', value: 'invoice' },
|
|
24
|
+
{ name: 'Договор (Contract)', value: 'contract' },
|
|
25
|
+
{ name: 'Накладная (Waybill)', value: 'waybill' },
|
|
26
|
+
{ name: 'Акт (Act)', value: 'act' },
|
|
27
|
+
],
|
|
28
|
+
default: 'invoice',
|
|
29
|
+
},
|
|
30
|
+
{ displayName: 'Название компании', name: 'companyName', type: 'string', default: 'ООО КНК ГРУПП' },
|
|
31
|
+
{ displayName: 'Имя клиента', name: 'clientName', type: 'string', default: 'Иван Иванов' },
|
|
32
|
+
// Настройки штампа
|
|
33
|
+
{ displayName: 'Показать штамп', name: 'showStamp', type: 'boolean', default: true },
|
|
34
|
+
{
|
|
35
|
+
displayName: 'Текст штампа',
|
|
36
|
+
name: 'status',
|
|
37
|
+
type: 'string',
|
|
38
|
+
default: 'ОПЛАЧЕНО',
|
|
39
|
+
displayOptions: { show: { showStamp: [true] } }
|
|
40
|
+
},
|
|
41
|
+
// Контент
|
|
42
|
+
{
|
|
43
|
+
displayName: 'Текст документа',
|
|
44
|
+
name: 'content',
|
|
45
|
+
type: 'string',
|
|
46
|
+
typeOptions: { rows: 5 },
|
|
47
|
+
default: 'Настоящий документ подтверждает...',
|
|
48
|
+
displayOptions: { show: { template: ['contract', 'act'] } }
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
displayName: 'Данные таблицы (JSON)',
|
|
52
|
+
name: 'details',
|
|
53
|
+
type: 'json',
|
|
54
|
+
default: '[]',
|
|
55
|
+
description: 'Пример: [{"item": "Услуга", "qty": 1, "price": 100, "total": 100}]',
|
|
56
|
+
displayOptions: { hide: { template: ['contract'] } }
|
|
57
|
+
},
|
|
58
|
+
// Стили
|
|
59
|
+
{ displayName: 'Цвет бренда', name: 'headerColor', type: 'color', default: '#1a2b3c' },
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async execute() {
|
|
64
|
+
const items = this.getInputData();
|
|
65
|
+
const returnData = [];
|
|
66
|
+
for (let i = 0; i < items.length; i++) {
|
|
67
|
+
const template = this.getNodeParameter('template', i);
|
|
68
|
+
// Вызываем статичный метод через имя класса
|
|
69
|
+
const title = ContractGenerator.getTitle(template);
|
|
70
|
+
const data = {
|
|
71
|
+
companyName: this.getNodeParameter('companyName', i),
|
|
72
|
+
clientName: this.getNodeParameter('clientName', i),
|
|
73
|
+
showStamp: this.getNodeParameter('showStamp', i),
|
|
74
|
+
status: this.getNodeParameter('showStamp', i) ? this.getNodeParameter('status', i) : '',
|
|
75
|
+
content: this.getNodeParameter('content', i, ''),
|
|
76
|
+
details: this.getNodeParameter('details', i, '[]'),
|
|
77
|
+
headerColor: this.getNodeParameter('headerColor', i),
|
|
78
|
+
title: title,
|
|
79
|
+
};
|
|
80
|
+
const buffer = await DocumentEngine_1.DocumentEngine.createPDF(data, template);
|
|
81
|
+
const binaryData = await this.helpers.prepareBinaryData(buffer, `${template}_${Date.now()}.pdf`);
|
|
82
|
+
returnData.push({ json: { success: true, timestamp: new Date() }, binary: { data: binaryData } });
|
|
83
|
+
}
|
|
84
|
+
return [returnData];
|
|
85
|
+
}
|
|
86
|
+
// Добавили static
|
|
87
|
+
static getTitle(template) {
|
|
88
|
+
const titles = {
|
|
89
|
+
invoice: 'Счет на оплату',
|
|
90
|
+
contract: 'Договор поставки',
|
|
91
|
+
act: 'Акт выполненных работ',
|
|
92
|
+
waybill: 'Товарная накладная'
|
|
93
|
+
};
|
|
94
|
+
return titles[template] || 'Документ';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.ContractGenerator = ContractGenerator;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class DocumentEngine {
|
|
2
|
+
static createPDF(data: any, template: string): Promise<Buffer>;
|
|
3
|
+
private static drawPattern;
|
|
4
|
+
private static drawHeader;
|
|
5
|
+
private static drawCards;
|
|
6
|
+
private static drawStamp;
|
|
7
|
+
private static drawTable;
|
|
8
|
+
private static drawSignatures;
|
|
9
|
+
private static drawFooter;
|
|
10
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DocumentEngine = void 0;
|
|
7
|
+
const pdfkit_1 = __importDefault(require("pdfkit"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
11
|
+
class DocumentEngine {
|
|
12
|
+
static async createPDF(data, template) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const doc = new pdfkit_1.default({ size: 'A4', margin: 40, bufferPages: true });
|
|
15
|
+
const chunks = [];
|
|
16
|
+
doc.on('data', chunk => chunks.push(chunk));
|
|
17
|
+
doc.on('end', () => resolve(Buffer.concat(chunks)));
|
|
18
|
+
doc.on('error', reject);
|
|
19
|
+
const fontPath = path_1.default.join(__dirname, 'Roboto-Regular.ttf');
|
|
20
|
+
if (fs_1.default.existsSync(fontPath))
|
|
21
|
+
doc.registerFont('Main', fontPath);
|
|
22
|
+
doc.font('Main');
|
|
23
|
+
const accent = data.headerColor || '#1a2b3c';
|
|
24
|
+
// 1. Фоновый паттерн (Фишка: микро-сетка)
|
|
25
|
+
this.drawPattern(doc);
|
|
26
|
+
// 2. Премиум Шапка
|
|
27
|
+
this.drawHeader(doc, data, accent);
|
|
28
|
+
// 3. Инфо-карточки
|
|
29
|
+
this.drawCards(doc, data, accent);
|
|
30
|
+
// 4. Штамп
|
|
31
|
+
if (data.showStamp && data.status) {
|
|
32
|
+
this.drawStamp(doc, data.status, accent);
|
|
33
|
+
}
|
|
34
|
+
// 5. Контент
|
|
35
|
+
let details = typeof data.details === 'string' ? JSON.parse(data.details || '[]') : data.details;
|
|
36
|
+
if (!Array.isArray(details))
|
|
37
|
+
details = [];
|
|
38
|
+
doc.y = 280;
|
|
39
|
+
switch (template) {
|
|
40
|
+
case 'invoice':
|
|
41
|
+
this.drawTable(doc, ['Наименование', 'Кол-во', 'Цена', 'Сумма'], details, accent);
|
|
42
|
+
break;
|
|
43
|
+
case 'contract':
|
|
44
|
+
doc.fillColor(accent).fontSize(14).text('ПРЕДМЕТ ДОГОВОРА').moveDown();
|
|
45
|
+
doc.fillColor('#444').fontSize(11).text(data.content, { align: 'justify', lineGap: 4 });
|
|
46
|
+
break;
|
|
47
|
+
case 'act':
|
|
48
|
+
doc.fillColor('#333').fontSize(11).text(data.content).moveDown();
|
|
49
|
+
this.drawTable(doc, ['Услуга', 'Период', 'Статус', 'Итого'], details, accent);
|
|
50
|
+
break;
|
|
51
|
+
case 'waybill':
|
|
52
|
+
this.drawTable(doc, ['Товар', 'Артикул', 'Ед.изм', 'Кол-во'], details, accent);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
// 6. Подписи и Футер
|
|
56
|
+
this.drawSignatures(doc, data, accent);
|
|
57
|
+
this.drawFooter(doc, accent);
|
|
58
|
+
doc.end();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
static drawPattern(doc) {
|
|
62
|
+
doc.save().strokeColor('#000').lineWidth(0.1).strokeOpacity(0.05);
|
|
63
|
+
for (let i = 0; i < 842; i += 25) {
|
|
64
|
+
doc.moveTo(0, i).lineTo(595, i).stroke();
|
|
65
|
+
}
|
|
66
|
+
doc.restore();
|
|
67
|
+
}
|
|
68
|
+
static drawHeader(doc, data, color) {
|
|
69
|
+
doc.rect(0, 0, 15, 140).fill(color); // Стильная полоса слева
|
|
70
|
+
doc.rect(40, 40, 520, 80).fillOpacity(0.03).fill(color).fillOpacity(1);
|
|
71
|
+
doc.fillColor(color).fontSize(22).text(data.companyName.toUpperCase(), 60, 60);
|
|
72
|
+
doc.fillColor('#666').fontSize(10).text(data.title, 60, 88);
|
|
73
|
+
const docId = crypto_1.default.randomBytes(4).toString('hex').toUpperCase();
|
|
74
|
+
doc.fillColor(color).fontSize(9).text('ID ДОКУМЕНТА', 400, 60, { align: 'right', width: 140 });
|
|
75
|
+
doc.fontSize(12).text(docId, 400, 72, { align: 'right', width: 140 });
|
|
76
|
+
doc.fontSize(9).fillColor('#999').text(new Date().toLocaleDateString('ru-RU'), 400, 88, { align: 'right', width: 140 });
|
|
77
|
+
}
|
|
78
|
+
static drawCards(doc, data, color) {
|
|
79
|
+
const card = (x, y, label, text) => {
|
|
80
|
+
doc.rect(x, y, 250, 50).fill('#fcfcfc').strokeColor('#eee').lineWidth(1).stroke();
|
|
81
|
+
doc.fillColor(color).fontSize(7).text(label, x + 10, y + 10);
|
|
82
|
+
doc.fillColor('#333').fontSize(10).text(text, x + 10, y + 25);
|
|
83
|
+
};
|
|
84
|
+
card(40, 160, 'ОТПРАВИТЕЛЬ', data.companyName);
|
|
85
|
+
card(310, 160, 'ПОЛУЧАТЕЛЬ', data.clientName);
|
|
86
|
+
}
|
|
87
|
+
static drawStamp(doc, text, color) {
|
|
88
|
+
doc.save();
|
|
89
|
+
doc.rotate(-15, { origin: [500, 250] });
|
|
90
|
+
doc.rect(430, 220, 120, 35).lineWidth(2).strokeColor(color).stroke();
|
|
91
|
+
doc.fillColor(color).fontSize(14).text(text.toUpperCase(), 430, 232, { width: 120, align: 'center' });
|
|
92
|
+
doc.restore();
|
|
93
|
+
}
|
|
94
|
+
static drawTable(doc, headers, rows, color) {
|
|
95
|
+
let y = doc.y + 20;
|
|
96
|
+
doc.rect(40, y, 520, 22).fill(color);
|
|
97
|
+
doc.fillColor('#fff').fontSize(8);
|
|
98
|
+
headers.forEach((h, i) => doc.text(h, 50 + (i * 130), y + 7));
|
|
99
|
+
y += 30;
|
|
100
|
+
doc.fillColor('#333').fontSize(9);
|
|
101
|
+
rows.forEach((row) => {
|
|
102
|
+
const vals = Object.values(row);
|
|
103
|
+
vals.forEach((v, i) => doc.text(String(v), 50 + (i * 130), y));
|
|
104
|
+
y += 20;
|
|
105
|
+
doc.moveTo(40, y - 5).lineTo(560, y - 5).strokeColor('#eee').stroke();
|
|
106
|
+
if (y > 700) {
|
|
107
|
+
doc.addPage();
|
|
108
|
+
y = 50;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
doc.y = y;
|
|
112
|
+
}
|
|
113
|
+
static drawSignatures(doc, data, color) {
|
|
114
|
+
const y = 680;
|
|
115
|
+
doc.moveTo(40, y).lineTo(560, y).strokeColor('#eee').stroke();
|
|
116
|
+
doc.fontSize(8).fillColor('#999').text('М.П. ПОДПИСЬ ОТПРАВИТЕЛЯ', 40, y + 15);
|
|
117
|
+
doc.text('ПОДПИСЬ ПОЛУЧАТЕЛЯ', 310, y + 15);
|
|
118
|
+
doc.fillColor('#333').fontSize(10).text(data.companyName, 40, y + 45);
|
|
119
|
+
doc.text(data.clientName, 310, y + 45);
|
|
120
|
+
doc.moveTo(40, y + 40).lineTo(200, y + 40).strokeColor('#ccc').stroke();
|
|
121
|
+
doc.moveTo(310, y + 40).lineTo(470, y + 40).stroke();
|
|
122
|
+
}
|
|
123
|
+
static drawFooter(doc, color) {
|
|
124
|
+
const y = 790;
|
|
125
|
+
const hash = crypto_1.default.randomBytes(16).toString('hex');
|
|
126
|
+
doc.fontSize(6).fillColor('#ccc').text(`SECURE HASH: ${hash}`, 40, y);
|
|
127
|
+
doc.fontSize(8).fillColor('#999').text('Документ сформирован автоматически и защищен цифровым идентификатором.', 40, y + 10);
|
|
128
|
+
doc.rect(530, y - 5, 30, 30).fill(color);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
exports.DocumentEngine = DocumentEngine;
|
|
Binary file
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-contract-lite",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Lightweight contract PDF generator for n8n",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"build": "tsc",
|
|
8
|
+
"build": "tsc ",
|
|
9
9
|
"prepare": "npm run build"
|
|
10
10
|
},
|
|
11
11
|
"keywords": [
|
|
@@ -14,15 +14,16 @@
|
|
|
14
14
|
"pdf",
|
|
15
15
|
"lightweight"
|
|
16
16
|
],
|
|
17
|
-
"author": "
|
|
17
|
+
"author": "KNK-GROUP",
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"docx": "^9.5.1",
|
|
20
21
|
"mustache": "^4.2.0",
|
|
21
22
|
"pdfkit": "^0.13.0"
|
|
22
23
|
},
|
|
23
24
|
"n8n": {
|
|
24
25
|
"nodes": [
|
|
25
|
-
"dist/ContractGenerator.js"
|
|
26
|
+
"dist/src/ContractGenerator.node.js"
|
|
26
27
|
]
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const srcDir = path.join(__dirname, '../src');
|
|
5
|
+
const distDir = path.join(__dirname, '../dist/src');
|
|
6
|
+
|
|
7
|
+
if (!fs.existsSync(distDir)) {
|
|
8
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
fs.readdirSync(srcDir).forEach(file => {
|
|
12
|
+
if (file.endsWith('.ttf')) {
|
|
13
|
+
fs.copyFileSync(path.join(srcDir, file), path.join(distDir, file));
|
|
14
|
+
console.log(`Copied ${file} to dist/src/`);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
import { DocumentEngine } from './DocumentEngine';
|
|
3
|
+
|
|
4
|
+
export class ContractGenerator implements INodeType {
|
|
5
|
+
description: INodeTypeDescription = {
|
|
6
|
+
displayName: 'Document Generator Pro (RU)',
|
|
7
|
+
name: 'documentGeneratorPro',
|
|
8
|
+
icon: 'fa:file-signature',
|
|
9
|
+
group: ['transform'],
|
|
10
|
+
version: 1,
|
|
11
|
+
description: 'Премиальный генератор документов с защитой',
|
|
12
|
+
defaults: { name: 'Генератор документов' },
|
|
13
|
+
inputs: ['main'],
|
|
14
|
+
outputs: ['main'],
|
|
15
|
+
properties: [
|
|
16
|
+
{
|
|
17
|
+
displayName: 'Тип шаблона',
|
|
18
|
+
name: 'template',
|
|
19
|
+
type: 'options',
|
|
20
|
+
options: [
|
|
21
|
+
{ name: 'Счет (Invoice)', value: 'invoice' },
|
|
22
|
+
{ name: 'Договор (Contract)', value: 'contract' },
|
|
23
|
+
{ name: 'Накладная (Waybill)', value: 'waybill' },
|
|
24
|
+
{ name: 'Акт (Act)', value: 'act' },
|
|
25
|
+
],
|
|
26
|
+
default: 'invoice',
|
|
27
|
+
},
|
|
28
|
+
{ displayName: 'Название компании', name: 'companyName', type: 'string', default: 'ООО КНК ГРУПП' },
|
|
29
|
+
{ displayName: 'Имя клиента', name: 'clientName', type: 'string', default: 'Иван Иванов' },
|
|
30
|
+
|
|
31
|
+
// Настройки штампа
|
|
32
|
+
{ displayName: 'Показать штамп', name: 'showStamp', type: 'boolean', default: true },
|
|
33
|
+
{
|
|
34
|
+
displayName: 'Текст штампа',
|
|
35
|
+
name: 'status',
|
|
36
|
+
type: 'string',
|
|
37
|
+
default: 'ОПЛАЧЕНО',
|
|
38
|
+
displayOptions: { show: { showStamp: [true] } }
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Контент
|
|
42
|
+
{
|
|
43
|
+
displayName: 'Текст документа',
|
|
44
|
+
name: 'content',
|
|
45
|
+
type: 'string',
|
|
46
|
+
typeOptions: { rows: 5 },
|
|
47
|
+
default: 'Настоящий документ подтверждает...',
|
|
48
|
+
displayOptions: { show: { template: ['contract', 'act'] } }
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
displayName: 'Данные таблицы (JSON)',
|
|
52
|
+
name: 'details',
|
|
53
|
+
type: 'json',
|
|
54
|
+
default: '[]',
|
|
55
|
+
description: 'Пример: [{"item": "Услуга", "qty": 1, "price": 100, "total": 100}]',
|
|
56
|
+
displayOptions: { hide: { template: ['contract'] } }
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// Стили
|
|
60
|
+
{ displayName: 'Цвет бренда', name: 'headerColor', type: 'color', default: '#1a2b3c' },
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
65
|
+
const items = this.getInputData();
|
|
66
|
+
const returnData: INodeExecutionData[] = [];
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < items.length; i++) {
|
|
69
|
+
const template = this.getNodeParameter('template', i) as string;
|
|
70
|
+
|
|
71
|
+
// Вызываем статичный метод через имя класса
|
|
72
|
+
const title = ContractGenerator.getTitle(template);
|
|
73
|
+
|
|
74
|
+
const data = {
|
|
75
|
+
companyName: this.getNodeParameter('companyName', i),
|
|
76
|
+
clientName: this.getNodeParameter('clientName', i),
|
|
77
|
+
showStamp: this.getNodeParameter('showStamp', i),
|
|
78
|
+
status: this.getNodeParameter('showStamp', i) ? this.getNodeParameter('status', i) : '',
|
|
79
|
+
content: this.getNodeParameter('content', i, ''),
|
|
80
|
+
details: this.getNodeParameter('details', i, '[]'),
|
|
81
|
+
headerColor: this.getNodeParameter('headerColor', i),
|
|
82
|
+
title: title,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const buffer = await DocumentEngine.createPDF(data, template);
|
|
86
|
+
const binaryData = await this.helpers.prepareBinaryData(buffer, `${template}_${Date.now()}.pdf`);
|
|
87
|
+
|
|
88
|
+
returnData.push({ json: { success: true, timestamp: new Date() }, binary: { data: binaryData } });
|
|
89
|
+
}
|
|
90
|
+
return [returnData];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Добавили static
|
|
94
|
+
private static getTitle(template: string): string {
|
|
95
|
+
const titles: any = {
|
|
96
|
+
invoice: 'Счет на оплату',
|
|
97
|
+
contract: 'Договор поставки',
|
|
98
|
+
act: 'Акт выполненных работ',
|
|
99
|
+
waybill: 'Товарная накладная'
|
|
100
|
+
};
|
|
101
|
+
return titles[template] || 'Документ';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import PDFDocument from 'pdfkit';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
|
|
6
|
+
export class DocumentEngine {
|
|
7
|
+
static async createPDF(data: any, template: string): Promise<Buffer> {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const doc = new PDFDocument({ size: 'A4', margin: 40, bufferPages: true });
|
|
10
|
+
const chunks: Buffer[] = [];
|
|
11
|
+
doc.on('data', chunk => chunks.push(chunk));
|
|
12
|
+
doc.on('end', () => resolve(Buffer.concat(chunks)));
|
|
13
|
+
doc.on('error', reject);
|
|
14
|
+
|
|
15
|
+
const fontPath = path.join(__dirname, 'Roboto-Regular.ttf');
|
|
16
|
+
if (fs.existsSync(fontPath)) doc.registerFont('Main', fontPath);
|
|
17
|
+
doc.font('Main');
|
|
18
|
+
|
|
19
|
+
const accent = data.headerColor || '#1a2b3c';
|
|
20
|
+
|
|
21
|
+
// 1. Фоновый паттерн (Фишка: микро-сетка)
|
|
22
|
+
this.drawPattern(doc);
|
|
23
|
+
|
|
24
|
+
// 2. Премиум Шапка
|
|
25
|
+
this.drawHeader(doc, data, accent);
|
|
26
|
+
|
|
27
|
+
// 3. Инфо-карточки
|
|
28
|
+
this.drawCards(doc, data, accent);
|
|
29
|
+
|
|
30
|
+
// 4. Штамп
|
|
31
|
+
if (data.showStamp && data.status) {
|
|
32
|
+
this.drawStamp(doc, data.status, accent);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 5. Контент
|
|
36
|
+
let details = typeof data.details === 'string' ? JSON.parse(data.details || '[]') : data.details;
|
|
37
|
+
if (!Array.isArray(details)) details = [];
|
|
38
|
+
|
|
39
|
+
doc.y = 280;
|
|
40
|
+
switch (template) {
|
|
41
|
+
case 'invoice':
|
|
42
|
+
this.drawTable(doc, ['Наименование', 'Кол-во', 'Цена', 'Сумма'], details, accent);
|
|
43
|
+
break;
|
|
44
|
+
case 'contract':
|
|
45
|
+
doc.fillColor(accent).fontSize(14).text('ПРЕДМЕТ ДОГОВОРА').moveDown();
|
|
46
|
+
doc.fillColor('#444').fontSize(11).text(data.content, { align: 'justify', lineGap: 4 });
|
|
47
|
+
break;
|
|
48
|
+
case 'act':
|
|
49
|
+
doc.fillColor('#333').fontSize(11).text(data.content).moveDown();
|
|
50
|
+
this.drawTable(doc, ['Услуга', 'Период', 'Статус', 'Итого'], details, accent);
|
|
51
|
+
break;
|
|
52
|
+
case 'waybill':
|
|
53
|
+
this.drawTable(doc, ['Товар', 'Артикул', 'Ед.изм', 'Кол-во'], details, accent);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 6. Подписи и Футер
|
|
58
|
+
this.drawSignatures(doc, data, accent);
|
|
59
|
+
this.drawFooter(doc, accent);
|
|
60
|
+
|
|
61
|
+
doc.end();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private static drawPattern(doc: any) {
|
|
66
|
+
doc.save().strokeColor('#000').lineWidth(0.1).strokeOpacity(0.05);
|
|
67
|
+
for(let i=0; i<842; i+=25) {
|
|
68
|
+
doc.moveTo(0, i).lineTo(595, i).stroke();
|
|
69
|
+
}
|
|
70
|
+
doc.restore();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private static drawHeader(doc: any, data: any, color: string) {
|
|
74
|
+
doc.rect(0, 0, 15, 140).fill(color); // Стильная полоса слева
|
|
75
|
+
doc.rect(40, 40, 520, 80).fillOpacity(0.03).fill(color).fillOpacity(1);
|
|
76
|
+
|
|
77
|
+
doc.fillColor(color).fontSize(22).text(data.companyName.toUpperCase(), 60, 60);
|
|
78
|
+
doc.fillColor('#666').fontSize(10).text(data.title, 60, 88);
|
|
79
|
+
|
|
80
|
+
const docId = crypto.randomBytes(4).toString('hex').toUpperCase();
|
|
81
|
+
doc.fillColor(color).fontSize(9).text('ID ДОКУМЕНТА', 400, 60, { align: 'right', width: 140 });
|
|
82
|
+
doc.fontSize(12).text(docId, 400, 72, { align: 'right', width: 140 });
|
|
83
|
+
doc.fontSize(9).fillColor('#999').text(new Date().toLocaleDateString('ru-RU'), 400, 88, { align: 'right', width: 140 });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private static drawCards(doc: any, data: any, color: string) {
|
|
87
|
+
const card = (x: number, y: number, label: string, text: string) => {
|
|
88
|
+
doc.rect(x, y, 250, 50).fill('#fcfcfc').strokeColor('#eee').lineWidth(1).stroke();
|
|
89
|
+
doc.fillColor(color).fontSize(7).text(label, x + 10, y + 10);
|
|
90
|
+
doc.fillColor('#333').fontSize(10).text(text, x + 10, y + 25);
|
|
91
|
+
};
|
|
92
|
+
card(40, 160, 'ОТПРАВИТЕЛЬ', data.companyName);
|
|
93
|
+
card(310, 160, 'ПОЛУЧАТЕЛЬ', data.clientName);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private static drawStamp(doc: any, text: string, color: string) {
|
|
97
|
+
doc.save();
|
|
98
|
+
doc.rotate(-15, { origin: [500, 250] });
|
|
99
|
+
doc.rect(430, 220, 120, 35).lineWidth(2).strokeColor(color).stroke();
|
|
100
|
+
doc.fillColor(color).fontSize(14).text(text.toUpperCase(), 430, 232, { width: 120, align: 'center' });
|
|
101
|
+
doc.restore();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private static drawTable(doc: any, headers: string[], rows: any[], color: string) {
|
|
105
|
+
let y = doc.y + 20;
|
|
106
|
+
doc.rect(40, y, 520, 22).fill(color);
|
|
107
|
+
doc.fillColor('#fff').fontSize(8);
|
|
108
|
+
headers.forEach((h, i) => doc.text(h, 50 + (i * 130), y + 7));
|
|
109
|
+
|
|
110
|
+
y += 30;
|
|
111
|
+
doc.fillColor('#333').fontSize(9);
|
|
112
|
+
rows.forEach((row: any) => {
|
|
113
|
+
const vals = Object.values(row);
|
|
114
|
+
vals.forEach((v: any, i) => doc.text(String(v), 50 + (i * 130), y));
|
|
115
|
+
y += 20;
|
|
116
|
+
doc.moveTo(40, y - 5).lineTo(560, y - 5).strokeColor('#eee').stroke();
|
|
117
|
+
if (y > 700) { doc.addPage(); y = 50; }
|
|
118
|
+
});
|
|
119
|
+
doc.y = y;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private static drawSignatures(doc: any, data: any, color: string) {
|
|
123
|
+
const y = 680;
|
|
124
|
+
doc.moveTo(40, y).lineTo(560, y).strokeColor('#eee').stroke();
|
|
125
|
+
doc.fontSize(8).fillColor('#999').text('М.П. ПОДПИСЬ ОТПРАВИТЕЛЯ', 40, y + 15);
|
|
126
|
+
doc.text('ПОДПИСЬ ПОЛУЧАТЕЛЯ', 310, y + 15);
|
|
127
|
+
|
|
128
|
+
doc.fillColor('#333').fontSize(10).text(data.companyName, 40, y + 45);
|
|
129
|
+
doc.text(data.clientName, 310, y + 45);
|
|
130
|
+
doc.moveTo(40, y + 40).lineTo(200, y + 40).strokeColor('#ccc').stroke();
|
|
131
|
+
doc.moveTo(310, y + 40).lineTo(470, y + 40).stroke();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private static drawFooter(doc: any, color: string) {
|
|
135
|
+
const y = 790;
|
|
136
|
+
const hash = crypto.randomBytes(16).toString('hex');
|
|
137
|
+
doc.fontSize(6).fillColor('#ccc').text(`SECURE HASH: ${hash}`, 40, y);
|
|
138
|
+
doc.fontSize(8).fillColor('#999').text('Документ сформирован автоматически и защищен цифровым идентификатором.', 40, y + 10);
|
|
139
|
+
doc.rect(530, y - 5, 30, 30).fill(color);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
Binary file
|
package/src/types.ts
ADDED
package/src/ContractGenerator.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import PDFDocument from 'pdfkit';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
INodeType,
|
|
6
|
-
INodeTypeDescription,
|
|
7
|
-
IExecuteFunctions,
|
|
8
|
-
INodeExecutionData,
|
|
9
|
-
} from 'n8n-workflow';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export class ContractGenerator implements INodeType {
|
|
13
|
-
description: INodeTypeDescription = {
|
|
14
|
-
displayName: 'Contract Generator Lite',
|
|
15
|
-
name: 'contractGeneratorLite',
|
|
16
|
-
group: ['transform'],
|
|
17
|
-
version: 1,
|
|
18
|
-
description: 'Generates a professional contract PDF (lightweight)',
|
|
19
|
-
defaults: { name: 'Contract Generator Lite' },
|
|
20
|
-
inputs: ['main'],
|
|
21
|
-
outputs: ['main'],
|
|
22
|
-
properties: [
|
|
23
|
-
{ displayName: 'Contract Data', name: 'contractData', type: 'json', default: {}, description: 'Contract variables' },
|
|
24
|
-
{ displayName: 'Seal Image', name: 'sealImage', type: 'string', default: '', description: 'Path to seal PNG/JPG' },
|
|
25
|
-
{ displayName: 'Signature Image', name: 'signatureImage', type: 'string', default: '', description: 'Path to signature PNG/JPG' },
|
|
26
|
-
],
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
30
|
-
const items = this.getInputData();
|
|
31
|
-
const returnData: INodeExecutionData[] = [];
|
|
32
|
-
|
|
33
|
-
for (let i = 0; i < items.length; i++) {
|
|
34
|
-
const data = this.getNodeParameter('contractData', i) as Record<string, any>;
|
|
35
|
-
const sealImage = this.getNodeParameter('sealImage', i) as string;
|
|
36
|
-
const signatureImage = this.getNodeParameter('signatureImage', i) as string;
|
|
37
|
-
|
|
38
|
-
['company_name', 'client_name', 'contract_number', 'date'].forEach(field => {
|
|
39
|
-
if (!data[field]) throw new Error(`Missing field: ${field}`);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const doc = new PDFDocument({ size: 'A4', margin: 50 });
|
|
43
|
-
const buffers: Buffer[] = [];
|
|
44
|
-
doc.on('data', buffers.push.bind(buffers));
|
|
45
|
-
doc.on('end', () => {});
|
|
46
|
-
|
|
47
|
-
// --- HEADER ---
|
|
48
|
-
doc.rect(0, 0, 595.28, 70).fill('#007BFF'); // голубой хедер
|
|
49
|
-
doc.fillColor('white').fontSize(26).text('KNK GROUP', 50, 25, { align: 'left' });
|
|
50
|
-
|
|
51
|
-
doc.moveDown(4);
|
|
52
|
-
doc.fillColor('black').fontSize(12);
|
|
53
|
-
|
|
54
|
-
// --- CONTRACT INFO ---
|
|
55
|
-
doc.text(`Договор № ${data.contract_number}`, { continued: true }).text(` от ${data.date}`);
|
|
56
|
-
doc.moveDown();
|
|
57
|
-
|
|
58
|
-
// --- PARTIES ---
|
|
59
|
-
doc.font('Helvetica-Bold').text('Исполнитель: ', { continued: true }).font('Helvetica').text(`${data.company_name}, ИНН ${data.inn || ''}`);
|
|
60
|
-
doc.font('Helvetica-Bold').text('Заказчик: ', { continued: true }).font('Helvetica').text(`${data.client_name}`);
|
|
61
|
-
doc.moveDown();
|
|
62
|
-
|
|
63
|
-
// --- SUBJECT ---
|
|
64
|
-
doc.font('Helvetica-Bold').text('Предмет договора:');
|
|
65
|
-
doc.font('Helvetica').text(data.subject || 'Оказание услуг по автоматизации бизнес-процессов', { indent: 20 });
|
|
66
|
-
doc.moveDown();
|
|
67
|
-
|
|
68
|
-
if (data.VAT) {
|
|
69
|
-
doc.font('Helvetica-Bold').text('НДС: ', { continued: true }).font('Helvetica').text(`включён в сумму ${data.amount || ''} руб.`);
|
|
70
|
-
doc.moveDown();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// --- SIGNATURES ---
|
|
74
|
-
const startY = doc.y + 30;
|
|
75
|
-
if (fs.existsSync(signatureImage)) {
|
|
76
|
-
doc.image(signatureImage, 50, startY, { width: 150 });
|
|
77
|
-
}
|
|
78
|
-
if (fs.existsSync(sealImage)) {
|
|
79
|
-
doc.image(sealImage, 400, startY, { width: 150 });
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
doc.end();
|
|
83
|
-
|
|
84
|
-
const pdfBuffer = Buffer.concat(buffers);
|
|
85
|
-
returnData.push({ json: { pdf: pdfBuffer.toString('base64') } });
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return [returnData];
|
|
89
|
-
}
|
|
90
|
-
}
|