Tutorial for Prestashop to decorate a controller – How to add paid and outstanding amount in order page
Tutorial for Prestashop to decorate a controller
- A better way to modify the controller is to decorate it. With such a method, we can extend the module with most of the original behaviour and customise the specific parts of the controller.
- In this tutorial we are going to build up a module which adds paid and outstanding amounts in the Order page without modifying any src files directly. Shown as following:
- Step 1, create a composer.json file in your module with the following code:
{
"name" : "yourname/yourmodule",
"autoload" : {
"psr-4" : {"Namespace\\" : "src/"},
"classmap": ["yourmodule.php"],
"config": {"prepend-autoloader" : false},
"type" : "prestashop-module"
}
}
Our example (modules/ordertotal/composer.json):
{
"name": "genkiware/ordertotal",
"license": "AFL-3.0",
"autoload": {
"psr-4": { "ordertotal\\": "src/" },
"classmap": ["ordertotal.php"],
"config": { "prepend-autoloader": false },
"type": "prestashop-module"
}
}
- Step 2, we need to create a basic module first, which you may generate by this link https://validator.prestashop.com/generator and install it to your prestashop module. (just choose a random hook at the last step, you may delete the related code after installing it.)
- Step 3, create a services.yml in modules/your-module/config/ file, with the following code:
- Services:
- custom_controller:
- class: Namespace\Controller\Admin\DemoController
- decorates:
- PrestaShopBundle\Controller\Admin\Improve\Design\CmsPageController
- arguments: ['@custom_controller.inner']
Our example ( modules/ordertotal/config/services.yml ):
services:
custom_controller:
class: ordertotal\Controller\Admin\DemoController
decorates: PrestaShopBundle\Controller\Admin\Sell\Order\OrderController
arguments: ['@custom_controller.inner']
- Step 4, add the following code to yourmodule.php
declare(strict_types=1);
// Needed for installing process
require_once __DIR__ . '/vendor/autoload.php';
class YourModule extends Module
{
Our example (modules/ordertotal/ordertotal.php):
declare(strict_types=1);
// Needed for installing process
require_once __DIR__ . '/vendor/autoload.php';
if (!defined('_PS_VERSION_')) {
exit;
}
class Ordertotal extends Module
{
After creating those files, then run
$ composer dumpautoload
in your module’s terminal.
- Step 5, create a demo controller in src/Controller/Admin/DemoController.php, which extends the original controller. Here is the “basic” version of code:
declare(strict_types=1);
namespace Namespace\Controller\Admin;
// The path that you are going to use
use ………………
use ………………
use ………………
...
use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController;
use Symfony\Component\HttpFoundation\Request;
class DemoController extends FrameworkBundleAdminController
{
private $decoratedController;
public function __construct(TheOriginalController $decoratedController)
{
$this->decoratedController = $decoratedController;
}
public function indexAction(ABC $temp1, DEF $temp2,.....)
{
return $this->decoratedController->indexAction($temp1, $temp2, ………………);
}
}
So the parameters will be passed to the function in the new controller instead of the original one. We can call and return the original function by using $this->decoratedController->. It means that nothing has changed. If we want modification, just add the code to the function in the new controller (with or without calling $this-decoratedController, depending on your objective).
Our example:
<?php
declare(strict_types=1);
namespace ordertotal\Controller\Admin;
use PrestaShopLogger;
use PrestaShopBundle\Controller\Admin\Sell\Order\OrderController;
use PrestaShopBundle\Controller\Admin\Sell\Order\ActionsBarButtonsCollection;
use Currency;
use Exception;
use InvalidArgumentException;
use PrestaShop\Decimal\Number;
use PrestaShop\PrestaShop\Core\Domain\Cart\Query\GetCartForOrderCreation;
use PrestaShop\PrestaShop\Core\Domain\CartRule\Exception\InvalidCartRuleDiscountValueException;
use PrestaShop\PrestaShop\Core\Domain\CustomerMessage\Command\AddOrderCustomerMessageCommand;
use PrestaShop\PrestaShop\Core\Domain\CustomerMessage\Exception\CannotSendEmailException;
use PrestaShop\PrestaShop\Core\Domain\CustomerMessage\Exception\CustomerMessageConstraintException;
use PrestaShop\PrestaShop\Core\Domain\Order\Command\AddCartRuleToOrderCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\Command\AddOrderFromBackOfficeCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\Command\BulkChangeOrderStatusCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\Command\ChangeOrderCurrencyCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\Command\ChangeOrderDeliveryAddressCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\Command\ChangeOrderInvoiceAddressCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\Command\DeleteCartRuleFromOrderCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\Command\DuplicateOrderCartCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\Command\ResendOrderEmailCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\Command\SendProcessOrderEmailCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\Command\UpdateOrderShippingDetailsCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\Command\UpdateOrderStatusCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\Exception\CannotEditDeliveredOrderProductException;
use PrestaShop\PrestaShop\Core\Domain\Order\Exception\CannotFindProductInOrderException;
use PrestaShop\PrestaShop\Core\Domain\Order\Exception\ChangeOrderStatusException;
use PrestaShop\PrestaShop\Core\Domain\Order\Exception\DuplicateProductInOrderException;
use PrestaShop\PrestaShop\Core\Domain\Order\Exception\DuplicateProductInOrderInvoiceException;
use PrestaShop\PrestaShop\Core\Domain\Order\Exception\InvalidAmountException;
use PrestaShop\PrestaShop\Core\Domain\Order\Exception\InvalidCancelProductException;
use PrestaShop\PrestaShop\Core\Domain\Order\Exception\InvalidOrderStateException;
use PrestaShop\PrestaShop\Core\Domain\Order\Exception\InvalidProductQuantityException;
use PrestaShop\PrestaShop\Core\Domain\Order\Exception\NegativePaymentAmountException;
use PrestaShop\PrestaShop\Core\Domain\Order\Exception\OrderConstraintException;
use PrestaShop\PrestaShop\Core\Domain\Order\Exception\OrderEmailSendException;
use PrestaShop\PrestaShop\Core\Domain\Order\Exception\OrderException;
use PrestaShop\PrestaShop\Core\Domain\Order\Exception\OrderNotFoundException;
use PrestaShop\PrestaShop\Core\Domain\Order\Exception\TransistEmailSendingException;
use PrestaShop\PrestaShop\Core\Domain\Order\Invoice\Command\GenerateInvoiceCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\Invoice\Command\UpdateInvoiceNoteCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\OrderConstraints;
use PrestaShop\PrestaShop\Core\Domain\Order\Payment\Command\AddPaymentCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\Product\Command\AddProductToOrderCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\Product\Command\DeleteProductFromOrderCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\Product\Command\UpdateProductInOrderCommand;
use PrestaShop\PrestaShop\Core\Domain\Order\Query\GetOrderForViewing;
use PrestaShop\PrestaShop\Core\Domain\Order\Query\GetOrderPreview;
use PrestaShop\PrestaShop\Core\Domain\Order\QueryResult\OrderForViewing;
use PrestaShop\PrestaShop\Core\Domain\Order\QueryResult\OrderPreview;
use PrestaShop\PrestaShop\Core\Domain\Order\QueryResult\OrderProductForViewing;
use PrestaShop\PrestaShop\Core\Domain\Order\ValueObject\OrderId;
use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductOutOfStockException;
use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductSearchEmptyPhraseException;
use PrestaShop\PrestaShop\Core\Domain\Product\Query\SearchProducts;
use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\FoundProduct;
use PrestaShop\PrestaShop\Core\Domain\ValueObject\QuerySorting;
use PrestaShop\PrestaShop\Core\Form\ConfigurableFormChoiceProviderInterface;
use PrestaShop\PrestaShop\Core\Grid\Definition\Factory\OrderGridDefinitionFactory;
use PrestaShop\PrestaShop\Core\Multistore\MultistoreContextCheckerInterface;
use PrestaShop\PrestaShop\Core\Order\OrderSiblingProviderInterface;
use PrestaShop\PrestaShop\Core\Search\Filters\OrderFilters;
use PrestaShopBundle\Component\CsvResponse;
use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController;
use PrestaShopBundle\Exception\InvalidModuleException;
use PrestaShopBundle\Form\Admin\Sell\Customer\PrivateNoteType;
use PrestaShopBundle\Form\Admin\Sell\Order\AddOrderCartRuleType;
use PrestaShopBundle\Form\Admin\Sell\Order\AddProductRowType;
use PrestaShopBundle\Form\Admin\Sell\Order\CartSummaryType;
use PrestaShopBundle\Form\Admin\Sell\Order\ChangeOrderAddressType;
use PrestaShopBundle\Form\Admin\Sell\Order\ChangeOrderCurrencyType;
use PrestaShopBundle\Form\Admin\Sell\Order\ChangeOrdersStatusType;
use PrestaShopBundle\Form\Admin\Sell\Order\EditProductRowType;
use PrestaShopBundle\Form\Admin\Sell\Order\OrderMessageType;
use PrestaShopBundle\Form\Admin\Sell\Order\OrderPaymentType;
use PrestaShopBundle\Form\Admin\Sell\Order\UpdateOrderShippingType;
use PrestaShopBundle\Form\Admin\Sell\Order\UpdateOrderStatusType;
use PrestaShopBundle\Security\Annotation\AdminSecurity;
use PrestaShopBundle\Security\Annotation\DemoRestricted;
use PrestaShopBundle\Service\Grid\ResponseBuilder;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class DemoController extends FrameworkBundleAdminController
{
const DEFAULT_PRODUCTS_NUMBER = 8;
const PRODUCTS_PAGINATION_OPTIONS = [8, 20, 50, 100];
// used for decorating controller
private $decoratedController;
public function __construct(OrderController $decoratedController)
{
$this->decoratedController = $decoratedController;
}
public function indexAction(Request $request, OrderFilters $filters)
{
return $this->decoratedController->indexAction($request, $filters);
}
public function placeAction(Request $request)
{
return $this->decoratedController->placeAction($request);
}
public function createAction(Request $request)
{
return $this->decoratedController->createAction($request);
}
public function searchAction(Request $request)
{
return $this->decoratedController->searchAction($request);
}
public function generateInvoicePdfAction($orderId)
{
return $this->decoratedController->generateInvoicePdfAction($orderId);
}
public function generateDeliverySlipPdfAction($orderId)
{
return $this->decoratedController->generateDeliverySlipPdfAction($orderId);
}
public function changeOrdersStatusAction(Request $request)
{
return $this->decoratedController->changeOrdersStatusAction($request);
}
public function exportAction(OrderFilters $filters)
{
return $this->decoratedController->exportAction($filters);
}
public function viewAction(int $orderId, Request $request): Response
{
try {
/** @var OrderForViewing $orderForViewing */
$orderForViewing = $this->getQueryBus()->handle(new GetOrderForViewing($orderId, QuerySorting::DESC));
} catch (OrderException $e) {
$this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages($e)));
return $this->redirectToRoute('admin_orders_index');
}
$formFactory = $this->get('form.factory');
$updateOrderStatusForm = $formFactory->createNamed(
'update_order_status',
UpdateOrderStatusType::class, [
'new_order_status_id' => $orderForViewing->getHistory()->getCurrentOrderStatusId(),
]
);
$updateOrderStatusActionBarForm = $formFactory->createNamed(
'update_order_status_action_bar',
UpdateOrderStatusType::class, [
'new_order_status_id' => $orderForViewing->getHistory()->getCurrentOrderStatusId(),
]
);
$addOrderCartRuleForm = $this->createForm(AddOrderCartRuleType::class, [], [
'order_id' => $orderId,
]);
$addOrderPaymentForm = $this->createForm(OrderPaymentType::class, [
'id_currency' => $orderForViewing->getCurrencyId(),
], [
'id_order' => $orderId,
]);
$orderMessageForm = $this->createForm(OrderMessageType::class, [
'lang_id' => $orderForViewing->getCustomer()->getLanguageId(),
], [
'action' => $this->generateUrl('admin_orders_send_message', ['orderId' => $orderId]),
]);
$orderMessageForm->handleRequest($request);
$changeOrderCurrencyForm = $this->createForm(ChangeOrderCurrencyType::class, [], [
'current_currency_id' => $orderForViewing->getCurrencyId(),
]);
$changeOrderAddressForm = null;
$privateNoteForm = null;
if (null !== $orderForViewing->getCustomer() && $orderForViewing->getCustomer()->getId() !== 0) {
$changeOrderAddressForm = $this->createForm(ChangeOrderAddressType::class, [], [
'customer_id' => $orderForViewing->getCustomer()->getId(),
]);
$privateNoteForm = $this->createForm(PrivateNoteType::class, [
'note' => $orderForViewing->getCustomer()->getPrivateNote(),
]);
}
$updateOrderShippingForm = $this->createForm(UpdateOrderShippingType::class, [
'new_carrier_id' => $orderForViewing->getCarrierId(),
], [
'order_id' => $orderId,
]);
$currencyDataProvider = $this->container->get('prestashop.adapter.data_provider.currency');
$orderCurrency = $currencyDataProvider->getCurrencyById($orderForViewing->getCurrencyId());
$addProductRowForm = $this->createForm(AddProductRowType::class, [], [
'order_id' => $orderId,
'currency_id' => $orderForViewing->getCurrencyId(),
'symbol' => $orderCurrency->symbol,
]);
$editProductRowForm = $this->createForm(EditProductRowType::class, [], [
'order_id' => $orderId,
'symbol' => $orderCurrency->symbol,
]);
$formBuilder = $this->get('prestashop.core.form.identifiable_object.builder.cancel_product_form_builder');
$backOfficeOrderButtons = new ActionsBarButtonsCollection();
try {
$this->dispatchHook(
'actionGetAdminOrderButtons', [
'controller' => $this,
'id_order' => $orderId,
'actions_bar_buttons_collection' => $backOfficeOrderButtons,
]);
$cancelProductForm = $formBuilder->getFormFor($orderId);
} catch (Exception $e) {
$this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages($e)));
return $this->redirectToRoute('admin_orders_index');
}
$this->handleOutOfStockProduct($orderForViewing);
$merchandiseReturnEnabled = (bool) $this->decoratedController->configuration->get('PS_ORDER_RETURN');
/** @var OrderSiblingProviderInterface $orderSiblingProvider */
$orderSiblingProvider = $this->get('prestashop.adapter.order.order_sibling_provider');
$paginationNum = (int) $this->decoratedController->configuration->get('PS_ORDER_PRODUCTS_NB_PER_PAGE', self::DEFAULT_PRODUCTS_NUMBER);
$paginationNumOptions = self::PRODUCTS_PAGINATION_OPTIONS;
if (!in_array($paginationNum, $paginationNumOptions)) {
$paginationNumOptions[] = $paginationNum;
}
sort($paginationNumOptions);
$metatitle = sprintf(
'%s %s %s',
$this->trans('Orders', 'Admin.Orderscustomers.Feature'),
$this->decoratedController->configuration->get('PS_NAVIGATION_PIPE', '>'),
$this->trans(
'Order %reference% from %firstname% %lastname%',
'Admin.Orderscustomers.Feature',
[
'%reference%' => $orderForViewing->getReference(),
'%firstname%' => $orderForViewing->getCustomer()->getFirstName(),
'%lastname%' => $orderForViewing->getCustomer()->getLastName(),
]
)
);
// start - edited
$paidAmount = 0;
$balanceAmount = 0;
$amountToPay = $orderForViewing->getPrices()->getTotalAmountFormatted();
$amountToPay = floatval(preg_replace('/[^\d\.]+/', '', $amountToPay));
$paymentArrayAmt = array_values((array)$orderForViewing->getPayments());
foreach($paymentArrayAmt[0] as $key){
$originvalue = $key->getAmount();
// is negative number
$neg = strpos((string)$originvalue, '-') !== false;
// remove everything except numbers and dot "."
$originvalue = preg_replace("/[^0-9\.]/", "", $originvalue);
// Set negative number
if( $neg ) {
$originvalue = '-' . $originvalue;
}
//convert string to float
$originvalue = (float) $originvalue;
// sum all order payment amount
$paidAmount += $originvalue;
}
// calculate from Total Amount substract Paid Amount
$balanceAmount = $amountToPay - $paidAmount;
// verify null value if true store default value 0 and set the price format
if (empty($balanceAmount)) {
$balanceAmount = $this->getContextLocale()->formatPrice(0, $orderCurrency->iso_code);
}else{
$balanceAmount = $this->getContextLocale()->formatPrice($balanceAmount, $orderCurrency->iso_code);
}
if (empty($paidAmount)) {
$paidAmount = $this->getContextLocale()->formatPrice(0, $orderCurrency->iso_code);
} else {
$paidAmount = $this->getContextLocale()->formatPrice($paidAmount, $orderCurrency->iso_code);
}
// end - edited
// start - edited - new render
return $this->render('@Modules/ordertotal/views/templates/admin/view.html.twig', [
// end - edited
// start - edited
'outstandingAmt' => $balanceAmount,
'orderPaymentPaidAmt' => $paidAmount,
// end - edited
'showContentHeader' => true,
'enableSidebar' => true,
'orderCurrency' => $orderCurrency,
'meta_title' => $metatitle,
'help_link' => $this->generateSidebarLink($request->attributes->get('_legacy_controller')),
'orderForViewing' => $orderForViewing,
'addOrderCartRuleForm' => $addOrderCartRuleForm->createView(),
'updateOrderStatusForm' => $updateOrderStatusForm->createView(),
'updateOrderStatusActionBarForm' => $updateOrderStatusActionBarForm->createView(),
'addOrderPaymentForm' => $addOrderPaymentForm->createView(),
'changeOrderCurrencyForm' => $changeOrderCurrencyForm->createView(),
'privateNoteForm' => $privateNoteForm ? $privateNoteForm->createView() : null,
'updateOrderShippingForm' => $updateOrderShippingForm->createView(),
'cancelProductForm' => $cancelProductForm->createView(),
'invoiceManagementIsEnabled' => $orderForViewing->isInvoiceManagementIsEnabled(),
'changeOrderAddressForm' => $changeOrderAddressForm ? $changeOrderAddressForm->createView() : null,
'orderMessageForm' => $orderMessageForm->createView(),
'addProductRowForm' => $addProductRowForm->createView(),
'editProductRowForm' => $editProductRowForm->createView(),
'backOfficeOrderButtons' => $backOfficeOrderButtons,
'merchandiseReturnEnabled' => $merchandiseReturnEnabled,
'priceSpecification' => $this->getContextLocale()->getPriceSpecification($orderCurrency->iso_code)->toArray(),
'previousOrderId' => $orderSiblingProvider->getPreviousOrderId($orderId),
'nextOrderId' => $orderSiblingProvider->getNextOrderId($orderId),
'paginationNum' => $paginationNum,
'paginationNumOptions' => $paginationNumOptions,
'isAvailableQuantityDisplayed' => $this->decoratedController->configuration->getBoolean('PS_STOCK_MANAGEMENT'),
]);
}
public function partialRefundAction(int $orderId, Request $request)
{
return $this->decoratedController->partialRefundAction($orderId, $request);
}
public function standardRefundAction(int $orderId, Request $request)
{
return $this->decoratedController->standardRefundAction($orderId, $request);
}
public function returnProductAction(int $orderId, Request $request)
{
return $this->decoratedController->returnProductAction($orderId, $request);
}
// since a private function called by function viewaction() and we need to modify the viewaciton().
private function handleOutOfStockProduct(OrderForViewing $orderForViewing)
{
$isStockManagementEnabled = $this->decoratedController->configuration->getBoolean('PS_STOCK_MANAGEMENT');
if (!$isStockManagementEnabled || $orderForViewing->isDelivered() || $orderForViewing->isShipped()) {
return;
}
foreach ($orderForViewing->getProducts()->getProducts() as $product) {
if ($product->getAvailableQuantity() <= 0) {
$this->addFlash(
'warning',
$this->trans('This product is out of stock:', 'Admin.Orderscustomers.Notification') . ' ' . $product->getName()
);
}
}
}
public function addProductAction(int $orderId, Request $request): Response
{
return $this->decoratedController->addProductAction($orderId, $request);
}
public function getProductPricesAction(int $orderId): Response
{
return $this->decoratedController->getProductPricesAction($orderId);
}
public function getInvoicesAction(int $orderId)
{
return $this->decoratedController->getInvoicesAction($orderId);
}
public function getDocumentsAction(int $orderId)
{
return $this->decoratedController->getDocumentsAction($orderId);
}
public function getShippingAction(int $orderId)
{
return $this->decoratedController->getShippingAction($orderId);
}
public function updateShippingAction(int $orderId, Request $request): RedirectResponse
{
return $this->decoratedController->updateShippingAction($orderId, $request);
}
public function removeCartRuleAction(int $orderId, int $orderCartRuleId): RedirectResponse
{
return $this->decoratedController->removeCartRuleAction($orderId, $orderCartRuleId);
}
public function updateInvoiceNoteAction(int $orderId, int $orderInvoiceId, Request $request): RedirectResponse
{
return $this->decoratedController->updateInvoiceNoteAction($orderId, $orderInvoiceId, $request);
}
public function updateProductAction(int $orderId, int $orderDetailId, Request $request): Response
{
return $this->decoratedController->updateProductAction($orderId, $orderDetailId, $request);
}
public function addCartRuleAction(int $orderId, Request $request): RedirectResponse
{
return $this->decoratedController->addCartRuleAction($orderId, $request);
}
public function updateStatusAction(int $orderId, Request $request): RedirectResponse
{
return $this->decoratedController->updateStatusAction($orderId, $request);
}
public function updateStatusFromListAction(int $orderId, Request $request): RedirectResponse
{
return $this->decoratedController->updateStatusFromListAction($orderId, $request);
}
public function addPaymentAction(int $orderId, Request $request): RedirectResponse
{
return $this->decoratedController->addPaymentAction($orderId, $request);
}
public function previewAction(int $orderId): JsonResponse
{
return $this->decoratedController->previewAction($orderId);
}
public function duplicateOrderCartAction(int $orderId)
{
return $this->decoratedController->duplicateOrderCartAction($orderId);
}
public function sendMessageAction(Request $request, int $orderId): Response
{
return $this->decoratedController->sendMessageAction($request, $orderId);
}
public function changeCustomerAddressAction(Request $request): RedirectResponse
{
return $this->decoratedController->changeCustomerAddressAction($request);
}
public function changeCurrencyAction(int $orderId, Request $request): RedirectResponse
{
return $this->decoratedController->changeCurrencyAction($orderId, $request);
}
public function resendEmailAction(int $orderId, int $orderStatusId, int $orderHistoryId): RedirectResponse
{
return $this->decoratedController->resendEmailAction($orderId, $orderStatusId, $orderHistoryId);
}
public function deleteProductAction(int $orderId, int $orderDetailId): JsonResponse
{
return $this->decoratedController->deleteProductAction($orderId, $orderDetailId);
}
public function getDiscountsAction(int $orderId): Response
{
return $this->decoratedController->getDiscountsAction($orderId);
}
public function getPricesAction(int $orderId): JsonResponse
{
return $this->decoratedController->getPricesAction($orderId);
}
public function getPaymentsAction(int $orderId): Response
{
return $this->decoratedController->getPaymentsAction($orderId);
}
public function getProductsListAction(int $orderId): Response
{
return $this->decoratedController->getProductsListAction($orderId);
}
public function generateInvoiceAction(int $orderId): RedirectResponse
{
return $this->decoratedController->generateInvoiceAction($orderId);
}
public function sendProcessOrderEmailAction(Request $request): JsonResponse
{
return $this->decoratedController->sendProcessOrderEmailAction($request);
}
public function cancellationAction(int $orderId, Request $request)
{
return $this->decoratedController->cancellationAction($orderId, $request);
}
public function configureProductPaginationAction(Request $request): JsonResponse
{
return $this->decoratedController->configureProductPaginationAction($request);
}
public function displayCustomizationImageAction(int $orderId, string $value)
{
return $this->decoratedController->displayCustomizationImageAction($orderId, $value);
}
public function searchProductsAction(Request $request): JsonResponse
{
return $this->decoratedController->searchProductsAction($request);
}
private function handleOrderStatusUpdate(int $orderId, int $orderStatusId): void
{
$this->decoratedController->handleOrderStatusUpdate($orderId, $orderStatusId);
}
// private funciton
private function getErrorMessages(Exception $e)
{
$refundableQuantity = 0;
if ($e instanceof InvalidCancelProductException) {
$refundableQuantity = $e->getRefundableQuantity();
}
$orderInvoiceNumber = '#unknown';
if ($e instanceof DuplicateProductInOrderInvoiceException) {
$orderInvoiceNumber = $e->getOrderInvoiceNumber();
}
return [
CannotEditDeliveredOrderProductException::class => $this->trans('You cannot edit the cart once the order delivered.', 'Admin.Orderscustomers.Notification'),
OrderNotFoundException::class => $e instanceof OrderNotFoundException ?
$this->trans(
'Order #%d cannot be loaded.',
'Admin.Orderscustomers.Notification',
['#%d' => $e->getOrderId()->getValue()]
) : '',
OrderEmailSendException::class => $this->trans(
'An error occurred while sending the e-mail to the customer.',
'Admin.Orderscustomers.Notification'
),
OrderException::class => $this->trans(
$e->getMessage(),
'Admin.Orderscustomers.Notification'
),
InvalidAmountException::class => $this->trans(
'Only numbers and decimal points (".") are allowed in the amount fields, e.g. 10.50 or 1050.',
'Admin.Orderscustomers.Notification'
),
InvalidCartRuleDiscountValueException::class => [
InvalidCartRuleDiscountValueException::INVALID_MIN_PERCENT => $this->trans(
'Percent value must be greater than 0.',
'Admin.Orderscustomers.Notification'
),
InvalidCartRuleDiscountValueException::INVALID_MAX_PERCENT => $this->trans(
'Percent value cannot exceed 100.',
'Admin.Orderscustomers.Notification'
),
InvalidCartRuleDiscountValueException::INVALID_MIN_AMOUNT => $this->trans(
'Amount value must be greater than 0.',
'Admin.Orderscustomers.Notification'
),
InvalidCartRuleDiscountValueException::INVALID_MAX_AMOUNT => $this->trans(
'Discount value cannot exceed the total price of this order.',
'Admin.Orderscustomers.Notification'
),
InvalidCartRuleDiscountValueException::INVALID_FREE_SHIPPING => $this->trans(
'Shipping discount value cannot exceed the total price of this order.',
'Admin.Orderscustomers.Notification'
),
],
InvalidCancelProductException::class => [
InvalidCancelProductException::INVALID_QUANTITY => $this->trans(
'Positive product quantity is required.',
'Admin.Notifications.Error'
),
InvalidCancelProductException::QUANTITY_TOO_HIGH => $this->trans(
'Please enter a maximum quantity of [1].',
'Admin.Orderscustomers.Notification',
['[1]' => $refundableQuantity]
),
InvalidCancelProductException::NO_REFUNDS => $this->trans(
'Please select at least one product.',
'Admin.Orderscustomers.Notification'
),
InvalidCancelProductException::INVALID_AMOUNT => $this->trans(
'Please enter a positive amount.',
'Admin.Orderscustomers.Notification'
),
InvalidCancelProductException::NO_GENERATION => $this->trans(
'Please generate at least one credit slip or voucher.',
'Admin.Orderscustomers.Notification'
),
],
InvalidModuleException::class => $this->trans(
'You must choose a payment module to create the order.',
'Admin.Orderscustomers.Notification'
),
ProductOutOfStockException::class => $this->trans(
'There are not enough products in stock.',
'Admin.Catalog.Notification'
),
NegativePaymentAmountException::class => $this->trans(
'Invalid value: the payment must be a positive amount.',
'Admin.Notifications.Error'
),
InvalidOrderStateException::class => [
InvalidOrderStateException::ALREADY_PAID => $this->trans(
'Invalid action: this order has already been paid.',
'Admin.Notifications.Error'
),
InvalidOrderStateException::DELIVERY_NOT_FOUND => $this->trans(
'Invalid action: this order has not been delivered.',
'Admin.Notifications.Error'
),
InvalidOrderStateException::UNEXPECTED_DELIVERY => $this->trans(
'Invalid action: this order has already been delivered.',
'Admin.Notifications.Error'
),
InvalidOrderStateException::NOT_PAID => $this->trans(
'Invalid action: this order has not been paid.',
'Admin.Notifications.Error'
),
InvalidOrderStateException::INVALID_ID => $this->trans(
'You must choose an order status to create the order.',
'Admin.Orderscustomers.Notification'
),
],
OrderConstraintException::class => [
OrderConstraintException::INVALID_CUSTOMER_MESSAGE => $this->trans(
'The order message given is invalid.',
'Admin.Orderscustomers.Notification'
),
],
InvalidProductQuantityException::class => $this->trans(
'Positive product quantity is required.',
'Admin.Notifications.Error'
),
DuplicateProductInOrderException::class => $this->trans(
'This product is already in your order, please edit the quantity instead.',
'Admin.Notifications.Error'
),
DuplicateProductInOrderInvoiceException::class => $this->trans(
'This product is already in the invoice [1], please edit the quantity instead.',
'Admin.Notifications.Error',
['[1]' => $orderInvoiceNumber]
),
CannotFindProductInOrderException::class => $this->trans(
'You cannot edit the price of a product that no longer exists in your catalog.',
'Admin.Notifications.Error'
),
];
}
// private function
private function getPaymentErrorMessages(Exception $e)
{
return array_merge($this->getErrorMessages($e), [
InvalidArgumentException::class => $this->trans(
'Only numbers and decimal points (".") are allowed in the amount fields of the payment block, e.g. 10.50 or 1050.',
'Admin.Orderscustomers.Notification'
),
OrderConstraintException::class => [
OrderConstraintException::INVALID_PAYMENT_METHOD => sprintf(
'%s %s %s',
$this->trans(
'The selected payment method is invalid.',
'Admin.Orderscustomers.Notification'
),
$this->trans(
'Invalid characters:',
'Admin.Notifications.Info'
),
AddPaymentCommand::INVALID_CHARACTERS_NAME
),
],
]);
}
// private function
private function handleChangeOrderStatusException(ChangeOrderStatusException $e)
{
$orderIds = array_merge(
$e->getOrdersWithFailedToUpdateStatus(),
$e->getOrdersWithFailedToSendEmail()
);
/** @var OrderId $orderId */
foreach ($orderIds as $orderId) {
$this->addFlash(
'error',
$this->trans(
'An error occurred while changing the status for order #%d, or we were unable to send an email to the customer.',
'Admin.Orderscustomers.Notification',
['#%d' => $orderId->getValue()]
)
);
}
foreach ($e->getOrdersWithAssignedStatus() as $orderId) {
$this->addFlash(
'error',
$this->trans(
'Order #%d has already been assigned this status.',
'Admin.Orderscustomers.Notification',
['#%d' => $orderId->getValue()]
)
);
}
}
// private function
private function getCustomerMessageErrorMapping(Exception $exception): array
{
return [
OrderNotFoundException::class => $exception instanceof OrderNotFoundException ?
$this->trans(
'Order #%d cannot be loaded.',
'Admin.Orderscustomers.Notification',
['#%d' => $exception->getOrderId()->getValue()]
) : '',
CustomerMessageConstraintException::class => [
CustomerMessageConstraintException::MISSING_MESSAGE => $this->trans(
'The %s field is not valid',
'Admin.Notifications.Error',
[
sprintf('"%s"', $this->trans('Message', 'Admin.Global')),
]
),
CustomerMessageConstraintException::INVALID_MESSAGE => $this->trans(
'The %s field is not valid',
'Admin.Notifications.Error',
[
sprintf('"%s"', $this->trans('Message', 'Admin.Global')),
]
),
],
];
}
}
- Step 6, since we need to modify the order page layout, we need to render new template files.
Our example:
(modules/ordertotal/views/templates/admin/view.html.twig)
{#**
* Copy from PrestaShopBundle/Resources/views/Admin/Sell/Order/Order/view.html.twig
*#}
{% set use_regular_h1_structure = false %}
{% set layoutTitle %}
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/header.html.twig' %}
{% endset %}
{% extends '@PrestaShop/Admin/layout.html.twig' %}
{% block content %}
<div id="order-view-page"
data-order-title="{{ 'Order'|trans({}, 'Admin.Global') }} #{{ orderForViewing.id }} {{ orderForViewing.reference }}">
<div class="row d-print-none">
{% set displayAdminOrderTopHookContent = renderhook('displayAdminOrderTop', {'id_order': orderForViewing.id}) %}
{% if displayAdminOrderTopHookContent != '' %}
<div class="col-md-12">
{{ displayAdminOrderTopHookContent|raw }}
</div>
{% endif %}
<div class="order-actions col-md-12">
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/order_actions.html.twig' %}
</div>
</div>
<div class="row d-none d-print-block mb-4">
<div class="col-md-12">
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/print_order_statistics.html.twig' %}
</div>
</div>
<div class="row d-none mb-4">
<div class="col-12" id="orderProductsModificationPosition"></div>
</div>
<div class="row d-none d-print-block mb-4">
<div class="col-md-12">
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/print_title.html.twig' %}
</div>
</div>
<div class="product-row row">
<div class="col-md-4 left-column">
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/customer.html.twig' %}
{{ renderhook('displayAdminOrderSide', {'id_order': orderForViewing.id}) }}
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/messages.html.twig' %}
{{ renderhook('displayAdminOrderSideBottom', {'id_order': orderForViewing.id}) }}
</div>
<div class="col-md-8 d-print-block right-column">
<div id="orderProductsOriginalPosition">
{# start - edit - rending a different products.html.twig in this module for outstanding and unpaid amt#}
{# {% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/products.html.twig' %} #}
{% include '@Modules/ordertotal/views/templates/admin/products.html.twig' %}
{# end - edit #}
</div>
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/details.html.twig' %}
{{ renderhook('displayAdminOrderMain', {'id_order': orderForViewing.id}) }}
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/payments.html.twig' %}
{{ renderhook('displayAdminOrderMainBottom', {'id_order': orderForViewing.id}) }}
</div>
</div>
{% if orderForViewing.sources.sources is not empty %}
<div class="product-row row">
<div class="col-md-12 left-column">
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/sources.html.twig' %}
</div>
</div>
{% endif %}
{% if orderForViewing.linkedOrders.linkedOrders is not empty %}
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/linked_orders.html.twig' %}
{% endif %}
{{ renderhook('displayAdminOrder', {'id_order': orderForViewing.id}) }}
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/Modal/add_order_discount_modal.html.twig' %}
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/Modal/update_shipping_modal.html.twig' %}
{% if orderForViewing.customer is not null and orderForViewing.customer.id != 0 %}
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/Modal/update_customer_address_modal.html.twig' %}
{% endif %}
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/Modal/view_all_messages_modal.html.twig' %}
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/Modal/view_product_pack_modal.html.twig' %}
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src="{{ asset('themes/new-theme/public/order_view.bundle.js') }}"></script>
{% endblock %}
{% set js_translatable = {
"The product was successfully added.": 'The product was successfully added.'|trans({}, 'Admin.Notifications.Success'),
"The product was successfully removed.": 'The product was successfully removed.'|trans({}, 'Admin.Notifications.Success'),
"[1] products were successfully added.": '[1] products were successfully added.'|trans({}, 'Admin.Notifications.Success'),
"[1] products were successfully removed.": '[1] products were successfully removed.'|trans({}, 'Admin.Notifications.Success'),
}|merge(js_translatable)
%}
( modules/ordertotal/views/templates/admin/products.html.twig )
{#**
* Copy of PrestaShopBundle/Resources/views/Admin/Sell/Order/Order/Blocks/View/products.html.twig
*#}
{% set isColumnLocationDisplayed = false %}
{% set isColumnRefundedDisplayed = false %}
{% for product in orderForViewing.products.products|slice(0, paginationNum) %}
{% if product.location is not empty %}
{% set isColumnLocationDisplayed = true %}
{% endif %}
{% if product.quantityRefunded > 0 %}
{% set isColumnRefundedDisplayed = true %}
{% endif %}
{% endfor %}
<div class="card" id="orderProductsPanel">
<div class="card-header">
<h3 class="card-header-title">
{{ 'Products'|trans({}, 'Admin.Global') }} (<span id="orderProductsPanelCount">{{ orderForViewing.products.products|length }}</span>)
</h3>
</div>
<div class="card-body">
<div class="spinner-order-products-container" id="orderProductsLoading">
<div class="spinner spinner-primary"></div>
</div>
{% set formOptions = {
'attr': {
'data-order-id': orderForViewing.id,
'data-is-delivered': orderForViewing.isDelivered,
'data-is-tax-included': orderForViewing.isTaxIncluded,
'data-discounts-amount': orderForViewing.prices.discountsAmountRaw,
'data-price-specification': priceSpecification|json_encode|replace("'", "'")|raw
}
} %}
{{ form_start(cancelProductForm, formOptions) }}
<table class="table" id="orderProductsTable" data-currency-precision="{{ orderCurrency.precision }}">
<thead>
<tr>
<th>
<p>{{ 'Product'|trans({}, 'Admin.Global') }}</p>
</th>
<th></th>
<th>
<p class="mb-0">{{ 'Price per unit'|trans({}, 'Admin.Advparameters.Feature') }}</p>
<small class="text-muted">{{ orderForViewing.taxMethod }}</small>
</th>
<th>
<p>{{ 'Quantity'|trans({}, 'Admin.Global') }}</p>
</th>
<th class="cellProductLocation{% if not isColumnLocationDisplayed %} d-none{% endif %}">
<p>{{ 'Stock location'|trans({}, 'Admin.Orderscustomers.Feature') }}</p>
</th>
<th class="cellProductRefunded{% if not isColumnRefundedDisplayed %} d-none{% endif %}">
<p>{{ 'Refunded'|trans({}, 'Admin.Orderscustomers.Feature') }}</p>
</th>
<th {% if not isAvailableQuantityDisplayed %}class="d-none"{% endif %}>
<p>{{ 'Available'|trans({}, 'Admin.Global') }}</p>
</th>
<th>
<p class="mb-0">{{ 'Total'|trans({}, 'Admin.Global') }}</p>
<small class="text-muted">{{ orderForViewing.taxMethod }}</small>
</th>
{% if orderForViewing.hasInvoice() %}
<th>
<p>{{ 'Invoice'|trans({}, 'Admin.Global') }}</p>
</th>
{% endif %}
{% if not orderForViewing.delivered %}
<th class="text-right product_actions d-print-none">
<p>{{ 'Actions'|trans({}, 'Admin.Global') }}</p>
</th>
{% endif %}
<th class="text-center cancel-product-element">
<p>{{ 'Partial refund'|trans({}, 'Admin.Orderscustomers.Feature') }}</p>
</th>
</tr>
</thead>
<tbody>
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/product_list.html.twig' %}
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/add_product_row.html.twig' %}
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/edit_product_row.html.twig' %}
</tbody>
</table>
<div class="row mb-3">
<div class="col-md-6 text-left d-print-none order-product-pagination">
<div class="form-group row">
<label for="paginator_select_page_limit" class="col-form-label ml-3">{{ "Items per page:"|trans({}, 'Admin.Catalog.Feature') }}</label>
<div class="col">
<select id="orderProductsTablePaginationNumberSelector" class="pagination-link custom-select">
{% for numPageOption in paginationNumOptions %}
<option value="{{ numPageOption }}"{% if numPageOption == paginationNum %} selected{% endif %}>{{ numPageOption }}</option>
{% endfor %}
</select>
</div>
</div>
{% set numPages = max(orderForViewing.products.products|length / paginationNum, 1)|round(0, 'ceil') %}
<nav aria-label="Products Navigation"{% if orderForViewing.products.products|length <= paginationNum %} class="d-none"{% endif %} id="orderProductsNavPagination">
<ul class="pagination" id="orderProductsTablePagination" data-num-per-page="{{ paginationNum }}" data-num-pages="{{ numPages }}">
<li class="page-item disabled" id="orderProductsTablePaginationPrev">
<a class="page-link" href="javascript:void(0);" aria-label="Previous">
<span aria-hidden="true">«</span>
<span class="sr-only">Previous</span>
</a>
</li>
{% for numPage in 1..numPages %}
<li class="page-item{% if numPage==1 %} active{% endif %}"><span class="page-link" data-order-id="{{ orderForViewing.id }}" data-page="{{ numPage }}">{{ numPage }}</span></li>
{% endfor %}
<li class="page-item d-none"><span class="page-link" data-order-id="{{ orderForViewing.id }}" data-page=""></span></li>
<li class="page-item" id="orderProductsTablePaginationNext">
<a class="page-link" href="javascript:void(0);" aria-label="Next">
<span aria-hidden="true">»</span>
<span class="sr-only">Next</span>
</a>
</li>
</ul>
</nav>
</div>
<div class="col-md-6 text-right discount-action">
{% if not orderForViewing.delivered %}
<button type="button" class="btn btn-outline-secondary js-product-action-btn mr-3" id="addProductBtn">
<i class="material-icons">add_circle_outline</i>
{{ 'Add a product'|trans({}, 'Admin.Orderscustomers.Feature') }}
</button>
{% endif %}
<button type="button" class="btn btn-outline-secondary js-product-action-btn" data-toggle="modal" data-target="#addOrderDiscountModal">
<i class="material-icons">confirmation_number</i>
{{ 'Add a discount'|trans({}, 'Admin.Orderscustomers.Feature') }}
</button>
</div>
</div>
{% include '@PrestaShop/Admin/Sell/Order/Order/Blocks/View/discount_list.html.twig' with {
'discounts': orderForViewing.discounts.discounts,
'orderId': orderForViewing.id
} %}
<div class="row">
<div class="col-md-12">
<div class="info-block">
<div class="row">
<div class="col-sm text-center">
<p class="text-muted mb-0"><strong>{{ 'Products'|trans({}, 'Admin.Global') }}</strong></p>
<strong id="orderProductsTotal">{{ orderForViewing.prices.productsPriceFormatted }}</strong>
</div>
<div id="order-discounts-total-container" class="col-sm text-center{% if not orderForViewing.prices.discountsAmountRaw.greaterThan((number(0))) %} d-none{% endif %}">
<p class="text-muted mb-0"><strong>{{ 'Discounts'|trans({}, 'Admin.Global') }}</strong></p>
<strong id="orderDiscountsTotal">-{{ orderForViewing.prices.discountsAmountFormatted }}</strong>
</div>
{% if orderForViewing.prices.wrappingPriceRaw.greaterThan(number(0)) %}
<div class="col-sm text-center">
<p class="text-muted mb-0"><strong>{{ 'Wrapping'|trans({}, 'Admin.Orderscustomers.Feature') }}</strong></p>
<strong id="orderWrappingTotal">{{ orderForViewing.prices.wrappingPriceFormatted }}</strong>
</div>
{% endif %}
<div id="order-shipping-total-container" class="col-sm text-center{% if not orderForViewing.prices.shippingPriceRaw.greaterThan((number(0))) %} d-none{% endif %}">
<p class="text-muted mb-0"><strong>{{ 'Shipping'|trans({}, 'Admin.Catalog.Feature') }}</strong></p>
<div class="shipping-price">
<strong id="orderShippingTotal">{{ orderForViewing.prices.shippingPriceFormatted }}</strong>
<div class="cancel-product-element shipping-refund-amount{% if orderForViewing.prices.shippingRefundableAmountRaw.lowerOrEqualThan(number(0)) %} hidden{% endif %}">
<div class="input-group">
{{ form_widget(cancelProductForm.shipping_amount) }}
<div class="input-group-append">
<div class="input-group-text">{{ orderCurrency.symbol }}</div>
</div>
</div>
<p class="text-center">(max {{ orderForViewing.prices.shippingRefundableAmountFormatted }} tax included)</p>
</div>
</div>
</div>
{% if not orderForViewing.taxIncluded %}
<div class="col-sm text-center">
<p class="text-muted mb-0"><strong>{{ 'Taxes'|trans({}, 'Admin.Global') }}</strong></p>
<strong id="orderTaxesTotal">{{ orderForViewing.prices.taxesAmountFormatted }}</strong>
</div>
{% endif %}
{#** edited - adding Balance amount **#}
<div class="col-sm text-center">
<p class="text-muted mb-0"><strong>{{ 'Outstanding'|trans({}, 'Admin.Global') }}</strong></p>
<strong id="outstandingTotal">{{ outstandingAmt }}</strong>
</div>
{#** edited - adding Balance amount **#}
<div class="col-sm text-center">
<p class="text-muted mb-0"><strong>{{ 'Paid Amt'|trans({}, 'Admin.Global') }}</strong></p>
<strong id="orderPaymentPaid">{{ orderPaymentPaidAmt }}</strong>
</div>
<div class="col-sm text-center">
<p class="text-muted mb-0"><strong>{{ 'Total'|trans({}, 'Admin.Global') }}</strong></p>
<span class="badge rounded badge-dark font-size-100" id="orderTotal">{{ orderForViewing.prices.totalAmountFormatted }}</span>
</div>
</div>
</div>
</div>
<div class="col-md-12">
<p class="mb-0 mt-1 text-center text-muted">
<small>
{{ 'For this customer group, prices are displayed as: [1]%tax_method%[/1]'|trans({
'%tax_method%': orderForViewing.taxMethod,
'[1]': '<strong>',
'[/1]': '</strong>'
}, 'Admin.Orderscustomers.Notification')|raw }}.
{% if not 'PS_ORDER_RETURN'|configuration %}
<strong>{{ 'Merchandise returns are disabled'|trans({}, 'Admin.Orderscustomers.Notification') }}</strong>
{% endif %}
</small>
</p>
<div class="cancel-product-element refund-checkboxes-container">
<div class="cancel-product-element form-group restock-products">
{{ form_widget(cancelProductForm.restock) }}
</div>
<div class="cancel-product-element form-group refund-credit-slip">
{{ form_widget(cancelProductForm.credit_slip) }}
</div>
<div class="cancel-product-element form-group refund-voucher">
{{ form_widget(cancelProductForm.voucher) }}
</div>
<div class="cancel-product-element shipping-refund{% if orderForViewing.prices.shippingRefundableAmountRaw.lowerOrEqualThan(number(0)) %} hidden{% endif %}">
<div class="form-group">
{{ form_widget(cancelProductForm.shipping) }}
<small class="shipping-refund-amount">({{ orderForViewing.prices.shippingRefundableAmountFormatted }})</small>
</div>
</div>
<div class="cancel-product-element form-group voucher-refund-type{% if orderForViewing.prices.discountsAmountRaw.lowerOrEqualThan(number(0)) %} hidden{% endif %}">
{{ 'This order has been partially paid by voucher. Choose the amount you want to refund:'|trans({}, 'Admin.Orderscustomers.Feature') }}
{{ form_widget(cancelProductForm.voucher_refund_type) }}
<div class="voucher-refund-type-negative-error">
{{ 'Error. You cannot refund a negative amount.'|trans({}, 'Admin.Orderscustomers.Notification') }}
</div>
</div>
</div>
</div>
<div class="cancel-product-element form-submit text-right">
{{ form_widget(cancelProductForm.cancel) }}
{{ form_widget(cancelProductForm.save) }}
</div>
</div>
{{ form_end(cancelProductForm) }}
</div>
</div>
Limitation: Still working while disavbled the module.
Reference:
https://devdocs.prestashop.com/1.7/modules/concepts/controllers/admin-controllers/override-decorate-controller/
Related Post
4° PrestaShop Meetup in Hong Kong: Cross-border eCommerce (Europe)
Deprecated: Function create_function() is deprecated in /opt/lampp/htdocs/genkiware/wp-content/themes/optima/single.php on line 185
Agenda - 自我介绍 - 欧洲最流行和世界第三大用户最多的電商开源软件【Prestashop】介紹 - 最新欧盟电商规则 (VAT, GDPR, GEO-BLOCKING等等)…
Happy Chinese New Year 2020!
Deprecated: Function create_function() is deprecated in /opt/lampp/htdocs/genkiware/wp-content/themes/optima/single.php on line 185
In the season of joy I present my sincere wishes…