221 lines
7.9 KiB
PHP
221 lines
7.9 KiB
PHP
<?php
|
||
/*
|
||
Plugin Name: YML → CSV
|
||
Description: Публичный эндпоинт ?yml2csv=1&src=URL[&target_cur=UAH][&rate=9.25][&margin=1.0][&precision=2] — генерирует CSV для WP All Import, строит category_path и конвертирует цену PLN→UAH.
|
||
Version: 1.2.0
|
||
Author: MrAkells
|
||
*/
|
||
|
||
if (!defined('ABSPATH')) exit;
|
||
|
||
/**
|
||
* Открытый эндпоинт: генерирует CSV на лету.
|
||
*
|
||
* Примеры:
|
||
* /?yml2csv=1&src=http://89.117.48.146/feeds/prom_feed.yml&target_cur=UAH
|
||
* /?yml2csv=1&src=http://.../feed.yml&target_cur=UAH&rate=9.25&margin=1.05&precision=0
|
||
*/
|
||
add_action('init', function () {
|
||
if (!isset($_GET['yml2csv'])) return;
|
||
|
||
$src = isset($_GET['src']) ? trim((string)$_GET['src']) : '';
|
||
if (!$src || !preg_match('~^https?://~i', $src)) {
|
||
status_header(400); echo 'Bad src'; exit;
|
||
}
|
||
|
||
$targetCur = strtoupper(trim((string)($_GET['target_cur'] ?? '')));
|
||
$rateParam = isset($_GET['rate']) ? (float)$_GET['rate'] : 0.0;
|
||
$margin = isset($_GET['margin']) ? max(0.0, (float)$_GET['margin']) : 1.0;
|
||
$precision = isset($_GET['precision']) ? max(0, (int)$_GET['precision']) : 2;
|
||
|
||
$xml = y2c_load_xml($src);
|
||
if (!$xml) { status_header(502); echo 'Fetch/parse error'; exit; }
|
||
|
||
$cats = y2c_extract_categories($xml);
|
||
$paths = y2c_build_paths($cats);
|
||
|
||
// Если нужен курс PLN→UAH (и курс не передан), притягиваем из НБУ.
|
||
$nbuRate = 0.0;
|
||
if ($rateParam <= 0 && $targetCur === 'UAH') {
|
||
$nbuRate = y2c_rate_pln_to_uah();
|
||
}
|
||
|
||
$rows = y2c_extract_offers_with_fx($xml, $cats, $paths, $targetCur, $rateParam, $nbuRate, $margin, $precision);
|
||
|
||
nocache_headers();
|
||
header('Content-Type: text/csv; charset=UTF-8');
|
||
header('Content-Disposition: inline; filename="yml_to_wpai.csv"');
|
||
|
||
$out = fopen('php://output', 'w');
|
||
if (!$out) { status_header(500); echo 'Cannot open output'; exit; }
|
||
|
||
// BOM для Excel/WPAI
|
||
fprintf($out, "\xEF\xBB\xBF");
|
||
|
||
// Заголовки
|
||
fputcsv($out, [
|
||
'sku','title','description',
|
||
'price','currency', // конечная цена/валюта (для WooCommerce)
|
||
'price_src','currency_src', // исходные
|
||
'stock',
|
||
'category_path','category_name','category_id',
|
||
'image_urls'
|
||
]);
|
||
|
||
foreach ($rows as $r) fputcsv($out, $r);
|
||
fclose($out);
|
||
exit;
|
||
});
|
||
|
||
/** Загрузка XML по URL */
|
||
function y2c_load_xml(string $url): ?SimpleXMLElement {
|
||
$res = wp_remote_get($url, ['timeout' => 25, 'redirection' => 5]);
|
||
if (is_wp_error($res)) return null;
|
||
$code = wp_remote_retrieve_response_code($res);
|
||
if ($code < 200 || $code >= 300) return null;
|
||
$body = wp_remote_retrieve_body($res);
|
||
if (!$body) return null;
|
||
libxml_use_internal_errors(true);
|
||
$xml = simplexml_load_string($body);
|
||
return $xml ?: null;
|
||
}
|
||
|
||
/** Вытаскиваем категории (id, parentId, name) */
|
||
function y2c_extract_categories(SimpleXMLElement $xml): array {
|
||
$out = [];
|
||
$nodes = $xml->shop->categories->category ?? $xml->categories->category ?? null;
|
||
if (!$nodes) return $out;
|
||
|
||
foreach ($nodes as $c) {
|
||
$id = (string)$c['id'];
|
||
if ($id === '') continue;
|
||
$out[$id] = [
|
||
'id' => $id,
|
||
'parent' => isset($c['parentId']) ? (string)$c['parentId'] : '',
|
||
'name' => trim((string)$c),
|
||
];
|
||
}
|
||
return $out;
|
||
}
|
||
|
||
/** Строим текстовые пути категорий "Parent > Child > Leaf" */
|
||
function y2c_build_paths(array $cats): array {
|
||
$paths = [];
|
||
foreach ($cats as $id => $_) {
|
||
if (isset($paths[$id])) continue;
|
||
$chain = [];
|
||
$cur = $id;
|
||
$seen = [];
|
||
while ($cur && !isset($paths[$cur])) {
|
||
if (isset($seen[$cur])) { $chain = []; break; } // защита от цикла
|
||
$seen[$cur] = true;
|
||
$chain[] = $cur;
|
||
$cur = $cats[$cur]['parent'] ?? '';
|
||
}
|
||
$prefix = ($cur && isset($paths[$cur])) ? $paths[$cur] : [];
|
||
for ($i = count($chain)-1; $i >= 0; $i--) {
|
||
$cid = $chain[$i];
|
||
$prefix[] = $cats[$cid]['name'] ?? '';
|
||
$paths[$cid] = $prefix;
|
||
}
|
||
}
|
||
$out = [];
|
||
foreach ($paths as $id => $arr) $out[$id] = implode(' > ', array_filter($arr));
|
||
return $out;
|
||
}
|
||
|
||
/** Курс PLN→UAH из НБУ */
|
||
function y2c_rate_pln_to_uah(): float {
|
||
// НБУ JSON: [{"r030":985,"txt":"Злотий польський","rate":...,"cc":"PLN","exchangedate":"..."}]
|
||
$res = wp_remote_get('https://bank.gov.ua/NBUStatService/v1/statdirectory/exchange?valcode=PLN&json', ['timeout'=>10]);
|
||
if (is_wp_error($res)) return 0.0;
|
||
if (wp_remote_retrieve_response_code($res) !== 200) return 0.0;
|
||
$data = json_decode(wp_remote_retrieve_body($res), true);
|
||
if (!is_array($data) || empty($data[0]['rate'])) return 0.0;
|
||
return (float)$data[0]['rate'];
|
||
}
|
||
|
||
/**
|
||
* Формирование строк офферов + конвертация цены.
|
||
* - Если задан rate>0 — используем его (любая исходная валюта будет трактоваться как PLN для умножения).
|
||
* - Если rate не задан и target_cur=UAH, то:
|
||
* - если исходная валюта = PLN → умножаем на курс НБУ,
|
||
* - иначе оставляем как есть (fail-safe).
|
||
* - margin применяется поверх конверсии, после чего округляем.
|
||
*/
|
||
function y2c_extract_offers_with_fx(
|
||
SimpleXMLElement $xml,
|
||
array $cats,
|
||
array $paths,
|
||
string $targetCur,
|
||
float $rateParam,
|
||
float $nbuRate,
|
||
float $margin,
|
||
int $precision
|
||
): array {
|
||
$offers = $xml->shop->offers->offer ?? $xml->offers->offer ?? null;
|
||
$rows = [];
|
||
if (!$offers) return $rows;
|
||
|
||
foreach ($offers as $o) {
|
||
$sku = (string)($o->vendorCode ?? $o['id'] ?? '');
|
||
$title = trim((string)($o->name ?? $o->model ?? $o->title ?? ''));
|
||
$desc = trim((string)($o->description ?? ''));
|
||
$priceSrc = (float)($o->price ?? 0);
|
||
$curSrc = strtoupper((string)($o->currencyId ?? 'PLN')); // по умолчанию считаем PLN, если нет поля
|
||
|
||
// Базово — без конверсии
|
||
$priceOut = $priceSrc;
|
||
$curOut = $curSrc;
|
||
|
||
// Конвертация
|
||
if ($targetCur !== '') {
|
||
if ($rateParam > 0) {
|
||
// Принудительный курс
|
||
$priceOut = $priceSrc * $rateParam;
|
||
$curOut = $targetCur;
|
||
} elseif ($targetCur === 'UAH' && $curSrc === 'PLN' && $nbuRate > 0) {
|
||
$priceOut = $priceSrc * $nbuRate;
|
||
$curOut = 'UAH';
|
||
}
|
||
}
|
||
|
||
// Наценка (если есть)
|
||
if ($margin > 0 && $margin !== 1.0) {
|
||
$priceOut = $priceOut * $margin;
|
||
}
|
||
|
||
// Округление
|
||
$priceOut = ($precision >= 0) ? round($priceOut, $precision) : $priceOut;
|
||
|
||
$stock = (string)($o->quantity ?? $o->stock_quantity ?? '');
|
||
$catId = (string)($o->categoryId ?? '');
|
||
$catName = $cats[$catId]['name'] ?? '';
|
||
$catPath = $paths[$catId] ?? $catName;
|
||
|
||
$imgs = [];
|
||
if (isset($o->picture)) {
|
||
foreach ($o->picture as $p) { $u = trim((string)$p); if ($u) $imgs[] = $u; }
|
||
} elseif (isset($o->images)) {
|
||
foreach ($o->images->image as $p) { $u = trim((string)$p); if ($u) $imgs[] = $u; }
|
||
}
|
||
|
||
$rows[] = [
|
||
$sku,
|
||
$title,
|
||
$desc,
|
||
$priceOut,
|
||
$curOut,
|
||
$priceSrc,
|
||
$curSrc,
|
||
$stock,
|
||
$catPath,
|
||
$catName,
|
||
$catId,
|
||
implode(',', $imgs),
|
||
];
|
||
}
|
||
return $rows;
|
||
}
|
||
|