order process

This commit is contained in:
Etienne Roudeix
2013-09-20 16:25:46 +02:00
parent 17548e3526
commit 8541499302
24 changed files with 718 additions and 95 deletions

View File

@@ -24,20 +24,14 @@
namespace Thelia\Action; namespace Thelia\Action;
use Propel\Runtime\ActiveQuery\ModelCriteria; use Propel\Runtime\ActiveQuery\ModelCriteria;
use Propel\Runtime\ActiveRecord\ActiveRecordInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Thelia\Core\Event\OrderEvent; use Thelia\Core\Event\OrderEvent;
use Thelia\Core\Event\TheliaEvents; use Thelia\Core\Event\TheliaEvents;
use Thelia\Exception\OrderException; use Thelia\Exception\OrderException;
use Thelia\Model\AttributeAvI18n; use Thelia\Exception\TheliaProcessException;
use Thelia\Model\AttributeAvI18nQuery;
use Thelia\Model\AttributeI18n;
use Thelia\Model\AttributeI18nQuery;
use Thelia\Model\AddressQuery; use Thelia\Model\AddressQuery;
use Thelia\Model\OrderProductAttributeCombination; use Thelia\Model\OrderProductAttributeCombination;
use Thelia\Model\ProductI18nQuery;
use Thelia\Model\Lang;
use Thelia\Model\ModuleQuery; use Thelia\Model\ModuleQuery;
use Thelia\Model\OrderProduct; use Thelia\Model\OrderProduct;
use Thelia\Model\OrderStatus; use Thelia\Model\OrderStatus;
@@ -45,7 +39,7 @@ use Thelia\Model\Map\OrderTableMap;
use Thelia\Model\OrderAddress; use Thelia\Model\OrderAddress;
use Thelia\Model\OrderStatusQuery; use Thelia\Model\OrderStatusQuery;
use Thelia\Model\ConfigQuery; use Thelia\Model\ConfigQuery;
use Thelia\Model\ProductI18n; use Thelia\Tools\I18n;
/** /**
* *
@@ -125,6 +119,7 @@ class Order extends BaseAction implements EventSubscriberInterface
$currency = $this->getSession()->getCurrency(); $currency = $this->getSession()->getCurrency();
$lang = $this->getSession()->getLang(); $lang = $this->getSession()->getLang();
$deliveryAddress = AddressQuery::create()->findPk($sessionOrder->chosenDeliveryAddress); $deliveryAddress = AddressQuery::create()->findPk($sessionOrder->chosenDeliveryAddress);
$taxCountry = $deliveryAddress->getCountry();
$invoiceAddress = AddressQuery::create()->findPk($sessionOrder->chosenInvoiceAddress); $invoiceAddress = AddressQuery::create()->findPk($sessionOrder->chosenInvoiceAddress);
$cart = $this->getSession()->getCart(); $cart = $this->getSession()->getCart();
$cartItems = $cart->getCartItems(); $cartItems = $cart->getCartItems();
@@ -182,23 +177,30 @@ class Order extends BaseAction implements EventSubscriberInterface
foreach($cartItems as $cartItem) { foreach($cartItems as $cartItem) {
$product = $cartItem->getProduct(); $product = $cartItem->getProduct();
/* get customer translation */ /* get translation */
$productI18n = $this->getI18n(ProductI18nQuery::create(), new ProductI18n(), $product->getId()); $productI18n = I18n::forceI18nRetrieving($this->getSession()->getLang()->getLocale(), 'Product', $product->getId());
$pse = $cartItem->getProductSaleElements(); $pse = $cartItem->getProductSaleElements();
/* check still in stock */ /* check still in stock */
if($cartItem->getQuantity() > $pse->getQuantity()) { if($cartItem->getQuantity() > $pse->getQuantity()) {
$e = new OrderException("Not enough stock", OrderException::NOT_ENOUGH_STOCK); throw new TheliaProcessException("Not enough stock", TheliaProcessException::CART_ITEM_NOT_ENOUGH_STOCK, $cartItem);
$e->cartItem = $cartItem;
throw $e;
} }
/* decrease stock */ /* decrease stock */
$pse->setQuantity( $pse->setQuantity(
$pse->getQuantity() - $cartItem->getQuantity() $pse->getQuantity() - $cartItem->getQuantity()
); );
$pse->save(); $pse->save($con);
/* get tax */
$taxRuleI18n = I18n::forceI18nRetrieving($this->getSession()->getLang()->getLocale(), 'TaxRule', $product->getTaxRuleId());
$taxDetail = $product->getTaxRule()->getTaxDetail(
$taxCountry,
$cartItem->getPromo() == 1 ? $cartItem->getPromoPrice() : $cartItem->getPrice(),
$this->getSession()->getLang()->getLocale()
);
$orderProduct = new OrderProduct(); $orderProduct = new OrderProduct();
$orderProduct $orderProduct
@@ -215,14 +217,22 @@ class Order extends BaseAction implements EventSubscriberInterface
->setWasNew($pse->getNewness()) ->setWasNew($pse->getNewness())
->setWasInPromo($cartItem->getPromo()) ->setWasInPromo($cartItem->getPromo())
->setWeight($pse->getWeight()) ->setWeight($pse->getWeight())
->setTaxRuleTitle($taxRuleI18n->getTitle())
->setTaxRuleDescription($taxRuleI18n->getDescription())
; ;
$orderProduct->setDispatcher($this->getDispatcher()); $orderProduct->setDispatcher($this->getDispatcher());
$orderProduct->save(); $orderProduct->save($con);
/* fulfill order_product_tax */
foreach($taxDetail as $tax) {
$tax->setOrderProductId($orderProduct->getId());
$tax->save($con);
}
/* fulfill order_attribute_combination and decrease stock */ /* fulfill order_attribute_combination and decrease stock */
foreach($pse->getAttributeCombinations() as $attributeCombination) { foreach($pse->getAttributeCombinations() as $attributeCombination) {
$attribute = $this->getI18n(AttributeI18nQuery::create(), new AttributeI18n(), $attributeCombination->getAttributeId()); $attribute = I18n::forceI18nRetrieving($this->getSession()->getLang()->getLocale(), 'Attribute', $attributeCombination->getAttributeId());
$attributeAv = $this->getI18n(AttributeAvI18nQuery::create(), new AttributeAvI18n(), $attributeCombination->getAttributeAvId()); $attributeAv = I18n::forceI18nRetrieving($this->getSession()->getLang()->getLocale(), 'AttributeAv', $attributeCombination->getAttributeAvId());
$orderAttributeCombination = new OrderProductAttributeCombination(); $orderAttributeCombination = new OrderProductAttributeCombination();
$orderAttributeCombination $orderAttributeCombination
@@ -237,7 +247,7 @@ class Order extends BaseAction implements EventSubscriberInterface
->setAttributeAvPostscriptum($attributeAv->getPostscriptum()) ->setAttributeAvPostscriptum($attributeAv->getPostscriptum())
; ;
$orderAttributeCombination->save(); $orderAttributeCombination->save($con);
} }
} }
@@ -248,12 +258,13 @@ class Order extends BaseAction implements EventSubscriberInterface
$this->getDispatcher()->dispatch(TheliaEvents::ORDER_BEFORE_PAYMENT, new OrderEvent($placedOrder)); $this->getDispatcher()->dispatch(TheliaEvents::ORDER_BEFORE_PAYMENT, new OrderEvent($placedOrder));
/* clear session */ /* clear session */
/* but memorize placed order */
$sessionOrder = new \Thelia\Model\Order(); $sessionOrder = new \Thelia\Model\Order();
$event->setOrder($sessionOrder); $event->setOrder($sessionOrder);
$event->setPlacedOrder($placedOrder);
$this->getSession()->setOrder($sessionOrder); $this->getSession()->setOrder($sessionOrder);
/* empty cart */ /* empty cart @todo */
$this->getSession()->getCart()->clear();
/* call pay method */ /* call pay method */
$paymentModuleReflection = new \ReflectionClass($paymentModule->getFullNamespace()); $paymentModuleReflection = new \ReflectionClass($paymentModule->getFullNamespace());
@@ -349,42 +360,4 @@ class Order extends BaseAction implements EventSubscriberInterface
return $request->getSession(); return $request->getSession();
} }
/**
* @param ModelCriteria $query
* @param ActiveRecordInterface $object
* @param $id
* @param array $needed = array('Title')
*
* @return ProductI18n
*/
protected function getI18n(ModelCriteria $query, ActiveRecordInterface $object, $id, $needed = array('Title'))
{
$i18n = $query
->filterById($id)
->filterByLocale(
$this->getSession()->getLang()->getLocale()
)->findOne();
/* or default translation */
if(null === $i18n) {
$i18n = $query
->filterById($id)
->filterByLocale(
Lang::getDefaultLanguage()->getLocale()
)->findOne();
}
if(null === $i18n) { // @todo something else ?
$i18n = $object;
foreach($needed as $need) {
$method = sprintf('get%s', $need);
if(method_exists($i18n, $method)) {
$i18n->$method('DEFAULT ' . strtoupper($need));
} else {
// @todo throw sg ?
}
}
}
return $i18n;
}
} }

