Adding pagination to the Prestashop Order History

In this article we will see how to add pagination to the order history in Prestashop, so that our customers can more easily reach their past orders information.

Download Project Files

Action Plan

Before starting with any modification, it’s important to have a clear action plan. There are three files we need to amend/extend, in order to paginate the customer order history:

  • Order.php (the Order class)
  • HistoryController.php (the controller ran in the order history page)
  • history.tpl (the template used to display orders)

In terms of hard modifications, we will only be directly editing the history.tpl file (although you can also decide to create a new theme instead of modifying the base one). For the Order class and History controllers we will, as I always recommend, place a couple of overrides. To extend the default functionality. Therefore, if you don’t know much about overrides, have a look at the Official Prestashop Documentation on overrides.

1. Extending the Order Class

Create a new file inside override/classes/Order (or simply override/classes/) and name it Order.php, then paste the following base override code inside php tags:

class Order extends OrderCore
{
}

The method we need to override is Order::GetCustomerOrders, and looks like the following in Prestashop 1.6.0.8:


	public static function getCustomerOrders($id_customer, $showHiddenStatus = false, Context $context = null)
	{
		if (!$context)
			$context = Context::getContext();

		$res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
		SELECT o.*, (SELECT SUM(od.`product_quantity`) FROM `'._DB_PREFIX_.'order_detail` od WHERE od.`id_order` = o.`id_order`) nb_products
		FROM `'._DB_PREFIX_.'orders` o
		WHERE o.`id_customer` = '.(int)$id_customer.'
		GROUP BY o.`id_order`
		ORDER BY o.`date_add` DESC');
		if (!$res)
			return array();

		foreach ($res as $key => $val)
		{
			$res2 = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
				SELECT os.`id_order_state`, osl.`name` AS order_state, os.`invoice`, os.`color` as order_state_color
				FROM `'._DB_PREFIX_.'order_history` oh
				LEFT JOIN `'._DB_PREFIX_.'order_state` os ON (os.`id_order_state` = oh.`id_order_state`)
				INNER JOIN `'._DB_PREFIX_.'order_state_lang` osl ON (os.`id_order_state` = osl.`id_order_state` AND osl.`id_lang` = '.(int)$context->language->id.')
			WHERE oh.`id_order` = '.(int)($val['id_order']).(!$showHiddenStatus ? ' AND os.`hidden` != 1' : '').'
				ORDER BY oh.`date_add` DESC, oh.`id_order_history` DESC
			LIMIT 1');

			if ($res2)
				$res[$key] = array_merge($res[$key], $res2[0]);

		}
		return $res;
	}

As you can see, there is no reference whatsoever to any kind of pagination. That’s what we need, so copy the whole method inside the override, then start by adding two extra parameters to the method itself


	public static function getCustomerOrders($id_customer, $showHiddenStatus = false, Context $context = null, $p = 1, $n = 5)
	{
		...
	}

$p will hold the page number, while $n will be the number of orders per page we want to grab. We are also setting defaults as the base method doesn’t have such values, and this would create issues with standard calls.

The next step is to take care of 0 values, just in case, so if $p is 0, for any reason, we set it’s value back to one.

	public static function getCustomerOrders($id_customer, $showHiddenStatus = false, Context $context = null, $p = 1, $n = 5)
	{
		if (!$context)
			$context = Context::getContext();
		if ($p < 1) $p = 1;
		
		$res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
		SELECT o.*, (SELECT SUM(od.`product_quantity`) FROM `'._DB_PREFIX_.'order_detail` od WHERE od.`id_order` = o.`id_order`) nb_products
		FROM `'._DB_PREFIX_.'orders` o
		WHERE o.`id_customer` = '.(int)$id_customer.'
		GROUP BY o.`id_order`
		ORDER BY o.`date_add` DESC');
		if (!$res)
			return array();

		...

	}

Then, the real deal, let’s limit the $res query results depending on the chosen page:

	public static function getCustomerOrders($id_customer, $showHiddenStatus = false, Context $context = null, $p = 1, $n = 5)
	{
		if (!$context)
			$context = Context::getContext();
		if ($p < 1) $p = 1;
		
		$res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
		SELECT o.*, (SELECT SUM(od.`product_quantity`) FROM `'._DB_PREFIX_.'order_detail` od WHERE od.`id_order` = o.`id_order`) nb_products
		FROM `'._DB_PREFIX_.'orders` o
		WHERE o.`id_customer` = '.(int)$id_customer.'
		GROUP BY o.`id_order`
		ORDER BY o.`date_add` DESC LIMIT '.(((int)$p - 1) * (int)$n).','.(int)$n );
		if (!$res)
			return array();

		...

	}

