banner('warning', 'Запланирован принудительный полный обмен — он выполнится при следующем обмене каталога. Отменить можно на вкладке «Обслуживание».');
}
if ($missingAt) {
$this->banner('info', 'После полной выгрузки в очереди обработка товаров, которых в ней не было' .
($missingAt > time() ? ' (через ' . human_time_diff(time(), (int) $missingAt) . ').' : '.'));
}
if ($cleanup['running']) {
$this->banner('info', sprintf('Идёт очистка неиспользуемых картинок: удалено %d.', $cleanup['deleted']));
}
}
// --- Вкладки ------------------------------------------------------------------
private function tabConnection(): void
{
$this->card('Адрес для 1С', function (): void {
?>
Укажите этот адрес, логин и пароль в 1С: Администрирование → Обмен данными → Обмен с сайтами
(в разных конфигурациях путь немного отличается).
card('Доступ', function (): void {
$this->toggle('enabled', 'Обмен включён', 'Выключите, чтобы временно запретить любые запросы от 1С.');
$this->field('Логин', 'nameAttr('login')
. ' value="' . esc_attr((string) $this->s['login']) . '">', 'Латиницей. Не связан с пользователями WordPress.');
$placeholder = $this->s['password_hash'] ? '•••••••• (задан — оставьте пустым, чтобы не менять)' : '';
$this->field('Пароль', 'nameAttr('password')
. ' value="" placeholder="' . esc_attr($placeholder) . '">', 'Хранится только в виде хеша. Используйте длинный случайный пароль.');
$this->field('Разрешённые IP', '',
'Необязательно. Если заполнено — обмен принимается только с этих адресов (по одному в строке). '
. 'После 10 неверных паролей адрес блокируется на 15 минут.');
});
$this->card('1С пишет «Сервер не получил логин/пароль»', function (): void {
?>
На части хостингов (PHP в режиме CGI/FastCGI) заголовок авторизации не доходит до PHP.
Для Apache добавьте в .htaccess в корне сайта перед правилами WordPress:
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
Для nginx + PHP-FPM: fastcgi_param HTTP_AUTHORIZATION $http_authorization;
card('Новые товары', function (): void {
$this->select('new_status', 'Статус новых товаров', [
'publish' => 'Опубликован', 'draft' => 'Черновик', 'pending' => 'На утверждении', 'private' => 'Личный',
]);
$this->toggle('match_by_sku', 'Связывать с товарами сайта по артикулу',
'Для первого обмена, если товары на сайте заведены вручную и у них нет Ид 1С. Иначе появятся дубли.');
$this->toggle('import_attributes', 'Загружать свойства 1С как атрибуты WooCommerce', 'Нужно для фильтров в каталоге.');
});
$this->card('Обновление существующих товаров', function (): void {
echo '
Выключите то, что правите на сайте вручную, — 1С не будет это перезаписывать. '
. 'Новые товары заполняются полностью. Цены и остатки — на вкладке «Цены и остатки».
';
$this->select('attributes_mode', 'Атрибуты', [
'sync' => 'Синхронизировать с 1С',
'merge' => 'Только добавлять значения из 1С, ничего не удалять',
'none' => 'Не трогать',
], 'Атрибуты, добавленные вручную (которых нет среди свойств 1С), не удаляются ни в каком режиме. Видимость и порядок сохраняются.');
echo '
Для отдельного товара есть галочка «Не обновлять из 1С» в карточке товара, вкладка «Запасы».
';
});
$this->card('Удаление и полная выгрузка', function (): void {
$this->select('deleted_action', 'Помеченные на удаление в 1С', [
'draft' => 'Переводить в черновики', 'trash' => 'Отправлять в корзину', 'none' => 'Ничего не делать',
], 'Если пометку в 1С снимут — товар вернётся с прежним статусом.');
$this->select('missing_action', 'Нет в полной выгрузке', [
'none' => 'Ничего не делать',
'draft' => 'Снимать с публикации (черновик)',
'outofstock' => 'Ставить «Нет в наличии»',
'trash' => 'Отправлять в корзину',
], 'Тип выгрузки (полная / только изменения) определяется автоматически по файлу из 1С. При выгрузке изменений этот пункт не применяется. '
. 'Касается только товаров из 1С; выполняется в фоне после обмена. Если пропало больше половины товаров — ничего не делается.');
});
$this->card('Скорость обмена', function (): void {
$this->toggle('skip_unchanged', 'Пропускать товары, которые в 1С не менялись',
'Главное ускорение: при повторном обмене обрабатываются только изменённые товары. '
. 'Чтобы один раз обновить всё — «Принудительный полный обмен» на вкладке «Обслуживание».');
$this->select('image_sizes', 'Размеры картинок при загрузке', [
'all' => 'Все размеры (как обычно в WordPress)',
'woo' => 'Только нужные WooCommerce — быстрее',
], 'Каждый размер — отдельный файл; темы и плагины добавляют свои. Вариант «только WooCommerce» ускоряет загрузку картинок в разы. '
. 'Если тема где-то использует свой размер, там будет показана ближайшая подходящая картинка.');
});
}
private function tabPrices(): void
{
$types = array_filter(array_map('strval', (array) get_option(Catalog::PRICES_OPTION, [])));
$this->card('Цены', function () use ($types): void {
echo '';
$this->field('Тип цены', 'nameAttr('price_type')
. ' value="' . esc_attr((string) $this->s['price_type']) . '">', 'Название или Ид типа цены из 1С. Пусто — первая цена в предложении.');
$this->field('Цена распродажи', 'nameAttr('sale_price_type')
. ' value="' . esc_attr((string) $this->s['sale_price_type']) . '">', 'Необязательно. Если цена этого типа ниже основной — станет ценой со скидкой.');
if ($types) {
echo '
';
}
$this->toggle('update_price', 'Обновлять цены у существующих товаров', 'Выключено — цена ставится только товару без цены.');
});
$this->card('Остатки', function (): void {
$this->toggle('manage_stock', 'Управлять запасами (точное количество)', 'Выключено — только «в наличии / нет в наличии».');
$this->toggle('update_stock', 'Обновлять остатки у существующих товаров', 'Остаток — сумма по всем складам 1С.');
});
}
private function tabOrders(): void
{
$this->card('Выгрузка заказов в 1С', function (): void {
$this->toggle('orders_enabled', 'Выгружать заказы в 1С', 'Заказ уходит после создания и после каждой смены статуса.');
$boxes = '';
foreach (wc_get_order_statuses() as $key => $label) {
$status = substr($key, 3);
$boxes .= sprintf(
'',
esc_attr(Settings::OPTION),
esc_attr($status),
checked(in_array($status, (array) $this->s['order_statuses'], true), true, false),
esc_html($label)
);
}
$this->field('Статусы для выгрузки', '
' . $boxes . '
');
$this->field('Валюта в документе', 'nameAttr('order_currency')
. ' value="' . esc_attr((string) $this->s['order_currency']) . '">', 'Пусто — код валюты заказа (RUB, EUR…). Некоторые конфигурации 1С ждут «руб».');
});
$this->card('Статусы из 1С', function (): void {
$this->toggle('order_import_statuses', 'Принимать статусы заказов из 1С',
'Отменён → «Отменён», отгружен → «Выполнен», оплачен → «Обработка». Только для заказов, которые сайт сам отправлял в 1С.');
});
}
private function tabAdvanced(): void
{
$this->card('Передача файлов', function (): void {
$this->toggle('use_zip', 'Принимать файлы в zip-архиве', 'Быстрее передача. Нужно расширение PHP zip.');
$this->field('Длительность шага, сек', 'nameAttr('time_limit')
. ' value="' . esc_attr((string) $this->s['time_limit']) . '">',
'Импорт идёт порциями, после каждой 1С повторяет запрос. Больше — меньше пауз между шагами; '
. 'уменьшите, если хостинг обрывает долгие запросы.');
$this->field('Размер части файла, МБ', 'nameAttr('file_limit_mb')
. ' value="' . esc_attr((string) $this->s['file_limit_mb']) . '">',
'Сейчас фактически: ' . size_format(Storage::fileLimit()) . ' (ограничено post_max_size на сервере).');
$this->select('response_charset', 'Кодировка ответов для 1С', [
'cp1251' => 'Windows-1251 (обычно для 1С)',
'utf-8' => 'UTF-8',
], 'Если в журнале обмена 1С вместо русских сообщений «РћР±СЂ…» — нужна Windows-1251.');
});
$this->card('Журнал', function (): void {
$this->toggle('debug', 'Подробный журнал обмена', 'Каждый шаг пишется в журнал. Ошибки пишутся всегда.');
printf('
',
esc_url(admin_url('admin.php?page=wc-status&tab=logs')));
});
}
private function tabService(): void
{
$this->card('Принудительный полный обмен', function (): void {
?>
Обычно плагин пропускает товары, которые в 1С не менялись. Принудительный обмен один раз
обработает все товары заново — содержание, категории, атрибуты и картинки, как при первом обмене.
Что учесть:
Выполнится при следующем обмене каталога, запущенном из 1С. Сам обмен эта кнопка не запускает.
Займёт столько же времени, сколько первый обмен (с картинками — может быть больше часа), и нагрузит хостинг.
Лучше запускать вечером или ночью.
Защиты из настроек продолжают работать: выключенные поля и товары с галочкой «Не обновлять из 1С» не перезаписываются.
Правки на сайте в разрешённых полях (название, описание, атрибуты…) будут заменены данными из 1С.
Одинаковые картинки повторно не загружаются. Если картинка в 1С изменилась — загрузится новая, старая останется в медиатеке
(её уберёт «Очистка картинок» ниже).
Перед запуском желательно сделать резервную копию базы.
Запланирован на следующий обмен
';
$this->toolButton('cancel_force', 'Отменить', 'secondary');
} else {
$this->toolButton('force_full', 'Запланировать принудительный полный обмен', 'primary',
'Следующий обмен каталога обработает все товары заново и займёт заметно больше времени. Продолжить?');
}
});
$this->card('Очистка картинок', function (): void {
$status = MediaCleanup::status();
$count = $status['running'] ? 0 : MediaCleanup::count();
?>
Удаляет из медиатеки картинки, загруженные обменом (этим или прежним плагином), которые больше
не стоят ни у одного товара или вариации. Например, дубли после перехода со старого плагина.
Картинки, загруженные вручную, не трогаются. Свежие (младше 2 часов) — тоже, на случай идущего обмена.
Если вы вставляли такие картинки в текст описаний или страниц — там они пропадут. Удаление необратимо, сделайте резервную копию.
Идёт очистка: удалено %d', (int) $status['deleted']);
} else {
if ($status['finished']) {
printf('
';
$this->toolButton('clear', 'Очистить временные файлы и прогресс импорта', 'secondary');
$this->toolButton('reset_maps', 'Сбросить кеш свойств и типов цен', 'secondary');
echo '
';
});
}
/**
* Внизу каждой вкладки: «Плагин бесплатный», кофе автору (сумма на выбор) и ссылка на визитку.
* Только в админке плагина, без всплывающих уведомлений — добровольно.
*/
private function supportBar(): void
{
if (GERSYNC_DONATE_URL === '' && GERSYNC_AUTHOR_URL === '') {
return;
}
echo '
Плагин бесплатный — все функции работают без оплаты.';
if (GERSYNC_DONATE_URL !== '') {
$withAmount = str_contains(GERSYNC_DONATE_URL, '{amount}') || str_contains(GERSYNC_DONATE_URL, '{cents}');
if ($withAmount) {
printf(
'☕ Угостить автора кофе:'
. ' €'
. ' Угостить',
esc_attr(GERSYNC_DONATE_URL),
esc_attr(GERSYNC_DONATE_DEFAULT),
esc_url(Admin::donateUrl(GERSYNC_DONATE_DEFAULT))
);
} else {
printf('☕ Угостить автора кофе',
esc_url(GERSYNC_DONATE_URL));
}
}
if (GERSYNC_AUTHOR_URL !== '') {
printf('Об авторе и других проектах →',
esc_url(GERSYNC_AUTHOR_URL));
}
echo '
';
}
// --- Элементы -------------------------------------------------------------------
private function card(string $title, callable $body, bool $collapsed = false): void
{
if ($collapsed) {
echo '' . esc_html($title) . '
';
$body();
echo '
';
return;
}
echo '
' . esc_html($title) . '
';
$body();
echo '
';
}
/**
* Поле «подпись слева — элемент справа». $control уже экранирован вызывающим.
*/
private function field(string $label, string $control, string $hint = ''): void
{
echo '