View File

@@ -21,6 +21,7 @@
<loop class="Thelia\Core\Template\Loop\FeatureAvailability" name="feature_availability"/> <loop class="Thelia\Core\Template\Loop\FeatureAvailability" name="feature_availability"/>
<loop class="Thelia\Core\Template\Loop\FeatureValue" name="feature_value"/> <loop class="Thelia\Core\Template\Loop\FeatureValue" name="feature_value"/>
<loop class="Thelia\Core\Template\Loop\Folder" name="folder"/> <loop class="Thelia\Core\Template\Loop\Folder" name="folder"/>
<loop class="Thelia\Core\Template\Loop\Module" name="module"/>
<loop class="Thelia\Core\Template\Loop\Order" name="order"/> <loop class="Thelia\Core\Template\Loop\Order" name="order"/>
<loop class="Thelia\Core\Template\Loop\OrderStatus" name="order-status"/> <loop class="Thelia\Core\Template\Loop\OrderStatus" name="order-status"/>
<loop class="Thelia\Core\Template\Loop\CategoryPath" name="category-path"/> <loop class="Thelia\Core\Template\Loop\CategoryPath" name="category-path"/>

View File

@@ -137,7 +137,11 @@
<route id="order.payment.process" path="/order/pay"> <route id="order.payment.process" path="/order/pay">
<default key="_controller">Thelia\Controller\Front\OrderController::pay</default> <default key="_controller">Thelia\Controller\Front\OrderController::pay</default>
<default key="_view">order_payment</default> </route>
<route id="order.placed" path="/order/placed/{order_id}">
<default key="_controller">Thelia\Controller\Front\OrderController::orderPlaced</default>
<default key="_view">order_placed</default>
</route> </route>
<!-- end order management process --> <!-- end order management process -->

View File

@@ -23,6 +23,7 @@
namespace Thelia\Controller\Front; namespace Thelia\Controller\Front;
use Propel\Runtime\Exception\PropelException; use Propel\Runtime\Exception\PropelException;
use Thelia\Exception\TheliaProcessException;
use Thelia\Form\Exception\FormValidationException; use Thelia\Form\Exception\FormValidationException;
use Thelia\Core\Event\OrderEvent; use Thelia\Core\Event\OrderEvent;
use Thelia\Core\Event\TheliaEvents; use Thelia\Core\Event\TheliaEvents;
@@ -32,9 +33,11 @@ use Thelia\Form\OrderPayment;
use Thelia\Log\Tlog; use Thelia\Log\Tlog;
use Thelia\Model\AddressQuery; use Thelia\Model\AddressQuery;
use Thelia\Model\AreaDeliveryModuleQuery; use Thelia\Model\AreaDeliveryModuleQuery;
use Thelia\Model\Base\OrderQuery;
use Thelia\Model\CountryQuery; use Thelia\Model\CountryQuery;
use Thelia\Model\ModuleQuery; use Thelia\Model\ModuleQuery;
use Thelia\Model\Order; use Thelia\Model\Order;
use Thelia\Tools\URL;
/** /**
* Class OrderController * Class OrderController
@@ -187,6 +190,36 @@ class OrderController extends BaseFrontController
$orderEvent = $this->getOrderEvent(); $orderEvent = $this->getOrderEvent();
$this->getDispatcher()->dispatch(TheliaEvents::ORDER_PAY, $orderEvent); $this->getDispatcher()->dispatch(TheliaEvents::ORDER_PAY, $orderEvent);
$placedOrder = $orderEvent->getPlacedOrder();
if(null !== $placedOrder && null !== $placedOrder->getId()) {
/* order has been placed */
$this->redirect(URL::getInstance()->absoluteUrl($this->getRoute('order.placed', array('order_id' => $orderEvent->getPlacedOrder()->getId()))));
} else {
/* order has not been placed */
$this->redirectToRoute("cart.view");
}
}
public function orderPlaced($order_id)
{
/* check if the placed order matched the customer */
$placedOrder = OrderQuery::create()->findPk(
$this->getRequest()->attributes->get('order_id')
);
if(null === $placedOrder) {
throw new TheliaProcessException("No placed order", TheliaProcessException::NO_PLACED_ORDER, $placedOrder);
}
$customer = $this->getSecurityContext()->getCustomerUser();
if(null === $customer || $placedOrder->getCustomerId() !== $customer->getId()) {
throw new TheliaProcessException("Received placed order id does not belong to the current customer", TheliaProcessException::PLACED_ORDER_ID_BAD_CURRENT_CUSTOMER, $placedOrder);
}
$this->getParserContext()->set("placed_order_id", $placedOrder->getId());
} }
protected function getOrderEvent() protected function getOrderEvent()

View File

@@ -28,6 +28,7 @@ use Thelia\Model\Order;
class OrderEvent extends ActionEvent class OrderEvent extends ActionEvent
{ {
protected $order = null; protected $order = null;
protected $placedOrder = null;
protected $invoiceAddress = null; protected $invoiceAddress = null;
protected $deliveryAddress = null; protected $deliveryAddress = null;
protected $deliveryModule = null; protected $deliveryModule = null;
@@ -51,6 +52,14 @@ class OrderEvent extends ActionEvent
$this->order = $order; $this->order = $order;
} }
/**
* @param Order $order
*/
public function setPlacedOrder(Order $order)
{
$this->placedOrder = $order;
}
/** /**
* @param $address * @param $address
*/ */
@@ -107,6 +116,14 @@ class OrderEvent extends ActionEvent
return $this->order; return $this->order;
} }
/**
* @return null|Order
*/
public function getPlacedOrder()
{
return $this->placedOrder;
}
/** /**
* @return null|int * @return null|int
*/ */

View File

@@ -81,6 +81,8 @@ class Cart extends BaseLoop
return $result; return $result;
} }
$taxCountry = CountryQuery::create()->findPk(64); // @TODO : make it magic;
foreach ($cartItems as $cartItem) { foreach ($cartItems as $cartItem) {
$product = $cartItem->getProduct(); $product = $cartItem->getProduct();
$productSaleElement = $cartItem->getProductSaleElements(); $productSaleElement = $cartItem->getProductSaleElements();
@@ -97,12 +99,8 @@ class Cart extends BaseLoop
->set("STOCK", $productSaleElement->getQuantity()) ->set("STOCK", $productSaleElement->getQuantity())
->set("PRICE", $cartItem->getPrice()) ->set("PRICE", $cartItem->getPrice())
->set("PROMO_PRICE", $cartItem->getPromoPrice()) ->set("PROMO_PRICE", $cartItem->getPromoPrice())
->set("TAXED_PRICE", $cartItem->getTaxedPrice( ->set("TAXED_PRICE", $cartItem->getTaxedPrice($taxCountry))
CountryQuery::create()->findOneById(64) // @TODO : make it magic ->set("PROMO_TAXED_PRICE", $cartItem->getTaxedPromoPrice($taxCountry))
))
->set("PROMO_TAXED_PRICE", $cartItem->getTaxedPromoPrice(
CountryQuery::create()->findOneById(64) // @TODO : make it magic
))
->set("IS_PROMO", $cartItem->getPromo() === 1 ? 1 : 0); ->set("IS_PROMO", $cartItem->getPromo() === 1 ? 1 : 0);
$result->addRow($loopResultRow); $result->addRow($loopResultRow);
} }

View File