The important part here is the LIMIT clause:

...
ORDER BY o.`date_add` DESC LIMIT '.(((int)$p - 1) * (int)$n).','.(int)$n );
...

Which is simply a 1:1 copy of the category pagination code, as it multiplies the current page, minus one, for the number or items per page, and grabs as many of them as specified by the number itself.

This is as far as we need to go for the current override. The final code will look like:


class Order extends OrderCore
{
	
	public static function getCustomerOrders($id_customer, $showHiddenStatus = false, Context $context = null, $p = 1, $n = 5)
	{
		if (!$context)
			$context = Context::getContext();

		if ($p < 1) $p = 1;
		
		$res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
		SELECT o.*, (SELECT SUM(od.`product_quantity`) FROM `'._DB_PREFIX_.'order_detail` od WHERE od.`id_order` = o.`id_order`) nb_products
		FROM `'._DB_PREFIX_.'orders` o
		WHERE o.`id_customer` = '.(int)$id_customer.'
		GROUP BY o.`id_order`
		ORDER BY o.`date_add` DESC LIMIT '.(((int)$p - 1) * (int)$n).','.(int)$n );
		if (!$res)
			return array();


		foreach ($res as $key => $val)
		{
			$res2 = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
				SELECT os.`id_order_state`, osl.`name` AS order_state, os.`invoice`, os.`color` as order_state_color
				FROM `'._DB_PREFIX_.'order_history` oh
				LEFT JOIN `'._DB_PREFIX_.'order_state` os ON (os.`id_order_state` = oh.`id_order_state`)
				INNER JOIN `'._DB_PREFIX_.'order_state_lang` osl ON (os.`id_order_state` = osl.`id_order_state` AND osl.`id_lang` = '.(int)$context->language->id.')
			WHERE oh.`id_order` = '.(int)($val['id_order']).(!$showHiddenStatus ? ' AND os.`hidden` != 1' : '').'
				ORDER BY oh.`date_add` DESC, oh.`id_order_history` DESC
			LIMIT 1');

			if ($res2)
				$res[$key] = array_merge($res[$key], $res2[0]);

		}
		return $res;
	}
}

2. Extending the order History Override

This part is the trickiest. We need to add a fair amount of code to this controller, so that all pagination variables are set in the template too. As a first step, let’s create the override itself in override/controllers/front/, declare the base class information, and then paste in the original initContent() method:


class HistoryController extends HistoryControllerCore
{
	
	public function initContent()
	{
		parent::initContent();

		if ($orders = Order::getCustomerOrders($this->context->customer->id))
			foreach ($orders as &$order)
			{
				$myOrder = new Order((int)$order['id_order']);
				if (Validate::isLoadedObject($myOrder))
					$order['virtual'] = $myOrder->isVirtual(false);
			}
		$this->context->smarty->assign(array(
			'orders' => $orders,
			'invoiceAllowed' => (int)(Configuration::get('PS_INVOICE')),
			'reorderingAllowed' => !(int)(Configuration::get('PS_DISALLOW_HISTORY_REORDERING')),
			'slowValidation' => Tools::isSubmit('slowvalidation')
		));

		$this->setTemplate(_PS_THEME_DIR_.'history.tpl');
	}

}

The first thing we want to do is take advantage of our new pagination variables in the getCustomerOrders method:


class HistoryController extends HistoryControllerCore
{
	
	public function initContent()
	{
		parent::initContent();

		$p = Tools::getValue('p');
		$n = 5;


		if ($orders = Order::getCustomerOrders($this->context->customer->id, false, null, $p, $n))
			foreach ($orders as &$order)
			{
				$myOrder = new Order((int)$order['id_order']);
				if (Validate::isLoadedObject($myOrder))
					$order['virtual'] = $myOrder->isVirtual(false);
			}
		...
	}

}

Notice we are hard setting the number of orders per page to 5, using the $n variable.

At this point, we can already test out our modifications. Reach the cache/ folder and erase class_index.php. Some recent versions have a small bug that prevents it from being created. If you run into this issue, try this autoload fix.

