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