1С и Ozon связывают тремя способами: типовым механизмом «Продажи на Ozon» в УТ 11.5, КА 2.5 и ERP 2.5, готовым модулем стороннего разработчика или своей доработкой через Ozon Seller API. Ниже — что умеет типовая интеграция (сверено по КА 2.5.20), чего в ней не хватает, когда нужен модуль или доработка, ориентиры по срокам. Код — диагностика типового обмена и добавление товаров в акции Ozon, которого в типовой нет.
| Способ | Когда подходит | На что смотреть |
|---|---|---|
| Типовая интеграция | УТ 11.5, КА 2.5, ERP 2.5 на актуальном релизе | релиз конфигурации; настраивается без программиста и обновляется вместе с типовой |
| Готовый модуль 1С для Ozon | БП 3.0, УНФ, старые релизы УТ или нужна функция, которой нет в типовой | поддержка вашей конфигурации, схем и кабинетов, открытый код, скорость обновлений под изменения API |
| Своя доработка | нестандартный процесс или дополнение типовой: акции, отчёты, правила остатков | расширение вместо правки типовой, где хранится ключ API, где видны ошибки обмена |
Своя интеграция с нуля устроена так же, как в статье про 1С и Яндекс Маркет; здесь — о типовой и о том, что к ней дописывают.
В КА 2.5 интеграция включается в разделе НСИ и администрирование → Настройка интеграции → Интеграция с маркетплейсами: флажок «Ozon», ссылки «Настройки подключения» и «Настройки обмена данными». Каждый кабинет маркетплейса Ozon в 1С — элемент справочника «Учетные записи маркетплейсов»: организация, идентификатор клиента (Client-Id), партнёр, контрагент, соглашение, договоры продажи через склады площадки и через собственные склады, виды цен. API-ключ хранится в безопасном хранилище, а не в реквизите справочника. Рабочее место «Управление продажами на Ozon» открывается командой Продажи на Ozon в разделе «Продажи» (группа «Сервис»): товарный каталог, цены, поставки на склады площадки, продажи, взаиморасчёты.
| Участок | Что делает типовая | Методы Ozon |
|---|---|---|
| Карточки | публикация товаров с атрибутами, фото и сертификатами, импорт карточек из кабинета | /v3/product/import, /v3/products/info/attributes |
| Цены | выгрузка цены со скидкой, до скидки и минимальной цены после всех акций; загрузка цен с учётом акций, рекомендованных и цен поставщика | /v1/product/import/prices, /v4/product/info/prices |
| Остатки FBS и rFBS | выгрузка по сопоставленным складам Ozon | /v2/products/stocks |
| Заказы FBS | загрузка в заказы клиентов, сборка, этикетки, отмены, коды маркировки | /v3/posting/fbs/unfulfilled/list, /v4/posting/fbs/ship |
| FBO | планирование поставок, остатки на складах Ozon, продажи со складов площадки | /v2/posting/fbo/list, /v2/analytics/stock_on_warehouses |
| Возвраты и финансы | возвраты FBO и FBS, отчёт о реализации, транзакции для взаиморасчётов | /v2/finance/realization, /v3/finance/transaction/list |
Остаток для выгрузки считается по регистру «Запасы и потребности»: в наличии минус резервы. Затем применяются ограничения учётной записи — процент остатка и страховой запас для номенклатуры, вида номенклатуры или товарной категории. Остатки уходят пачками по 100 товаров, не чаще 80 запросов в минуту. У каждой учётной записи свой набор регламентных заданий — на остатки, цены, каталог, заказы, статусы, этикетки и продажи.
/v1/actions в модулях интеграции нет, есть только выгрузка и загрузка цен с учётом акций. Добавлять товары в акцию по правилам из 1С — доработка, код ниже.Ошибки обмена типовая пишет в журнал регистрации с событием «Интеграция с Ozon» (как искать — в статье про журнал регистрации). Но часто ошибки нет: задание выключено или не запускалось. Функция ниже собирает по каждому кабинету состояние регламентных заданий, флаг запрета обновления и дату последней подтверждённой выгрузки остатков; запрос можно сначала проверить в бесплатной консоли запросов.
// Общий модуль OzonДиагностика в расширении: флажок «Сервер», остальные сняты.
// Вызывать под администратором: БСП отдает регламентные задания только ему.
// По каждому кабинету Ozon: регламентные задания типовой интеграции,
// результат их последнего запуска и дата последней подтвержденной выгрузки остатков.
Функция СостояниеОбменаOzon() Экспорт
Результат = Новый ТаблицаЗначений;
Результат.Колонки.Добавить("УчетнаяЗапись");
Результат.Колонки.Добавить("Операция", Новый ОписаниеТипов("Строка"));
Результат.Колонки.Добавить("Включено", Новый ОписаниеТипов("Булево"));
Результат.Колонки.Добавить("ПоследнийЗапуск", Новый ОписаниеТипов("Дата"));
Результат.Колонки.Добавить("Состояние", Новый ОписаниеТипов("Строка"));
Результат.Колонки.Добавить("Ошибка", Новый ОписаниеТипов("Строка"));
Результат.Колонки.Добавить("ОбновлениеЗапрещено", Новый ОписаниеТипов("Булево"));
Результат.Колонки.Добавить("ОстаткиВыгружены", Новый ОписаниеТипов("Дата"));
Запрос = Новый Запрос(
"ВЫБРАТЬ
| УчетныеЗаписи.Ссылка КАК УчетнаяЗапись,
| УчетныеЗаписи.НеОбновлятьДанныеТорговойПлощадки КАК ОбновлениеЗапрещено,
| ЕСТЬNULL(МАКСИМУМ(Остатки.ДатаВыгрузкиОстатков), ДАТАВРЕМЯ(1, 1, 1)) КАК ОстаткиВыгружены
|ИЗ
| Справочник.УчетныеЗаписиМаркетплейсов КАК УчетныеЗаписи
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ОстаткиТоваровМаркетплейсов КАК Остатки
| ПО УчетныеЗаписи.Ссылка = Остатки.УчетнаяЗаписьМаркетплейса
|ГДЕ
| УчетныеЗаписи.ВидМаркетплейса = ЗНАЧЕНИЕ(Перечисление.ВидыМаркетплейсов.МаркетплейсOzon)
| И НЕ УчетныеЗаписи.ПометкаУдаления
|
|СГРУППИРОВАТЬ ПО
| УчетныеЗаписи.Ссылка,
| УчетныеЗаписи.НеОбновлятьДанныеТорговойПлощадки");
Выборка = Запрос.Выполнить().Выбрать();
// Ключ задания в типовой: префикс операции + уникальный идентификатор учетной записи.
Префиксы = ИнтеграцияСМаркетплейсомOzonКлиентСервер.ПрефиксыСервиса(Истина);
Пока Выборка.Следующий() Цикл
Идентификатор = Строка(Выборка.УчетнаяЗапись.УникальныйИдентификатор());
Для Каждого Префикс Из Префиксы Цикл
НоваяСтрока = Результат.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, Выборка);
НоваяСтрока.Операция = Префикс.Ключ;
// В обычной (не облачной) базе НайтиЗадания возвращает массив РегламентноеЗадание.
Задания = РегламентныеЗаданияСервер.НайтиЗадания(
Новый Структура("Ключ", Префикс.Значение + Идентификатор));
Если Задания.Количество() = 0 Тогда
НоваяСтрока.Состояние = "задание не создано";
Продолжить;
КонецЕсли;
НоваяСтрока.Включено = Задания[0].Использование;
Последнее = Задания[0].ПоследнееЗадание; // ФоновоеЗадание или Неопределено
Если Последнее = Неопределено Тогда
НоваяСтрока.Состояние = "не запускалось";
Продолжить;
КонецЕсли;
НоваяСтрока.ПоследнийЗапуск = Последнее.Начало;
НоваяСтрока.Состояние = Строка(Последнее.Состояние);
Если Последнее.ИнформацияОбОшибке <> Неопределено Тогда
НоваяСтрока.Ошибка = КраткоеПредставлениеОшибки(Последнее.ИнформацияОбОшибке);
КонецЕсли;
КонецЦикла;
КонецЦикла;
Возврат Результат;
КонецФункции
Что важно в коде:
ПрефиксыСервиса, её ключи (ВыгрузкаОстатков и другие) попадают в колонку «Операция».ПоследнееЗадание — последнее фоновое задание, запущенное по расписанию: время старта, состояние, ошибка. В модели сервиса НайтиЗадания возвращает таблицу очереди заданий, функция рассчитана на обычную базу.Код размещают в общем модуле расширения с флажком «Сервер». Расширение подключайте без безопасного режима или с профилем безопасности: нужны интернет и привилегированный режим. Второй справочник кабинетов не нужен: Client-Id берём из учётной записи типовой, ключ — из безопасного хранилища под тем же владельцем, что и типовой модуль. Это внутреннее устройство типовой, после обновления релиза его стоит перепроверить. Общие приёмы работы с HTTPСоединение — в статье HTTP-запрос из 1С 8.3.
// Общий модуль OzonАкции в расширении: флажок «Сервер», остальные сняты.
// Вызывает метод Ozon Seller API от имени учетной записи типовой интеграции.
// Возвращает структуру: Успех, КодСостояния, Данные (Соответствие), ТекстОшибки.
Функция ВызватьМетодOzon(УчетнаяЗапись, HTTPМетод, АдресМетода, Тело = Неопределено) Экспорт
Результат = Новый Структура("Успех, КодСостояния, Данные, ТекстОшибки", Ложь, 0, Неопределено, "");
// Client-Id хранится в учетной записи, ключ — в безопасном хранилище
// под тем же владельцем и именем, что использует типовой модуль.
Владелец = ИнтеграцияСМаркетплейсомOzonКлиентСервер.ПрефиксыСервиса().НастройкиСервиса
+ Строка(УчетнаяЗапись.УникальныйИдентификатор());
УстановитьПривилегированныйРежим(Истина);
КлючAPI = ОбщегоНазначения.ПрочитатьДанныеИзБезопасногоХранилища(Владелец, "KeyAPI", Ложь);
ИдентификаторКлиента = ОбщегоНазначения.ЗначениеРеквизитаОбъекта(УчетнаяЗапись, "ИдентификаторКлиента");
УстановитьПривилегированныйРежим(Ложь);
Если Не ЗначениеЗаполнено(КлючAPI) Или Не ЗначениеЗаполнено(ИдентификаторКлиента) Тогда
Результат.ТекстОшибки = "В учетной записи не заполнены Client-Id или API-ключ";
Возврат Результат;
КонецЕсли;
Заголовки = Новый Соответствие;
Заголовки.Вставить("Client-Id", ИдентификаторКлиента);
Заголовки.Вставить("Api-Key", КлючAPI);
Заголовки.Вставить("Content-Type", "application/json");
Запрос = Новый HTTPЗапрос(АдресМетода, Заголовки);
Если Тело <> Неопределено Тогда
Запись = Новый ЗаписьJSON;
Запись.УстановитьСтроку();
ЗаписатьJSON(Запись, Тело);
Запрос.УстановитьТелоИзСтроки(Запись.Закрыть(), КодировкаТекста.UTF8,
ИспользованиеByteOrderMark.НеИспользовать);
КонецЕсли;
// Прокси берем из настроек БСП, как это делает типовой модуль.
Прокси = Неопределено;
Если ОбщегоНазначения.ПодсистемаСуществует("СтандартныеПодсистемы.ПолучениеФайловИзИнтернета") Тогда
МодульПолучениеФайлов = ОбщегоНазначения.ОбщийМодуль("ПолучениеФайловИзИнтернета");
Прокси = МодульПолучениеФайлов.ПолучитьПрокси("https");
КонецЕсли;
Попытка
Соединение = Новый HTTPСоединение("api-seller.ozon.ru", 443, , , Прокси, 60,
ОбщегоНазначенияКлиентСервер.НовоеЗащищенноеСоединение());
Ответ = Соединение.ВызватьHTTPМетод(HTTPМетод, Запрос);
Исключение
Результат.ТекстОшибки = КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
Возврат Результат;
КонецПопытки;
Результат.КодСостояния = Ответ.КодСостояния;
ТекстОтвета = Строка(Ответ.ПолучитьТелоКакСтроку(КодировкаТекста.UTF8));
Если ЗначениеЗаполнено(ТекстОтвета) Тогда
Попытка
Чтение = Новый ЧтениеJSON;
Чтение.УстановитьСтроку(ТекстОтвета);
Результат.Данные = ПрочитатьJSON(Чтение, Истина);
Исключение
Результат.Данные = Неопределено; // не JSON: например, страница прокси
КонецПопытки;
КонецЕсли;
Результат.Успех = (Ответ.КодСостояния = 200);
Если Не Результат.Успех Тогда
// Тело ошибки Ozon: {"code": ..., "message": ..., "details": [...]}.
Сообщение = Неопределено;
Если ТипЗнч(Результат.Данные) = Тип("Соответствие") Тогда
Сообщение = Результат.Данные.Получить("message");
КонецЕсли;
Результат.ТекстОшибки = СтрШаблон("HTTP %1: %2", Ответ.КодСостояния,
?(ЗначениеЗаполнено(Сообщение), Сообщение, Лев(ТекстОтвета, 300)));
КонецЕсли;
Возврат Результат;
КонецФункции
Список акций кабинета отдаёт GET /v1/actions: идентификатор, название, даты, участие кабинета, число товаров в акции и доступных к добавлению.
// Акции, доступные кабинету: GET /v1/actions.
Функция АкцииOzon(УчетнаяЗапись) Экспорт
Акции = Новый ТаблицаЗначений;
Акции.Колонки.Добавить("Идентификатор", Новый ОписаниеТипов("Число"));
Акции.Колонки.Добавить("Название", Новый ОписаниеТипов("Строка"));
Акции.Колонки.Добавить("ТипАкции", Новый ОписаниеТипов("Строка"));
Акции.Колонки.Добавить("Начало", Новый ОписаниеТипов("Строка"));
Акции.Колонки.Добавить("Окончание", Новый ОписаниеТипов("Строка"));
Акции.Колонки.Добавить("УжеУчаствуем", Новый ОписаниеТипов("Булево"));
Акции.Колонки.Добавить("ТоваровВАкции", Новый ОписаниеТипов("Число"));
Акции.Колонки.Добавить("МожноДобавить", Новый ОписаниеТипов("Число"));
Результат = ВызватьМетодOzon(УчетнаяЗапись, "GET", "/v1/actions");
Если Не Результат.Успех Или ТипЗнч(Результат.Данные) <> Тип("Соответствие") Тогда
ВызватьИсключение "Ozon, список акций. " + Результат.ТекстОшибки;
КонецЕсли;
Список = Результат.Данные.Получить("result");
Если ТипЗнч(Список) <> Тип("Массив") Тогда
Возврат Акции;
КонецЕсли;
Для Каждого Акция Из Список Цикл
НоваяСтрока = Акции.Добавить();
НоваяСтрока.Идентификатор = Акция.Получить("id");
НоваяСтрока.Название = Акция.Получить("title");
НоваяСтрока.ТипАкции = Акция.Получить("action_type");
НоваяСтрока.Начало = Акция.Получить("date_start");
НоваяСтрока.Окончание = Акция.Получить("date_end");
НоваяСтрока.УжеУчаствуем = (Акция.Получить("is_participating") = Истина);
НоваяСтрока.ТоваровВАкции = Акция.Получить("participating_products_count");
НоваяСтрока.МожноДобавить = Акция.Получить("potential_products_count");
КонецЦикла;
Возврат Акции;
КонецФункции
Товары добавляет POST /v1/actions/products/activate: в теле идентификатор акции и массив товаров с product_id, акционной ценой и количеством. product_id типовая сохраняет в регистре «Статусы публикации объектов маркетплейса Ozon», когда карточка загружена на площадку.
// Пустая таблица для ДобавитьТоварыВАкциюOzon. Колонки типизированы:
// без типов запрос не поместит таблицу во временную.
Функция НоваяТаблицаТоваровАкции() Экспорт
Таблица = Новый ТаблицаЗначений;
Таблица.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
Таблица.Колонки.Добавить("Характеристика", Новый ОписаниеТипов("СправочникСсылка.ХарактеристикиНоменклатуры"));
Таблица.Колонки.Добавить("Упаковка", Новый ОписаниеТипов("СправочникСсылка.УпаковкиЕдиницыИзмерения"));
Таблица.Колонки.Добавить("ЦенаАкции", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
Таблица.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 0)));
Возврат Таблица;
КонецФункции
// Добавляет товары в акцию: POST /v1/actions/products/activate.
// Возвращает массив строк с причинами, по которым Ozon не принял товары.
Функция ДобавитьТоварыВАкциюOzon(УчетнаяЗапись, ИдентификаторАкции, ТоварыАкции) Экспорт
Отказы = Новый Массив;
// product_id берем из регистра, который типовая заполняет при публикации карточек.
Запрос = Новый Запрос(
"ВЫБРАТЬ
| Товары.Номенклатура КАК Номенклатура,
| Товары.Характеристика КАК Характеристика,
| Товары.Упаковка КАК Упаковка,
| Товары.ЦенаАкции КАК ЦенаАкции,
| Товары.Количество КАК Количество
|ПОМЕСТИТЬ ВТТовары
|ИЗ
| &ТоварыАкции КАК Товары
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Статусы.ИдентификаторОбъектаМаркетплейса КАК ИдентификаторТовара,
| ВТТовары.ЦенаАкции КАК ЦенаАкции,
| ВТТовары.Количество КАК Количество
|ИЗ
| ВТТовары КАК ВТТовары
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.СтатусыПубликацииОбъектовМаркетплейсаOzon КАК Статусы
| ПО ВТТовары.Номенклатура = Статусы.Номенклатура
| И ВТТовары.Характеристика = Статусы.Характеристика
| И ВТТовары.Упаковка = Статусы.Упаковка
|ГДЕ
| Статусы.УчетнаяЗаписьМаркетплейса = &УчетнаяЗапись
| И Статусы.ВидОбъектаМаркетплейса = ЗНАЧЕНИЕ(Перечисление.ВидыОбъектовМаркетплейсов.Товар)
| И Статусы.Статус = ЗНАЧЕНИЕ(Перечисление.СтатусыОбъектовМаркетплейсаOzon.Загружен)
| И ВТТовары.ЦенаАкции > 0");
Запрос.УстановитьПараметр("ТоварыАкции", ТоварыАкции);
Запрос.УстановитьПараметр("УчетнаяЗапись", УчетнаяЗапись);
Выборка = Запрос.Выполнить().Выбрать();
РазмерПачки = 100; // предел метода сверьте с документацией Ozon
Пачка = Новый Массив;
Продолжать = Истина;
Пока Продолжать И Выборка.Следующий() Цикл
ИдентификаторТовара = СтроковыеФункцииКлиентСервер.СтрокаВЧисло(Выборка.ИдентификаторТовара);
Если Не ЗначениеЗаполнено(ИдентификаторТовара) Тогда
Продолжить; // карточка еще не получила product_id
КонецЕсли;
// stock — количество для акций со скидкой на остаток.
Пачка.Добавить(Новый Структура("product_id, action_price, stock",
ИдентификаторТовара, Выборка.ЦенаАкции, Выборка.Количество));
Если Пачка.Количество() = РазмерПачки Тогда
Продолжать = ОтправитьПачкуВАкцию(УчетнаяЗапись, ИдентификаторАкции, Пачка, Отказы);
Пачка = Новый Массив;
КонецЕсли;
КонецЦикла;
Если Продолжать И Пачка.Количество() > 0 Тогда
ОтправитьПачкуВАкцию(УчетнаяЗапись, ИдентификаторАкции, Пачка, Отказы);
КонецЕсли;
Возврат Отказы;
КонецФункции
// Отправляет одну пачку. Ложь — продолжать нет смысла: лимит, ключ, сбой связи.
Функция ОтправитьПачкуВАкцию(УчетнаяЗапись, ИдентификаторАкции, Пачка, Отказы)
Тело = Новый Структура("action_id, products", ИдентификаторАкции, Пачка);
Результат = ВызватьМетодOzon(УчетнаяЗапись, "POST", "/v1/actions/products/activate", Тело);
Если Не Результат.Успех Тогда
Отказы.Добавить("Пачка не принята. " + Результат.ТекстОшибки);
Возврат (Результат.КодСостояния = 400); // ошибка в данных пачки: следующие отправляем
КонецЕсли;
Ответ = ?(ТипЗнч(Результат.Данные) = Тип("Соответствие"), Результат.Данные.Получить("result"), Неопределено);
Отклоненные = ?(ТипЗнч(Ответ) = Тип("Соответствие"), Ответ.Получить("rejected"), Неопределено);
Если ТипЗнч(Отклоненные) = Тип("Массив") Тогда
Для Каждого Отклоненный Из Отклоненные Цикл
Отказы.Добавить(СтрШаблон("product_id %1: %2",
Формат(Отклоненный.Получить("product_id"), "ЧГ=0"), Отклоненный.Получить("reason")));
КонецЦикла;
КонецЕсли;
Возврат Истина;
КонецФункции
Разбор ключевых мест:
ВызватьМетодOzon не бросает исключений: возвращает код HTTP и поле message из ответа Ozon, а решение, продолжать ли, принимает вызывающий код.rejected Ozon возвращает непринятые товары с причиной. Частая причина — цена выше допустимой: предельную цену по каждому товару возвращает POST /v1/actions/candidates в поле max_action_price.Акционную цену удобно держать в отдельном виде цен 1С, который утверждает менеджер, а добавление запускать регламентным заданием. Снимают товары с акции методом /v1/actions/products/deactivate через ту же обёртку.
Цена складывается из часов программиста; ставки — на странице сколько стоит доработка 1С. Ниже — типичная трудоёмкость для одного кабинета и базы без тяжёлых доработок. Это не обязательство площадки: точную оценку программисты дают бесплатно по описанию вашей задачи.
| Задача | Что входит | Ориентир |
|---|---|---|
| Настроить типовую интеграцию | подключение, склады, виды цен, сопоставление карточек, проверка цикла от заказа до отгрузки | 1–3 рабочих дня |
| Обновить конфигурацию до релиза с интеграцией | обновление, перенос доработок, проверка | от дня до нескольких недель, зависит от доработок базы |
| Акции поверх типовой | код из статьи, форма выбора акции, правило цены, регламентное задание | 2–4 рабочих дня |
| Своё правило остатков для нескольких кабинетов | приоритеты кабинетов, общий резерв | 1–3 рабочих дня |
| Отчёт по юнит-экономике | загрузка транзакций Ozon, отчёт на СКД по SKU | 2–5 рабочих дней |
| Своя интеграция без типового механизма | товары, остатки, цены, заказы FBS, статусы | от 2–3 недель |
Готовый модуль добавляет к этому лицензию или подписку по цене разработчика. Дороже выходят несколько организаций и кабинетов, несопоставленные артикулы, маркируемые товары и сильно изменённая типовая конфигурация.
offer_id — ваш артикул, product_id — товар в кабинете, sku — товар в системе Ozon. Акции работают с product_id, отчёт по остаткам на складах Ozon — с sku и артикулом.В конфигурации УТ 11.4.14 объектов интеграции с маркетплейсами нет. Варианты: обновление до УТ 11.5, модуль стороннего разработчика или своя интеграция по API Ozon.
Да: каждый кабинет — отдельная учётная запись со своим Client-Id, ключом, организацией и заданиями. Общий склад делят процентом остатка и страховым запасом.
Да: планирование поставок на склады Ozon, остатки на складах площадки и продажи с них. Заказы FBO собирает и отгружает Ozon, поэтому от 1С нужны поставки и отражение продаж, а не сборка.
Причину Ozon возвращает в rejected. Чаще всего цена выше допустимой (max_action_price) или товара нет среди кандидатов акции — их список отдаёт /v1/actions/candidates.
Если типовой механизм уже есть, оплачиваются настройка и сопоставление товаров — дни работы. Модуль — лицензия плюс настройка, своя интеграция с нуля — недели. Точную сумму даёт оценка задачи.
Если в вашей конфигурации нет типового механизма, нужны акции, своё распределение остатков между кабинетами или отчёт по юнит-экономике, закажите интеграцию 1С с маркетплейсами. Укажите конфигурацию и релиз, схемы работы и число кабинетов и разместите задание: оценка бесплатная, ответ — в течение рабочего дня.
Станьте частью сообщества!
Войдите или зарегистрируйтесь, и вы сможете участвовать в обсуждениях.