@@ -0,0 +1,137 @@
<?php
/*************************************************************************************/
/* */
/* Thelia */
/* */
/* Copyright (c) OpenStudio */
/* email : info@thelia.net */
/* web : http://www.thelia.net */
/* */
/* This program is free software; you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation; either version 3 of the License */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*************************************************************************************/
namespace Thelia\Core\Template\Loop;
use Propel\Runtime\ActiveQuery\Criteria;
use Thelia\Core\Template\Element\BaseI18nLoop;
use Thelia\Core\Template\Element\LoopResult;
use Thelia\Core\Template\Element\LoopResultRow;
use Thelia\Core\Template\Loop\Argument\ArgumentCollection;
use Thelia\Core\Template\Loop\Argument\Argument;
use Thelia\Model\ModuleQuery;
use Thelia\Module\BaseModule;
use Thelia\Type;
/**
*
* Module loop
*
*
* Class Module
* @package Thelia\Core\Template\Loop
* @author Etienne Roudeix <eroudeix@openstudio.fr>
*/
class Module extends BaseI18nLoop
{
public $timestampable = true;
/**
* @return ArgumentCollection
*/
protected function getArgDefinitions()
{
return new ArgumentCollection(
Argument::createIntListTypeArgument('id'),
new Argument(
'module_type',
new Type\TypeCollection(
new Type\EnumListType(array(
BaseModule::CLASSIC_MODULE_TYPE,
BaseModule::DELIVERY_MODULE_TYPE,
BaseModule::PAYMENT_MODULE_TYPE,
))
)
),
Argument::createIntListTypeArgument('exclude'),
Argument::createBooleanOrBothTypeArgument('active', Type\BooleanOrBothType::ANY)
);
}
/**
* @param $pagination
*
* @return \Thelia\Core\Template\Element\LoopResult
*/
public function exec(&$pagination)
{
$search = ModuleQuery::create();
/* manage translations */
$locale = $this->configureI18nProcessing($search);
$id = $this->getId();
if (null !== $id) {
$search->filterById($id, Criteria::IN);
}
$moduleType = $this->getModule_type();
if (null !== $moduleType) {
$search->filterByType($moduleType, Criteria::IN);
}
$exclude = $this->getExclude();
if (!is_null($exclude)) {
$search->filterById($exclude, Criteria::NOT_IN);
}
$active = $this->getActive();
if($active !== Type\BooleanOrBothType::ANY) {
$search->filterByActivate($active ? 1 : 0, Criteria::EQUAL);
}
$search->orderByPosition();
/* perform search */
$modules = $this->search($search, $pagination);
$loopResult = new LoopResult($modules);
foreach ($modules as $module) {
$loopResultRow = new LoopResultRow($loopResult, $module, $this->versionable, $this->timestampable, $this->countable);
$loopResultRow->set("ID", $module->getId())
->set("IS_TRANSLATED",$module->getVirtualColumn('IS_TRANSLATED'))
->set("LOCALE",$locale)
->set("TITLE",$module->getVirtualColumn('i18n_TITLE'))
->set("CHAPO", $module->getVirtualColumn('i18n_CHAPO'))
->set("DESCRIPTION", $module->getVirtualColumn('i18n_DESCRIPTION'))
->set("POSTSCRIPTUM", $module->getVirtualColumn('i18n_POSTSCRIPTUM'))
->set("CODE", $module->getCode())
->set("TYPE", $module->getType())
->set("ACTIVE", $module->getActivate())
->set("CLASS", $module->getFullNamespace())
->set("POSITION", $module->getPosition());
$loopResult->addRow($loopResultRow);
}
return $loopResult;
}
}

View File

@@ -23,12 +23,16 @@
namespace Thelia\Core\Template\Loop; namespace Thelia\Core\Template\Loop;
use Propel\Runtime\ActiveQuery\Criteria;
use Thelia\Core\Template\Element\BaseLoop; use Thelia\Core\Template\Element\BaseLoop;
use Thelia\Core\Template\Element\LoopResult; use Thelia\Core\Template\Element\LoopResult;
use Thelia\Core\Template\Element\LoopResultRow;
use Thelia\Core\Template\Loop\Argument\ArgumentCollection; use Thelia\Core\Template\Loop\Argument\ArgumentCollection;
use Thelia\Core\Template\Loop\Argument\Argument; use Thelia\Core\Template\Loop\Argument\Argument;
use Thelia\Model\OrderQuery;
use Thelia\Type\TypeCollection;
use Thelia\Type;
/** /**
* *
* @package Thelia\Core\Template\Loop * @package Thelia\Core\Template\Loop
@@ -37,19 +41,94 @@ use Thelia\Core\Template\Loop\Argument\Argument;
*/ */
class Order extends BaseLoop class Order extends BaseLoop
{ {
public $countable = true;
public $timestampable = true;
public $versionable = false;
public function getArgDefinitions() public function getArgDefinitions()
{ {
return new ArgumentCollection(); return new ArgumentCollection(
Argument::createIntListTypeArgument('id'),
new Argument(
'customer',
new TypeCollection(
new Type\IntType(),
new Type\EnumType(array('current'))
),
'current'
),
Argument::createIntListTypeArgument('status')
);
} }
/** /**
* @param $pagination
* *
* * @return LoopResult
* @return \Thelia\Core\Template\Element\LoopResult
*/ */
public function exec(&$pagination) public function exec(&$pagination)
{ {
// TODO : a coder ! $search = OrderQuery::create();
return new LoopResult();
$id = $this->getId();
if (null !== $id) {
$search->filterById($id, Criteria::IN);
}
$customer = $this->getCustomer();
if ($customer === 'current') {
$currentCustomer = $this->securityContext->getCustomerUser();
if ($currentCustomer === null) {
return new LoopResult();
} else {
$search->filterByCustomerId($currentCustomer->getId(), Criteria::EQUAL);
}
} else {
$search->filterByCustomerId($customer, Criteria::EQUAL);
}
$status = $this->getStatus();
if (null !== $status) {
$search->filterByStatusId($status, Criteria::IN);
}
$orders = $this->search($search, $pagination);
$loopResult = new LoopResult($orders);
foreach ($orders as $order) {
$tax = 0;
$amount = $order->getTotalAmount($tax);
$loopResultRow = new LoopResultRow($loopResult, $order, $this->versionable, $this->timestampable, $this->countable);
$loopResultRow
->set("ID", $order->getId())
->set("REF", $order->getRef())
->set("CUSTOMER", $order->getCustomerId())
->set("DELIVERY_ADDRESS", $order->getDeliveryOrderAddressId())
->set("INVOICE_ADDRESS", $order->getInvoiceOrderAddressId())
->set("INVOICE_DATE", $order->getInvoiceDate())
->set("CURRENCY", $order->getCurrencyId())
->set("CURRENCY_RATE", $order->getCurrencyRate())
->set("TRANSACTION_REF", $order->getTransactionRef())
->set("DELIVERY_REF", $order->getDeliveryRef())
->set("INVOICE_REF", $order->getInvoiceRef())
->set("POSTAGE", $order->getPostage())
->set("PAYMENT_MODULE", $order->getPaymentModuleId())
->set("DELIVERY_MODULE", $order->getDeliveryModuleId())
->set("STATUS", $order->getStatusId())
->set("LANG", $order->getLangId())
->set("POSTAGE", $order->getPostage())
->set("TOTAL_TAX", $tax)
->set("TOTAL_AMOUNT", $amount - $tax)
->set("TOTAL_TAXED_AMOUNT", $amount)
;
$loopResult->addRow($loopResultRow);
}
return $loopResult;
} }
} }

