diff --git a/classes/checkout/DeliveryOptionsFinder.php b/classes/checkout/DeliveryOptionsFinder.php index ddab5f9db7e64..7e5d02c1a8044 100644 --- a/classes/checkout/DeliveryOptionsFinder.php +++ b/classes/checkout/DeliveryOptionsFinder.php @@ -25,6 +25,7 @@ */ use PrestaShop\PrestaShop\Adapter\Presenter\Object\ObjectPresenter; use PrestaShop\PrestaShop\Adapter\Product\PriceFormatter; +use PrestaShop\PrestaShop\Core\Checkout\DeliveryOptionsBuilder; use Symfony\Contracts\Translation\TranslatorInterface; class DeliveryOptionsFinderCore @@ -72,7 +73,10 @@ public function getSelectedDeliveryOption() public function getDeliveryOptions() { + return (new DeliveryOptionsBuilder($this->context, $this->priceFormatter, $this->translator))->getDeliveryOptions(); + $delivery_option_list = $this->context->cart->getDeliveryOptionList(); + $include_taxes = !Product::getTaxCalculationMethod((int) $this->context->cart->id_customer) && (int) Configuration::get('PS_TAX'); $display_taxes_label = (Configuration::get('PS_TAX') && $this->context->country->display_tax_label && !Configuration::get('AEUC_LABEL_TAX_INC_EXC')); diff --git a/classes/order/OrderDetail.php b/classes/order/OrderDetail.php index 7be2aaf6873e5..507124767cbfc 100644 --- a/classes/order/OrderDetail.php +++ b/classes/order/OrderDetail.php @@ -199,7 +199,7 @@ class OrderDetailCore extends ObjectModel 'id_order' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true], 'id_order_invoice' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 'id_warehouse' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true], - 'id_order_carrier' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true], + 'id_order_carrier' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 'id_shop' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true], 'product_id' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 'product_attribute_id' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], diff --git a/install-dev/data/db_structure.sql b/install-dev/data/db_structure.sql index 5125e3e561c58..9a9df0b9aa9ed 100644 --- a/install-dev/data/db_structure.sql +++ b/install-dev/data/db_structure.sql @@ -1302,6 +1302,7 @@ CREATE TABLE IF NOT EXISTS `PREFIX_order_invoice_tax` ( /* order detail (every product inside an order) */ CREATE TABLE `PREFIX_order_detail` ( `id_order_detail` int(10) unsigned NOT NULL auto_increment, + `id_order_carrier` int(10) unsigned DEFAULT 0, `id_order` int(10) unsigned NOT NULL, `id_order_invoice` int(11) DEFAULT NULL, `id_warehouse` int(10) unsigned DEFAULT '0', @@ -3031,7 +3032,3 @@ CREATE TABLE `PREFIX_access` ( KEY `IDX_564352A15FCA037F` (`id_profile`), KEY `IDX_564352A18C6DE0E5` (`id_authorization_role`) ) ENGINE=ENGINE_TYPE DEFAULT CHARSET=utf8mb4 COLLATION; - -ALTER TABLE `PREFIX_order_carrier` ADD `id_order_invoice` INT(10) UNSIGNED DEFAULT NULL AFTER `id_order`; - -ALTER TABLE `PREFIX_order_detail` ADD `id_order_carrier` INT(10) UNSIGNED DEFAULT NULL AFTER `id_order`; diff --git a/src/Core/Checkout/DeliveryOption.php b/src/Core/Checkout/DeliveryOption.php new file mode 100644 index 0000000000000..76f20fe78bf73 --- /dev/null +++ b/src/Core/Checkout/DeliveryOption.php @@ -0,0 +1,52 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +namespace PrestaShop\PrestaShop\Core\Checkout; + +use Product; + +class DeliveryOption { + /** + * @var array + */ + private $combination = []; + + public function addCombination($combination) + { + $this->combination[] = $combination; + } + + public function getCombination() + { + return $this->combination; + } + + public function getCombinationByCarrier() + { + $result = []; + foreach ($this->combination as $combination) { + $result[$combination['id_carrier']]['products'][] = $combination['product']; + $result[$combination['id_carrier']]['carrierName'] = $combination['display_name']; + } + + return $result; + } +} diff --git a/src/Core/Checkout/DeliveryOptionsBuilder.php b/src/Core/Checkout/DeliveryOptionsBuilder.php new file mode 100644 index 0000000000000..14bf5ebdcbf33 --- /dev/null +++ b/src/Core/Checkout/DeliveryOptionsBuilder.php @@ -0,0 +1,187 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +namespace PrestaShop\PrestaShop\Core\Checkout; + +use PrestaShop\PrestaShop\Core\Checkout\DeliveryOption; +use PrestaShop\PrestaShop\Adapter\Product\PriceFormatter; +use Symfony\Contracts\Translation\TranslatorInterface; +use Context; +use Configuration; +use Country; +use Product; +use Carrier; + +class DeliveryOptionsBuilder +{ + /** + * @var Context + */ + private $context; + + /** + * @var PriceFormatter + */ + private $priceFormatter; + + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var array + */ + private $deliveryOptions = []; + + public function __construct(Context $context, PriceFormatter $priceFormatter, TranslatorInterface $translator) + { + $this->context = $context; + $this->priceFormatter = $priceFormatter; + $this->translator = $translator; + } + + public function getSelectedCarriers() {} + + public function getDeliveryOptions() + { + $this->findAllCombinations($this->context->cart->getProducts(), 0, null); + $this->calculateShippingCost(); + + return $this->deliveryOptions; + } + + public function findAllCombinations(array $products, int $currentPosition, $deliveryOption) + { + $carriersFromCurrentPosition = (new Product($products[$currentPosition]['id_product']))->getCarriers(); + $nextPosition = isset($products[$currentPosition + 1]) ? $currentPosition + 1 : -1; + + foreach($carriersFromCurrentPosition as $carrier) { + if ($deliveryOption === null) { + $deliveryOption = new DeliveryOption(); + } + + $cpyDeliveryOption = clone($deliveryOption); + + $cpyDeliveryOption->addCombination([ + 'product' => $products[$currentPosition], + 'id_carrier' => $carrier['id_carrier'], + 'display_name' => $carrier['name'] + ]); + + if ($nextPosition !== -1) { + $this->findAllCombinations($products, $nextPosition, $cpyDeliveryOption); + } else { + $this->deliveryOptions[]['productsGroupedByCarrier'] = $cpyDeliveryOption->getCombinationByCarrier(); + } + } + } + + private function calculateShippingCost() + { + $includeTaxes = !Product::getTaxCalculationMethod((int) $this->context->cart->id_customer) && (int) Configuration::get('PS_TAX'); + $displayTaxesLabel = (Configuration::get('PS_TAX') && $this->context->country->display_tax_label && !Configuration::get('AEUC_LABEL_TAX_INC_EXC')); + $priceWithoutTax = 0; + $priceWithTax = 0; + + // TODO: maybe split this to distinct function + foreach($this->deliveryOptions as $key => $deliveryOption) { + $this->deliveryOptions[$key]['details']['price_without_tax'] = 0; + $this->deliveryOptions[$key]['details']['price_with_tax'] = 0; + + foreach($deliveryOption['productsGroupedByCarrier'] as $carrierId => $products) { + $priceWithoutTax += $this->context->cart->getPackageShippingCost($carrierId, false, new Country($this->context->language->id), $products['products']); + $priceWithTax += $this->context->cart->getPackageShippingCost($carrierId, true, new Country($this->context->language->id), $products['products']); + $this->deliveryOptions[$key]['details']['price_without_tax'] = $priceWithoutTax; + $this->deliveryOptions[$key]['details']['price_with_tax'] = $priceWithTax; + + if ($this->isFreeShipping($carrierId)) { + $this->deliveryOptions[$key]['details']['price'] = $this->translator->trans( + 'Free', + [], + 'Shop.Theme.Checkout' + ); + } else { + if ($includeTaxes) { + $this->deliveryOptions[$key]['details']['price'] = $this->priceFormatter->format($priceWithTax); + if ($displayTaxesLabel) { + $this->deliveryOptions[$key]['details']['price'] = $this->translator->trans( + '%price% tax incl.', + ['%price%' => $this->deliveryOptions[$key]['details']['price']], + 'Shop.Theme.Checkout' + ); + } + } else { + $this->deliveryOptions[$key]['details']['price'] = $this->priceFormatter->format($priceWithoutTax); + if ($displayTaxesLabel) { + $this->deliveryOptions[$key]['details']['price'] = $this->translator->trans( + '%price% tax excl.', + ['%price%' => $this->deliveryOptions[$key]['details']['price']], + 'Shop.Theme.Checkout' + ); + } + } + } + $this->deliveryOptions[$key]['details']['carrier_name'] = $this->getCarrierName($key)['carrierName']; + $this->deliveryOptions[$key]['details']['ids_carriers'] = $this->getCarrierName($key)['idsCarriers']; + + } + $priceWithoutTax = 0; + $priceWithTax = 0; + } + } + + private function isFreeShipping(int $carrierId) + { + $free_shipping = false; + $carrier = new Carrier($carrierId); + + if ($carrier->is_free) { + $free_shipping = true; + } else { + foreach ($this->context->cart->getCartRules() as $rule) { + if ($rule['free_shipping'] && !$rule['carrier_restriction']) { + $free_shipping = true; + + break; + } + } + } + + return $free_shipping; + } + + private function getCarrierName($index) + { + $details = []; + + foreach($this->deliveryOptions[$index]['productsGroupedByCarrier'] as $carrierId => $products) { + $carrier = new Carrier($carrierId); + $details['carrierName'][] = $carrier->name; + $details['idsCarriers'][] = $carrierId; + } + + $details['carrierName'] = implode(' + ', $details['carrierName']); + $details['idsCarriers'] = implode(',', $details['idsCarriers']); + + return $details; + } +} diff --git a/src/Core/Checkout/DeliveryOptionsBuilderCore.php b/src/Core/Checkout/DeliveryOptionsBuilderCore.php new file mode 100644 index 0000000000000..58dd910b35c95 --- /dev/null +++ b/src/Core/Checkout/DeliveryOptionsBuilderCore.php @@ -0,0 +1,149 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +use PrestaShop\PrestaShop\Adapter\Presenter\Object\ObjectPresenter; +use PrestaShop\PrestaShop\Adapter\Product\PriceFormatter; +use PrestaShop\PrestaShop\Core\Checkout\DeliveryOptionsBuilder; +use Symfony\Contracts\Translation\TranslatorInterface; + +class DeliveryOptionsFinderCore +{ + private $context; + private $objectPresenter; + private $translator; + private $priceFormatter; + + public function __construct( + Context $context, + TranslatorInterface $translator, + ObjectPresenter $objectPresenter, + PriceFormatter $priceFormatter + ) { + $this->context = $context; + $this->objectPresenter = $objectPresenter; + $this->translator = $translator; + $this->priceFormatter = $priceFormatter; + } + + private function isFreeShipping($cart, array $carrier) + { + $free_shipping = false; + + if ($carrier['is_free']) { + $free_shipping = true; + } else { + foreach ($cart->getCartRules() as $rule) { + if ($rule['free_shipping'] && !$rule['carrier_restriction']) { + $free_shipping = true; + + break; + } + } + } + + return $free_shipping; + } + + public function getSelectedDeliveryOption() + { + return current($this->context->cart->getDeliveryOption(null, false, false)); + } + + public function getDeliveryOptions() + { + $test = new DeliveryOptionsBuilder($this->context); + + return $test->getDeliveryOptions(); + + $delivery_option_list = $this->context->cart->getDeliveryOptionList(); + $include_taxes = !Product::getTaxCalculationMethod((int) $this->context->cart->id_customer) && (int) Configuration::get('PS_TAX'); + $display_taxes_label = (Configuration::get('PS_TAX') && $this->context->country->display_tax_label && !Configuration::get('AEUC_LABEL_TAX_INC_EXC')); + + $carriers_available = []; + + if (isset($delivery_option_list[$this->context->cart->id_address_delivery])) { + foreach ($delivery_option_list[$this->context->cart->id_address_delivery] as $id_carriers_list => $carriers_list) { + foreach ($carriers_list as $carriers) { + if (is_array($carriers)) { + foreach ($carriers as $carrier) { + $carrier = array_merge($carrier, $this->objectPresenter->present($carrier['instance'])); + $delay = $carrier['delay'][$this->context->language->id]; + unset($carrier['instance'], $carrier['delay']); + $carrier['delay'] = $delay; + if ($this->isFreeShipping($this->context->cart, $carriers_list)) { + $carrier['price'] = $this->translator->trans( + 'Free', + [], + 'Shop.Theme.Checkout' + ); + } else { + if ($include_taxes) { + $carrier['price'] = $this->priceFormatter->format($carriers_list['total_price_with_tax']); + if ($display_taxes_label) { + $carrier['price'] = $this->translator->trans( + '%price% tax incl.', + ['%price%' => $carrier['price']], + 'Shop.Theme.Checkout' + ); + } + } else { + $carrier['price'] = $this->priceFormatter->format($carriers_list['total_price_without_tax']); + if ($display_taxes_label) { + $carrier['price'] = $this->translator->trans( + '%price% tax excl.', + ['%price%' => $carrier['price']], + 'Shop.Theme.Checkout' + ); + } + } + } + + if (count($carriers) > 1) { + $carrier['label'] = $carrier['price']; + } else { + $carrier['label'] = $carrier['name'] . ' - ' . $carrier['delay'] . ' - ' . $carrier['price']; + } + + // If carrier related to a module, check for additionnal data to display + $carrier['extraContent'] = ''; + if ($carrier['is_module']) { + if ($moduleId = Module::getModuleIdByName($carrier['external_module_name'])) { + // Hook called only for the module concerned + $carrier['extraContent'] = Hook::exec('displayCarrierExtraContent', ['carrier' => $carrier], $moduleId); + } + } + + $carriers_available[$id_carriers_list] = $carrier; + } + } + } + } + } + + return $carriers_available; + } +}