Then, reach the order history page and try various settings for $n and $p to see if it works. To test pagination you can also add ?p=3 or any number to the url. Of course, you will get no orders if you go above the total order number with the page.

Setting up pagination in the controller

As we cannot ask customers to input a page number in the url, we need to prepare the template to receive pagination variables. RIght after the previous code, add:

		// to paginate, get the total order number 
		$nb_orders = Order::getCustomerNbOrders($this->context->customer->id);
		$pages_nb = ceil($nb_orders / (int)$n);

		$range = 2; /* how many pages around page selected */
		$start = (int)($p - $range);
		if ($start < 1)
			$start = 1;
		$stop = (int)($p + $range);
		if ($stop > $pages_nb)
			$stop = (int)$pages_nb;

		if (!$p) $p = 1;


		$this->context->smarty->assign(array(
			'pages_nb' => $pages_nb,
			'prev_p' => $p != 1 ? $p - 1 : 1,
			'next_p' => (int)$p + 1  > $pages_nb ? $pages_nb : $p + 1,
			'requestPage' => $this->context->link->getPageLink('history'),
			'p' => $p,
			'n' => $n,
			'range' => $range,
			'start' => $start,
			'stop' => $stop,
		));


Explanation: we are getting some of these pagination variables directly from the default pagination system. We need to know the total number of orders, the number of pages, and the range around the current page for which we can show links (so we are not displaying ALL available pages, but we use dots). Then, we simply assign all these (and a couple more, like the request page, the previous and next numbers.

The final override:



/**
* History Controller Override to allow pagination
*/

class HistoryController extends HistoryControllerCore
{
	
	public function initContent()
	{
		parent::initContent();

		$p = Tools::getValue('p');
		$n = 10;


		if ($orders = Order::getCustomerOrders($this->context->customer->id, false, null, $p, $n))
			foreach ($orders as &$order)
			{
				$myOrder = new Order((int)$order['id_order']);
				if (Validate::isLoadedObject($myOrder))
					$order['virtual'] = $myOrder->isVirtual(false);
			}


		// to paginate, get the total order number 
		$nb_orders = Order::getCustomerNbOrders($this->context->customer->id);
		$pages_nb = ceil($nb_orders / (int)$n);

		$range = 2; /* how many pages around page selected */
		$start = (int)($p - $range);
		if ($start < 1)
			$start = 1;
		$stop = (int)($p + $range);
		if ($stop > $pages_nb)
			$stop = (int)$pages_nb;

		if (!$p) $p = 1;

		$this->context->smarty->assign(array(
			'pages_nb' => $pages_nb,
			'prev_p' => $p != 1 ? $p - 1 : 1,
			'next_p' => (int)$p + 1  > $pages_nb ? $pages_nb : $p + 1,
			'requestPage' => $this->context->link->getPageLink('history'),
			'p' => $p,
			'n' => $n,
			'range' => $range,
			'start' => $start,
			'stop' => $stop,
		));

		$this->context->smarty->assign(array(
			'orders' => $orders,
			'invoiceAllowed' => (int)(Configuration::get('PS_INVOICE')),
			'reorderingAllowed' => !(int)(Configuration::get('PS_DISALLOW_HISTORY_REORDERING')),
			'slowValidation' => Tools::isSubmit('slowvalidation')
		));

		$this->setTemplate(_PS_THEME_DIR_.'history.tpl');
	}

}

Adding pagination to the template

This will be the easiest part. Unless your version is really different (major release) from the one used in this tutorial (1.6.0.8), you should be able to get away with this by simply copying and pasting the provided snippet. First, open up history.tpl, and place the cursor right before

<div id="block-order-detail" class="unvisible">&nbsp;</div>

But after the closing table tag. Then simply paste in the following:



		{if $start!=$stop}
			<ul class="pagination">
				{if $p != 1 && $p}
					{assign var='p_previous' value=$p-1}
					<li id="pagination_previous{if isset($paginationId)}_{$paginationId}{/if}" class="pagination_previous">
						<a rel="nofollow" href="{$requestPage}?p={$prev_p}">
							<i class="icon-chevron-left"></i> <b>{l s='Previous'}</b>
						</a>
					</li>
				{else}
					<li id="pagination_previous{if isset($paginationId)}_{$paginationId}{/if}" class="disabled pagination_previous">
						<span>
							<i class="icon-chevron-left"></i> <b>{l s='Previous'}</b>
						</span>
					</li>
				{/if}
				{if $start==3}
					<li>
						<a rel="nofollow"  href="{$link->goPage($requestPage, 1)}">
							<span>1</span>
						</a>
					</li>
					<li>
						<a rel="nofollow"  href="{$link->goPage($requestPage, 2)}">
							<span>2</span>
						</a>
					</li>
				{/if}
				{if $start==2}
					<li>
						<a rel="nofollow"  href="{$link->goPage($requestPage, 1)}">
							<span>1</span>
						</a>
					</li>
				{/if}
				{if $start>3}
					<li>
						<a rel="nofollow"  href="{$link->goPage($requestPage, 1)}">
							<span>1</span>
						</a>
					</li>
					<li class="truncate">
						<span>
							<span>...</span>
						</span>
					</li>
				{/if}
				{section name=pagination start=$start loop=$stop+1 step=1}
					{if $p == $smarty.section.pagination.index}
						<li class="active current">
							<span>
								<span>{$p|escape:'html':'UTF-8'}</span>
							</span>
						</li>
					{else}
						<li>
							<a rel="nofollow" href="{$link->goPage($requestPage, $smarty.section.pagination.index)}">
								<span>{$smarty.section.pagination.index|escape:'html':'UTF-8'}</span>
							</a>
						</li>
					{/if}
				{/section}
				{if $pages_nb>$stop+2}
					<li class="truncate">
						<span>
							<span>...</span>
						</span>
					</li>
					<li>
						<a href="{$link->goPage($requestPage, $pages_nb)}">
							<span>{$pages_nb|intval}</span>
						</a>
					</li>
				{/if}
				{if $pages_nb==$stop+1}
					<li>
						<a href="{$link->goPage($requestPage, $pages_nb)}">
							<span>{$pages_nb|intval}</span>
						</a>
					</li>
				{/if}
				{if $pages_nb==$stop+2}
					<li>
						<a href="{$link->goPage($requestPage, $pages_nb-1)}">
							<span>{$pages_nb-1|intval}</span>
						</a>
					</li>
					<li>
						<a href="{$link->goPage($requestPage, $pages_nb)}">
							<span>{$pages_nb|intval}</span>
						</a>
					</li>
				{/if}
				{if $pages_nb > 1 AND $p != $pages_nb}
					{assign var='p_next' value=$p+1}
					<li id="pagination_next{if isset($paginationId)}_{$paginationId}{/if}" class="pagination_next">
						<a rel="nofollow" href="{$requestPage}?p={$next_p}">
							<b>{l s='Next'}</b> <i class="icon-chevron-right"></i>
						</a>
					</li>
				{else}
					<li id="pagination_next{if isset($paginationId)}_{$paginationId}{/if}" class="disabled pagination_next">
						<span>
							<b>{l s='Next'}</b> <i class="icon-chevron-right"></i>
						</span>
					</li>
				{/if}
			</ul>
		{/if}



And save. It’s a bit troublesome to explain everything going on here. It’s basically a 1:1 copy from the default pagination, where I removed most of the useless variables.

Make sure the order number is more than the orders per page you setup in the controller, or pagination will not show up!

You like the tuts and want to say "thank you"? Well, you can always feel free to donate:

  • KevinNash

    Hi,
    There is a problem with your order.php override, it causes the customer order number to be wrong when we go in the customer detail.

    Can you tahe a look and correct it please ?

  • Jose Ramon Guzman Sobrino

    This change also affects the back office and does pagination, what would change?

  • raknjak

    Great and still works for me on 1.6.1.4.
    For my multistore I had to change the getCustomerNbOrders() method as I want to see all orders in all shops:

    public static function getCustomerNbOrders($id_customer)
    {
    $sql = ‘SELECT COUNT(`id_order`) AS nb
    FROM `’._DB_PREFIX_.’orders`
    WHERE `id_customer` = ‘.(int)$id_customer;
    $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql);

    return isset($result[‘nb’]) ? $result[‘nb’] : 0;
    }

  • Chandra Wangsa S

    thank you so much.
    works perfectly.

  • lordbdp

    Are there some modifications to do for PS 1.5.6.2 ?

You like the tuts and want to say "thank you"? Well, you can always feel free to donate: