n8n-nodes-contract-lite 4.0.3 → 4.0.4

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.
@@ -5,12 +5,12 @@ const DocumentEngine_1 = require("./DocumentEngine");
5
5
  class ContractGenerator {
6
6
  constructor() {
7
7
  this.description = {
8
- displayName: 'Document Generator Elite',
8
+ displayName: 'Document Generator Pro (Elite)',
9
9
  name: 'documentGeneratorElite',
10
10
  icon: 'fa:file-signature',
11
11
  group: ['transform'],
12
12
  version: 1,
13
- description: 'Профессиональные документы (Счета, Акты, Договоры) с поддержкой кириллицы',
13
+ description: 'Премиальная генерация документов с исправленной логикой сторон',
14
14
  defaults: { name: 'Генератор документов' },
15
15
  inputs: ['main'],
16
16
  outputs: ['main'],
@@ -22,55 +22,55 @@ class ContractGenerator {
22
22
  options: [
23
23
  { name: 'Счет на оплату', value: 'invoice' },
24
24
  { name: 'Акт выполненных работ', value: 'act' },
25
- { name: 'Договор поставки', value: 'contract' },
26
- { name: 'Счет-оферта', value: 'offer' },
25
+ { name: 'Договор', value: 'contract' },
27
26
  ],
28
27
  default: 'invoice',
29
28
  },
30
29
  {
31
- displayName: 'Дизайн-код',
30
+ displayName: 'Тема оформления',
32
31
  name: 'presetTheme',
33
32
  type: 'options',
34
33
  options: [
35
- { name: 'Corporate (Строгий)', value: 'corporate' },
36
- { name: 'Modern (Цветной)', value: 'modern' },
37
- { name: 'Minimal (Ч/Б)', value: 'minimal' },
34
+ { name: 'Modern (Чистый синий)', value: 'modern' },
35
+ { name: 'Accent (С боковой полосой)', value: 'accent' },
36
+ { name: 'Minimal (Классика)', value: 'minimal' },
38
37
  ],
39
38
  default: 'modern',
40
39
  },
41
- { displayName: 'Цвет акцента', name: 'headerColor', type: 'color', default: '#1e40af' },
40
+ { displayName: 'Цвет бренда', name: 'headerColor', type: 'color', default: '#2563eb' },
42
41
  {
43
- displayName: 'Основные данные',
44
- name: 'mainData',
42
+ displayName: 'Данные ПОСТАВЩИКА (Ваши)',
43
+ name: 'supplierData',
45
44
  type: 'collection',
46
- placeholder: 'Добавить данные',
45
+ placeholder: 'Ваши реквизиты',
47
46
  default: {},
48
47
  options: [
49
- { displayName: 'Номер документа', name: 'docNumber', type: 'string', default: '101' },
50
- { displayName: 'Дата', name: 'date', type: 'string', default: new Date().toLocaleDateString('ru-RU') },
51
- { displayName: 'Название компании', name: 'companyName', type: 'string', default: 'ООО ВЕКТОР' },
52
- { displayName: 'ФИО клиента', name: 'clientName', type: 'string', default: 'ИП Иванов Иван Иванович' },
48
+ { displayName: 'Название компании', name: 'companyName', type: 'string', default: 'ООО КНК ГРУПП' },
49
+ { displayName: 'ИНН', name: 'inn', type: 'string', default: '7701234567' },
50
+ { displayName: 'КПП', name: 'kpp', type: 'string', default: '770101001' },
51
+ { displayName: 'Банк', name: 'bankName', type: 'string', default: 'ПАО СБЕРБАНК' },
52
+ { displayName: 'БИК', name: 'bik', type: 'string', default: '044525225' },
53
+ { displayName: 'Расчетный счет', name: 'account', type: 'string', default: '40702810000000000000' },
54
+ { displayName: 'Корр. счет', name: 'corrAccount', type: 'string', default: '30101810400000000225' },
53
55
  ]
54
56
  },
55
57
  {
56
- displayName: 'Банковские реквизиты (для счетов)',
57
- name: 'bankDetails',
58
+ displayName: 'Данные ПОКУПАТЕЛЯ (Клиент)',
59
+ name: 'clientData',
58
60
  type: 'collection',
59
- placeholder: 'Добавить реквизиты',
61
+ placeholder: 'Реквизиты клиента',
60
62
  default: {},
61
63
  options: [
62
- { displayName: 'Название банка', name: 'bankName', type: 'string', default: 'ПАО СБЕРБАНК' },
63
- { displayName: 'БИК', name: 'bik', type: 'string', default: '044525225' },
64
- { displayName: 'Расчетный счет', name: 'account', type: 'string', default: '40702810000000000000' },
65
- { displayName: 'Корр. счет', name: 'corrAccount', type: 'string', default: '30101810400000000225' },
66
- { displayName: 'ИНН', name: 'inn', type: 'string', default: '7701234567' },
67
- { displayName: 'КПП', name: 'kpp', type: 'string', default: '770101001' },
64
+ { displayName: 'ФИО / Название', name: 'clientName', type: 'string', default: 'ИП Иванов Иван' },
65
+ { displayName: 'Адрес', name: 'clientAddress', type: 'string', default: 'г. Москва, ул. Ленина 1' },
68
66
  ]
69
67
  },
70
- { displayName: 'Текст/Условия', name: 'content', type: 'string', typeOptions: { rows: 3 }, default: 'Оплата данного счета означает согласие с условиями поставки.' },
71
- { displayName: 'Таблица товаров (JSON)', name: 'details', type: 'json', default: '[{"№": 1, "Товар": "Услуги дизайна", "Кол-во": 1, "Цена": "15000"}]' },
72
- { displayName: 'Показать печать', name: 'showStamp', type: 'boolean', default: true },
73
- { displayName: 'Текст печати', name: 'status', type: 'string', default: 'ОПЛАЧЕНО', displayOptions: { show: { showStamp: [true] } } },
68
+ { displayName: 'Номер документа', name: 'docNumber', type: 'string', default: '101' },
69
+ { displayName: 'Дата', name: 'date', type: 'string', default: new Date().toLocaleDateString('ru-RU') },
70
+ { displayName: 'Таблица товаров (JSON)', name: 'details', type: 'json', default: '[{"№": 1, "Товар": "Услуги консалтинга", "Кол": 1, "Цена": 15000, "Сумма": 15000}]' },
71
+ { displayName: 'Доп. условия (Текст)', name: 'content', type: 'string', typeOptions: { rows: 3 }, default: 'Оплата счета означает согласие с условиями.' },
72
+ { displayName: 'Показать штамп', name: 'showStamp', type: 'boolean', default: true },
73
+ { displayName: 'Текст штампа', name: 'status', type: 'string', default: 'ОПЛАЧЕНО', displayOptions: { show: { showStamp: [true] } } },
74
74
  ],
75
75
  };
76
76
  }
