Замініть увесь код у редакторі Apps Script на цей. Зміна одна: пункт «Створити кошторис для клієнта» показує список угод з CRM, і доданий пункт «Сформувати PDF».
ESTIMATE_SECRET.const РЯДКІВ = 4;
const РЯДКІВ_ВЛАСНІ = 3;
const ВІДСТУП = 4;
const ПІДПИС_ПЛЮС = '➕ поставте галочку, щоб додати рядок';
const КАТЕГОРІЇ = [
['1.1', 'Меблі, обладнання, інструменти, машини та механізми, інші основні засоби', ''],
['1.2', 'Транспортні засоби (комерційні/виробничі цілі)', ''],
['1.3', 'Матеріали та послуги для ремонтних робіт приміщень/обладнання', 0.50],
['1.4', 'Тварини і птиця, насадження, саджанці, посівний матеріал, сировина, матеріали, товари та послуги за бізнес-планом', 0.50],
['1.5', 'Фінансовий лізинг обладнання (крім авто, мотоциклів, ТЗ особистого користування)', 0.50],
['2.1', 'Орендна плата за нежитлове приміщення / земельну ділянку', 0.25],
['2.2', 'Орендна плата за обладнання', 0.10],
['3.1', 'Ліцензійне програмне забезпечення (бухгалтерське, CRM/ERP, касове, кібербезпека)', 0.25],
['3.2', 'Веб-сайт, інтернет-магазин, цифрові інструменти автоматизації', 0.10],
['3.3', 'Налаштування, інтеграція та супровід цифрових рішень', 0.20],
['4.1', 'Послуги маркетингу та реклами', 0.10],
['4.2', 'Айдентика, дизайн пакування, макети, друк промоматеріалів, фото/відеоконтент, постпродакшн', 0.50],
['4.3', 'Роялті за використання авторського права, суміжних прав, промзразків', 0.20],
['4.4', 'Створення за замовленням об’єктів авторського права', 0.20],
['4.5', 'Редагування, коректура тексту, макетування друкованої продукції (креативні індустрії)', 0.50],
['5', 'Облаштування робочих місць (меблі, обладнання, інструменти, спецодяг, ЗІЗ)', ''],
['6', 'Реєстрація ТМ / промзразка, патентний повірений, права інтелектуальної власності', 0.20]
];
const ВЛАСНІ_ДОДАТКОВІ = [
['7', 'Страхування (для автомобілів)'],
['8', 'Інтернет і телефон'],
['9', 'Податки']
];
const ЗАПАСНА = '1.4';
const П = 4;
const СИНІЙ = '#1f4e79';
const ЗЕЛЕНИЙ = '#c6efce';
const ЗЕЛЕНИЙ_ТЕМН = '#a9d08e';
const ЗЕЛЕНИЙ_СВІТ = '#e2efda';
const ЧЕРВОНИЙ = '#ffc7ce';
const ЗАГОЛОВОК = '#d9e2f3';
const СІРИЙ = '#f2f2f2';
const ПАПКА = 'Кошториси клієнтів';
function підпис_(запис) {
const ліміт = запис[2];
const хвіст = (ліміт === '' || ліміт === null)
? ' (без ліміту)'
: ' (не більше ' + Math.round(ліміт * 100) + '% від суми гранту)';
return запис[1] + хвіст;
}
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('📋 Кошторис 738')
.addItem('📥 Імпорт витрат з тексту', 'імпортЗТексту')
.addItem('📁 Імпорт витрат з файлу', 'імпортЗФайлу')
.addSeparator()
.addItem('➕ Додати рядок у категорію', 'додатиРядок')
.addItem('➖ Видалити поточний рядок', 'видалитиРядок')
.addSeparator()
.addItem('📄 Створити кошторис для клієнта', 'створитиФайлКлієнта')
.addItem('🧾 Сформувати PDF', 'сформуватиPDF')
.addItem('🧾 PDF з файлу клієнта', 'PDFзФайлу')
.addItem('🔧 Додати інструменти у файл клієнта', 'дооснаститиФайл')
.addItem('🧹 Очистити кошторис', 'очиститиКошторис')
.addSeparator()
.addItem('⚙️ Перевірити кнопки «+»', 'увімкнутиТригер')
.addItem('🔧 Перебудувати шаблон', 'створитиШаблонКошторису')
.addToUi();
}
function увімкнутиТригер() {
// Кнопки «+» тепер працюють через простий тригер onEdit — він є в кожній
// копії сам собою. Старий встановлений тригер треба зняти, інакше на одну
// галочку додаватиметься два рядки.
var знято = 0;
ScriptApp.getProjectTriggers().forEach(function (t) {
if (t.getHandlerFunction() === 'приЗміні_' || t.getHandlerFunction() === 'onEdit') {
ScriptApp.deleteTrigger(t);
знято++;
}
});
SpreadsheetApp.getUi().alert(
'Кнопки «+» працюють автоматично — і в цій таблиці, і в кожній копії.\n\n' +
(знято ? 'Знято зайвих тригерів: ' + знято + '. Вони більше не потрібні.'
: 'Нічого налаштовувати не треба.'));
}
// Назва onEdit — не випадкова. Це «простий тригер»: Google запускає його
// в будь-якій копії таблиці автоматично, нічого встановлювати не треба.
// Раніше тут був встановлений тригер (приЗміні_), а встановлені тригери
// при копіюванні файлу НЕ переносяться — тому в збережених кошторисах
// галочка «+» не працювала.
function onEdit(e) {
if (!e || !e.range) return;
const арк = e.range.getSheet();
if (арк.getName() !== 'Кошторис') return;
if (e.range.getColumn() !== 3) return;
if (e.value !== 'TRUE') return;
const r = e.range.getRow();
const тип = арк.getRange(r, 9).getValue();
if (тип !== 'P' && тип !== 'PX') return;
арк.getRange(r, 3).setValue(false);
вставитиПорожній_(арк, r, тип === 'PX');
}
function вставитиПорожній_(кш, рядокПлюса, власний) {
кш.insertRowBefore(рядокПлюса);
const нов = рядокПлюса;
кш.getRange(нов, 1, 1, 7).setBackground('#ffffff').setFontWeight('normal')
.setFontColor('#000000').setFontStyle('normal')
.setBorder(true, true, true, true, true, true);
кш.getRange(нов, 1, 1, 9).clearContent();
кш.getRange(нов, 2).clearNote();
кш.getRange(нов, 3).clearDataValidations();
if (власний) {
const тип = кш.getRange(1, 9, кш.getLastRow(), 1).getValues();
var влЗаг = 0;
for (var q = П; q < нов; q++) if (тип[q - 1][0] === 'O') влЗаг = q;
кш.getRange(нов, 9).setValue('X');
кш.getRange(нов, 1).setFormula('=IF($B' + нов + '="";"";COUNTIFS($B$' + (влЗаг + 1) +
':$B' + нов + ';"<>";$I$' + (влЗаг + 1) + ':$I' + нов + ';"X"))');
} else {
кш.getRange(нов, 1).setFormula('=IF($B' + нов + '="";"";COUNTIFS($B$' + П + ':$B' + нов +
';"<>";$I$' + П + ':$I' + нов + ';""))');
}
кш.getRange(нов, 6).setFormula('=IF(OR($C' + нов + '="";$D' + нов + '="");"";$C' + нов +
'*$D' + нов + ')');
кш.getRange(нов, 3).setNumberFormat('#,##0.00');
кш.getRange(нов, 4).setNumberFormat('#,##0');
кш.getRange(нов, 6).setNumberFormat('#,##0.00');
кш.getRange(нов, 5).setHorizontalAlignment('center');
кш.getRange(нов, 1).setHorizontalAlignment('center');
перерахувати_(кш);
кш.getRange(нов, 2).activate();
}
function оформитиПлюс_(кш, r, власний) {
кш.getRange(r, 1, 1, 7).setBackground(СІРИЙ).setFontColor('#666666')
.setFontWeight('normal').setBorder(true, true, true, true, true, true);
кш.getRange(r, 2).setValue(ПІДПИС_ПЛЮС).setFontStyle('italic');
кш.getRange(r, 3).insertCheckboxes().setValue(false);
кш.getRange(r, 9).setValue(власний ? 'PX' : 'P');
}
// ==================== ІМПОРТ ====================
function імпортЗТексту() {
const html = HtmlService.createHtmlOutput(
'<div style="font-family:Arial;padding:8px">' +
'<p style="font-size:13px;margin:0 0 6px">Вставте текст із витратами клієнта — ' +
'у будь-якому вигляді, як прислав. Посилання не видаляйте: з них береться модель і бренд.</p>' +
'<textarea id="t" style="width:100%;height:230px;font-size:13px"></textarea>' +
'<div style="margin-top:8px"><button onclick="go()" id="b" ' +
'style="padding:7px 16px;font-size:13px">Розібрати</button> ' +
'<span id="s" style="color:#666;font-size:12px"></span></div></div>' +
'<script>function go(){document.getElementById("b").disabled=true;' +
'document.getElementById("s").innerText="Аналізую, зачекайте 10–20 секунд...";' +
'google.script.run.withSuccessHandler(function(){google.script.host.close();})' +
'.withFailureHandler(function(e){document.getElementById("b").disabled=false;' +
'document.getElementById("s").innerText="Помилка: "+e.message;})' +
'.обробитиТекст(document.getElementById("t").value);}</script>')
.setWidth(560).setHeight(380);
SpreadsheetApp.getUi().showModalDialog(html, 'Імпорт витрат з тексту');
}
function імпортЗФайлу() {
const ui = SpreadsheetApp.getUi();
const р = ui.prompt('Імпорт з файлу',
'Введіть точну назву файлу на Google Диску або вставте посилання на нього:',
ui.ButtonSet.OK_CANCEL);
if (р.getSelectedButton() !== ui.Button.OK) return;
const запит = р.getResponseText().trim();
if (!запит) return;
var файл = null;
const м = запит.match(/[-\w]{25,}/);
try {
if (м) файл = DriveApp.getFileById(м[0]);
else {
const іт = DriveApp.getFilesByName(запит);
if (іт.hasNext()) файл = іт.next();
}
} catch (e) { файл = null; }
if (!файл) { ui.alert('Файл не знайдено. Перевірте назву або посилання.'); return; }
var текст = '';
const mime = файл.getMimeType();
try {
if (mime === MimeType.GOOGLE_SHEETS || mime === MimeType.MICROSOFT_EXCEL ||
mime === MimeType.MICROSOFT_EXCEL_LEGACY || mime === MimeType.CSV) {
var idТабл = файл.getId(), копіяТабл = null;
if (mime !== MimeType.GOOGLE_SHEETS) {
копіяТабл = Drive.Files.copy({ title: '__imp__', mimeType: MimeType.GOOGLE_SHEETS },
файл.getId());
idТабл = копіяТабл.id;
}
SpreadsheetApp.openById(idТабл).getSheets().forEach(function (sh) {
sh.getDataRange().getValues().forEach(function (row) { текст += row.join(' | ') + '\n'; });
});
if (копіяТабл) DriveApp.getFileById(копіяТабл.id).setTrashed(true);
} else {
const копіяДок = Drive.Files.copy({ title: '__imp__', mimeType: MimeType.GOOGLE_DOCS },
файл.getId());
текст = DocumentApp.openById(копіяДок.id).getBody().getText();
DriveApp.getFileById(копіяДок.id).setTrashed(true);
}
} catch (e) {
ui.alert('Не вдалося прочитати файл.\n\nЯкщо це PDF-скан, текст із нього не витягується — ' +
'скопіюйте дані вручну і скористайтесь пунктом «Імпорт витрат з тексту».\n\n' + e.message);
return;
}
if (!текст.trim()) { ui.alert('Файл прочитано, але тексту в ньому не знайдено.'); return; }
обробитиТекст(текст);
}
function обробитиТекст(текст) {
const ui = SpreadsheetApp.getUi();
if (!текст || !текст.trim()) throw new Error('Текст порожній.');
const результат = GrantAPI.розібрати(текст);
const сирі = (результат && результат.items) ? результат.items : [];
const коди = КАТЕГОРІЇ.map(function (c) { return c[0]; });
const позиції = [];
сирі.forEach(function (p) {
const назва = String(p.name || '').trim();
const ціна = Number(p.price);
if (!назва || !ціна || ціна <= 0) return;
var кільк = Number(p.qty);
if (!кільк || кільк <= 0) кільк = 1;
var код = String(p.cat || '').trim();
var сумнів = (p.sure === false);
if (коди.indexOf(код) === -1) { код = ЗАПАСНА; сумнів = true; }
позиції.push({ назва: назва, ціна: ціна, кільк: кільк,
од: String(p.unit || '').trim(), код: код, сумнів: сумнів,
джерело: String(p.src || '').trim() });
});
if (!позиції.length) throw new Error('Модель не знайшла жодної позиції витрат у цьому тексті.');
var зведення = '', сум = 0;
позиції.forEach(function (p) {
зведення += (p.сумнів ? '⚠️ ' : '') + p.назва + ' — ' +
p.ціна.toFixed(2) + ' × ' + p.кільк + (p.од ? ' ' + p.од : '') +
' → кат. ' + p.код + (p.джерело ? ' 🔗' : '') + '\n';
сум += p.ціна * p.кільк;
});
const відп = ui.alert('Розпізнано позицій: ' + позиції.length,
зведення + '\nЗагальна сума: ' + сум.toFixed(2) + ' грн\n\n' +
'⚠️ — категорію під питанням, перевірте. 🔗 — збережено посилання у примітці.\n\n' +
'Додати до кошторису?', ui.ButtonSet.YES_NO);
if (відп !== ui.Button.YES) return;
вставитиПозиції_(позиції);
}
function вставитиПозиції_(позиції) {
const кш = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Кошторис');
const групи = {};
позиції.forEach(function (p) {
if (!групи[p.код]) групи[p.код] = [];
групи[p.код].push(p);
});
Object.keys(групи).sort().reverse().forEach(function (код) {
const запис = КАТЕГОРІЇ.filter(function (c) { return c[0] === код; })[0];
if (!запис) return;
const назваКат = підпис_(запис);
const last = кш.getLastRow();
const колB = кш.getRange(1, 2, last, 1).getValues();
const колI = кш.getRange(1, 9, last, 1).getValues();
var шапка = 0, плюс = 0;
for (var r = П; r <= last; r++) {
if (колI[r - 1][0] === 'H' && колB[r - 1][0] === назваКат) { шапка = r; break; }
}
if (!шапка) return;
for (var r2 = шапка + 1; r2 <= last; r2++) {
if (колI[r2 - 1][0] === 'P') { плюс = r2; break; }
}
if (!плюс) return;
var вільні = [];
for (var r3 = шапка + 1; r3 < плюс; r3++) {
if (String(колB[r3 - 1][0]).trim() === '') вільні.push(r3);
}
const треба = групи[код].length;
if (вільні.length < треба) {
const бракує = треба - вільні.length;
кш.insertRowsBefore(плюс, бракує);
for (var b = 0; b < бракує; b++) {
var нр = плюс + b;
кш.getRange(нр, 1, 1, 7).setBackground('#ffffff').setFontColor('#000000')
.setFontWeight('normal').setFontStyle('normal')
.setBorder(true, true, true, true, true, true);
кш.getRange(нр, 3).clearDataValidations();
кш.getRange(нр, 1, 1, 9).clearContent();
вільні.push(нр);
}
}
групи[код].forEach(function (p, idx) {
var r4 = вільні[idx];
var комірка = кш.getRange(r4, 2);
комірка.setValue((p.сумнів ? '⚠️ ' : '') + p.назва).setFontStyle('normal');
if (p.джерело) комірка.setNote('Джерело ціни:\n' + p.джерело);
кш.getRange(r4, 3).setValue(p.ціна);
кш.getRange(r4, 4).setValue(p.кільк);
кш.getRange(r4, 5).setValue(p.од || '');
кш.getRange(r4, 1).setFormula('=IF($B' + r4 + '="";"";COUNTIFS($B$' + П + ':$B' + r4 +
';"<>";$I$' + П + ':$I' + r4 + ';""))');
кш.getRange(r4, 6).setFormula('=IF(OR($C' + r4 + '="";$D' + r4 + '="");"";$C' + r4 +
'*$D' + r4 + ')');
кш.getRange(r4, 3).setNumberFormat('#,##0.00');
кш.getRange(r4, 4).setNumberFormat('#,##0');
кш.getRange(r4, 5).setHorizontalAlignment('center');
кш.getRange(r4, 6).setNumberFormat('#,##0.00');
кш.getRange(r4, 1).setHorizontalAlignment('center');
});
});
перерахувати_(кш);
SpreadsheetApp.flush();
}
// ==================== ШАБЛОН ====================
function створитиШаблонКошторису() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const N = КАТЕГОРІЇ.length;
const КІНЕЦЬ = П + N - 1;
const СУМА = КІНЕЦЬ + 2;
const тимч = ss.insertSheet('__tmp__');
ss.getSheets().forEach(function (s) {
if (s.getName() !== '__tmp__') ss.deleteSheet(s);
});
SpreadsheetApp.flush();
// -------- КОНТРОЛЬ --------
const к = ss.insertSheet('Контроль');
к.clearConditionalFormatRules();
к.getRange(1, 1, к.getMaxRows(), к.getMaxColumns()).setBackground('#ffffff');
к.getRange('A1').setValue('КОНТРОЛЬ ЛІМІТІВ ЗА НАПРЯМАМИ ВИТРАТ (постанова КМУ № 738)');
к.getRange('A1:H1').merge().setFontSize(13).setFontWeight('bold')
.setBackground(СИНІЙ).setFontColor('#ffffff').setVerticalAlignment('middle');
к.setRowHeight(1, 32);
к.getRange(3, 1, 1, 8).setValues([['Код', 'Категорія витрат (напрям за постановою)',
'Ліміт, %', 'Ліміт, грн', 'Факт, грн', 'Факт, %', 'Запас / перевищення, грн', 'Статус']])
.setFontWeight('bold').setBackground(ЗАГОЛОВОК).setWrap(true).setHorizontalAlignment('center');
const катДані = КАТЕГОРІЇ.map(function (c) { return [c[0], підпис_(c), c[2]]; });
к.getRange(П, 1, N, 1).setNumberFormat('@');
к.getRange(П, 1, N, 3).setValues(катДані);
const фК = [], фJ = [];
for (var i = П; i <= КІНЕЦЬ; i++) {
фК.push([
'=IF($C' + i + '="";"";$C' + i + '*$E$' + СУМА + ')',
'=SUMIF(Кошторис!$B:$B;$B' + i + ';Кошторис!$F:$F)',
'=IF($E$' + СУМА + '=0;"";$E' + i + '/$E$' + СУМА + ')',
'=IF($C' + i + '="";"";$D' + i + '-$E' + i + ')',
'=IF($E' + i + '=0;"—";IF($C' + i + '="";"Без ліміту";IF($E' + i + '>$D' + i +
';"ПЕРЕВИЩЕННЯ";"У межах ліміту")))'
]);
фJ.push(['=IF($E' + i + '=0;"";IF($C' + i + '="";"OK";IF($E' + i + '>$D' + i +
';"ПЕРЕВИЩЕННЯ";"OK")))']);
}
к.getRange(П, 4, N, 5).setFormulas(фК);
к.getRange(П, 10, N, 1).setFormulas(фJ);
к.getRange(СУМА, 2).setValue('ЗАГАЛЬНА СУМА КОШТОРИСУ (= сума гранту), грн:').setFontWeight('bold');
к.getRange(СУМА, 5).setFormula('=SUMIF(Кошторис!$I:$I;"T";Кошторис!$F:$F)').setFontWeight('bold');
к.getRange(СУМА, 2, 1, 4).setBackground('#ffe699');
к.getRange(П, 3, N, 1).setNumberFormat('0%');
к.getRange(П, 4, N, 1).setNumberFormat('#,##0.00');
к.getRange(П, 5, N, 1).setNumberFormat('#,##0.00');
к.getRange(П, 6, N, 1).setNumberFormat('0.0%');
к.getRange(П, 7, N, 1).setNumberFormat('#,##0.00');
к.getRange(СУМА, 5).setNumberFormat('#,##0.00');
к.getRange(П, 8, N, 1).setHorizontalAlignment('center').setFontWeight('bold');
к.getRange(П, 1, N, 1).setHorizontalAlignment('center');
к.getRange(П, 2, N, 1).setWrap(true);
к.getRange(3, 1, N + 1, 8).setBorder(true, true, true, true, true, true);
к.setColumnWidth(1, 55); к.setColumnWidth(2, 460); к.setColumnWidth(3, 75);
к.setColumnWidth(4, 115); к.setColumnWidth(5, 115); к.setColumnWidth(6, 80);
к.setColumnWidth(7, 145); к.setColumnWidth(8, 150);
к.hideColumns(9, 2);
к.setFrozenRows(3);
к.setConditionalFormatRules([
SpreadsheetApp.newConditionalFormatRule().whenFormulaSatisfied('=$J' + П + '="ПЕРЕВИЩЕННЯ"')
.setBackground(ЧЕРВОНИЙ).setFontColor('#9c0006')
.setRanges([к.getRange(П, 1, N, 8)]).build(),
SpreadsheetApp.newConditionalFormatRule().whenFormulaSatisfied('=$J' + П + '="OK"')
.setBackground(ЗЕЛЕНИЙ).setFontColor('#006100')
.setRanges([к.getRange(П, 1, N, 8)]).build()
]);
// -------- КОШТОРИС --------
const кш = ss.insertSheet('Кошторис', 0);
кш.clearConditionalFormatRules();
кш.getRange(1, 1, кш.getMaxRows(), кш.getMaxColumns())
.setBackground('#ffffff').setFontColor('#000000');
кш.getRange('A1').setValue('КОШТОРИС ВИТРАТ ГРАНТОВИХ КОШТІВ');
кш.getRange('A1:G1').merge().setFontSize(14).setFontWeight('bold')
.setBackground(СИНІЙ).setFontColor('#ffffff').setVerticalAlignment('middle');
кш.setRowHeight(1, 34);
кш.getRange('A2').setValue('Клієнт:').setFontWeight('bold');
кш.getRange(3, 1, 1, 7).setValues([['№', 'Категорія витрат / найменування',
'Ціна за од., грн', 'Кільк.', 'Од.', 'Сума, грн', '% від кошторису']])
.setFontWeight('bold').setBackground(ЗАГОЛОВОК).setWrap(true).setHorizontalAlignment('center');
const тіло = [], заг = [], плюси = [], влЗаголовки = [], влПлюси = [];
var р = П;
for (var t = 0; t < N; t++) {
заг.push(р);
тіло.push([КАТЕГОРІЇ[t][0], підпис_(КАТЕГОРІЇ[t]), '', '', '',
'=SUM($F$' + (р + 1) + ':$F$' + (р + РЯДКІВ) + ')',
'=IF(SUMIF($I:$I;"T";$F:$F)=0;"";$F' + р + '/SUMIF($I:$I;"T";$F:$F))',
'=IFERROR(VLOOKUP($B' + р + ';Контроль!$B:$J;9;FALSE);"")', 'H']);
for (var d = 1; d <= РЯДКІВ; d++) {
var rr = р + d;
тіло.push([
'=IF($B' + rr + '="";"";COUNTIFS($B$' + П + ':$B' + rr + ';"<>";$I$' + П + ':$I' + rr + ';""))',
'', '', '', '',
'=IF(OR($C' + rr + '="";$D' + rr + '="");"";$C' + rr + '*$D' + rr + ')', '', '', '']);
}
плюси.push(р + РЯДКІВ + 1);
тіло.push(['', ПІДПИС_ПЛЮС, '', '', '', '', '', '', 'P']);
р += 2 + РЯДКІВ;
}
const РАЗ = р;
тіло.push(['', 'РАЗОМ ЗА КОШТОРИСОМ:', '', '', '',
'=SUMIF($I$' + П + ':$I$' + (РАЗ - 1) + ';"H";$F$' + П + ':$F$' + (РАЗ - 1) + ')',
'=IF($F' + РАЗ + '=0;"";1)', '', 'T']);
р += 1;
for (var v = 0; v < ВІДСТУП; v++) { тіло.push(['', '', '', '', '', '', '', '', '']); р += 1; }
const ВЛ_ЗАГ = р;
тіло.push(['', 'ВИТРАТИ ВЛАСНИХ КОШТІВ (довідково, не входять до суми гранту)',
'', '', '', '', '', '', 'O']);
р += 1;
const ВЛАСНІ_ВСІ = КАТЕГОРІЇ.map(function (c) { return [c[0], c[1]]; })
.concat(ВЛАСНІ_ДОДАТКОВІ);
for (var t2 = 0; t2 < ВЛАСНІ_ВСІ.length; t2++) {
влЗаголовки.push(р);
тіло.push([ВЛАСНІ_ВСІ[t2][0], ВЛАСНІ_ВСІ[t2][1], '', '', '',
'=SUM($F$' + (р + 1) + ':$F$' + (р + РЯДКІВ_ВЛАСНІ) + ')', '', '', 'OH']);
for (var d2 = 1; d2 <= РЯДКІВ_ВЛАСНІ; d2++) {
var ww = р + d2;
тіло.push([
'=IF($B' + ww + '="";"";COUNTIFS($B$' + (ВЛ_ЗАГ + 1) + ':$B' + ww + ';"<>";$I$' +
(ВЛ_ЗАГ + 1) + ':$I' + ww + ';"X"))',
'', '', '', '',
'=IF(OR($C' + ww + '="";$D' + ww + '="");"";$C' + ww + '*$D' + ww + ')', '', '', 'X']);
}
влПлюси.push(р + РЯДКІВ_ВЛАСНІ + 1);
тіло.push(['', ПІДПИС_ПЛЮС, '', '', '', '', '', '', 'PX']);
р += 2 + РЯДКІВ_ВЛАСНІ;
}
const ВЛ_РАЗ = р;
тіло.push(['', 'РАЗОМ ВЛАСНІ КОШТИ:', '', '', '',
'=SUMIF($I$' + (ВЛ_ЗАГ + 1) + ':$I$' + (ВЛ_РАЗ - 1) + ';"OH";$F$' + (ВЛ_ЗАГ + 1) +
':$F$' + (ВЛ_РАЗ - 1) + ')', '', '', 'OT']);
кш.getRange(П, 1, тіло.length, 1).setNumberFormat('@');
кш.getRange(П, 1, тіло.length, 9).setValues(тіло);
заг.forEach(function (h) {
кш.getRange(h, 1, 1, 7).setFontWeight('bold').setBackground(ЗАГОЛОВОК);
});
влЗаголовки.forEach(function (h) {
кш.getRange(h, 1, 1, 7).setFontWeight('bold').setBackground(ЗЕЛЕНИЙ_СВІТ)
.setFontColor('#1a3a12');
});
плюси.forEach(function (pp) { оформитиПлюс_(кш, pp, false); });
влПлюси.forEach(function (pp) { оформитиПлюс_(кш, pp, true); });
кш.getRange(РАЗ, 1, 1, 7).setBackground('#ffe699').setFontWeight('bold');
кш.getRange(РАЗ, 2).setHorizontalAlignment('right');
кш.getRange(ВЛ_ЗАГ, 1, 1, 7).setBackground(ЗЕЛЕНИЙ_ТЕМН).setFontWeight('bold')
.setFontColor('#1a3a12');
кш.getRange(ВЛ_РАЗ, 1, 1, 7).setBackground(ЗЕЛЕНИЙ).setFontWeight('bold')
.setFontColor('#006100');
кш.getRange(ВЛ_РАЗ, 2).setHorizontalAlignment('right');
кш.getRange(П, 4, ВЛ_РАЗ - П + 1, 1).setNumberFormat('#,##0');
кш.getRange(П, 6, ВЛ_РАЗ - П + 1, 1).setNumberFormat('#,##0.00');
кш.getRange(П, 7, РАЗ - П + 1, 1).setNumberFormat('0.0%');
кш.getRange(П, 1, ВЛ_РАЗ - П + 1, 1).setHorizontalAlignment('center');
кш.getRange(П, 5, ВЛ_РАЗ - П + 1, 1).setHorizontalAlignment('center');
кш.getRange(П, 2, ВЛ_РАЗ - П + 1, 1).setWrap(true);
кш.getRange(3, 1, РАЗ - 2, 7).setBorder(true, true, true, true, true, true);
кш.getRange(ВЛ_ЗАГ, 1, ВЛ_РАЗ - ВЛ_ЗАГ + 1, 7).setBorder(true, true, true, true, true, true);
кш.setColumnWidth(1, 50); кш.setColumnWidth(2, 600); кш.setColumnWidth(3, 130);
кш.setColumnWidth(4, 80); кш.setColumnWidth(5, 70); кш.setColumnWidth(6, 150);
кш.setColumnWidth(7, 110);
кш.hideColumns(8, 2);
кш.setFrozenRows(3);
кш.setConditionalFormatRules([
SpreadsheetApp.newConditionalFormatRule().whenFormulaSatisfied('=$H' + П + '="ПЕРЕВИЩЕННЯ"')
.setBackground(ЧЕРВОНИЙ).setFontColor('#9c0006')
.setRanges([кш.getRange(П, 1, РАЗ - П + 1, 7)]).build()
]);
к.protect().setDescription('Формули контролю лімітів').setWarningOnly(true);
ss.deleteSheet(тимч);
кш.getRange(П + 1, 2).activate();
SpreadsheetApp.flush();
}
// ==================== СЕРВІС ====================
function перерахувати_(кш) {
const last = кш.getLastRow();
const тип = кш.getRange(1, 9, last, 1).getValues();
const грант = [], власні = [];
var разом = 0, влЗаг = 0, влРаз = 0;
for (var r = П; r <= last; r++) {
var м = тип[r - 1][0];
if (м === 'H') грант.push(r);
if (м === 'OH') власні.push(r);
if (м === 'T') разом = r;
if (м === 'O') влЗаг = r;
if (м === 'OT') влРаз = r;
}
грант.forEach(function (h) {
var end = h;
for (var z = h + 1; z <= last; z++) { if (тип[z - 1][0] === 'P') { end = z - 1; break; } }
кш.getRange(h, 6).setFormula('=SUM($F$' + (h + 1) + ':$F$' + end + ')');
кш.getRange(h, 7).setFormula('=IF(SUMIF($I:$I;"T";$F:$F)=0;"";$F' + h +
'/SUMIF($I:$I;"T";$F:$F))');
кш.getRange(h, 8).setFormula('=IFERROR(VLOOKUP($B' + h + ';Контроль!$B:$J;9;FALSE);"")');
});
власні.forEach(function (h) {
var end = h;
for (var z = h + 1; z <= last; z++) { if (тип[z - 1][0] === 'PX') { end = z - 1; break; } }
кш.getRange(h, 6).setFormula('=SUM($F$' + (h + 1) + ':$F$' + end + ')');
});
if (разом) {
кш.getRange(разом, 6).setFormula('=SUMIF($I$' + П + ':$I$' + (разом - 1) +
';"H";$F$' + П + ':$F$' + (разом - 1) + ')');
кш.getRange(разом, 7).setFormula('=IF($F' + разом + '=0;"";1)');
}
if (влЗаг && влРаз) {
кш.getRange(влРаз, 6).setFormula('=SUMIF($I$' + (влЗаг + 1) + ':$I$' + (влРаз - 1) +
';"OH";$F$' + (влЗаг + 1) + ':$F$' + (влРаз - 1) + ')');
}
}
function додатиРядок() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const ui = SpreadsheetApp.getUi();
if (ss.getActiveSheet().getName() !== 'Кошторис') {
ui.alert('Перейдіть на аркуш «Кошторис» і станьте на рядок потрібної категорії.'); return;
}
const кш = ss.getSheetByName('Кошторис');
const r = кш.getActiveRange().getRow();
if (r < П) { ui.alert('Станьте на рядок усередині потрібної категорії.'); return; }
const last = кш.getLastRow();
const тип = кш.getRange(1, 9, last, 1).getValues();
var плюс = 0, власний = false;
for (var z = r; z <= last; z++) {
if (тип[z - 1][0] === 'P') { плюс = z; власний = false; break; }
if (тип[z - 1][0] === 'PX') { плюс = z; власний = true; break; }
}
if (!плюс) { ui.alert('Станьте на рядок усередині потрібної категорії.'); return; }
вставитиПорожній_(кш, плюс, власний);
}
function видалитиРядок() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const ui = SpreadsheetApp.getUi();
if (ss.getActiveSheet().getName() !== 'Кошторис') {
ui.alert('Перейдіть на аркуш «Кошторис».'); return;
}
const кш = ss.getSheetByName('Кошторис');
const r = кш.getActiveRange().getRow();
const тип = кш.getRange(r, 9).getValue();
if (r < П || ['H', 'OH', 'T', 'O', 'OT', 'P', 'PX'].indexOf(тип) > -1) {
ui.alert('Видаляти можна лише рядок позиції.'); return;
}
кш.deleteRow(r);
перерахувати_(кш);
}
function очиститиКошторис() {
const ui = SpreadsheetApp.getUi();
const в = ui.alert('Очистити кошторис?',
'Усі внесені позиції буде видалено (разом із блоком власних коштів). ' +
'Структура категорій залишиться.', ui.ButtonSet.YES_NO);
if (в !== ui.Button.YES) return;
const кш = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Кошторис');
const last = кш.getLastRow();
const тип = кш.getRange(1, 9, last, 1).getValues();
for (var r = П; r <= last; r++) {
if (тип[r - 1][0] === '' || тип[r - 1][0] === 'X') {
кш.getRange(r, 2, 1, 4).clearContent();
кш.getRange(r, 2).clearNote();
}
}
кш.getRange('B2').clearContent();
}
// ==================== ЗВʼЯЗОК З CRM ====================
//
// Кошторис зберігається одразу під угоду: файл отримує назву угоди,
// доступ за посиланням і сам прописується в поле «Таблиця з даними».
// Ключ доступу лежить у властивостях скрипта, а не в коді.
const API_КОШТОРИС = 'https://app.bdn.com.ua/api/estimate';
// Хто може РЕДАГУВАТИ створені кошториси. Решті — лише перегляд за
// посиланням. Додавайте пошти через кому, у лапках.
const РЕДАКТОРИ = [
'borisenkodn86@gmail.com',
'fopconsult.org.ua@gmail.com',
'borisenkodv88@gmail.com',
];
function ключ_() {
const k = PropertiesService.getScriptProperties().getProperty('ESTIMATE_SECRET');
if (!k) throw new Error('Не задано ESTIMATE_SECRET у властивостях скрипта.');
return k;
}
function запитCRM_(шлях, тіло) {
тіло.secret = ключ_();
const відп = UrlFetchApp.fetch(API_КОШТОРИС + шлях, {
method: 'post', contentType: 'application/json',
payload: JSON.stringify(тіло), muteHttpExceptions: true
});
const дані = JSON.parse(відп.getContentText() || '{}');
if (дані.error) throw new Error(дані.error);
return дані;
}
// УВАГА: назви цих двох функцій БЕЗ підкреслення в кінці. В Apps Script
// підкреслення робить функцію приватною, і google.script.run її не бачить —
// діалог тоді вічно висить на «Завантажую…».
function списокУгод() {
return запитCRM_('/deals', {}).deals;
}
function перенестиСкрипт_(idФайла, назва) {
// Копія таблиці приходить без скрипта: ні makeCopy, ні Drive.Files.copy
// привʼязаний проєкт не переносять — перевірено на живих файлах. Єдиний
// спосіб — створити проєкт у копії через Apps Script API і залити туди
// той самий код.
//
// Потрібні дві речі, які робляться ОДИН раз:
// 1. увімкнути Apps Script API: script.google.com/home/usersettings
// 2. дозвіл script.projects у маніфесті проєкту
// Поки їх немає — функція просто не спрацює, і кошторис збережеться без
// інструментів. Зривати збереження вона не має права.
const заг = { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() };
const мій = ScriptApp.getScriptId();
const вміст = JSON.parse(UrlFetchApp.fetch(
'https://script.googleapis.com/v1/projects/' + мій + '/content',
{ headers: заг, muteHttpExceptions: true }).getContentText());
if (!вміст.files) {
throw new Error((вміст.error && вміст.error.message) || 'скрипт не прочитано');
}
const проєкт = JSON.parse(UrlFetchApp.fetch(
'https://script.googleapis.com/v1/projects', {
method: 'post', contentType: 'application/json', headers: заг,
payload: JSON.stringify({ title: 'Кошторис — ' + назва, parentId: idФайла }),
muteHttpExceptions: true }).getContentText());
if (!проєкт.scriptId) {
throw new Error((проєкт.error && проєкт.error.message) || 'проєкт не створено');
}
const відп = UrlFetchApp.fetch(
'https://script.googleapis.com/v1/projects/' + проєкт.scriptId + '/content', {
method: 'put', contentType: 'application/json', headers: заг,
payload: JSON.stringify({ files: вміст.files }), muteHttpExceptions: true });
const рез = JSON.parse(відп.getContentText());
if (рез.error) throw new Error(рез.error.message);
return true;
}
function зберегтиДляУгоди(id, назва) {
const папки = DriveApp.getFoldersByName(ПАПКА);
const папка = папки.hasNext() ? папки.next() : DriveApp.createFolder(ПАПКА);
// Копіюємо через розширений сервіс Drive, а не через makeCopy: makeCopy
// залишає скрипт позаду, і в клієнтському файлі немає ні меню, ні кнопок.
// Якщо розширений сервіс недоступний — повертаємось до makeCopy, бо файл
// має створитись у будь-якому разі.
var файл;
try {
const копіяDrive = Drive.Files.copy(
{ title: назва, parents: [{ id: папка.getId() }] },
SpreadsheetApp.getActiveSpreadsheet().getId());
файл = DriveApp.getFileById(копіяDrive.id);
} catch (e) {
Logger.log('Drive.Files.copy не спрацював: ' + e.message);
файл = DriveApp.getFileById(SpreadsheetApp.getActiveSpreadsheet().getId())
.makeCopy(назва, папка);
}
const копія = SpreadsheetApp.openById(файл.getId());
копія.getSheetByName('Кошторис').getRange('B2').setValue(назва);
SpreadsheetApp.flush();
// Усім за посиланням — перегляд; названим особам — редагування.
файл.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
if (РЕДАКТОРИ.length) {
// Помилка в одній пошті не повинна зривати збереження: файл уже
// створено, і кошторис має потрапити в CRM у будь-якому разі.
РЕДАКТОРИ.forEach(function (пошта) {
try { файл.addEditor(пошта); } catch (e) { Logger.log('не додано ' + пошта + ': ' + e.message); }
});
}
// Інструменти в копію — якщо дозволи вже видані. Не вийшло — не біда:
// кошторис усе одно збережеться і потрапить у картку.
try {
перенестиСкрипт_(файл.getId(), назва);
} catch (e) {
Logger.log('скрипт у копію не перенесено: ' + e.message);
}
запитCRM_('/attach', { dealId: id, url: файл.getUrl() });
return файл.getUrl();
}
function створитиФайлКлієнта() {
const html = HtmlService.createHtmlOutput(
'<div style="font-family:Arial;padding:10px;font-size:13px">' +
'<p style="margin:0 0 8px">Оберіть угоду — файл отримає її назву, доступ за ' +
'посиланням і сам пропишеться в CRM у полі «Таблиця з даними».</p>' +
'<select id="u" style="width:100%;padding:6px;font-size:13px">' +
'<option>Завантажую…</option></select>' +
'<div style="margin-top:10px"><button id="b" onclick="go()" disabled ' +
'style="padding:7px 16px;font-size:13px">Створити</button> ' +
'<span id="s" style="color:#666;margin-left:8px"></span></div></div>' +
'<script>' +
'google.script.run.withSuccessHandler(function(d){' +
'var s=document.getElementById("u");s.innerHTML="";' +
'if(!d||!d.length){s.innerHTML="<option>Немає угод на потрібних етапах</option>";return;}' +
'var g={};d.forEach(function(x){(g[x.step]=g[x.step]||[]).push(x);});' +
'Object.keys(g).forEach(function(k){var o=document.createElement("optgroup");o.label=k;' +
'g[k].forEach(function(x){var e=document.createElement("option");e.value=x.id;' +
'e.text=x.name;o.appendChild(e);});s.appendChild(o);});' +
'document.getElementById("b").disabled=false;})' +
'.withFailureHandler(function(e){document.getElementById("u").innerHTML=' +
'"<option>"+e.message+"</option>";}).списокУгод();' +
'function go(){var s=document.getElementById("u");' +
'var id=s.value,nm=s.options[s.selectedIndex].text;' +
'document.getElementById("b").disabled=true;' +
'document.getElementById("s").innerText="Створюю…";' +
'google.script.run.withSuccessHandler(function(u){' +
'document.getElementById("s").innerHTML="Готово: <a href=\\""+u+"\\" target=\\"_blank\\">відкрити</a>";})' +
'.withFailureHandler(function(e){document.getElementById("b").disabled=false;' +
'document.getElementById("s").innerText="Помилка: "+e.message;})' +
'.зберегтиДляУгоди(id,nm);}' +
'</script>').setWidth(520).setHeight(230);
SpreadsheetApp.getUi().showModalDialog(html, 'Створити кошторис для клієнта');
}
// ==================== PDF ====================
//
// У PDF ідуть ТІЛЬКИ заповнені рядки і ТІЛЬКИ грантові кошти. Порожні
// категорії не друкуються взагалі, блок власних коштів не друкується
// ніколи — це вимога до документа, який іде на комісію.
//
// Документ збирається з нуля в тимчасовій таблиці, а не ховає зайве в
// цій: інакше в PDF потрапили б приховані службові стовпці й формули,
// що посилаються на оригінал.
function зібратиГрантові_(ss) {
const кш = (ss || SpreadsheetApp.getActiveSpreadsheet()).getSheetByName('Кошторис');
if (!кш) throw new Error('У файлі немає аркуша «Кошторис».');
const last = кш.getLastRow();
const дані = кш.getRange(1, 1, last, 9).getValues();
const блоки = [];
var поточний = null;
for (var r = П; r <= last; r++) {
const тип = String(дані[r - 1][8] || '');
if (тип === 'O' || тип === 'T') break; // далі власні кошти або підсумок
if (тип === 'H') {
поточний = { код: String(дані[r - 1][0] || ''), назва: String(дані[r - 1][1] || ''),
позиції: [] };
блоки.push(поточний);
continue;
}
if (тип !== '') continue; // рядки «+» та службові
if (!поточний) continue;
const назва = String(дані[r - 1][1] || '').trim();
const сума = Number(дані[r - 1][5]) || 0;
if (!назва || !сума) continue; // порожній рядок не друкуємо
поточний.позиції.push([назва, Number(дані[r - 1][2]) || 0,
Number(дані[r - 1][3]) || 0,
String(дані[r - 1][4] || ''), сума]);
}
return блоки.filter(function (b) { return b.позиції.length; });
}
function сформуватиPDF() {
зробитиPDF_(SpreadsheetApp.getActiveSpreadsheet());
}
function PDFзФайлу() {
// Клієнтські файли лишаються без скрипта, тож PDF для них робимо звідси:
// читаємо чужу таблицю за посиланням і збираємо документ так само.
const ui = SpreadsheetApp.getUi();
const р = ui.prompt('PDF з файлу клієнта',
'Вставте посилання на файл кошторису:', ui.ButtonSet.OK_CANCEL);
if (р.getSelectedButton() !== ui.Button.OK) return;
const м = String(р.getResponseText() || '').match(/[-\w]{25,}/);
if (!м) { ui.alert('У посиланні не знайдено ідентифікатор файлу.'); return; }
try {
зробитиPDF_(SpreadsheetApp.openById(м[0]));
} catch (e) {
ui.alert('Не вдалося: ' + e.message);
}
}
function зробитиPDF_(ss) {
const ui = SpreadsheetApp.getUi();
const клієнт = String(ss.getSheetByName('Кошторис').getRange('B2').getValue() || '').trim();
const блоки = зібратиГрантові_(ss);
if (!блоки.length) {
ui.alert('У кошторисі немає жодної заповненої позиції грантових коштів.');
return;
}
var разом = 0;
блоки.forEach(function (b) {
b.сума = 0;
b.позиції.forEach(function (p) { b.сума += p[4]; });
разом += b.сума;
});
const тимч = SpreadsheetApp.create('__pdf__');
const а = тимч.getSheets()[0];
а.setName('Кошторис');
const рядки = [];
рядки.push(['КОШТОРИС ВИТРАТ ГРАНТОВИХ КОШТІВ', '', '', '', '']);
рядки.push([клієнт ? 'Клієнт: ' + клієнт : '', '', '', '', '']);
рядки.push(['', '', '', '', '']);
рядки.push(['Найменування', 'Ціна за од., грн', 'Кільк.', 'Од.', 'Сума, грн']);
const заголовки = [], підсумки = [];
блоки.forEach(function (b) {
заголовки.push(рядки.length + 1);
рядки.push([b.код + '. ' + b.назва, '', '', '', b.сума]);
b.позиції.forEach(function (p) { рядки.push(p); });
});
підсумки.push(рядки.length + 1);
рядки.push(['РАЗОМ ЗА КОШТОРИСОМ:', '', '', '', разом]);
а.getRange(1, 1, рядки.length, 5).setValues(рядки);
а.getRange('A1:E1').merge().setFontSize(13).setFontWeight('bold')
.setBackground(СИНІЙ).setFontColor('#ffffff').setHorizontalAlignment('center');
а.setRowHeight(1, 30);
а.getRange('A2:E2').merge().setFontWeight('bold');
а.getRange(4, 1, 1, 5).setFontWeight('bold').setBackground(ЗАГОЛОВОК)
.setHorizontalAlignment('center').setWrap(true);
заголовки.forEach(function (r) {
а.getRange(r, 1, 1, 5).setFontWeight('bold').setBackground(ЗАГОЛОВОК);
});
підсумки.forEach(function (r) {
а.getRange(r, 1, 1, 5).setFontWeight('bold').setBackground('#ffe699');
});
а.getRange(4, 1, рядки.length - 3, 5).setBorder(true, true, true, true, true, true);
а.getRange(5, 2, рядки.length - 4, 1).setNumberFormat('#,##0.00');
а.getRange(5, 3, рядки.length - 4, 1).setNumberFormat('#,##0');
а.getRange(5, 5, рядки.length - 4, 1).setNumberFormat('#,##0.00');
а.getRange(5, 3, рядки.length - 4, 2).setHorizontalAlignment('center');
а.getRange(1, 1, рядки.length, 1).setWrap(true);
а.setColumnWidth(1, 420); а.setColumnWidth(2, 110); а.setColumnWidth(3, 60);
а.setColumnWidth(4, 55); а.setColumnWidth(5, 120);
SpreadsheetApp.flush();
const адреса = 'https://docs.google.com/spreadsheets/d/' + тимч.getId() + '/export?' + [
'format=pdf', 'portrait=true', 'fitw=true', 'gridlines=false', 'sheetnames=false',
'printtitle=false', 'pagenumbers=false', 'top_margin=0.5', 'bottom_margin=0.5',
'left_margin=0.5', 'right_margin=0.5', 'gid=' + а.getSheetId()].join('&');
const назва = 'Кошторис' + (клієнт ? ' — ' + клієнт : '') + '.pdf';
var файлPDF;
try {
const blob = UrlFetchApp.fetch(адреса, {
headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() }
}).getBlob().setName(назва);
const папки = DriveApp.getFoldersByName(ПАПКА);
const папка = папки.hasNext() ? папки.next() : DriveApp.createFolder(ПАПКА);
файлPDF = папка.createFile(blob);
} finally {
DriveApp.getFileById(тимч.getId()).setTrashed(true);
}
const html = HtmlService.createHtmlOutput(
'<p style="font-family:Arial;font-size:14px">Готово. Категорій у документі: <b>' +
блоки.length + '</b>, на суму <b>' + разом.toFixed(2) + ' грн</b>.</p>' +
'<p style="font-family:Arial;font-size:14px"><a href="' + файлPDF.getUrl() +
'" target="_blank"><b>' + назва + '</b></a></p>').setWidth(430).setHeight(150);
ui.showModalDialog(html, 'PDF сформовано');
}
function дооснаститиФайл() {
// Скрипт переноситься в копію лише при створенні. Кошториси, збережені
// раніше, лишаються без меню — цей пункт дає їх дооснастити, не
// перестворюючи файл і не втрачаючи вже внесені дані.
const ui = SpreadsheetApp.getUi();
const р = ui.prompt('Додати інструменти',
'Вставте посилання на файл кошторису клієнта:', ui.ButtonSet.OK_CANCEL);
if (р.getSelectedButton() !== ui.Button.OK) return;
const м = String(р.getResponseText() || '').match(/[-\w]{25,}/);
if (!м) { ui.alert('У посиланні не знайдено ідентифікатор файлу.'); return; }
var назва = '';
try {
назва = DriveApp.getFileById(м[0]).getName();
} catch (e) {
ui.alert('Файл не знайдено або немає доступу:\n' + e.message);
return;
}
try {
перенестиСкрипт_(м[0], назва);
} catch (e) {
ui.alert('Не вдалося додати інструменти.\n\n' + e.message +
'\n\nПеревірте: увімкнено Apps Script API на script.google.com/home/usersettings ' +
'і в маніфесті є дозвіл script.projects.');
return;
}
ui.alert('Готово: «' + назва + '».\n\nВідкрийте файл заново — меню зʼявиться ' +
'після перезавантаження сторінки.');
}