* @copyright 2007-2018 PrestaShop SA * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) * International Registered Trademark & Property of PrestaShop SA */ namespace PrestaShop\PrestaShop\Adapter\Presenter\Cart; use PrestaShop\PrestaShop\Adapter\Presenter\PresenterInterface; use PrestaShop\PrestaShop\Adapter\Presenter\Product\ProductListingPresenter; use PrestaShop\PrestaShop\Core\Product\ProductPresentationSettings; use PrestaShop\PrestaShop\Adapter\Product\PriceFormatter; use PrestaShop\PrestaShop\Adapter\Product\ProductColorsRetriever; use PrestaShop\PrestaShop\Adapter\Image\ImageRetriever; use Context; use Cart; use Product; use Configuration; use Symfony\Component\Translation\TranslatorInterface; use TaxConfiguration; use CartRule; use Tools; use Hook; class CartPresenter implements PresenterInterface { /** * @var PriceFormatter */ private $priceFormatter; /** * @var \Link */ private $link; /** * @var TranslatorInterface */ private $translator; /** * @var ImageRetriever */ private $imageRetriever; /** * @var TaxConfiguration */ private $taxConfiguration; public function __construct() { $context = Context::getContext(); $this->priceFormatter = new PriceFormatter(); $this->link = $context->link; $this->translator = $context->getTranslator(); $this->imageRetriever = new ImageRetriever($this->link); $this->taxConfiguration = new TaxConfiguration(); } /** * @return bool */ private function includeTaxes() { return $this->taxConfiguration->includeTaxes(); } /** * @param array $rawProduct * * @return \PrestaShop\PrestaShop\Adapter\Presenter\Product\ProductLazyArray|\PrestaShop\PrestaShop\Adapter\Presenter\Product\ProductListingLazyArray */ private function presentProduct(array $rawProduct) { $settings = new ProductPresentationSettings(); $settings->catalog_mode = Configuration::isCatalogMode(); $settings->include_taxes = $this->includeTaxes(); $settings->allow_add_variant_to_cart_from_listing = (int) Configuration::get('PS_ATTRIBUTE_CATEGORY_DISPLAY'); $settings->stock_management_enabled = Configuration::get('PS_STOCK_MANAGEMENT'); $settings->showPrices = Configuration::showPrices(); if (isset($rawProduct['attributes']) && is_string($rawProduct['attributes'])) { $rawProduct['attributes'] = $this->getAttributesArrayFromString($rawProduct['attributes']); } $rawProduct['remove_from_cart_url'] = $this->link->getRemoveFromCartURL( $rawProduct['id_product'], $rawProduct['id_product_attribute'] ); $rawProduct['up_quantity_url'] = $this->link->getUpQuantityCartURL( $rawProduct['id_product'], $rawProduct['id_product_attribute'] ); $rawProduct['down_quantity_url'] = $this->link->getDownQuantityCartURL( $rawProduct['id_product'], $rawProduct['id_product_attribute'] ); $rawProduct['update_quantity_url'] = $this->link->getUpdateQuantityCartURL( $rawProduct['id_product'], $rawProduct['id_product_attribute'] ); $resetFields = array( 'ecotax_rate', 'specific_prices', 'customizable', 'online_only', 'reduction', 'new', 'condition', 'pack', ); foreach ($resetFields as $field) { if (!array_key_exists($field, $rawProduct)) { $rawProduct[$field] = ''; } } if ($this->includeTaxes()) { $rawProduct['price_amount'] = $rawProduct['price_wt']; $rawProduct['price'] = $this->priceFormatter->format($rawProduct['price_wt']); } else { $rawProduct['price_amount'] = $rawProduct['price']; $rawProduct['price'] = $rawProduct['price_tax_exc'] = $this->priceFormatter->format($rawProduct['price']); } if ($rawProduct['price_amount'] && $rawProduct['unit_price_ratio'] > 0) { $rawProduct['unit_price'] = $rawProduct['price_amount'] / $rawProduct['unit_price_ratio']; } $rawProduct['total'] = $this->priceFormatter->format( $this->includeTaxes() ? $rawProduct['total_wt'] : $rawProduct['total'] ); $rawProduct['quantity_wanted'] = $rawProduct['cart_quantity']; $presenter = new ProductListingPresenter( $this->imageRetriever, $this->link, $this->priceFormatter, new ProductColorsRetriever(), $this->translator ); return $presenter->present( $settings, $rawProduct, Context::getContext()->language ); } /** * @param array $products * @param Cart $cart * * @return array */ public function addCustomizedData(array $products, Cart $cart) { return array_map(function ($product) use ($cart) { $customizations = array(); $data = Product::getAllCustomizedDatas($cart->id, null, true, null, (int) $product['id_customization']); if (!$data) { $data = array(); } $id_product = (int) $product['id_product']; $id_product_attribute = (int) $product['id_product_attribute']; if (array_key_exists($id_product, $data)) { if (array_key_exists($id_product_attribute, $data[$id_product])) { foreach ($data[$id_product] as $byAddress) { foreach ($byAddress as $byAddressCustomizations) { foreach ($byAddressCustomizations as $customization) { $presentedCustomization = array( 'quantity' => $customization['quantity'], 'fields' => array(), 'id_customization' => null, ); foreach ($customization['datas'] as $byType) { foreach ($byType as $data) { $field = array(); switch ($data['type']) { case Product::CUSTOMIZE_FILE: $field['type'] = 'image'; $field['image'] = $this->imageRetriever->getCustomizationImage( $data['value'] ); break; case Product::CUSTOMIZE_TEXTFIELD: $field['type'] = 'text'; $field['text'] = $data['value']; break; default: $field['type'] = null; } $field['label'] = $data['name']; $field['id_module'] = $data['id_module']; $presentedCustomization['id_customization'] = $data['id_customization']; $presentedCustomization['fields'][] = $field; } } $product['up_quantity_url'] = $this->link->getUpQuantityCartURL( $product['id_product'], $product['id_product_attribute'], $presentedCustomization['id_customization'] ); $product['down_quantity_url'] = $this->link->getDownQuantityCartURL( $product['id_product'], $product['id_product_attribute'], $presentedCustomization['id_customization'] ); $product['remove_from_cart_url'] = $this->link->getRemoveFromCartURL( $product['id_product'], $product['id_product_attribute'], $presentedCustomization['id_customization'] ); $product['update_quantity_url'] = $this->link->getUpdateQuantityCartURL( $product['id_product'], $product['id_product_attribute'], $presentedCustomization['id_customization'] ); $presentedCustomization['up_quantity_url'] = $this->link->getUpQuantityCartURL( $product['id_product'], $product['id_product_attribute'], $presentedCustomization['id_customization'] ); $presentedCustomization['down_quantity_url'] = $this->link->getDownQuantityCartURL( $product['id_product'], $product['id_product_attribute'], $presentedCustomization['id_customization'] ); $presentedCustomization['remove_from_cart_url'] = $this->link->getRemoveFromCartURL( $product['id_product'], $product['id_product_attribute'], $presentedCustomization['id_customization'] ); $presentedCustomization['update_quantity_url'] = $product['update_quantity_url']; $customizations[] = $presentedCustomization; } } } } } usort($customizations, function (array $a, array $b) { if ( $a['quantity'] > $b['quantity'] || count($a['fields']) > count($b['fields']) || $a['id_customization'] > $b['id_customization'] ) { return -1; } else { return 1; } }); $product['customizations'] = $customizations; return $product; }, $products); } /** * @param Cart $cart * @param bool $shouldSeparateGifts * * @return array * * @throws \Exception */ public function present($cart, $shouldSeparateGifts = false) { if (!is_a($cart, 'Cart')) { throw new \Exception('CartPresenter can only present instance of Cart'); } if ($shouldSeparateGifts) { $rawProducts = $cart->getProductsWithSeparatedGifts(); } else { $rawProducts = $cart->getProducts(true); } $products = array_map(array($this, 'presentProduct'), $rawProducts); $products = $this->addCustomizedData($products, $cart); $subtotals = array(); $productsTotalExcludingTax = $cart->getOrderTotal(false, Cart::ONLY_PRODUCTS); $total_excluding_tax = $cart->getOrderTotal(false); //$total_including_tax = $cart->getOrderTotal(true); /* TheCoreDev le 14/02/2020 - Anomalie 0000017 Le TTC calculé par Prestashop pouvant être erroné dans certains cas (problème de la méthode de calcul de la TVA), nous recalculons (pour l'instant "en dur" le montant de la TVA et le rajoutons au HT calculé (correctement) par Prestashop. */ $total_including_tax = $total_excluding_tax * 1.20; $total_discount = $cart->getDiscountSubtotalWithoutGifts(); $totalCartAmount = $cart->getOrderTotal($this->includeTaxes(), Cart::ONLY_PRODUCTS); $subtotals['products'] = array( 'type' => 'products', 'label' => $this->translator->trans('Subtotal', array(), 'Shop.Theme.Checkout'), 'amount' => $totalCartAmount, 'value' => $this->priceFormatter->format($totalCartAmount), ); if ($total_discount) { $subtotals['discounts'] = array( 'type' => 'discount', 'label' => $this->translator->trans('Discount', array(), 'Shop.Theme.Checkout'), 'amount' => $total_discount, 'value' => $this->priceFormatter->format($total_discount), ); } else { $subtotals['discounts'] = null; } if ($cart->gift) { $giftWrappingPrice = ($cart->getGiftWrappingPrice($this->includeTaxes()) != 0) ? $cart->getGiftWrappingPrice($this->includeTaxes()) : 0; $subtotals['gift_wrapping'] = array( 'type' => 'gift_wrapping', 'label' => $this->translator->trans('Gift wrapping', array(), 'Shop.Theme.Checkout'), 'amount' => $giftWrappingPrice, 'value' => ($giftWrappingPrice > 0) ? $this->priceFormatter->convertAndFormat($giftWrappingPrice) : $this->translator->trans('Free', array(), 'Shop.Theme.Checkout'), ); } if (!$cart->isVirtualCart()) { $shippingCost = $cart->getTotalShippingCost(null, $this->includeTaxes()); } else { $shippingCost = 0; } $subtotals['shipping'] = array( 'type' => 'shipping', 'label' => $this->translator->trans('Shipping', array(), 'Shop.Theme.Checkout'), 'amount' => $shippingCost, 'value' => $shippingCost != 0 ? $this->priceFormatter->format($shippingCost) : $this->translator->trans('Free', array(), 'Shop.Theme.Checkout'), ); $subtotals['tax'] = null; if (Configuration::get('PS_TAX_DISPLAY')) { $taxAmount = $total_including_tax - $total_excluding_tax; $subtotals['tax'] = array( 'type' => 'tax', 'label' => ($this->includeTaxes()) ? $this->translator->trans('Included taxes', array(), 'Shop.Theme.Checkout') : $this->translator->trans('Taxes', array(), 'Shop.Theme.Checkout'), 'amount' => $taxAmount, 'value' => $this->priceFormatter->format($taxAmount), ); } $totals = array( 'total' => array( 'type' => 'total', 'label' => $this->translator->trans('Total', array(), 'Shop.Theme.Checkout'), 'amount' => $this->includeTaxes() ? $total_including_tax : $total_excluding_tax, 'value' => $this->priceFormatter->format( $this->includeTaxes() ? $total_including_tax : $total_excluding_tax ), ), 'total_including_tax' => array( 'type' => 'total', 'label' => $this->translator->trans('Total (tax incl.)', array(), 'Shop.Theme.Checkout'), 'amount' => $total_including_tax, 'value' => $this->priceFormatter->format($total_including_tax), ), 'total_excluding_tax' => array( 'type' => 'total', 'label' => $this->translator->trans('Total (tax excl.)', array(), 'Shop.Theme.Checkout'), 'amount' => $total_excluding_tax, 'value' => $this->priceFormatter->format($total_excluding_tax), ), ); $products_count = array_reduce($products, function ($count, $product) { return $count + $product['quantity']; }, 0); $summary_string = $products_count === 1 ? $this->translator->trans('1 item', array(), 'Shop.Theme.Checkout') : $this->translator->trans('%count% items', array('%count%' => $products_count), 'Shop.Theme.Checkout') ; $minimalPurchase = $this->priceFormatter->convertAmount((float) Configuration::get('PS_PURCHASE_MINIMUM')); Hook::exec('overrideMinimalPurchasePrice', array( 'minimalPurchase' => &$minimalPurchase, )); // TODO: move it to a common parent, since it's copied in OrderPresenter and ProductPresenter $labels = array( 'tax_short' => ($this->includeTaxes()) ? $this->translator->trans('(tax incl.)', array(), 'Shop.Theme.Global') : $this->translator->trans('(tax excl.)', array(), 'Shop.Theme.Global'), 'tax_long' => ($this->includeTaxes()) ? $this->translator->trans('(tax included)', array(), 'Shop.Theme.Global') : $this->translator->trans('(tax excluded)', array(), 'Shop.Theme.Global'), ); $discounts = $cart->getDiscounts(); $vouchers = $this->getTemplateVarVouchers($cart); $cartRulesIds = array_flip(array_map( function ($voucher) { return $voucher['id_cart_rule']; }, $vouchers['added'] )); $discounts = array_filter($discounts, function ($discount) use ($cartRulesIds) { return !array_key_exists($discount['id_cart_rule'], $cartRulesIds); }); return array( 'products' => $products, 'totals' => $totals, 'subtotals' => $subtotals, 'products_count' => $products_count, 'summary_string' => $summary_string, 'labels' => $labels, 'id_address_delivery' => $cart->id_address_delivery, 'id_address_invoice' => $cart->id_address_invoice, 'is_virtual' => $cart->isVirtualCart(), 'vouchers' => $vouchers, 'discounts' => $discounts, 'minimalPurchase' => $minimalPurchase, 'minimalPurchaseRequired' => ($this->priceFormatter->convertAmount($productsTotalExcludingTax) < $minimalPurchase) ? $this->translator->trans( 'A minimum shopping cart total of %amount% (tax excl.) is required to validate your order. Current cart total is %total% (tax excl.).', array( '%amount%' => $this->priceFormatter->convertAndFormat($minimalPurchase), '%total%' => $this->priceFormatter->convertAndFormat($productsTotalExcludingTax), ), 'Shop.Theme.Checkout' ) : '', ); } private function getTemplateVarVouchers(Cart $cart) { $cartVouchers = $cart->getCartRules(); $vouchers = array(); $cartHasTax = is_null($cart->id) ? false : $cart::getTaxesAverageUsed($cart); foreach ($cartVouchers as $cartVoucher) { $vouchers[$cartVoucher['id_cart_rule']]['id_cart_rule'] = $cartVoucher['id_cart_rule']; $vouchers[$cartVoucher['id_cart_rule']]['name'] = $cartVoucher['name']; $vouchers[$cartVoucher['id_cart_rule']]['reduction_percent'] = $cartVoucher['reduction_percent']; $vouchers[$cartVoucher['id_cart_rule']]['reduction_currency'] = $cartVoucher['reduction_currency']; // Voucher reduction depending of the cart tax rule // if $cartHasTax & voucher is tax excluded, set amount voucher to tax included if ($cartHasTax && $cartVoucher['reduction_tax'] == '0') { $cartVoucher['reduction_amount'] = $cartVoucher['reduction_amount'] * (1 + $cartHasTax / 100); } $vouchers[$cartVoucher['id_cart_rule']]['reduction_amount'] = $cartVoucher['reduction_amount']; if (array_key_exists('gift_product', $cartVoucher) && $cartVoucher['gift_product']) { $cartVoucher['reduction_amount'] = $cartVoucher['value_real']; } if (isset($cartVoucher['reduction_percent']) && $cartVoucher['reduction_amount'] == '0.00') { $cartVoucher['reduction_formatted'] = $cartVoucher['reduction_percent'] . '%'; } elseif (isset($cartVoucher['reduction_amount']) && $cartVoucher['reduction_amount'] > 0) { $cartVoucher['reduction_formatted'] = $this->priceFormatter->convertAndFormat($cartVoucher['reduction_amount']); } $vouchers[$cartVoucher['id_cart_rule']]['reduction_formatted'] = '-' . $cartVoucher['reduction_formatted']; $vouchers[$cartVoucher['id_cart_rule']]['delete_url'] = $this->link->getPageLink( 'cart', true, null, array( 'deleteDiscount' => $cartVoucher['id_cart_rule'], 'token' => Tools::getToken(false), ) ); } return array( 'allowed' => (int) CartRule::isFeatureActive(), 'added' => $vouchers, ); } /** * Receives a string containing a list of attributes affected to the product and returns them as an array. * * @param string $attributes * * @return array Converted attributes in an array */ protected function getAttributesArrayFromString($attributes) { $separator = Configuration::get('PS_ATTRIBUTE_ANCHOR_SEPARATOR'); $pattern = '/(?>(?P[^:]+:[^:]+)' . $separator . '+(?!' . $separator . '([^:' . $separator . '])+:))/'; $attributesArray = array(); $matches = array(); if (!preg_match_all($pattern, $attributes . $separator, $matches)) { return $attributesArray; } foreach ($matches['attribute'] as $attribute) { list($key, $value) = explode(':', $attribute); $attributesArray[trim($key)] = ltrim($value); } return $attributesArray; } }