@@ -79,20 +79,17 @@ class ContractGenerator {
79
79
  const returnData = [];
80
80
  for (let i = 0; i < items.length; i++) {
81
81
  const templateType = this.getNodeParameter('templateType', i);
82
- const mainData = this.getNodeParameter('mainData', i);
83
- const bankDetails = this.getNodeParameter('bankDetails', i);
84
- const titles = {
85
- invoice: 'СЧЕТ НА ОПЛАТУ',
86
- act: 'АКТ ВЫПОЛНЕННЫХ РАБОТ',
87
- contract: 'ДОГОВОР',
88
- offer: 'СЧЕТ-ОФЕРТА'
89
- };
82
+ const supplier = this.getNodeParameter('supplierData', i);
83
+ const client = this.getNodeParameter('clientData', i);
84
+ const titles = { invoice: 'СЧЕТ НА ОПЛАТУ', act: 'АКТ ПРИЕМКИ-СДАЧИ', contract: 'ДОГОВОР' };
90
85
  const data = {
91
- ...mainData,
92
- ...bankDetails,
86
+ ...supplier,
87
+ ...client,
93
88
  title: titles[templateType],
94
- content: this.getNodeParameter('content', i),
89
+ docNumber: this.getNodeParameter('docNumber', i),
90
+ date: this.getNodeParameter('date', i),
95
91
  details: this.getNodeParameter('details', i),
92
+ content: this.getNodeParameter('content', i),
96
93
  showStamp: this.getNodeParameter('showStamp', i),
97
94
  status: this.getNodeParameter('status', i, 'ОПЛАЧЕНО'),
98
95
  };
@@ -1,9 +1,10 @@
1
1
  export declare class DocumentEngine {
2
2
  static createPDF(data: any, settings: any): Promise<Buffer>;
3
- private static drawLayout;
4
- private static drawBankSection;
3
+ private static drawTemplateDecor;
4
+ private static drawBankGrid;
5
5
  private static drawParties;
6
6
  private static drawTable;
7
7
  private static drawTotals;
8
8
  private static drawSignatures;
9
+ private static drawRealisticStamp;
9
10
  }
@@ -6,157 +6,191 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.DocumentEngine = void 0;
7
7
  const pdfkit_1 = __importDefault(require("pdfkit"));
8
8
  const path_1 = __importDefault(require("path"));
9
- const fs_1 = __importDefault(require("fs"));
10
9
  class DocumentEngine {
11
10
  static async createPDF(data, settings) {
12
11
  return new Promise((resolve, reject) => {
13
- const doc = new pdfkit_1.default({ size: 'A4', margin: 40 });
12
+ const doc = new pdfkit_1.default({
13
+ size: 'A4',
14
+ margin: 0, // Управляем полями вручную для точности дизайна
15
+ bufferPages: true
16
+ });
14
17
  const chunks = [];
15
18
  doc.on('data', chunk => chunks.push(chunk));
16
19
  doc.on('end', () => resolve(Buffer.concat(chunks)));
17
20
  doc.on('error', reject);
18
- // РЕГИСТРАЦИЯ ШРИФТОВ (Решает проблему иероглифов)
19
21
  const fontPath = path_1.default.join(__dirname, 'Roboto-Regular.ttf');
20
22
  const fontBoldPath = path_1.default.join(__dirname, 'Roboto-Bold.ttf');
21
- if (fs_1.default.existsSync(fontPath)) {
22
- doc.registerFont('Main', fontPath);
23
- doc.registerFont('Bold', fontBoldPath);
24
- }
25
- else {
26
- console.warn("Шрифт Roboto не найден по пути:", fontPath);
27
- }
28
- const accent = settings.headerColor || '#000000';
29
- // ПРИМЕНЯЕМ ШРИФТ СРАЗУ
23
+ doc.registerFont('Main', fontPath);
24
+ doc.registerFont('Bold', fontBoldPath);
30
25
  doc.font('Main');
31
- // 1. РИСУЕМ СЕТКУ И ДЕКОР В ЗАВИСИМОСТИ ОТ ТЕМЫ
32
- this.drawLayout(doc, settings.presetTheme, accent);
33
- // 2. БЛОК БАНКОВСКИХ РЕКВИЗИТОВ (Для Счетов и Актов)
34
- if (['invoice', 'act'].includes(settings.templateType)) {
35
- this.drawBankSection(doc, data, accent);
36
- }
37
- // 3. ЗАГОЛОВОК ДОКУМЕНТА
38
- doc.y += 20;
39
- doc.font('Bold').fontSize(18).fillColor('#000').text(`${data.title} № ${data.docNumber || 'б/н'} от ${data.date}`, { align: 'left' });
40
- doc.moveDown(1);
41
- doc.rect(doc.x, doc.y, 515, 2).fill(accent);
42
- doc.moveDown(1);
43
- // 4. СТОРОНЫ (ОТПРАВИТЕЛЬ / ПОЛУЧАТЕЛЬ)
44
- this.drawParties(doc, data);
45
- // 5. ТАБЛИЦА С ДАННЫМИ
46
- let details = [];
47
- try {
48
- details = typeof data.details === 'string' ? JSON.parse(data.details) : data.details;
49
- }
50
- catch (e) {
51
- details = [];
52
- }
53
- if (Array.isArray(details) && details.length > 0) {
54
- this.drawTable(doc, details, accent, settings.presetTheme);
26
+ const accent = settings.headerColor || '#2563eb';
27
+ const theme = settings.presetTheme || 'modern';
28
+ // 1. ФОН И СЕТКА
29
+ this.drawTemplateDecor(doc, theme, accent);
30
+ // 2. ШАПКА: БАНКОВСКИЕ РЕКВИЗИТЫ (в стиле 1С, но чище)
31
+ let currentY = 50;
32
+ if (settings.templateType !== 'contract') {
33
+ currentY = this.drawBankGrid(doc, data, 50, 40);
55
34
  }
35
+ // 3. ЗАГОЛОВОК
36
+ doc.y = currentY + 30;
37
+ doc.x = 40;
38
+ doc.font('Bold').fontSize(20).fillColor('#111827').text(`${data.title} № ${data.docNumber}`, { continued: true });
39
+ doc.font('Main').fontSize(14).fillColor('#6b7280').text(` от ${data.date}`);
40
+ doc.moveDown(0.5);
41
+ doc.rect(40, doc.y, 515, 1.5).fill(accent);
42
+ doc.moveDown(1.5);
43
+ // 4. СТОРОНЫ (БЛОКИ ПОСТАВЩИК / ПОКУПАТЕЛЬ)
44
+ this.drawParties(doc, data, accent);
45
+ // 5. ТАБЛИЦА ТОВАРОВ
46
+ this.drawTable(doc, data, accent);
56
47
  // 6. ИТОГИ
57
- this.drawTotals(doc, details);
58
- // 7. КОНТЕНТ (УСЛОВИЯ И Т.Д.)
48
+ this.drawTotals(doc, data);
49
+ // 7. УСЛОВИЯ
59
50
  if (data.content) {
60
51
  doc.moveDown(2);
61
- doc.font('Bold').fontSize(10).text('Дополнительные условия:');
62
- doc.font('Main').fontSize(9).fillColor('#444').text(data.content, { width: 500, align: 'justify' });
52
+ doc.x = 40;
53
+ doc.font('Bold').fontSize(9).fillColor(accent).text('ДОПОЛНИТЕЛЬНЫЕ УСЛОВИЯ');
54
+ doc.font('Main').fontSize(9).fillColor('#374151').text(data.content, { width: 400, lineGap: 2 });
63
55
  }
64
- // 8. ПЕЧАТЬ И ПОДПИСИ
56
+ // 8. ПОДПИСИ И ШТАМП
65
57
  this.drawSignatures(doc, data, accent);
66
58
  doc.end();
67
59
  });
68
60
  }
69
- static drawLayout(doc, theme, accent) {
70
- if (theme === 'corporate') {
71
- doc.rect(0, 0, 5, 842).fill(accent); // Строгая линия сбоку
61
+ static drawTemplateDecor(doc, theme, accent) {
62
+ if (theme === 'modern') {
63
+ doc.rect(0, 0, 595, 5).fill(accent);
72
64
  }
73
- else if (theme === 'modern') {
74
- doc.rect(40, 40, 515, 100).fillOpacity(0.03).fill(accent).fillOpacity(1); // Легкий фон шапки
65
+ else if (theme === 'accent') {
66
+ doc.rect(0, 0, 30, 842).fill(accent);
75
67
  }
76
68
  }
77
- static drawBankSection(doc, data, accent) {
78
- doc.font('Main').fontSize(8).fillColor('#666');
79
- const startY = doc.y;
80
- // Таблица банка (как в 1С)
81
- doc.rect(40, startY, 515, 60).strokeColor('#ccc').lineWidth(0.5).stroke();
82
- doc.moveTo(40, startY + 30).lineTo(555, startY + 30).stroke();
83
- doc.moveTo(300, startY).lineTo(300, startY + 60).stroke();
84
- doc.fillColor('#000').text('БАНК ПОЛУЧАТЕЛЯ:', 45, startY + 5);
85
- doc.font('Bold').text(data.bankName || 'ПАО СБЕРБАНК', 45, startY + 15);
86
- doc.font('Main').text('БИК', 305, startY + 5);
87
- doc.font('Bold').text(data.bik || '044525225', 340, startY + 5);
88
- doc.font('Main').text('Сч. №', 305, startY + 15);
89
- doc.font('Bold').text(data.corrAccount || '30101810400000000225', 340, startY + 15);
90
- doc.font('Main').text('ИНН ' + (data.inn || '7700000000'), 45, startY + 35);
91
- doc.text('КПП ' + (data.kpp || '770001001'), 150, startY + 35);
92
- doc.text('ПОЛУЧАТЕЛЬ: ' + data.companyName, 45, startY + 45);
93
- doc.text('Сч. №', 305, startY + 35);
94
- doc.font('Bold').text(data.account || '40702810000000001234', 340, startY + 35);
95
- doc.y = startY + 70;
69
+ static drawBankGrid(doc, data, x, y) {
70
+ doc.font('Main').fontSize(7).fillColor('#9ca3af');
71
+ const width = 515;
72
+ const rowH = 15;
73
+ // Рисуем рамку сетки
74
+ doc.rect(x, y, width, 55).strokeColor('#e5e7eb').lineWidth(0.5).stroke();
75
+ // Линии
76
+ doc.moveTo(x, y + 35).lineTo(x + width, y + 35).stroke(); // Горизонтальная
77
+ doc.moveTo(x + 300, y).lineTo(x + 300, y + 55).stroke(); // Вертикальная
78
+ doc.moveTo(x + 300, y + rowH).lineTo(x + width, y + rowH).stroke();
79
+ doc.moveTo(x + 300, y + rowH * 2).lineTo(x + width, y + rowH * 2).stroke();
80
+ // Текст
81
+ doc.fillColor('#6b7280').text('БАНК ПОЛУЧАТЕЛЯ', x + 5, y + 5);
82
+ doc.font('Bold').fillColor('#111827').fontSize(9).text(data.bankName || 'ПАО СБЕРБАНК', x + 5, y + 15);
83
+ doc.font('Main').fillColor('#6b7280').fontSize(7).text('БИК', x + 305, y + 4);
84
+ doc.font('Bold').fillColor('#111827').fontSize(9).text(data.bik || '044525225', x + 340, y + 4);
85
+ doc.font('Main').fillColor('#6b7280').text('Сч. №', x + 305, y + 19);
86
+ doc.font('Bold').text(data.corrAccount || '30101810400000000225', x + 340, y + 19);
87
+ doc.font('Main').fillColor('#6b7280').text('ИНН ' + data.inn, x + 5, y + 38);
88
+ doc.text('КПП ' + data.kpp, x + 120, y + 38);
89
+ doc.text('Сч. №', x + 305, y + 38);
90
+ doc.font('Bold').text(data.account || '40702810000000001234', x + 340, y + 38);
91
+ doc.font('Main').fillColor('#6b7280').text('ПОЛУЧАТЕЛЬ', x + 5, y + 46);
92
+ doc.font('Bold').fillColor('#111827').text(data.companyName, x + 70, y + 46);
93
+ return y + 55;
96
94
  }
97
- static drawParties(doc, data) {
95
+ static drawParties(doc, data, accent) {
98
96
  const y = doc.y;
99
- doc.font('Main').fontSize(9).fillColor('#666');
100
- doc.text('ПОСТАВЩИК:', 40, y);
101
- doc.font('Bold').fillColor('#000').text(data.companyName, 110, y, { width: 440 });
102
- doc.moveDown(0.5);
103
- const y2 = doc.y;
104
- doc.font('Main').fillColor('#666').text('ПОКУПАТЕЛЬ:', 40, y2);
105
- doc.font('Bold').fillColor('#000').text(data.clientName, 110, y2, { width: 440 });
106
- doc.moveDown(1);
97
+ const width = 250;
98
+ // Поставщик
99
+ doc.font('Bold').fontSize(8).fillColor(accent).text('ПОСТАВЩИК (ИСПОЛНИТЕЛЬ)', 40, y);
100
+ doc.font('Bold').fontSize(10).fillColor('#111827').text(data.companyName, 40, y + 12, { width });
101
+ doc.font('Main').fontSize(8).fillColor('#6b7280').text(`ИНН: ${data.inn}, ${data.address || 'Москва, Россия'}`, 40, y + 25, { width });
102
+ // Покупатель
103
+ doc.font('Bold').fontSize(8).fillColor(accent).text('ПОКУПАТЕЛЬ (ЗАКАЗЧИК)', 305, y);
104
+ doc.font('Bold').fontSize(10).fillColor('#111827').text(data.clientName, 305, y + 12, { width });
105
+ doc.font('Main').fontSize(8).fillColor('#6b7280').text('Юридический адрес клиента', 305, y + 25, { width });
106
+ doc.y = y + 50;
107
107
  }
108
- static drawTable(doc, rows, accent, theme) {
109
- let y = doc.y;
110
- const headers = Object.keys(rows[0]);
111
- const colCount = headers.length;
112
- const colWidth = 515 / colCount;
113
- // Header
114
- doc.rect(40, y, 515, 20).fill(theme === 'minimal' ? '#eee' : accent);
115
- doc.font('Bold').fontSize(8).fillColor(theme === 'minimal' ? '#000' : '#fff');
108
+ static drawTable(doc, data, accent) {
109
+ let details = [];
110
+ try {
111
+ details = typeof data.details === 'string' ? JSON.parse(data.details) : data.details;
112
+ }
113
+ catch (e) { }
114
+ let y = doc.y + 10;
115
+ const x = 40;
116
+ const colWidths = [30, 280, 50, 75, 80]; // №, Товар, Кол-во, Цена, Сумма
117
+ const headers = ['№', 'НАИМЕНОВАНИЕ ТОВАРОВ / УСЛУГ', 'КОЛ-ВО', 'ЦЕНА', 'СУММА'];
118
+ // Заголовок таблицы
119
+ doc.rect(x, y, 515, 20).fill(accent);
120
+ doc.font('Bold').fontSize(7).fillColor('#ffffff');
121
+ let currentX = x;
116
122
  headers.forEach((h, i) => {
117
- doc.text(h.toUpperCase(), 45 + (i * colWidth), y + 6, { width: colWidth - 10, align: 'center' });
123
+ doc.text(h, currentX, y + 7, { width: colWidths[i], align: i > 1 ? 'right' : 'center' });
124
+ currentX += colWidths[i];
118
125
  });
126
+ // Строки
119
127
  y += 20;
120
- doc.font('Main').fontSize(8).fillColor('#000');
121
- rows.forEach((row, index) => {
122
- const values = Object.values(row);
123
- const rowHeight = 20;
124
- if (index % 2 === 0)
125
- doc.rect(40, y, 515, rowHeight).fill('#f9f9f9').fillColor('#000');
126
- values.forEach((v, i) => {
127
- doc.text(String(v), 45 + (i * colWidth), y + 6, { width: colWidth - 10, align: 'center' });
128
+ doc.font('Main').fontSize(8).fillColor('#374151');
129
+ details.forEach((row, i) => {
130
+ const rowValues = Object.values(row);
131
+ const rowHeight = 22;
132
+ // Зебра
133
+ if (i % 2 === 0) {
134
+ doc.rect(x, y, 515, rowHeight).fill('#f9fafb');
135
+ }
136
+ doc.fillColor('#374151');
137
+ let itemX = x;
138
+ rowValues.forEach((val, j) => {
139
+ const align = j > 1 ? 'right' : (j === 0 ? 'center' : 'left');
140
+ const padding = j === 1 ? 10 : 0;
141
+ doc.text(String(val), itemX + padding, y + 7, { width: colWidths[j] - padding * 2, align });
142
+ itemX += colWidths[j];
128
143
  });
129
144
  y += rowHeight;
130
- doc.moveTo(40, y).lineTo(555, y).strokeColor('#eee').lineWidth(0.5).stroke();
145
+ doc.moveTo(x, y).lineTo(x + 515, y).strokeColor('#f3f4f6').lineWidth(0.5).stroke();
131
146
  });
132
147
  doc.y = y;
133
148
  }
134
- static drawTotals(doc, rows) {
135
- // Упрощенный расчет итога
149
+ static drawTotals(doc, data) {
150
+ let details = [];
151
+ try {
152
+ details = typeof data.details === 'string' ? JSON.parse(data.details) : data.details;
153
+ }
154
+ catch (e) { }
136
155
  let total = 0;
137
- rows.forEach((r) => {
138
- const price = parseFloat(String(r['Цена'] || r['Сумма'] || 0).replace(/\s/g, ''));
139
- total += isNaN(price) ? 0 : price;
156
+ details.forEach((item) => {
157
+ const values = Object.values(item);
158
+ const sum = parseFloat(String(values[values.length - 1]).replace(/\s/g, ''));
159
+ total += isNaN(sum) ? 0 : sum;
140
160
  });
141
- doc.moveDown(1);
142
- doc.font('Bold').fontSize(10).text(`ИТОГО К ОПЛАТЕ: ${total.toLocaleString('ru-RU')} руб.`, { align: 'right' });
143
- doc.font('Main').fontSize(8).text(`Всего наименований ${rows.length}, на сумму ${total.toLocaleString('ru-RU')} руб.`, { align: 'left' });
144
- doc.moveDown(1);
161
+ const y = doc.y + 15;
162
+ doc.x = 350;
163
+ doc.font('Main').fontSize(9).fillColor('#6b7280').text('Итого:', { continued: true });
164
+ doc.font('Bold').fillColor('#111827').text(` ${total.toLocaleString('ru-RU')} руб.`, { align: 'right', width: 200 });
165
+ doc.moveDown(0.3);
166
+ doc.font('Main').fontSize(10).fillColor('#6b7280').text('Всего к оплате:', { continued: true });
167
+ doc.font('Bold').fontSize(12).fillColor('#111827').text(` ${total.toLocaleString('ru-RU')} руб.`, { align: 'right', width: 200 });
145
168
  }
146
169
  static drawSignatures(doc, data, accent) {
147
- const y = doc.y + 40;
148
- doc.font('Bold').fontSize(9);
149
- doc.text('Руководитель ____________________', 40, y);
150
- doc.text('Бухгалтер ____________________', 320, y);
170
+ const y = 720;
171
+ doc.rect(40, y - 20, 515, 0.5).fill('#e5e7eb');
172
+ doc.font('Bold').fontSize(8).fillColor('#111827');
173
+ doc.text('РУКОВОДИТЕЛЬ', 40, y);
174
+ doc.text('БУХГАЛТЕР', 305, y);
175
+ doc.moveTo(40, y + 25).lineTo(200, y + 25).strokeColor('#9ca3af').stroke();
176
+ doc.moveTo(305, y + 25).lineTo(465, y + 25).stroke();
177
+ doc.font('Main').fontSize(7).fillColor('#9ca3af').text('(Подпись / Расшифровка)', 40, y + 30);
178
+ doc.text('(Подпись / Расшифровка)', 305, y + 30);
151
179
  if (data.showStamp) {
152
- // Рисуем штамп «по-настоящему»
153
- doc.save();
154
- doc.rotate(-10, { origin: [150, y + 20] });
155
- doc.rect(80, y - 10, 120, 50).lineWidth(2).strokeColor(accent).strokeOpacity(0.5).stroke();
156
- doc.fillColor(accent).fillOpacity(0.5).fontSize(12).text(data.status, 80, y + 5, { width: 120, align: 'center' });
157
- doc.fontSize(7).text(data.companyName, 80, y + 25, { width: 120, align: 'center' });
158
- doc.restore();
180
+ this.drawRealisticStamp(doc, 130, y - 30, data.status, accent, data.companyName);
159
181
  }
160
182
  }
183
+ static drawRealisticStamp(doc, x, y, text, color, company) {
184
+ doc.save();
185
+ doc.rotate(-12, { origin: [x + 60, y + 30] });
186
+ // Внешняя рамка
187
+ doc.rect(x, y, 130, 60).lineWidth(1.5).strokeColor(color).strokeOpacity(0.4).stroke();
188
+ // Внутренняя рамка
189
+ doc.rect(x + 3, y + 3, 124, 54).lineWidth(0.5).stroke();
190
+ doc.fillColor(color).fillOpacity(0.5);
191
+ doc.font('Bold').fontSize(14).text(text, x, y + 15, { width: 130, align: 'center' });
192
+ doc.font('Main').fontSize(7).text(company, x, y + 35, { width: 130, align: 'center' });
193
+ doc.restore();
194
+ }
161
195
  }
162
196
  exports.DocumentEngine = DocumentEngine;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-contract-lite",
3
- "version": "4.0.3",
3
+ "version": "4.0.4",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "n8n": {