В личном кабинете партнёра Астрал есть всё нужное, но оно живёт вне 1С:
- **Сверка за месяц.** Таблица «Основные лицензии»: все подключения абонентов за месяц с ценой для абонента и отчислениями партнёра. По ней Астрал выставляет счёт, и её же нужно превратить в поступление в Бухгалтерии.
- **Сроки тарифов и сертификатов.** Отчёт «Сроки действия тарифов» по каждому абоненту. По нему планируются продления.
- **Клиенты.** Абоненты в кабинете и контрагенты в 1С сопоставляются только по ИНН и КПП, руками.
Хотелось, чтобы всё это оказалось в 1С рядом с карточкой клиента, обновлялось само по расписанию, а поступление за месяц создавалось автоматически и не плодило дублей.
Что делает расширение:
**Загрузка сверки.** За каждый месяц выбранного периода расширение получает с портала таблицу сверки и записывает её в регистр сведений «Астрал сверка». Записи за месяц при каждой загрузке полностью заменяются, это снимок сверки на месяц. Если сверка за месяц на портале ещё не построена, месяц пропускается с сообщением.
**Загрузка сроков тарифов и сертификатов.** Отчёт «Сроки действия тарифов» записывается в регистр «Астрал отчётность сведения»: клиент, тариф, даты начала и окончания действия тарифа и сертификата, владелец сертификата. Записи за день полностью заменяются.
**Клиенты.** Для каждой строки клиент ищется в справочнике «Клиенты» расширения по ИНН и КПП. Если клиента нет, он создаётся.
**Поступление за месяц.** Кнопкой «Создать поступление за месяц» или регламентным заданием создаётся документ «Поступление (акт, накладная, УПД)» с видом операции «Услуги»: одна строка на строку сверки, сумма строки равна цене минус отчисления партнёра (сублицензионная схема), без НДС. Организация, поставщик, договор, номенклатура, счёт и статья затрат берутся из настроек.
**Защита от дублей.** Связь «месяц сверки → документ» хранится в отдельном регистре. За месяц существует не более одного действующего документа. Непроведённый документ при повторном запуске перезаполняется по актуальной сверке, проведённый не трогается.
**Регламентные задания.** Ежедневно в 05:00 загружаются сверка за прошлый и текущий месяц и сроки тарифов на сегодня. 5-го числа в 06:00 обновляется сверка за прошлый месяц и создаётся или перезаполняется поступление. Результаты пишутся в журнал регистрации, событие «АстралПортал.РегламентноеЗадание».
**Отчёты.** «Сверка Астрал» показывает строки сверки по месяцам с итогами по цене и отчислениям. «Продление лицензий и сертификатов» показывает, у кого и когда заканчивается тариф или сертификат.
## Как устроен обмен с порталом
Официального API у личного кабинета нет, расширение повторяет запросы, которые делает браузер. Подсмотреть их можно во вкладке Network инструментов разработчика.
### Авторизация
В первой версии статьи авторизация шла через `Authorisation.aspx/LogOn` с cookie сессии. Этот способ больше не работает: портал перешёл на JWT. Теперь один POST на `/api/v2/auth/token` с логином и паролем возвращает массив токенов, из него берётся токен с типом `access`. Дальше во все запросы он передаётся в заголовке `Cookie` как `AccessToken=...`.
Тело = Новый Структура;
Тело.Вставить("login", Логин);
Тело.Вставить("password", Пароль);
Запрос = Новый HTTPЗапрос("/api/v2/auth/token");
Запрос.Заголовки.Вставить("Content-Type", "application/json");
Запрос.Заголовки.Вставить("Accept", "application/json");
Запрос.Заголовки.Вставить("Origin", "https://reg.astralnalog.ru");
Запрос.УстановитьТелоИзСтроки(ЗаписатьВСтрокуJSON(Тело));
Ответ = Соединение.ОтправитьДляОбработки(Запрос);
ТелоОтвета = Ответ.ПолучитьТелоКакСтроку();
Если Ответ.КодСостояния = 400 Или Ответ.КодСостояния = 401 Тогда
ВызватьИсключение "Портал Астрал отклонил вход: неверный логин или пароль.";
ИначеЕсли Ответ.КодСостояния <> 200 Тогда
ВызватьИсключение СтрШаблон("Ошибка входа на портал Астрал (HTTP %1): %2",
Ответ.КодСостояния, Лев(ТелоОтвета, 500));
КонецЕсли;
Токены = ПрочитатьИзСтрокиJSON(ТелоОтвета);
Если ТипЗнч(Токены) = Тип("Массив") Тогда
Для Каждого Токен Из Токены Цикл
Если ТипЗнч(Токен) = Тип("Соответствие") И Токен.Получить("tokenType") = "access" Тогда
Возврат Токен.Получить("value");
КонецЕсли;
КонецЦикла;
КонецЕсли;
ВызватьИсключение "Портал Астрал не вернул токен доступа: " + Лев(ТелоОтвета, 500);
КонецФункции
Функция НовыйЗапросКОтчету(Путь, Токен) Экспорт
Запрос = Новый HTTPЗапрос(Путь);
Запрос.Заголовки.Вставить("Content-Type", "application/json");
Запрос.Заголовки.Вставить("Accept", "application/json, text/javascript, */*; q=0.01");
Запрос.Заголовки.Вставить("X-Requested-With", "XMLHttpRequest");
Запрос.Заголовки.Вставить("Cookie", "AccessToken=" + Токен);
Возврат Запрос;
КонецФункции
Сверка живёт в старой части портала на ASP.NET. Метод `Balance.aspx/getGridData` принимает JSON с фильтром сетки jqGrid и параметрами `year` и `month`, а отвечает обёрткой `{"d": "<json-строка>"}`. Внутри: `balanceId`, признаки согласования и объект `data` с полями `totalpages` и `rows`. Страницы читаются в цикле, пока не дойдём до `totalpages`.
Внимание! Стоит обратить внимание на:
Если за месяц сверки ещё нет, портал отвечает кодом 500 и телом "{"d": null}". Это не ошибка, а «месяца нет», и расширение его пропускает.
Двойной JSON. Поле "d" приходит строкой, в которой лежит ещё один JSON. Его нужно разобрать второй раз.
Запрос = НовыйЗапросКОтчету(Путь, Токен);
Запрос.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
Запрос.УстановитьТелоИзСтроки(ЗаписатьВСтрокуJSON(Тело));
Ответ = Соединение.ОтправитьДляОбработки(Запрос);
ТелоОтвета = Ответ.ПолучитьТелоКакСтроку();
Если Ответ.КодСостояния = 500 И СтрНачинаетсяС(СокрЛ(ТелоОтвета), "{") Тогда
// WebMethod вернул исключение: для сверки так выглядит еще не построенный месяц ({"d":null}).
Обертка = ПрочитатьИзСтрокиJSON(ТелоОтвета);
Если ТипЗнч(Обертка) = Тип("Соответствие") И Обертка.Получить("d") = Неопределено И Обертка.Получить("Message") = Неопределено Тогда
Возврат Неопределено;
КонецЕсли;
КонецЕсли;
ПроверитьОтветОтчета(Ответ, ТелоОтвета, Описание);
Обертка = ПрочитатьИзСтрокиJSON(ТелоОтвета);
Если ТипЗнч(Обертка) <> Тип("Соответствие") Тогда
ВызватьИсключение СтрШаблон("Портал Астрал вернул ответ неожиданной структуры (%1).", Описание);
КонецЕсли;
Данные = Обертка.Получить("d");
Если Данные = Неопределено Тогда
Возврат Неопределено;
КонецЕсли;
Если ТипЗнч(Данные) = Тип("Строка") И (СтрНачинаетсяС(СокрЛ(Данные), "{") Или СтрНачинаетсяС(СокрЛ(Данные), "[")) Тогда
Данные = ПрочитатьИзСтрокиJSON(Данные);
КонецЕсли;
Возврат Данные;
КонецФункции
Функция ПолучитьСтраницуСверки(Соединение, Токен, Месяц, Страница)
Фильтр = Новый Соответствие;
Фильтр.Вставить("_search", Ложь);
Фильтр.Вставить("nd", ТекущаяУниверсальнаяДатаВМиллисекундах());
Фильтр.Вставить("rows", 500);
Фильтр.Вставить("page", Страница);
Фильтр.Вставить("sidx", "AbonentName");
Фильтр.Вставить("sord", "asc");
Для Каждого Имя Из СтрРазделить("Inn,Kpp,AbonentName,RegistrationNumber,RegistrationDate,TariffId,TariffName,Cost,CpDeductions,CompanyGroupName,OfficeName,OfficeAcceptDate,TariffZoneName,IsPrimary,tariffZone", ",") Цикл
Фильтр.Вставить(Имя, Неопределено);
КонецЦикла;
Фильтр.Вставить("partnerId", 0);
Фильтр.Вставить("year", Формат(Год(Месяц), "ЧГ=0"));
Фильтр.Вставить("month", Формат(Месяц(Месяц), "ЧГ=0"));
Фильтр.Вставить("ProxyAgentData", Ложь);
Тело = Новый Соответствие;
Тело.Вставить("filter", Фильтр);
Описание = СтрШаблон("сверка за %1, страница %2", Формат(Месяц, "ДФ='MMMM yyyy'"), Страница);
Результат = АстралПортал.ВызватьМетодСтраницы(Соединение, Токен, "/Balance.aspx/getGridData", Тело, Описание);
Если Результат = Неопределено Тогда
Возврат Неопределено;
КонецЕсли;
Если ТипЗнч(Результат) <> Тип("Соответствие") Тогда
ВызватьИсключение "Портал Астрал вернул сверку неожиданной структуры: " + Лев(Строка(Результат), 200);
КонецЕсли;
Возврат Результат;
КонецФункции
Запись месяца в регистр. Суммы портал отдаёт строкой вида `8 000,00 S81;`, даты строкой `дд.ММ.гггг`, признаки строкой `true`/`false`, для каждого случая есть своя функция разбора в модуле АстралПортал:
Функция ЗаписатьМесяц(Месяц, Заголовок, Строки, КэшКлиентов)
СогласованаПартнером = АстралПортал.БулевоИзСтрокиПортала(Заголовок.Получить("partnerAccepted"));
СогласованаЦП = АстралПортал.БулевоИзСтрокиПортала(Заголовок.Получить("saleCenterAccepted"));
НомерСверки = АстралПортал.СтрокаЯчейки(Заголовок.Получить("balanceId"));
НаборЗаписей = РегистрыСведений.АстралСверка1.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Период.Установить(Месяц);
Ключи = Новый Соответствие;
НачатьТранзакцию();
Попытка
Для Каждого Строка Из Строки Цикл
Ячейки = Строка.Получить("cell");
Если ТипЗнч(Ячейки) <> Тип("Массив") Или Ячейки.Количество() < 21 Тогда
Продолжить;
КонецЕсли;
ИНН = АстралПортал.СтрокаЯчейки(Ячейки[0]);
Наименование = АстралПортал.СтрокаЯчейки(Ячейки[2]);
Если ПустаяСтрока(ИНН) И ПустаяСтрока(Наименование) Тогда
Продолжить; // итоговая строка
КонецЕсли;
КПП = АстралПортал.СтрокаЯчейки(Ячейки[1]);
Идентификатор = АстралПортал.СтрокаЯчейки(Строка.Получить("id"));
Если ПустаяСтрока(Идентификатор) Тогда
Идентификатор = ИНН + "|" + КПП + "|" + АстралПортал.СтрокаЯчейки(Ячейки[7]) + "|" + АстралПортал.СтрокаЯчейки(Ячейки[3]);
КонецЕсли;
Идентификатор = Лев(Идентификатор, 100);
Клиент = АстралПортал.НайтиИлиСоздатьКлиента(Наименование, ИНН, КПП, КэшКлиентов);
КлючЗаписи = Идентификатор + "#" + XMLСтрока(Клиент);
Если Ключи.Получить(КлючЗаписи) <> Неопределено Тогда
Продолжить;
КонецЕсли;
Ключи.Вставить(КлючЗаписи, Истина);
Запись = НаборЗаписей.Добавить();
Запись.Период = Месяц;
Запись.Клиент = Клиент;
Запись.ИдентификаторСтроки = Идентификатор;
Запись.ИНН = Лев(ИНН, 20);
Запись.КПП = Лев(КПП, 10);
Запись.НаименованиеОрганизации = Лев(Наименование, 250);
Запись.ДатаРегистрации = АстралПортал.ДатаИзСтрокиПортала(Ячейки[3]);
Запись.Логин = Лев(АстралПортал.СтрокаЯчейки(Ячейки[5]), 100);
Запись.ИдентификаторТарифа = Лев(АстралПортал.СтрокаЯчейки(Ячейки[7]), 20);
Запись.НаименованиеТарифа = Лев(АстралПортал.СтрокаЯчейки(Ячейки[8]), 150);
Запись.ТарифнаяЗона = Лев(АстралПортал.СтрокаЯчейки(Ячейки[9]), 150);
Запись.ПервичныйТариф = АстралПортал.БулевоИзСтрокиПортала(Ячейки[10]);
Запись.Оплачен = АстралПортал.БулевоИзСтрокиПортала(Ячейки[13]);
Запись.Цена = АстралПортал.ЧислоИзСтрокиПортала(Ячейки[17]);
Запись.ОтчисленияПартнера = АстралПортал.ЧислоИзСтрокиПортала(Ячейки[18]);
Запись.ОфисРегистрации = Лев(АстралПортал.СтрокаЯчейки(Ячейки[20]), 150);
Запись.СверкаСогласованаПартнером = СогласованаПартнером;
Запись.СверкаСогласованаЦП = СогласованаЦП;
Запись.НомерСверки = Лев(НомерСверки, 20);
КонецЦикла;
НаборЗаписей.Записать(Истина);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Возврат НаборЗаписей.Количество();
КонецФункции
В первой версии отчёт «Сроки действия тарифов» читался постранично через `GetTariffValidityPeriodReportData` по 10 строк. Оказалось, что эта сетка на портале нестабильна и иногда теряет часть строк. Поэтому основной способ теперь другой: попросить портал сформировать файл отчёта целиком, скачать zip, достать из него xlsx и прочитать его табличным документом. Постраничная загрузка осталась запасным вариантом на случай, если файл получить не удалось.
Три шага: `CreateTariffValidityPeriodReport` возвращает путь к файлу на сервере, `Download?fullPath=...` отдаёт zip, дальше `ЧтениеZipФайла` и `ТабличныйДокумент.Прочитать`. Колонки ищутся по заголовкам, а не по номерам, чтобы не зависеть от их порядка в файле.
Функция ПрочитатьСтрокиИзФайлаОтчета(Соединение, Токен, ДатаФильтраНачало, ДатаФильтраКонец)
// 1. Формирование файла на сервере портала.
Тело = Новый Соответствие;
Тело.Вставить("filterContext", КонтекстФильтраОтчета(1, ДатаФильтраНачало, ДатаФильтраКонец));
Запрос = АстралПортал.НовыйЗапросКОтчету("/ReportModule/CreateTariffValidityPeriodReport", Токен);
Запрос.УстановитьТелоИзСтроки(АстралПортал.ЗаписатьВСтрокуJSON(Тело));
Ответ = Соединение.ОтправитьДляОбработки(Запрос);
ТелоОтвета = Ответ.ПолучитьТелоКакСтроку();
АстралПортал.ПроверитьОтветОтчета(Ответ, ТелоОтвета, "формирование файла отчета");
Результат = АстралПортал.ПрочитатьИзСтрокиJSON(ТелоОтвета);
Если ТипЗнч(Результат) <> Тип("Соответствие") Или Результат.Получить("isError") = Истина
Или Не ЗначениеЗаполнено(Результат.Получить("reportPath")) Тогда
ВызватьИсключение "Портал Астрал не сформировал файл отчета: " + Лев(ТелоОтвета, 300);
КонецЕсли;
ПутьНаСервере = Результат.Получить("reportPath");
// 2. Скачивание zip во временный файл.
ИмяАрхива = ПолучитьИмяВременногоФайла("zip");
Запрос = АстралПортал.НовыйЗапросКОтчету("/ReportModule/Download?fullPath="
+ КодироватьСтроку(ПутьНаСервере, СпособКодированияСтроки.КодировкаURL), Токен);
Запрос.Заголовки.Удалить("Content-Type");
Запрос.Заголовки.Вставить("Accept", "*/*");
Ответ = Соединение.Получить(Запрос, ИмяАрхива);
Если Ответ.КодСостояния <> 200 Тогда
УдалитьФайлы(ИмяАрхива);
ВызватьИсключение СтрШаблон("Портал Астрал не отдал файл отчета (HTTP %1).", Ответ.КодСостояния);
КонецЕсли;
// 3. Распаковка и чтение xlsx.
КаталогРаспаковки = ПолучитьИмяВременногоФайла("dir");
СоздатьКаталог(КаталогРаспаковки);
Попытка
Чтение = Новый ЧтениеZipФайла(ИмяАрхива);
Чтение.ИзвлечьВсе(КаталогРаспаковки, РежимВосстановленияПутейФайловZIP.НеВосстанавливать);
Чтение.Закрыть();
Файлы = НайтиФайлы(КаталогРаспаковки, "*.xlsx", Истина);
Если Файлы.Количество() = 0 Тогда
ВызватьИсключение "В архиве отчета нет файла xlsx.";
КонецЕсли;
ТабличныйДокумент = Новый ТабличныйДокумент;
ТабличныйДокумент.Прочитать(Файлы[0].ПолноеИмя, СпособЧтенияЗначенийТабличногоДокумента.Текст);
Строки = СтрокиИзТабличногоДокумента(ТабличныйДокумент);
Исключение
УдалитьФайлы(КаталогРаспаковки);
УдалитьФайлы(ИмяАрхива);
ВызватьИсключение;
КонецПопытки;
УдалитьФайлы(КаталогРаспаковки);
УдалитьФайлы(ИмяАрхива);
Возврат Строки;
КонецФункции
Обёртка, которая выбирает способ загрузки и пишет снимок записей в регистр:
Функция ПолучитьСведенияАстралОтчетности(ДатаНачала, ДатаОкончания, Логин, Пароль) Экспорт
Соединение = АстралПортал.НовоеСоединение();
Токен = АстралПортал.ПолучитьТокенДоступа(Соединение, Логин, Пароль);
ДатаФильтраНачало = Формат(ДатаНачала, "ДФ=dd.MM.yyyy");
ДатаФильтраКонец = Формат(ДатаОкончания, "ДФ=dd.MM.yyyy");
Попытка
СтрокиОтчета = ПрочитатьСтрокиИзФайлаОтчета(Соединение, Токен, ДатаФильтраНачало, ДатаФильтраКонец);
Исключение
ИнформацияОбОшибке = ИнформацияОбОшибке();
ЗаписьЖурналаРегистрации("АстралОтчетность.ЗагрузкаФайла", УровеньЖурналаРегистрации.Предупреждение, , ,
"Не удалось получить файл отчета, используется постраничная загрузка: "
+ ПодробноеПредставлениеОшибки(ИнформацияОбОшибке));
СтрокиОтчета = ПрочитатьСтрокиПостранично(Соединение, Токен, ДатаФильтраНачало, ДатаФильтраКонец);
КонецПопытки;
Возврат ЗаписатьСнимокОтчета(СтрокиОтчета, НачалоДня(ТекущаяДата()));
КонецФункции
Портал может выдать несколько строк на один продукт (разные сертификаты, расширение тарифа), поэтому ключ строки в регистре складывается из Guid продукта и короткого CRC32-хеша полей сертификата и расширения.
Как создаётся поступление:
Схема расчётов сублицензионная - Астрал как центр продаж выставляет партнёру счёт на сумму «цена минус отчисления партнёра» по каждому подключению. Документ создаётся один на месяц.
Строки сверки с суммой к оплате ноль или меньше (например Тест-Драйв, где отчисления равны цене) в документ не попадают, их количество показывается в сообщении.
Чтобы не ссылаться на предопределённые элементы базовой конфигурации, номер субконто для статьи затрат определяется по типу значения вида субконто, а счета учёта заполняет типовой механизм БП через флаг в дополнительных свойствах документа:
Функция ИндексСубконтоСтатьиЗатрат(СчетЗатрат)
Если Не ЗначениеЗаполнено(СчетЗатрат) Тогда
Возврат 0;
КонецЕсли;
Индекс = 0;
Для Каждого ВидСубконто Из СчетЗатрат.ВидыСубконто Цикл
Индекс = Индекс + 1;
Если ВидСубконто.ВидСубконто.ТипЗначения.СодержитТип(Тип("СправочникСсылка.СтатьиЗатрат")) Тогда
Возврат Индекс;
КонецЕсли;
КонецЦикла;
Возврат 0;
КонецФункции
// Типовой механизм БП заполнит незаполненные счета учета (расчетов, затрат, НДС) перед записью.
Документ.ДополнительныеСвойства.Вставить("ЗаполнитьСчетаУчетаПередЗаписью", Истина);
Если Проводить Тогда
Документ.Записать(РежимЗаписиДокумента.Проведение);
Иначе
Документ.Записать(РежимЗаписиДокумента.Запись);
КонецЕсли;
Ставьте плюсики, если вам доработка зашла. А если не зашла, то тоже ставьте плюсики. Огромное уважение, если дочитали этот текст до конца.
Проверено на следующих конфигурациях и релизах:
- Бухгалтерия предприятия КОРП, редакция 3.0, релизы 3.0.205.22
Вступайте в нашу телеграмм-группу Инфостарт