View File

@@ -30,7 +30,6 @@ class OrderException extends \RuntimeException
*/ */
public $cartRoute = "cart.view"; public $cartRoute = "cart.view";
public $orderDeliveryRoute = "order.delivery"; public $orderDeliveryRoute = "order.delivery";
public $cartItem = null;
public $arguments = array(); public $arguments = array();
@@ -40,8 +39,6 @@ class OrderException extends \RuntimeException
const UNDEFINED_DELIVERY = 200; const UNDEFINED_DELIVERY = 200;
const NOT_ENOUGH_STOCK = 300;
public function __construct($message, $code = null, $arguments = array(), $previous = null) public function __construct($message, $code = null, $arguments = array(), $previous = null)
{ {
if(is_array($arguments)) { if(is_array($arguments)) {

View File

@@ -39,6 +39,7 @@ class TaxEngineException extends \RuntimeException
const UNDEFINED_TAX_RULES_COLLECTION = 503; const UNDEFINED_TAX_RULES_COLLECTION = 503;
const UNDEFINED_REQUIREMENTS = 504; const UNDEFINED_REQUIREMENTS = 504;
const UNDEFINED_REQUIREMENT_VALUE = 505; const UNDEFINED_REQUIREMENT_VALUE = 505;
const UNDEFINED_TAX_RULE = 506;
const BAD_AMOUNT_FORMAT = 601; const BAD_AMOUNT_FORMAT = 601;

View File

@@ -0,0 +1,54 @@
<?php
/*************************************************************************************/
/* */
/* Thelia */
/* */
/* Copyright (c) OpenStudio */
/* email : info@thelia.net */
/* web : http://www.thelia.net */
/* */
/* This program is free software; you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation; either version 3 of the License */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*************************************************************************************/
namespace Thelia\Exception;
/**
* these exception are non fatal exception, due to thelia process exception
* or customer random navigation
*
* they redirect the customer who trig them to a specific error page // @todo
*
* Class TheliaProcessException
* @package Thelia\Exception
*/
class TheliaProcessException extends \RuntimeException
{
public $data = null;
const UNKNOWN_EXCEPTION = 0;
const CART_ITEM_NOT_ENOUGH_STOCK = 100;
const NO_PLACED_ORDER = 101;
const PLACED_ORDER_ID_BAD_CURRENT_CUSTOMER = 102;
public function __construct($message, $code = null, $data = null, $previous = null)
{
$this->data = $data;
if ($code === null) {
$code = self::UNKNOWN_EXCEPTION;
}
parent::__construct($message, $code, $previous);
}
}

View File

@@ -2,11 +2,15 @@
namespace Thelia\Model; namespace Thelia\Model;
use Propel\Runtime\ActiveQuery\Criteria;
use Propel\Runtime\Connection\ConnectionInterface; use Propel\Runtime\Connection\ConnectionInterface;
use Propel\Runtime\Propel; use Propel\Runtime\Propel;
use Thelia\Core\Event\OrderEvent; use Thelia\Core\Event\OrderEvent;
use Thelia\Core\Event\TheliaEvents; use Thelia\Core\Event\TheliaEvents;
use Thelia\Model\Base\Order as BaseOrder; use Thelia\Model\Base\Order as BaseOrder;
use Thelia\Model\Base\OrderProductTaxQuery;
use Thelia\Model\Map\OrderProductTaxTableMap;
use Thelia\Model\OrderProductQuery;
use Thelia\Model\Map\OrderTableMap; use Thelia\Model\Map\OrderTableMap;
use \PDO; use \PDO;
@@ -38,13 +42,27 @@ class Order extends BaseOrder
/** /**
* calculate the total amount * calculate the total amount
* *
* @TODO create body method * @param int $tax
* *
* @return int * @return int|string|Base\double
*/ */
public function getTotalAmount() public function getTotalAmount(&$tax = 0)
{ {
return 2; $amount = 0;
$tax = 0;
/* browse all products */
$orderProductIds = array();
foreach($this->getOrderProducts() as $orderProduct) {
$taxAmount = OrderProductTaxQuery::create()
->withColumn('SUM(' . OrderProductTaxTableMap::AMOUNT . ')', 'total_tax')
->filterByOrderProductId($orderProduct->getId(), Criteria::EQUAL)
->findOne();
$amount += ($orderProduct->getWasInPromo() == 1 ? $orderProduct->getPromoPrice() : $orderProduct->getPrice()) * $orderProduct->getQuantity();
$tax += round($taxAmount->getVirtualColumn('total_tax'), 2) * $orderProduct->getQuantity();
}
return $amount + $tax + $this->getPostage(); // @todo : manage discount
} }
/** /**

View File

@@ -2,8 +2,11 @@
namespace Thelia\Model; namespace Thelia\Model;
use Propel\Runtime\Exception\PropelException;
use Propel\Runtime\Propel;
use Thelia\Model\Base\OrderQuery as BaseOrderQuery; use Thelia\Model\Base\OrderQuery as BaseOrderQuery;
use \PDO;
use Thelia\Model\Map\OrderTableMap;
/** /**
* Skeleton subclass for performing query and update operations on the 'order' table. * Skeleton subclass for performing query and update operations on the 'order' table.
@@ -15,6 +18,38 @@ use Thelia\Model\Base\OrderQuery as BaseOrderQuery;
* long as it does not already exist in the output directory. * long as it does not already exist in the output directory.
* *
*/ */
class OrderQuery extends BaseOrderQuery { class OrderQuery extends BaseOrderQuery
{
/**
* PROPEL SHOULD FIX IT
*
* Find object by primary key using raw SQL to go fast.
* Bypass doSelect() and the object formatter by using generated code.
*
* @param mixed $key Primary key to use for the query
* @param ConnectionInterface $con A connection object
*
* @return Order A model object, or null if the key is not found
*/
protected function findPkSimple($key, $con)
{
$sql = 'SELECT ID, REF, CUSTOMER_ID, INVOICE_ORDER_ADDRESS_ID, DELIVERY_ORDER_ADDRESS_ID, INVOICE_DATE, CURRENCY_ID, CURRENCY_RATE, TRANSACTION_REF, DELIVERY_REF, INVOICE_REF, POSTAGE, PAYMENT_MODULE_ID, DELIVERY_MODULE_ID, STATUS_ID, LANG_ID, CREATED_AT, UPDATED_AT FROM `order` WHERE ID = :p0';
try {
$stmt = $con->prepare($sql);
$stmt->bindValue(':p0', $key, PDO::PARAM_INT);
$stmt->execute();
} catch (\Exception $e) {
Propel::log($e->getMessage(), Propel::LOG_ERR);
throw new PropelException(sprintf('Unable to execute SELECT statement [%s]', $sql), 0, $e);
}
$obj = null;
if ($row = $stmt->fetch(\PDO::FETCH_NUM)) {
$obj = new Order();
$obj->hydrate($row);
OrderTableMap::addInstanceToPool($obj, (string) $key);
}
$stmt->closeCursor();
return $obj;
}
} // OrderQuery } // OrderQuery

View File

@@ -3,7 +3,25 @@
namespace Thelia\Model; namespace Thelia\Model;
use Thelia\Model\Base\TaxRule as BaseTaxRule; use Thelia\Model\Base\TaxRule as BaseTaxRule;
use Thelia\TaxEngine\Calculator;
use Thelia\TaxEngine\OrderProductTaxCollection;
class TaxRule extends BaseTaxRule { class TaxRule extends BaseTaxRule
{
/**
* @param Country $country
* @param $untaxedAmount
* @param null $askedLocale
*
* @return OrderProductTaxCollection
*/
public function getTaxDetail(Country $country, $untaxedAmount, $askedLocale = null)
{
$taxCalculator = new Calculator();
$taxCollection = new OrderProductTaxCollection();
$taxCalculator->loadTaxRule($this, $country)->getTaxedPrice($untaxedAmount, $taxCollection, $askedLocale);
return $taxCollection;
}
} }

View File

@@ -21,13 +21,19 @@ class TaxRuleQuery extends BaseTaxRuleQuery
{ {
const ALIAS_FOR_TAX_RULE_COUNTRY_POSITION = 'taxRuleCountryPosition'; const ALIAS_FOR_TAX_RULE_COUNTRY_POSITION = 'taxRuleCountryPosition';
public function getTaxCalculatorCollection(Product $product, Country $country) /**
* @param TaxRule $taxRule
* @param Country $country
*
* @return array|mixed|\Propel\Runtime\Collection\ObjectCollection
*/
public function getTaxCalculatorCollection(TaxRule $taxRule, Country $country)
{ {
$search = TaxQuery::create() $search = TaxQuery::create()
->filterByTaxRuleCountry( ->filterByTaxRuleCountry(
TaxRuleCountryQuery::create() TaxRuleCountryQuery::create()
->filterByCountry($country, Criteria::EQUAL) ->filterByCountry($country, Criteria::EQUAL)
->filterByTaxRuleId($product->getTaxRuleId()) ->filterByTaxRuleId($taxRule->getId())
->orderByPosition() ->orderByPosition()
->find() ->find()
) )

View File

@@ -25,7 +25,9 @@
namespace Thelia\Module; namespace Thelia\Module;
use Symfony\Component\DependencyInjection\ContainerAware; use Symfony\Component\DependencyInjection\ContainerAware;
use Thelia\Model\ModuleI18nQuery;
use Thelia\Model\Map\ModuleImageTableMap; use Thelia\Model\Map\ModuleImageTableMap;
use Thelia\Model\ModuleI18n;
use Thelia\Tools\Image; use Thelia\Tools\Image;
use Thelia\Exception\ModuleException; use Thelia\Exception\ModuleException;
use Thelia\Model\Module; use Thelia\Model\Module;
@@ -76,6 +78,27 @@ abstract class BaseModule extends ContainerAware
return $this->container; return $this->container;
} }
public function setTitle(Module $module, $titles)
{
if(is_array($titles)) {
foreach($titles as $locale => $title) {
$moduleI18n = ModuleI18nQuery::create()->filterById($module->getId())->filterByLocale($locale)->findOne();
if(null === $moduleI18n) {
$moduleI18n = new ModuleI18n();
$moduleI18n
->setId($module->getId())
->setLocale($locale)
->setTitle($title)
;
$moduleI18n->save();
} else {
$moduleI18n->setTitle($title);
$moduleI18n->save();
}
}
}
}
public function deployImageFolder(Module $module, $folderPath) public function deployImageFolder(Module $module, $folderPath)
{ {
try { try {

View File

@@ -24,8 +24,11 @@ namespace Thelia\TaxEngine;
use Thelia\Exception\TaxEngineException; use Thelia\Exception\TaxEngineException;
use Thelia\Model\Country; use Thelia\Model\Country;
use Thelia\Model\OrderProductTax;
use Thelia\Model\Product; use Thelia\Model\Product;
use Thelia\Model\TaxRule;
use Thelia\Model\TaxRuleQuery; use Thelia\Model\TaxRuleQuery;
use Thelia\Tools\I18n;
/** /**
* Class Calculator * Class Calculator
@@ -68,14 +71,34 @@ class Calculator
$this->product = $product; $this->product = $product;
$this->country = $country; $this->country = $country;
$this->taxRulesCollection = $this->taxRuleQuery->getTaxCalculatorCollection($product, $country); $this->taxRulesCollection = $this->taxRuleQuery->getTaxCalculatorCollection($product->getTaxRule(), $country);
return $this; return $this;
} }
public function getTaxAmountFromUntaxedPrice($untaxedPrice) public function loadTaxRule(TaxRule $taxRule, Country $country)
{ {
return $this->getTaxedPrice($untaxedPrice) - $untaxedPrice; $this->product = null;
$this->country = null;
$this->taxRulesCollection = null;
if($taxRule->getId() === null) {
throw new TaxEngineException('TaxRule id is empty in Calculator::loadTaxRule', TaxEngineException::UNDEFINED_TAX_RULE);
}
if($country->getId() === null) {
throw new TaxEngineException('Country id is empty in Calculator::loadTaxRule', TaxEngineException::UNDEFINED_COUNTRY);
}
$this->country = $country;
$this->taxRulesCollection = $this->taxRuleQuery->getTaxCalculatorCollection($taxRule, $country);
return $this;
}
public function getTaxAmountFromUntaxedPrice($untaxedPrice, &$taxCollection = null)
{
return $this->getTaxedPrice($untaxedPrice, $taxCollection) - $untaxedPrice;
} }
public function getTaxAmountFromTaxedPrice($taxedPrice) public function getTaxAmountFromTaxedPrice($taxedPrice)
@@ -83,7 +106,15 @@ class Calculator
return $taxedPrice - $this->getUntaxedPrice($taxedPrice); return $taxedPrice - $this->getUntaxedPrice($taxedPrice);
} }
public function getTaxedPrice($untaxedPrice) /**
* @param $untaxedPrice
* @param null $taxCollection returns OrderProductTaxCollection
* @param null $askedLocale
*
* @return int
* @throws \Thelia\Exception\TaxEngineException
*/
public function getTaxedPrice($untaxedPrice, &$taxCollection = null, $askedLocale = null)
{ {
if(null === $this->taxRulesCollection) { if(null === $this->taxRulesCollection) {
throw new TaxEngineException('Tax rules collection is empty in Calculator::getTaxAmount', TaxEngineException::UNDEFINED_TAX_RULES_COLLECTION); throw new TaxEngineException('Tax rules collection is empty in Calculator::getTaxAmount', TaxEngineException::UNDEFINED_TAX_RULES_COLLECTION);
@@ -97,6 +128,9 @@ class Calculator
$currentPosition = 1; $currentPosition = 1;
$currentTax = 0; $currentTax = 0;
if(null !== $taxCollection) {
$taxCollection = new OrderProductTaxCollection();
}
foreach($this->taxRulesCollection as $taxRule) { foreach($this->taxRulesCollection as $taxRule) {
$position = (int)$taxRule->getTaxRuleCountryPosition(); $position = (int)$taxRule->getTaxRuleCountryPosition();
@@ -109,7 +143,17 @@ class Calculator
$currentPosition = $position; $currentPosition = $position;
} }
$currentTax += $taxType->calculate($taxedPrice); $taxAmount = round($taxType->calculate($taxedPrice), 2);
$currentTax += $taxAmount;
if(null !== $taxCollection) {
$taxI18n = I18n::forceI18nRetrieving($askedLocale, 'Tax', $taxRule->getId());
$orderProductTax = new OrderProductTax();
$orderProductTax->setTitle($taxI18n->getTitle());
$orderProductTax->setDescription($taxI18n->getDescription());
$orderProductTax->setAmount($taxAmount);
$taxCollection->addTax($orderProductTax);
}
} }
$taxedPrice += $currentTax; $taxedPrice += $currentTax;

View File

@@ -0,0 +1,126 @@
<?php
/*************************************************************************************/
/* */
/* Thelia */
/* */
/* Copyright (c) OpenStudio */
/* email : info@thelia.net */
/* web : http://www.thelia.net */
/* */
/* This program is free software; you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation; either version 3 of the License */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*************************************************************************************/
namespace Thelia\TaxEngine;
use Thelia\Model\OrderProductTax;
/**
*
* @author Etienne Roudeix <eroudeix@openstudio.fr>
*
*/
class OrderProductTaxCollection implements \Iterator
{
private $position;
protected $taxes = array();
public function __construct()
{
foreach (func_get_args() as $tax) {
$this->addTax($tax);
}
}
public function isEmpty()
{
return count($this->taxes) == 0;
}
/**
* @param OrderProductTax $tax
*
* @return OrderProductTaxCollection
*/
public function addTax(OrderProductTax $tax)
{
$this->taxes[] = $tax;
return $this;
}
public function getCount()
{
return count($this->taxes);
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Return the current element
* @link http://php.net/manual/en/iterator.current.php
* @return OrderProductTax
*/
public function current()
{
return $this->taxes[$this->position];
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Move forward to next element
* @link http://php.net/manual/en/iterator.next.php
* @return void Any returned value is ignored.
*/
public function next()
{
$this->position++;
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Return the key of the current element
* @link http://php.net/manual/en/iterator.key.php
* @return mixed scalar on success, or null on failure.
*/
public function key()
{
return $this->position;
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Checks if current position is valid
* @link http://php.net/manual/en/iterator.valid.php
* @return boolean The return value will be casted to boolean and then evaluated.
* Returns true on success or false on failure.
*/
public function valid()
{
return isset($this->taxes[$this->position]);
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Rewind the Iterator to the first element
* @link http://php.net/manual/en/iterator.rewind.php
* @return void Any returned value is ignored.
*/
public function rewind()
{
$this->position = 0;
}
public function getKey($key)
{
return isset($this->taxes[$key]) ? $this->taxes[$key] : null;
}
}

View File

@@ -86,7 +86,7 @@ class CalculatorTest extends \PHPUnit_Framework_TestCase
$taxRuleQuery = $this->getMock('\Thelia\Model\TaxRuleQuery', array('getTaxCalculatorCollection')); $taxRuleQuery = $this->getMock('\Thelia\Model\TaxRuleQuery', array('getTaxCalculatorCollection'));
$taxRuleQuery->expects($this->once()) $taxRuleQuery->expects($this->once())
->method('getTaxCalculatorCollection') ->method('getTaxCalculatorCollection')
->with($productQuery, $countryQuery) ->with($productQuery->getTaxRule(), $countryQuery)
->will($this->returnValue('foo')); ->will($this->returnValue('foo'));
$rewritingUrlQuery = $this->getProperty('taxRuleQuery'); $rewritingUrlQuery = $this->getProperty('taxRuleQuery');

View File

@@ -23,6 +23,8 @@
namespace Thelia\Tools; namespace Thelia\Tools;
use Propel\Runtime\ActiveQuery\ModelCriteria;
use Propel\Runtime\ActiveRecord\ActiveRecordInterface;
use Thelia\Model\Lang; use Thelia\Model\Lang;
/** /**
@@ -54,4 +56,39 @@ class I18n
return \DateTime::createFromFormat($currentDateFormat, $date); return \DateTime::createFromFormat($currentDateFormat, $date);
} }
public static function forceI18nRetrieving($askedLocale, $modelName, $id, $needed = array('Title'))
{
$i18nQueryClass = sprintf("\\Thelia\\Model\\%sI18nQuery", $modelName);
$i18nClass = sprintf("\\Thelia\\Model\\%sI18n", $modelName);
/* get customer language translation */
$i18n = $i18nQueryClass::create()
->filterById($id)
->filterByLocale(
$askedLocale
)->findOne();
/* or default translation */
if(null === $i18n) {
$i18n = $i18nQueryClass::create()
->filterById($id)
->filterByLocale(
Lang::getDefaultLanguage()->getLocale()
)->findOne();
}
if(null === $i18n) { // @todo something else ?
$i18n = new $i18nClass();;
$i18n->setId($id);
foreach($needed as $need) {
$method = sprintf('set%s', $need);
if(method_exists($i18n, $method)) {
$i18n->$method('DEFAULT ' . strtoupper($need));
} else {
// @todo throw sg ?
}
}
}
return $i18n;
}
} }

View File

@@ -1153,7 +1153,7 @@ INSERT INTO `tax` (`id`, `type`, `serialized_requirements`, `created_at`, `upda
INSERT INTO `tax_i18n` (`id`, `locale`, `title`) INSERT INTO `tax_i18n` (`id`, `locale`, `title`)
VALUES VALUES
(1, 'fr_FR', 'TVA française à 19.6%'), (1, 'fr_FR', 'TVA française à 19.6%'),
(1, 'en_UK', 'french 19.6% tax'); (1, 'en_US', 'french 19.6% tax');
INSERT INTO `tax_rule` (`id`, `is_default`, `created_at`, `updated_at`) INSERT INTO `tax_rule` (`id`, `is_default`, `created_at`, `updated_at`)
VALUES VALUES
@@ -1162,7 +1162,7 @@ INSERT INTO `tax_rule` (`id`, `is_default`, `created_at`, `updated_at`)
INSERT INTO `tax_rule_i18n` (`id`, `locale`, `title`) INSERT INTO `tax_rule_i18n` (`id`, `locale`, `title`)
VALUES VALUES
(1, 'fr_FR', 'TVA française à 19.6%'), (1, 'fr_FR', 'TVA française à 19.6%'),
(1, 'en_UK', 'french 19.6% tax'); (1, 'en_US', 'french 19.6% tax');
INSERT INTO `tax_rule_country` (`tax_rule_id`, `country_id`, `tax_id`, `position`, `created_at`, `updated_at`) INSERT INTO `tax_rule_country` (`tax_rule_id`, `country_id`, `tax_id`, `position`, `created_at`, `updated_at`)
VALUES VALUES

View File

@@ -71,6 +71,15 @@ class Cheque extends BaseModule implements PaymentModuleInterface
if(ModuleImageQuery::create()->filterByModule($module)->count() == 0) { if(ModuleImageQuery::create()->filterByModule($module)->count() == 0) {
$this->deployImageFolder($module, sprintf('%s/images', __DIR__)); $this->deployImageFolder($module, sprintf('%s/images', __DIR__));
} }
/* set module title */
$this->setTitle(
$module,
array(
"en_US" => "Cheque",
"fr_FR" => "Cheque",
)
);
} }
public function destroy() public function destroy()

View File

@@ -71,6 +71,15 @@ class FakeCB extends BaseModule implements PaymentModuleInterface
if(ModuleImageQuery::create()->filterByModule($module)->count() == 0) { if(ModuleImageQuery::create()->filterByModule($module)->count() == 0) {
$this->deployImageFolder($module, sprintf('%s/images', __DIR__)); $this->deployImageFolder($module, sprintf('%s/images', __DIR__));
} }
/* set module title */
$this->setTitle(
$module,
array(
"en_US" => "Credit Card",
"fr_FR" => "Credit Card",
)
);
} }
public function destroy() public function destroy()

View File

@@ -24,9 +24,11 @@
<a href="cart-step4.php" role="button" class="btn btn-step active"><span class="step-nb">4</span> <span class="step-label">Secure payment</span></a> <a href="cart-step4.php" role="button" class="btn btn-step active"><span class="step-nb">4</span> <span class="step-label">Secure payment</span></a>
</div> </div>
{loop type="order" name="placed-order" id=$placed_order_id}
<div id="payment-success" class="panel"> <div id="payment-success" class="panel">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title">You chose to pay by : <span class="payment-method">Cheque</span></h3> <h3 class="panel-title">You chose to pay by : <span class="payment-method">{loop name="payment-module" type="module" id=$PAYMENT_MODULE}{$TITLE}{/loop}</span></h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<h3>Thank you for the trust you place in us.</h3> <h3>Thank you for the trust you place in us.</h3>
@@ -35,15 +37,17 @@
<dl class="dl-horizontal"> <dl class="dl-horizontal">
<dt>Order number : </dt> <dt>Order number : </dt>
<dd>PRO123456788978979</dd> <dd>{$REF}</dd>
<dt>Date : </dt> <dt>Date : </dt>
<dd>02/12/2013</dd> <dd>{format_date date=$CREATE_DATE output="date"}</dd>
<dt>Total : </dt> <dt>Total : </dt>
<dd>$216.25</dd> <dd>{loop type="currency" name="order-currency" id=$CURRENCY}{$SYMBOL}{/loop} {$TOTAL_TAXED_AMOUNT}</dd>
</dl> </dl>
</div> </div>
</div> </div>
{/loop}
<a href="{navigate to="index"}" role="button" class="btn btn-checkout-home"><span>Go home</span></a> <a href="{navigate to="index"}" role="button" class="btn btn-checkout-home"><span>Go home</span></a>
</article> </article>