Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <?php
- /**
- * PrestaShop 1.7.8.x – Активни продукти репорт (Bootstrap 5 + DataTables)
- * Единичен файл (не модул). Постави го в корена на магазина или подпапка.
- * Съвместим с PHP 7.3.
- * Автор: Код програмиране
- */
- // ---------- НАМИРАНЕ НА /config/config.inc.php (без твърди пътища) ----------
- function ps_find_config() {
- $dir = __DIR__;
- for ($i = 0; $i < 8; $i++) {
- $candidate = rtrim($dir, '/').'/config/config.inc.php';
- if (is_file($candidate)) return $candidate;
- $parent = dirname($dir);
- if ($parent === $dir) break;
- $dir = $parent;
- }
- return '';
- }
- $cfg = ps_find_config();
- if (!$cfg) { http_response_code(500); echo 'Не намирам /config/config.inc.php.'; exit; }
- require_once $cfg;
- // Допълнителни include-и (някои инсталации изискват това)
- if (defined('_PS_ROOT_DIR_')) {
- $extra = array(
- _PS_ROOT_DIR_.'/config/defines.inc.php',
- _PS_ROOT_DIR_.'/config/autoload.php',
- _PS_ROOT_DIR_.'/config/bootstrap.php',
- _PS_ROOT_DIR_.'/config/alias.php',
- _PS_ROOT_DIR_.'/config/defines_uri.inc.php',
- _PS_ROOT_DIR_.'/config/ini_sets.inc.php',
- );
- foreach ($extra as $ex) { if (is_file($ex)) require_once $ex; }
- }
- // ---------- ИНИЦИАЛИЗАЦИЯ НА PRESTASHOP ----------
- try {
- if (!defined('_PS_VERSION_')) {
- require_once _PS_ROOT_DIR_.'/config/config.inc.php';
- }
- // Инициализиране на контекста
- if (class_exists('Context')) {
- $context = Context::getContext();
- if (!$context->controller) {
- $context->controller = new FrontController();
- $context->controller->init();
- }
- }
- // Гарантиране на shop контекст
- if (Shop::isFeatureActive()) {
- if (!Shop::getContext()) {
- Shop::setContext(Shop::CONTEXT_SHOP, (int)Configuration::get('PS_SHOP_DEFAULT'));
- }
- }
- } catch (Exception $e) {
- http_response_code(500);
- header('Content-Type: application/json; charset=utf-8');
- echo json_encode(array('error' => 'PrestaShop initialization failed: ' . $e->getMessage()));
- exit;
- }
- // ---------- ГАРАНТИРАН КОНТЕКСТ ----------
- $context = Context::getContext();
- // Shop
- if (!$context->shop || !(int)$context->shop->id) {
- $idShop = (int)Configuration::get('PS_SHOP_DEFAULT');
- if ($idShop) {
- $context->shop = new Shop($idShop);
- Shop::setContext(Shop::CONTEXT_SHOP, $idShop);
- }
- }
- // Language
- if (!$context->language || !(int)$context->language->id) {
- $idLang = (int)Configuration::get('PS_LANG_DEFAULT');
- if ($idLang) $context->language = new Language($idLang);
- }
- // Currency
- if (!$context->currency || !(int)$context->currency->id) {
- $idCur = (int)Configuration::get('PS_CURRENCY_DEFAULT');
- if ($idCur) $context->currency = new Currency($idCur);
- }
- // Country
- if (!$context->country || !(int)$context->country->id) {
- $idCountry = (int)Configuration::get('PS_COUNTRY_DEFAULT');
- if ($idCountry) $context->country = new Country($idCountry);
- }
- $link = $context->link;
- // ---------- ПОМОЩНИ ФУНКЦИИ ----------
- function ps_get_active_languages() {
- static $langs = null;
- if ($langs === null) {
- $langs = Language::getLanguages(true, Context::getContext()->shop->id);
- }
- return $langs;
- }
- function ps_currency() {
- static $cur = null;
- if ($cur === null) {
- $defaultId = (int)Configuration::get('PS_CURRENCY_DEFAULT');
- $cur = new Currency($defaultId);
- }
- return $cur;
- }
- // Активни продукти (ID desc)
- function ps_fetch_active_products($offset=0, $limit=100000) {
- $sql = (new DbQuery())
- ->select('p.id_product, p.reference')
- ->from('product', 'p')
- ->innerJoin('product_shop', 'ps', 'p.id_product = ps.id_product AND ps.id_shop = '.(int)Context::getContext()->shop->id)
- ->where('ps.active = 1')
- ->orderBy('p.id_product DESC')
- ->limit((int)$limit, (int)$offset);
- $rows = Db::getInstance()->executeS($sql);
- return $rows ? $rows : array();
- }
- // Имена и link_rewrite за всички активни езици
- function ps_fetch_names_for_product($id_product) {
- $langs = ps_get_active_languages();
- $ids = array();
- foreach ($langs as $l) $ids[] = (int)$l['id_lang'];
- if (!$ids) return array();
- $sql = (new DbQuery())
- ->select('pl.id_lang, pl.name, pl.link_rewrite')
- ->from('product_lang', 'pl')
- ->where('pl.id_product='.(int)$id_product)
- ->where('pl.id_shop='.(int)Context::getContext()->shop->id)
- ->where('pl.id_lang IN ('.implode(',', $ids).')');
- $rows = Db::getInstance()->executeS($sql);
- $out = array();
- if ($rows) {
- foreach ($rows as $r) {
- $out[(int)$r['id_lang']] = array(
- 'name' => $r['name'],
- 'link_rewrite' => $r['link_rewrite'],
- );
- }
- }
- return $out;
- }
- // id на кавър изображението
- function ps_cover_image_id($id_product) {
- $cover = Image::getCover($id_product);
- if ($cover && !empty($cover['id_image'])) return (int)$cover['id_image'];
- return null;
- }
- // URL за миниатюра + URL към продукта
- function ps_image_and_product_links($id_product, $namesByLang, $link) {
- $langs = ps_get_active_languages();
- $firstLangId = (int)$langs[0]['id_lang'];
- $linkRewrite = isset($namesByLang[$firstLangId]['link_rewrite']) ? $namesByLang[$firstLangId]['link_rewrite'] : 'product';
- $coverId = ps_cover_image_id($id_product);
- // Определяне на типа изображение
- $thumbType = 'home_default';
- if (class_exists('ImageType')) {
- if (method_exists('ImageType','getFormattedName')) {
- $thumbType = ImageType::getFormattedName('home');
- } elseif (method_exists('ImageType','getFormatedName')) {
- $thumbType = ImageType::getFormatedName('home');
- }
- }
- $imgUrl = $coverId ? $link->getImageLink($linkRewrite, $coverId, $thumbType) : null;
- $productUrl = $link->getProductLink($id_product, $linkRewrite, null, null, $firstLangId);
- return array($imgUrl, $productUrl);
- }
- // Наличности по комбинации
- function ps_sizes_and_qty($id_product) {
- $langs = ps_get_active_languages();
- $id_lang = (int)$langs[0]['id_lang'];
- $prod = new Product($id_product, false, $id_lang, Context::getContext()->shop->id);
- $combs = $prod->getAttributeCombinations($id_lang);
- if (!$combs) $combs = array();
- $byAttr = array();
- foreach ($combs as $c) {
- $ipa = (int)$c['id_product_attribute'];
- if (!isset($byAttr[$ipa])) $byAttr[$ipa] = array('attrs'=>array(), 'ipa'=>$ipa);
- $gname = isset($c['group_name']) ? mb_strtolower($c['group_name']) : '';
- $aname = isset($c['attribute_name']) ? $c['attribute_name'] : '';
- $byAttr[$ipa]['attrs'][] = array('group'=>$gname, 'name'=>$aname);
- }
- $preferSize = array();
- foreach ($byAttr as $ipa => $data) {
- foreach ($data['attrs'] as $a) {
- if (preg_match('/(size|размер)/iu', $a['group'])) { $preferSize[$ipa] = true; break; }
- }
- }
- $rows = array();
- foreach ($byAttr as $ipa => $data) {
- $qty = (int)StockAvailable::getQuantityAvailableByProduct($id_product, $ipa, (int)Context::getContext()->shop->id);
- $sizeLabel = null;
- if (!empty($preferSize[$ipa])) {
- foreach ($data['attrs'] as $a) {
- if (preg_match('/(size|размер)/iu', $a['group'])) { $sizeLabel = $a['name']; break; }
- }
- }
- if ($sizeLabel === null) {
- $parts = array(); foreach ($data['attrs'] as $a) { $parts[] = $a['name']; }
- $sizeLabel = implode(' / ', $parts);
- }
- $rows[] = array('label'=>$sizeLabel, 'qty'=>$qty);
- }
- $withStock = array(); foreach ($rows as $r) { if ($r['qty'] > 0) $withStock[] = $r; }
- return $withStock ? $withStock : $rows;
- }
- // Общо наличност
- function ps_total_qty($id_product) {
- return (int)StockAvailable::getQuantityAvailableByProduct($id_product, 0, (int)Context::getContext()->shop->id);
- }
- // Цени
- function ps_prices($id_product) {
- $ctx = Context::getContext();
- $idShop = (int)$ctx->shop->id;
- $idCur = (int)$ctx->currency->id;
- // Базова цена от product_shop
- $q = new DbQuery();
- $q->select('ps.price')
- ->from('product_shop', 'ps')
- ->where('ps.id_product='.(int)$id_product)
- ->where('ps.id_shop='.(int)$idShop);
- $row = Db::getInstance()->getRow($q);
- $base = $row ? (float)$row['price'] : 0.0;
- // Търсене на специфични цени
- $now = date('Y-m-d H:i:s');
- $spQ = new DbQuery();
- $spQ->select('reduction, reduction_type')
- ->from('specific_price', 'sp')
- ->where('sp.id_product='.(int)$id_product)
- ->where('sp.id_shop IN (0,'.(int)$idShop.')')
- ->where('sp.id_currency IN (0,'.(int)$idCur.')')
- ->where('sp.id_country IN (0,'.(int)$ctx->country->id.')')
- ->where('sp.id_group = 0')
- ->where('sp.id_customer = 0')
- ->where('(sp.from = "0000-00-00 00:00:00" OR sp.from <= "'.$now.'")')
- ->where('(sp.to = "0000-00-00 00:00:00" OR sp.to >= "'.$now.'")')
- ->orderBy('sp.id_specific_price DESC');
- $sp = Db::getInstance()->getRow($spQ);
- $current = $base;
- if ($sp && (float)$sp['reduction'] > 0) {
- if ($sp['reduction_type'] === 'percentage') {
- $current = $base * (1.0 - (float)$sp['reduction']);
- } else { // amount
- $current = $base - (float)$sp['reduction'];
- }
- if ($current < 0) $current = 0.0;
- }
- // ДОБАВЕТЕ ТУК УМНОЖЕНИЕТО С 1.2 ЗА ДДС
- $base_with_vat = $base * 1.2;
- $current_with_vat = $current * 1.2;
- return array((float)$base, (float)$current);
- }
- // Форматиране на цена
- function ps_fmt_price($price) {
- // Добавяне на ДДС
- $price_with_vat = $price * 1.2;
- return Tools::displayPrice($price, ps_currency());
- }
- // Събиране на данните
- function ps_collect_data() {
- $products = ps_fetch_active_products(0, 100000);
- $langs = ps_get_active_languages();
- $out = array();
- foreach ($products as $p) {
- $id = (int)$p['id_product'];
- $ref = $p['reference'];
- $names = ps_fetch_names_for_product($id);
- list($imgUrl, $pUrl) = ps_image_and_product_links($id, $names, Context::getContext()->link);
- // Имена за всички активни езици
- $nameParts = array();
- foreach ($langs as $l) {
- $id_lang = (int)$l['id_lang'];
- $iso = $l['iso_code'];
- $nm = isset($names[$id_lang]['name']) ? $names[$id_lang]['name'] : '';
- if ($nm !== '') { $nameParts[] = '<p>['.$iso.'] '.$nm.'</p>'; }
- }
- $namesAll = implode("\n", $nameParts);
- // Размери + количества
- $sizes = ps_sizes_and_qty($id);
- $sizeParts = array();
- foreach ($sizes as $s) {
- $label = isset($s['label']) ? $s['label'] : '';
- $qty = isset($s['qty']) ? (int)$s['qty'] : 0;
- $sizeParts[] = $label . ': ' . $qty;
- }
- $sizesStr = implode("\n", $sizeParts);
- $totalQty = ps_total_qty($id);
- list($base, $curr) = ps_prices($id);
- // ДОБАВЕТЕ ДДС ТУК ДИРЕКТНО
- $base_with_vat = $base * 1.2;
- $curr_with_vat = $curr * 1.2;
- $out[] = array(
- 'id' => $id,
- 'reference' => $ref,
- 'image' => $imgUrl,
- 'plink' => $pUrl,
- 'names' => $namesAll,
- 'sizes' => $sizesStr,
- 'total' => $totalQty,
- 'base' => ps_fmt_price($base * 1.2), //Това умножение по 1,2 се прави за да показва цената с ДДС
- 'current' => ps_fmt_price($curr * 1.2), //Това умножение по 1,2 се прави за да показва цената с ДДС
- 'has_disc' => ($curr < $base) ? 1 : 0,
- //'disc_percent' => ($base > 0 && $curr < $base) ? (int)round((($base - $curr) / $base) * 100) : 0,
- 'disc_percent' => ($base > 0 && $curr < $base)
- ? (int) round((($base - $curr) / $base) * 100)
- : 0,
- );
- }
- return $out;
- }
- // ---------- РЕЖИМИ (AJAX/EXPORT/VIEW) ----------
- if (isset($_GET['action']) && $_GET['action'] === 'data') {
- header('Content-Type: application/json; charset=utf-8');
- try {
- $data = ps_collect_data();
- echo json_encode(array('data'=>$data), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
- } catch (Exception $e) {
- echo json_encode(array('error'=>'Грешка при събиране на данни: ' . $e->getMessage()));
- }
- exit;
- }
- // Експорт към Excel (CSV UTF-8 BOM)
- if (isset($_GET['export']) && $_GET['export'] === '1') {
- try {
- $rows = ps_collect_data();
- $filename = 'products_export_' . date('Ymd_His') . '.csv';
- header('Content-Type: text/csv; charset=utf-8');
- header('Content-Disposition: attachment; filename="' . $filename . '"');
- echo "\xEF\xBB\xBF";
- $out = fopen('php://output', 'w');
- fputcsv($out, array(
- 'ID','Reference','Product URL','Image URL','Names (all languages)',
- 'Sizes (qty)','Total Qty','Base Price','Current Price','Discount %',
- ), ';');
- foreach ($rows as $r) {
- fputcsv($out, array(
- $r['id'],
- $r['reference'],
- $r['plink'],
- $r['image'],
- preg_replace("/\r?\n/", ' | ', $r['names']),
- preg_replace("/\r?\n/", ' | ', $r['sizes']),
- $r['total'],
- $r['base'],
- $r['current'],
- ($r['disc_percent'] > 0 ? ('-' . $r['disc_percent'] . '%') : ''),
- ), ';');
- }
- fclose($out);
- } catch (Exception $e) {
- header('Content-Type: text/plain; charset=utf-8');
- echo 'Грешка при експорт: ' . $e->getMessage();
- }
- exit;
- }
- ?>
- <!doctype html>
- <html lang="bg">
- <head>
- <meta charset="utf-8">
- <title>Активни продукти – репорт</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <!-- Bootstrap 5 -->
- <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
- <!-- DataTables (Bootstrap 5 theme) -->
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/dataTables.bootstrap5.min.css">
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/responsive.bootstrap5.min.css">
- <style>
- body { background:#f8f9fa; padding-top: 20px; }
- .price-current { font-weight: 600; }
- .price-base { text-decoration: line-through; opacity: .7; margin-right:.5rem; }
- td pre { white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: .85rem; margin: 0; }
- .thumb { width:56px; height:56px; object-fit:cover; border-radius:.5rem; }
- .card { border: none; box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); }
- .table td { vertical-align: middle; }
- </style>
- </head>
- <body>
- <div class="container-fluid py-4">
- <div class="d-flex align-items-center justify-content-between mb-4">
- <h1 class="h3 mb-0 text-primary">Активни продукти / <a href="/check-products.php" class="btn btn-warning" >Неактивни с наличност за продажба!</a></h1>
- <div class="d-flex gap-2">
- <a href="?export=1" class="btn btn-success">
- 📊 Експорт в Excel (CSV)
- </a>
- </div>
- </div>
- <!-- Лоудър -->
- <div id="loader" class="card shadow-sm mb-4">
- <div class="card-body d-flex align-items-center gap-3 py-4">
- <div class="spinner-border text-primary" role="status" aria-hidden="true"></div>
- <div>
- <div class="fw-semibold">Зареждане на данни…</div>
- <div class="text-muted small">Може да отнеме малко време при много продукти.</div>
- </div>
- </div>
- </div>
- <div class="card shadow-sm">
- <div class="card-body p-0">
- <table id="products" class="table table-striped table-hover align-middle" style="width:100%">
- <thead class="table-light">
- <tr>
- <th width="80">ID</th>
- <th width="100">Ref</th>
- <th width="70">Снимка</th>
- <th>Име (всички езици)</th>
- <th>Размери (наличности)</th>
- <th width="120">Общо наличност</th>
- <th width="150">Цена (с ДДС)</th>
- </tr>
- </thead>
- <tbody></tbody>
- </table>
- </div>
- </div>
- </div>
- <!-- JS -->
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
- <!-- DataTables -->
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/js/jquery.dataTables.min.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/js/dataTables.bootstrap5.min.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/js/dataTables.responsive.min.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/js/responsive.bootstrap5.min.js"></script>
- <script>
- (function() {
- const tableEl = $('#products');
- const loaderEl = $('#loader');
- fetch('?action=data')
- .then(r => {
- if (!r.ok) throw new Error('Network error: ' + r.status);
- return r.json();
- })
- .then(json => {
- if (json.error) {
- throw new Error(json.error);
- }
- loaderEl.remove();
- const data = json.data || [];
- if (data.length === 0) {
- loaderEl.find('.card-body').html(
- '<div class="alert alert-warning mb-0">Няма активни продукти за показване.</div>'
- );
- return;
- }
- const table = tableEl.DataTable({
- data: data,
- responsive: true,
- deferRender: true,
- pageLength: 500,
- lengthMenu: [100, 250, 500, 1000],
- order: [[0, 'desc']],
- columns: [
- {
- data: 'id',
- className: 'fw-bold text-center'
- },
- {
- data: 'reference',
- defaultContent: '-',
- className: 'text-center'
- },
- {
- data: null,
- orderable: false,
- searchable: false,
- className: 'text-center',
- render: function(row) {
- if (!row.image) return '<span class="text-muted">Няма</span>';
- const href = row.plink || '#';
- return '<a href="' + href + '" target="_blank" rel="noopener" title="Отвори продукта">' +
- '<img src="' + row.image + '" class="thumb" alt="thumb" onerror="this.style.display=\'none\'">' +
- '</a>';
- }
- },
- {
- data: 'names',
- render: function(data) {
- return data ? '<pre>' + data.replace(/\n/g,'\n') + '</pre>' : '<span class="text-muted">Няма име</span>';
- }
- },
- {
- data: 'sizes',
- render: function(data) {
- return data ? '<pre class="text-small">' + data.replace(/\n/g,'\n') + '</pre>' : '<span class="text-muted">Няма варианти</span>';
- }
- },
- {
- data: 'total',
- className: 'text-center fw-bold',
- render: function(data) {
- const qty = parseInt(data) || 0;
- const cls = qty > 0 ? 'text-success' : 'text-danger';
- return '<span class="' + cls + '">' + qty + '</span>';
- }
- },
- {
- data: null,
- className: 'text-end',
- render: function(row) {
- if (row.has_disc && row.base !== row.current) {
- var badge = (typeof row.disc_percent !== 'undefined' && row.disc_percent > 0)
- ? ' <span class="badge bg-success ms-2" style="padding:10px; width:100px; font-size:20pt;">-' + row.disc_percent + '%</span>'
- : '';
- return '<div>' +
- '<span class="price-base text-muted text-decoration-line-through me-2">' + row.base + '</span>' +
- '<span class="price-current text-danger" style="font-size:25pt;font-weight:bold;">' + row.current + '</span>' +
- badge +
- '</div>';
- }
- return '<span class="price-current">' + row.current + '</span>';
- }
- }
- ],
- dom: '<"row mb-3"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>><"row"<"col-sm-12"tr>><"row mt-3"<"col-sm-12 col-md-5"i><"col-sm-12 col-md-7"p>>',
- language: {
- url: 'https://cdn.datatables.net/plug-ins/1.13.10/i18n/bg.json',
- search: "Търсене:",
- lengthMenu: "Покажи _MENU_ записа",
- info: "Показване на _START_ до _END_ от _TOTAL_ записа",
- paginate: {
- first: "Първа",
- last: "Последна",
- next: "Следваща",
- previous: "Предишна"
- }
- }
- });
- })
- .catch(err => {
- console.error('Error:', err);
- loaderEl.find('.card-body').html(
- '<div class="alert alert-danger mb-0">Грешка при зареждане на данните: ' + err.message + '</div>'
- );
- });
- })();
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment