first commit
This commit is contained in:
57
README.md
Normal file
57
README.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# YML → CSV (open endpoint for WP All Import)
|
||||
|
||||
Генерирует CSV из Prom.ua YML-фида по открытому эндпоинту. Строит `category_path` для иерархии категорий и конвертирует цену из PLN в UAH (НБУ или вручную).
|
||||
|
||||
## Установка
|
||||
|
||||
1. Создай папку `wp-content/plugins/yml2csv/`.
|
||||
2. Сохрани файл плагина как `wp-content/plugins/yml2csv/yml2csv.php`.
|
||||
3. Активируй плагин в админке WordPress.
|
||||
|
||||
## Эндпоинт
|
||||
|
||||
`GET /?yml2csv=1&src=...&target_cur=UAH[&rate=...][&margin=...][&precision=...]`
|
||||
|
||||
**Параметры:**
|
||||
|
||||
* `src` — URL YML-фида (обязательный).
|
||||
* `target_cur` — целевая валюта. Автоконверсия реализована для `UAH` при исходном `PLN`.
|
||||
* `rate` — фиксированный курс PLN→UAH. Если задан, НБУ не вызывается.
|
||||
* `margin` — множитель наценки (например, `1.07` = +7%). По умолчанию `1.0`.
|
||||
* `precision` — знаков после запятой для финальной цены. По умолчанию `2`.
|
||||
|
||||
## Примеры
|
||||
|
||||
```text
|
||||
https://example.com/?yml2csv=1&src=http://127.0.0.1/feeds/prom_feed.yml&target_cur=UAH
|
||||
https://example.com/?yml2csv=1&src=http://127.0.0.1/feeds/prom_feed.yml&target_cur=UAH&rate=9.25
|
||||
https://example.com/?yml2csv=1&src=http://127.0.0.1/feeds/prom_feed.yml&target_cur=UAH&margin=1.07&precision=0
|
||||
```
|
||||
|
||||
## Формат CSV
|
||||
|
||||
Колонки:
|
||||
|
||||
```
|
||||
sku, title, description,
|
||||
price, currency, // конечные значения для WooCommerce
|
||||
price_src, currency_src, // исходные значения из фида
|
||||
stock,
|
||||
category_path, category_name, category_id,
|
||||
image_urls // через запятую
|
||||
```
|
||||
|
||||
## Маппинг в WP All Import
|
||||
|
||||
* Title → `title`
|
||||
* Content → `description`
|
||||
* Images → `image_urls` (comma-separated)
|
||||
* Regular Price → `price`
|
||||
* SKU → `sku`
|
||||
* Categories → включить «hierarchical (parent/child)» и указать поле `category_path`
|
||||
* (опц.) сохранить `price_src` / `currency_src` как метаполя
|
||||
|
||||
## Лицензия
|
||||
|
||||
MIT.
|
||||
|
||||
220
yml2csv.php
Normal file
220
yml2csv.php
Normal file
@@ -0,0 +1,220 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user