Фоновое задание 1С — это вызов серверной процедуры в отдельном сеансе: код работает сам по себе, а пользователь в это время продолжает пользоваться программой. В фон уносят перепроведение за месяц, загрузку больших файлов, обмены, тяжёлые расчёты — всё, из-за чего форма иначе висит с песочными часами. Ниже — как запустить фоновое задание программно через ФоновыеЗадания.Выполнить, передать параметры, забрать результат, защититься от двойного запуска и чем отличается поведение в файловой и клиент-серверной базе. Примеры рассчитаны на платформу 1С 8.3 и управляемые формы.
Если код должен срабатывать по расписанию, нужно регламентное задание — оно настраивается иначе, см. статью «Регламентное задание 1С: создание и запуск программно».
Запуск фонового задания — один вызов на сервере:
Задание = ФоновыеЗадания.Выполнить(ИмяМетода, Параметры, Ключ, Наименование);
| Параметр | Что передавать |
|---|---|
ИмяМетода | Строка вида "ИмяОбщегоМодуля.ИмяМетода". Метод — экспортная процедура или функция неглобального общего модуля, доступного на сервере. |
Параметры | Массив значений в том же порядке и количестве, что и параметры метода. Необязательный. |
Ключ | Строка. Пока активно задание с этим ключом и тем же методом, второе такое же не запустится. Необязательный. |
Наименование | Произвольный текст, его видно в списках заданий. Необязательный. |
Метод возвращает объект ФоновоеЗадание. Главное в нём — УникальныйИдентификатор: по нему задание потом находят через ФоновыеЗадания.НайтиПоУникальномуИдентификатору(). Менеджер ФоновыеЗадания на тонком и веб-клиенте недоступен, поэтому из управляемой формы запуск всегда идёт через серверную процедуру.
Процедуру модуля формы, модуля объекта или обработки передать в задание напрямую нельзя — нужен общий модуль. Для примера ниже создайте общий модуль ПерепроведениеВФоне с флажком «Сервер» и снятыми «Глобальный», «Клиент» и «Вызов сервера».
Задача: перепровести реализации за период, показать прогресс и список документов, которые не провелись. Сначала серверная часть — методы, которые выполнит задание.
// Общий модуль «ПерепроведениеВФоне».
// Свойства модуля: «Сервер» — да; «Глобальный», «Клиент»,
// «Вызов сервера» — нет.
// Документ «РеализацияТоваровУслуг» есть в УТ 11, БП 3.0, КА 2, ERP 2;
// в другой конфигурации подставьте свой вид документа.
#Область ПрограммныйИнтерфейс
// Метод фонового задания, который запускает форма.
// Параметры приходят копиями: изменения внутри наружу не вернутся.
//
// Параметры:
// ДатаНачала, ДатаОкончания - Дата - период документов.
// АдресРезультата - Строка - адрес во временном хранилище,
// созданный вызывающей стороной.
//
Процедура ПерепровестиЗаПериод(ДатаНачала, ДатаОкончания, АдресРезультата) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Док.Ссылка КАК Ссылка
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Док
|ГДЕ
| Док.Проведен
| И Док.Дата МЕЖДУ &ДатаНачала И &ДатаОкончания
|
|УПОРЯДОЧИТЬ ПО
| Док.Дата";
Запрос.УстановитьПараметр("ДатаНачала", НачалоДня(ДатаНачала));
Запрос.УстановитьПараметр("ДатаОкончания", КонецДня(ДатаОкончания));
Ссылки = Запрос.Выполнить().Выгрузить().ВыгрузитьКолонку("Ссылка");
Результат = Новый Структура;
Результат.Вставить("Всего", Ссылки.Количество());
Результат.Вставить("Ошибки", ПровестиСписок(Ссылки, Истина));
// Кладём результат по заранее известному адресу —
// так его увидит сеанс, который запустил задание.
ПоместитьВоВременноеХранилище(Результат, АдресРезультата);
КонецПроцедуры
// Метод фонового задания для одной порции (параллельная обработка).
Процедура ПровестиПорцию(Ссылки, АдресРезультата) Экспорт
ПоместитьВоВременноеХранилище(ПровестиСписок(Ссылки, Ложь), АдресРезультата);
КонецПроцедуры
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
Функция ПровестиСписок(Ссылки, СообщатьПрогресс)
Ошибки = НоваяТаблицаОшибок();
Всего = Ссылки.Количество();
Обработано = 0;
Для Каждого Ссылка Из Ссылки Цикл
// Каждый документ — своя транзакция: ошибка одного
// не откатывает уже проведённые.
Попытка
ДокументОбъект = Ссылка.ПолучитьОбъект();
ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
Исключение
НоваяОшибка = Ошибки.Добавить();
НоваяОшибка.Документ = Ссылка;
НоваяОшибка.Текст = КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
КонецПопытки;
Обработано = Обработано + 1;
Если СообщатьПрогресс И Обработано % 100 = 0 Тогда
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = СтрШаблон("Проведено %1 из %2", Обработано, Всего);
Сообщение.Сообщить();
КонецЕсли;
КонецЦикла;
Возврат Ошибки;
КонецФункции
Функция НоваяТаблицаОшибок()
Ошибки = Новый ТаблицаЗначений;
Ошибки.Колонки.Добавить("Документ");
Ошибки.Колонки.Добавить("Текст", Новый ОписаниеТипов("Строка"));
Возврат Ошибки;
КонецФункции
#КонецОбласти
Теперь форма обработки: запуск, опрос состояния, вывод итога и отмена.
// Модуль формы обработки «ПерепроведениеРеализаций».
// Реквизиты формы: ДатаНачала, ДатаОкончания (Дата),
// ТекстСостояния (Строка), АдресРезультата (Строка),
// ИдентификаторЗадания (УникальныйИдентификатор).
// Команды: Перепровести, ОтменитьЗадание.
&НаКлиенте
Процедура Перепровести(Команда)
ТекстОтказа = ЗапуститьЗаданиеНаСервере();
Если ЗначениеЗаполнено(ТекстОтказа) Тогда
ПоказатьПредупреждение(, ТекстОтказа);
Возврат;
КонецЕсли;
ТекстСостояния = "Задание запущено";
// Опрос раз в 2 секунды, пока не отключим.
ПодключитьОбработчикОжидания("ПроверитьЗадание", 2);
КонецПроцедуры
&НаКлиенте
Процедура ОтменитьЗадание(Команда)
ОтменитьНаСервере(ИдентификаторЗадания);
КонецПроцедуры
&НаКлиенте
Процедура ПроверитьЗадание()
Ответ = СостояниеЗадания(ИдентификаторЗадания);
// Прогресс остаётся в строке состояния, остальные сообщения
// (например, причины отказа проведения) показываем пользователю.
Для Каждого ТекстСообщения Из Ответ.Сообщения Цикл
Если СтрНачинаетсяС(ТекстСообщения, "Проведено ") Тогда
ТекстСостояния = ТекстСообщения;
Иначе
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = ТекстСообщения;
Сообщение.Сообщить();
КонецЕсли;
КонецЦикла;
Если Ответ.Статус = "Активно" Тогда
Возврат;
КонецЕсли;
ОтключитьОбработчикОжидания("ПроверитьЗадание");
Если Ответ.Статус = "Завершено" Тогда
ТекстСостояния = ИтогНаСервере();
Иначе
ТекстСостояния = "Не выполнено: " + Ответ.ТекстОшибки;
КонецЕсли;
КонецПроцедуры
&НаСервере
Функция ЗапуститьЗаданиеНаСервере()
ИмяМетода = "ПерепроведениеВФоне.ПерепровестиЗаПериод";
КлючЗадания = "ПерепроведениеРеализаций"; // один запуск на всю базу
Отбор = Новый Структура("ИмяМетода, Ключ, Состояние",
ИмяМетода, КлючЗадания, СостояниеФоновогоЗадания.Активно);
// Без администрирования видны только свои задания. Проверяем
// и задания коллег, чтобы до Выполнить() вернуть понятный текст.
УстановитьПривилегированныйРежим(Истина);
Активные = ФоновыеЗадания.ПолучитьФоновыеЗадания(Отбор);
УстановитьПривилегированныйРежим(Ложь);
Если Активные.Количество() > 0 Тогда
Возврат "Перепроведение уже выполняется, запущено в "
+ Формат(Активные[0].Начало, "ДФ=ЧЧ:мм");
КонецЕсли;
// Адрес привязан к форме: значение проживёт, пока форма открыта.
АдресРезультата = ПоместитьВоВременноеХранилище(Неопределено, УникальныйИдентификатор);
ПараметрыЗадания = Новый Массив;
ПараметрыЗадания.Добавить(ДатаНачала);
ПараметрыЗадания.Добавить(ДатаОкончания);
ПараметрыЗадания.Добавить(АдресРезультата);
Попытка
Задание = ФоновыеЗадания.Выполнить(ИмяМетода, ПараметрыЗадания,
КлючЗадания, "Перепроведение реализаций за период");
Исключение
// Кто-то успел запустить задание с тем же ключом.
Возврат КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
КонецПопытки;
ИдентификаторЗадания = Задание.УникальныйИдентификатор;
Возврат "";
КонецФункции
&НаСервереБезКонтекста
Функция СостояниеЗадания(Знач Идентификатор)
Ответ = Новый Структура("Статус, Сообщения, ТекстОшибки", "Активно", Новый Массив, "");
Задание = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(Идентификатор);
Если Задание = Неопределено Тогда
Ответ.Статус = "Ошибка";
Ответ.ТекстОшибки = "задание не найдено";
Возврат Ответ;
КонецЕсли;
// Истина — забрать сообщения и очистить очередь задания.
СообщенияЗадания = Задание.ПолучитьСообщенияПользователю(Истина);
Если СообщенияЗадания <> Неопределено Тогда
Для Каждого Сообщение Из СообщенияЗадания Цикл
Ответ.Сообщения.Добавить(Сообщение.Текст);
КонецЦикла;
КонецЕсли;
Если Задание.Состояние = СостояниеФоновогоЗадания.Завершено Тогда
Ответ.Статус = "Завершено";
ИначеЕсли Задание.Состояние = СостояниеФоновогоЗадания.ЗавершеноАварийно Тогда
Ответ.Статус = "Ошибка";
Если Задание.ИнформацияОбОшибке <> Неопределено Тогда
Ответ.ТекстОшибки = КраткоеПредставлениеОшибки(Задание.ИнформацияОбОшибке);
КонецЕсли;
ИначеЕсли Задание.Состояние = СостояниеФоновогоЗадания.Отменено Тогда
Ответ.Статус = "Ошибка";
Ответ.ТекстОшибки = "задание отменено";
КонецЕсли;
Возврат Ответ;
КонецФункции
&НаСервере
Функция ИтогНаСервере()
Результат = ПолучитьИзВременногоХранилища(АдресРезультата);
УдалитьИзВременногоХранилища(АдресРезультата);
Если ТипЗнч(Результат) <> Тип("Структура") Тогда
Возврат "Задание завершилось, но результат не записан";
КонецЕсли;
Для Каждого СтрокаОшибки Из Результат.Ошибки Цикл
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = Строка(СтрокаОшибки.Документ) + ": " + СтрокаОшибки.Текст;
Сообщение.Сообщить();
КонецЦикла;
Возврат СтрШаблон("Готово: документов %1, с ошибками %2",
Результат.Всего, Результат.Ошибки.Количество());
КонецФункции
&НаСервереБезКонтекста
Процедура ОтменитьНаСервере(Знач Идентификатор)
Задание = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(Идентификатор);
Если Задание <> Неопределено
И Задание.Состояние = СостояниеФоновогоЗадания.Активно Тогда
Задание.Отменить();
КонецЕсли;
КонецПроцедуры
Массив параметров платформа сериализует и передаёт в новый сеанс. Поэтому, во-первых, изменения параметров внутри метода к вызывающему не вернутся. Во-вторых, передать можно только то, что сериализуется: примитивные типы, ссылки, Массив, Структура, Соответствие, ТаблицаЗначений, ХранилищеЗначения. Форму, ОбработкаОбъект, Запрос или МенеджерВременныхТаблиц не передать — передавайте исходные данные. Данные формы сначала превратите в обычные значения через РеквизитФормыВЗначение().
Вернуть значение как из функции задание не может. Рабочая схема такая: вызывающая сторона заранее создаёт адрес ПоместитьВоВременноеХранилище(Неопределено, УникальныйИдентификатор) и передаёт его параметром, задание кладёт результат по этому адресу, форма после завершения читает его через ПолучитьИзВременногоХранилища(). С идентификатором формы значение живёт, пока форма открыта. Адрес без идентификатора формы живёт недолго, и результат может исчезнуть раньше, чем его прочитают. Прочитали — освободите память: УдалитьИзВременногоХранилища().
Пользователь нажал кнопку дважды или то же самое запустил коллега — без ключа получатся два перепроведения одновременно. С ключом второй вызов Выполнить завершится исключением «Задание с таким значением ключа уже выполняется». В примере сначала ищем активное задание через ФоновыеЗадания.ПолучитьФоновыеЗадания() с отбором по методу, ключу и состоянию и показываем понятный текст, а исключение перехватываем на случай, если кто-то успел запустить задание между проверкой и вызовом. Константа в ключе — одно задание на базу, ключ с именем пользователя — по одному на каждого.
Форма не должна ждать задание внутри серверного вызова, иначе окно снова зависнет. Поэтому клиент подключает обработчик ожидания и раз в две секунды вызывает СостояниеЗадания(). Функция внеконтекстная, чтобы не гонять данные формы на сервер, и возвращает статус строкой: системное перечисление СостояниеФоновогоЗадания на тонком клиенте недоступно.
На сервере ждать уместно, когда без этого не обойтись, — например, раздать порции нескольким заданиям и дождаться их. Метод ФоновыеЗадания.ОжидатьЗавершенияВыполнения(МассивЗаданий, Таймаут) возвращает управление, когда завершились все задания, истёк таймаут или одно из заданий завершилось аварийно, поэтому ждать нужно в цикле до общего срока. Начиная с 8.3.13 для этого есть ФоновоеЗадание.ОжидатьЗавершенияВыполнения(Таймаут) и ФоновыеЗадания.ОжидатьЗавершенияВыполнения(МассивЗаданий, Таймаут). Прежний ОжидатьЗавершения() объявлен устаревшим: по истечении таймаута он выбрасывает исключение. Таймаут указывайте всегда, иначе зависшее задание подвесит и того, кто его ждёт. И учтите, что объект ФоновоеЗадание — снимок на момент получения: его Состояние само не обновляется, актуальное перечитывают через НайтиПоУникальномуИдентификатору().
Сообщения, выведенные заданием через СообщениеПользователю, копятся у задания. ПолучитьСообщенияПользователю(Истина) забирает накопленное и очищает очередь, так что каждый опрос получает только новые строки. Прогресс выводят в строку состояния, а остальные сообщения, в том числе причины отказа проведения, показывают отдельными сообщениями пользователю — иначе они потеряются при следующем опросе.
Задание, запущенное через ФоновыеЗадания.Выполнить, работает от имени пользователя, который его запустил: те же роли и те же ограничения доступа на уровне записей. Нет у пользователя права проводить документы — в фоне его тоже не будет.
Но сеанс у задания новый, отсюда неочевидные эффекты:
Регламентные задания выполняются от имени пользователя, указанного в их настройках. Проверить, что код сейчас работает в фоне, можно так:
// Выполняется ли текущий код в фоновом задании (только на сервере).
ТекущееЗадание = ПолучитьТекущийСеансИнформационнойБазы().ПолучитьФоновоеЗадание();
Если ТекущееЗадание <> Неопределено Тогда
// Мы в фоне: формы и вопросы недоступны, пишем в журнал регистрации.
ЗаписьЖурналаРегистрации("ПерепроведениеВФоне", УровеньЖурналаРегистрации.Информация,
, , "Задание: " + ТекущееЗадание.Наименование + ", пользователь: " + ИмяПользователя());
КонецЕсли;
Код запуска одинаковый, а поведение разное.
| Клиент-серверная база | Файловая база | |
|---|---|---|
| Где выполняется | В рабочих процессах кластера серверов 1С | В отдельном потоке приложения, которое запустило задание: клиента или веб-сервера |
| Несколько заданий | Параллельно | По очереди, в порядке поступления |
| Пользователь закрыл программу | Задание продолжает работу | Задание прерывается вместе с приложением |
В файловой базе распараллеливать бессмысленно: порции пойдут одна за другой. Хуже того, задание, которое запускает другое и ждёт его, может простоять до таймаута — вложенное стоит в очереди за ним. А пользователей файловой базы стоит предупредить, что закрывать программу до конца длительной операции нельзя.
Параллельная обработка с запасным вариантом для файловой базы выглядит так:
// Добавьте в общий модуль «ПерепроведениеВФоне».
// Вызывается на сервере: делит список на порции и проводит
// их в нескольких фоновых заданиях одновременно.
Функция ПерепровестиПараллельно(Ссылки, ЧислоПотоков = 4) Экспорт
// В файловой базе задания идут по очереди: выигрыша не будет,
// а ожидание вложенных заданий может простоять до таймаута.
Если ЭтоФайловаяБаза() Или Ссылки.Количество() < ЧислоПотоков * 10 Тогда
Возврат ПровестиСписок(Ссылки, Ложь);
КонецЕсли;
РазмерПорции = Цел((Ссылки.Количество() + ЧислоПотоков - 1) / ЧислоПотоков);
Задания = Новый Массив;
Адреса = Новый Массив;
Порция = Новый Массив;
Для Индекс = 0 По Ссылки.ВГраница() Цикл
Порция.Добавить(Ссылки[Индекс]);
Если Порция.Количество() < РазмерПорции И Индекс < Ссылки.ВГраница() Тогда
Продолжить;
КонецЕсли;
Адрес = ПоместитьВоВременноеХранилище(Неопределено, Новый УникальныйИдентификатор);
ПараметрыЗадания = Новый Массив;
ПараметрыЗадания.Добавить(Порция);
ПараметрыЗадания.Добавить(Адрес);
Задания.Добавить(ФоновыеЗадания.Выполнить("ПерепроведениеВФоне.ПровестиПорцию",
ПараметрыЗадания, , "Перепроведение, порция " + Формат(Задания.Количество() + 1, "ЧГ=")));
Адреса.Добавить(Адрес);
Порция = Новый Массив;
КонецЦикла;
// Платформа 8.3.13 и новее. Метод возвращает управление и при
// аварийном завершении одной порции, поэтому ждём до общего срока в цикле.
Срок = ТекущаяДатаСеанса() + 3600;
Ожидаемые = Задания;
Пока Ожидаемые.Количество() > 0 И ТекущаяДатаСеанса() < Срок Цикл
ФоновыеЗадания.ОжидатьЗавершенияВыполнения(Ожидаемые, Срок - ТекущаяДатаСеанса());
Ожидаемые = Новый Массив;
Для Каждого ЗапущенноеЗадание Из Задания Цикл
Актуальное = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(
ЗапущенноеЗадание.УникальныйИдентификатор);
Если Актуальное <> Неопределено
И Актуальное.Состояние = СостояниеФоновогоЗадания.Активно Тогда
Ожидаемые.Добавить(Актуальное);
КонецЕсли;
КонецЦикла;
КонецЦикла;
Ошибки = НоваяТаблицаОшибок();
Для Индекс = 0 По Задания.ВГраница() Цикл
// Объект ФоновоеЗадание — снимок: состояние перечитываем.
Задание = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(
Задания[Индекс].УникальныйИдентификатор);
ОшибкиПорции = ПолучитьИзВременногоХранилища(Адреса[Индекс]);
УдалитьИзВременногоХранилища(Адреса[Индекс]);
Если Задание <> Неопределено
И Задание.Состояние = СостояниеФоновогоЗадания.Завершено
И ОшибкиПорции <> Неопределено Тогда
Для Каждого СтрокаОшибки Из ОшибкиПорции Цикл
ЗаполнитьЗначенияСвойств(Ошибки.Добавить(), СтрокаОшибки);
КонецЦикла;
Продолжить;
КонецЕсли;
Если Задание = Неопределено Тогда
Причина = "задание не найдено";
ИначеЕсли Задание.Состояние = СостояниеФоновогоЗадания.Активно Тогда
Задание.Отменить();
Причина = "не уложилась в час, отменена";
ИначеЕсли Задание.ИнформацияОбОшибке <> Неопределено Тогда
Причина = КраткоеПредставлениеОшибки(Задание.ИнформацияОбОшибке);
Иначе
Причина = Строка(Задание.Состояние);
КонецЕсли;
НоваяОшибка = Ошибки.Добавить();
НоваяОшибка.Текст = СтрШаблон("Порция %1: %2", Индекс + 1, Причина);
КонецЦикла;
Возврат Ошибки;
КонецФункции
Функция ЭтоФайловаяБаза()
// В БСП есть готовая ОбщегоНазначения.ИнформационнаяБазаФайловая().
Возврат СтрНайти(ВРег(СтрокаСоединенияИнформационнойБазы()), "FILE=") > 0;
КонецФункции
Две оговорки. Порции, которые пишут в одни и те же остатки, будут ждать друг друга на блокировках, поэтому делите список так, чтобы пересечений было меньше, — например, по складам. И не распараллеливайте перепроведение там, где важен порядок документов: партионный учёт и последовательности требуют хронологии.
Отладка: по умолчанию точки останова в фоновых заданиях не срабатывают. В конфигураторе в «Отладка → Подключение» включите автоматическое подключение фоновых заданий, а в клиент-серверной базе ещё и запустите сервер в режиме отладки — см. как включить отладку на сервере 1С.
Экспорт, ошиблись в имени — задание не выполнится, а ошибка будет о том, что метод не найден. Сверьте строку "Модуль.Метод" посимвольно.ПоместитьВоВременноеХранилище(Результат) без адреса — значение легло по новому адресу, о котором форма не знает. Или адрес создали без идентификатора формы, и значение успело исчезнуть.Выполнить завершится ошибкой. Передавайте данные, а не объекты.ОжидатьЗавершенияВыполнения() внутри процедуры &НаСервере формы — окно висит так же, как без фона.ИнформацияОбОшибке: разверните её через ПодробноеПредставлениеОшибки() и сверьтесь с журналом регистрации.Программно — ФоновыеЗадания.ПолучитьФоновыеЗадания(Отбор) с отбором по ключу, состоянию, имени метода или наименованию. В конфигурациях на БСП есть обработка «Регламентные и фоновые задания» в разделе «Администрирование»: там видны состояние, время начала и текст ошибки. В клиент-серверной базе сеансы заданий видны и в консоли кластера.
Через ФоновыеЗадания.Выполнить — только метод общего модуля, поэтому обычно пишут тонкую обёртку в общем модуле, которая вызывает нужный код. В конфигурациях на БСП есть готовый механизм длительных операций: общий модуль ДлительныеОперации (функция ВыполнитьВФоне) и ДлительныеОперацииКлиент.ОжидатьЗавершение на клиенте. Состав их параметров зависит от версии БСП.
Ограничение на практике — ресурсы сервера: каждое задание держит свой сеанс, память и соединение с СУБД. Отправная точка — потоков не больше, чем свободных ядер у сервера 1С, дальше смотрите на блокировки и загрузку. Про настройку самого сервера — в статье об установке сервера 1С.
Найти по идентификатору и вызвать Отменить() — в примере это команда «Отменить задание». Состояние станет Отменено, незавершённая транзакция откатится. Что задание успело зафиксировать до отмены, останется в базе: в нашем случае это уже перепроведённые документы.
В клиент-серверной базе нет: задание доработает, но результат, привязанный к форме, пропадёт вместе с ней. Если итог нужен и после закрытия, пишите его в базу или журнал регистрации. В файловой базе задание живёт, пока открыто приложение, которое его запустило.
Если долгая операция подвешивает пользователям 1С, а переделать её на фоновые задания с прогрессом, отменой и защитой от двойного запуска некогда, — это типовая доработка. Опишите, какая операция тормозит и в какой конфигурации, и разместите задачу: оценка бесплатная, ответ — в течение рабочего дня. Направления работ — на странице услуг по доработке 1С, отдельно про ситуацию «1С тормозит».
Станьте частью сообщества!
Войдите или зарегистрируйтесь, и вы сможете участвовать в обсуждениях.