diff --git a/core/lib/Thelia/Config/Resources/export.xml b/core/lib/Thelia/Config/Resources/export.xml index 29040f34f..b212b8dbc 100644 --- a/core/lib/Thelia/Config/Resources/export.xml +++ b/core/lib/Thelia/Config/Resources/export.xml @@ -27,6 +27,17 @@ + + + Clients + Exporter toutes les informations à propos de vos clients + + + Customers + Export all the information about your customers + + + Prix des produits Hors-Taxes diff --git a/core/lib/Thelia/ImportExport/AbstractHandler.php b/core/lib/Thelia/ImportExport/AbstractHandler.php index 6857fff88..e1ecaca95 100644 --- a/core/lib/Thelia/ImportExport/AbstractHandler.php +++ b/core/lib/Thelia/ImportExport/AbstractHandler.php @@ -12,6 +12,7 @@ namespace Thelia\ImportExport; use Symfony\Component\DependencyInjection\ContainerInterface; +use Thelia\Model\Lang; /** * Class AbstractHandler @@ -23,12 +24,15 @@ abstract class AbstractHandler /** @var \Symfony\Component\DependencyInjection\ContainerInterface */ protected $container; + protected $defaultLocale; /** * @param ContainerInterface $container * * Dependency injection: load the container to be able to get parameters and services */ public function __construct(ContainerInterface $container) { + $this->defaultLocale = Lang::getDefaultLanguage()->getLocale(); + $this->container = $container; } diff --git a/core/lib/Thelia/ImportExport/Export/ExportHandler.php b/core/lib/Thelia/ImportExport/Export/ExportHandler.php index 50360953c..d14fa6bca 100644 --- a/core/lib/Thelia/ImportExport/Export/ExportHandler.php +++ b/core/lib/Thelia/ImportExport/Export/ExportHandler.php @@ -14,6 +14,7 @@ namespace Thelia\ImportExport\Export; use Propel\Runtime\ActiveQuery\Criterion\Exception\InvalidValueException; use Propel\Runtime\ActiveQuery\ModelCriteria; use Thelia\Core\FileFormat\Formatting\FormatterData; +use Thelia\Core\Template\Element\BaseLoop; use Thelia\Core\Translation\Translator; use Thelia\Model\Lang; use Thelia\ImportExport\AbstractHandler; @@ -30,60 +31,6 @@ abstract class ExportHandler extends AbstractHandler /** @var array */ protected $order; - public function addI18nCondition( - ModelCriteria $query, - $i18nTableName, - $tableIdColumn, - $i18nIdColumn, - $localeColumn, - $locale - ) { - - $locale = $this->real_escape($locale); - $defaultLocale = $this->real_escape(Lang::getDefaultLanguage()->getLocale()); - - $query - ->_and() - ->where( - "CASE WHEN ".$tableIdColumn." IN". - "(SELECT DISTINCT ".$i18nIdColumn." ". - "FROM `".$i18nTableName."` ". - "WHERE locale=$locale) ". - - "THEN ".$localeColumn." = $locale ". - "ELSE ".$localeColumn." = $defaultLocale ". - "END" - ) - ; - } - - /** - * @param $str - * @return string - * - * Really escapes a string for SQL request. - */ - protected function real_escape($str) - { - $str = trim($str, "\"'"); - - $return = "CONCAT("; - $len = strlen($str); - - for($i = 0; $i < $len; ++$i) { - $return .= "CHAR(".ord($str[$i])."),"; - } - - if ($i > 0) { - $return = substr($return, 0, -1); - } else { - $return = "\"\""; - } - $return .= ")"; - - return $return; - } - /** * @return array * @@ -96,6 +43,17 @@ abstract class ExportHandler extends AbstractHandler return array(); } + /** + * @return null|array + * + * You may override this method to return an array, containing + * the aliases to use. + */ + protected function getAliases() + { + return null; + } + /** * @return array * @@ -133,11 +91,24 @@ abstract class ExportHandler extends AbstractHandler $query = $this->buildDataSet($lang); if ($query instanceof ModelCriteria) { + return $data->loadModelCriteria($query); } elseif (is_array($query)) { + return $data->setData($query); + } elseif ($query instanceof BaseLoop) { + $pagination = null; + $results = $query->exec($pagination); + + for ($results->rewind(); $results->valid(); $results->next() ) { + $current = $results->current(); + + $data->addRow($current->getVarVal()); + } + + return $data; } - + throw new InvalidValueException( Translator::getInstance()->trans( "The method \"%class\"::buildDataSet must return an array or a ModelCriteria", @@ -148,18 +119,63 @@ abstract class ExportHandler extends AbstractHandler ); } + public function addI18nCondition( + ModelCriteria $query, + $i18nTableName, + $tableIdColumn, + $i18nIdColumn, + $localeColumn, + $locale + ) { + + $locale = $this->real_escape($locale); + $defaultLocale = $this->real_escape($this->defaultLocale); + + $query + ->_and() + ->where( + "CASE WHEN ".$tableIdColumn." IN". + "(SELECT DISTINCT ".$i18nIdColumn." ". + "FROM `".$i18nTableName."` ". + "WHERE locale=$locale) ". + + "THEN ".$localeColumn." = $locale ". + "ELSE ".$localeColumn." = $defaultLocale ". + "END" + ) + ; + } + /** - * @return null|array + * @param $str + * @return string * + * Really escapes a string for SQL request. */ - protected function getAliases() + protected function real_escape($str) { - return null; + $str = trim($str, "\"'"); + + $return = "CONCAT("; + $len = strlen($str); + + for($i = 0; $i < $len; ++$i) { + $return .= "CHAR(".ord($str[$i])."),"; + } + + if ($i > 0) { + $return = substr($return, 0, -1); + } else { + $return = "\"\""; + } + $return .= ")"; + + return $return; } /** * @param Lang $lang - * @return ModelCriteria|array + * @return ModelCriteria|array|BaseLoop */ abstract protected function buildDataSet(Lang $lang); } \ No newline at end of file diff --git a/core/lib/Thelia/ImportExport/Export/Type/CustomerExport.php b/core/lib/Thelia/ImportExport/Export/Type/CustomerExport.php new file mode 100644 index 000000000..bbfa5bf8b --- /dev/null +++ b/core/lib/Thelia/ImportExport/Export/Type/CustomerExport.php @@ -0,0 +1,360 @@ + + */ +class CustomerExport extends ExportHandler +{ + /** + * @return string|array + * + * Define all the type of formatters that this can handle + * return a string if it handle a single type ( specific exports ), + * or an array if multiple. + * + * Thelia types are defined in \Thelia\Core\FileFormat\FormatType + * + * example: + * return array( + * FormatType::TABLE, + * FormatType::UNBOUNDED, + * ); + */ + public function getHandledTypes() + { + return array( + FormatType::TABLE, + FormatType::UNBOUNDED, + ); + } + + /** + * @param Lang $lang + * @return array|\Propel\Runtime\ActiveQuery\ModelCriteria + * + * The tax engine of Thelia is in PHP, so we can't compute orders for each customers + * directly in SQL, we need two SQL queries, and some computing to get the last order amount and total amount. + */ + public function buildDataSet(Lang $lang) + { + $locale = $lang->getLocale(); + $defaultLocale = Lang::getDefaultLanguage()->getLocale(); + /** + * This first query get each customer info and addresses. + */ + $newsletterJoin = new Join(CustomerTableMap::EMAIL, NewsletterTableMap::EMAIL, Criteria::LEFT_JOIN); + + $query = CustomerQuery::create() + ->useCustomerTitleQuery("customer_title_") + ->useCustomerTitleI18nQuery("customer_title_i18n_") + ->addAsColumn("title_TITLE", "customer_title_i18n_.SHORT") + ->endUse() + ->endUse() + ->useAddressQuery() + ->useCountryQuery() + ->useCountryI18nQuery() + ->addAsColumn("address_COUNTRY", CountryI18nTableMap::TITLE) + ->endUse() + ->endUse() + ->useCustomerTitleQuery("address_title") + ->useCustomerTitleI18nQuery("address_title_i18n") + ->addAsColumn("address_TITLE", "address_title_i18n.SHORT") + ->endUse() + ->endUse() + ->addAsColumn("address_LABEL", AddressTableMap::LABEL) + ->addAsColumn("address_FIRST_NAME", AddressTableMap::FIRSTNAME) + ->addAsColumn("address_LAST_NAME", AddressTableMap::LASTNAME) + ->addAsColumn("address_COMPANY", AddressTableMap::COMPANY) + ->addAsColumn("address_ADDRESS1", AddressTableMap::ADDRESS1) + ->addAsColumn("address_ADDRESS2", AddressTableMap::ADDRESS2) + ->addAsColumn("address_ADDRESS3", AddressTableMap::ADDRESS3) + ->addAsColumn("address_ZIPCODE", AddressTableMap::ZIPCODE) + ->addAsColumn("address_CITY", AddressTableMap::CITY) + ->addAsColumn("address_PHONE", AddressTableMap::PHONE) + ->addAsColumn("address_CELLPHONE", AddressTableMap::CELLPHONE) + ->addAsColumn("address_IS_DEFAULT", AddressTableMap::IS_DEFAULT) + ->endUse() + ->addJoinObject($newsletterJoin) + ->addAsColumn("newsletter_IS_REGISTRED", "IF(NOT ISNULL(".NewsletterTableMap::EMAIL."),1,0)") + ->select([ + CustomerTableMap::ID, + CustomerTableMap::REF, + CustomerTableMap::LASTNAME, + CustomerTableMap::FIRSTNAME, + CustomerTableMap::EMAIL, + CustomerTableMap::DISCOUNT, + CustomerTableMap::CREATED_AT, + "title_TITLE", + "address_TITLE", + "address_LABEL", + "address_COMPANY", + "address_FIRST_NAME", + "address_LAST_NAME", + "address_ADDRESS1", + "address_ADDRESS2", + "address_ADDRESS3", + "address_ZIPCODE", + "address_CITY", + "address_COUNTRY", + "address_PHONE", + "address_CELLPHONE", + "address_IS_DEFAULT", + "newsletter_IS_REGISTRED", + ]) + ->orderById() + ; + + $this->addI18nCondition( + $query, + CountryI18nTableMap::TABLE_NAME, + CountryTableMap::ID, + CountryI18nTableMap::ID, + CountryI18nTableMap::LOCALE, + $locale + ); + + $this->addI18nCondition( + $query, + CustomerTitleI18nTableMap::TABLE_NAME, + "`customer_title_`.ID", + "`customer_title_i18n_`.ID", + "`customer_title_i18n_`.LOCALE", + $locale + ); + + $this->addI18nCondition( + $query, + CustomerTitleI18nTableMap::TABLE_NAME, + "`address_title`.ID", + "`address_title_i18n`.ID", + "`address_title_i18n`.LOCALE", + $locale + ); + + /** @var CustomerQuery $query */ + $results = $query + ->find() + ->toArray() + ; + + /** + * Then get the orders + */ + $orders = OrderQuery::create() + ->useCustomerQuery() + ->orderById() + ->endUse() + ->find() + ; + + /** + * And add them info the array + */ + $orders->rewind(); + + $arrayLength = count($results); + + $previousCustomerId = null; + + for ($i = 0; $i < $arrayLength; ++$i) { + $currentCustomer = &$results[$i]; + + $currentCustomerId = $currentCustomer[CustomerTableMap::ID]; + unset ($currentCustomer[CustomerTableMap::ID]); + + if ($currentCustomerId === $previousCustomerId) { + $currentCustomer["title_TITLE"] = ""; + $currentCustomer[CustomerTableMap::LASTNAME] = ""; + $currentCustomer[CustomerTableMap::FIRSTNAME] = ""; + $currentCustomer[CustomerTableMap::EMAIL] = ""; + $currentCustomer["address_COMPANY"] = ""; + $currentCustomer["newsletter_IS_REGISTRED"] = ""; + $currentCustomer[CustomerTableMap::CREATED_AT] = ""; + $currentCustomer[CustomerTableMap::DISCOUNT] = ""; + + $currentCustomer += [ + "order_TOTAL" => "", + "last_order_AMOUNT" => "", + "last_order_DATE" => "", + ]; + } else { + + /** + * Reformat created_at date + */ + $date = $currentCustomer[CustomerTableMap::CREATED_AT]; + $dateTime = new \DateTime($date); + $currentCustomer[CustomerTableMap::CREATED_AT] = $dateTime->format($lang->getDatetimeFormat()); + + + /** + * Then compute everything about the orders + */ + $total = 0; + $lastOrderAmount = 0; + $lastOrderDate = null; + $lastOrder = null; + $lastOrderCurrencyCode = null; + + $defaultCurrency = Currency::getDefaultCurrency(); + $defaultCurrencyCode = $defaultCurrency + ->setLocale($locale) + ->getCode() + ; + + if (empty($defaultCurrencyCode)) { + $defaultCurrencyCode = $defaultCurrency + ->setLocale($defaultLocale) + ->getCode() + ; + } + + $formattedDate = null; + + /** @var \Thelia\Model\Order $currentOrder */ + while (false !== $currentOrder = $orders->current()) { + if ($currentCustomerId != $currentOrder->getCustomerId()) { + break; + } + + $amount = $currentOrder->getTotalAmount($tax); + if (0 < $rate = $currentOrder->getCurrencyRate()) { + $amount = round($amount / $rate, 2); + } + + $total += $amount; + + /** @var \DateTime $date */ + $date = $currentOrder->getCreatedAt(); + + if (null === $lastOrderDate || $date > $lastOrderDate) { + $lastOrder = $currentOrder; + $lastOrderDate = $date; + } + + $orders->next(); + } + + if ($lastOrderDate !== null) { + $formattedDate = $lastOrderDate->format($lang->getDatetimeFormat()); + + $orderCurrency = $lastOrder->getCurrency(); + $lastOrderCurrencyCode = $orderCurrency + ->setLocale($locale) + ->getCode() + ; + + if (empty($lastOrderCurrencyCode)) { + $lastOrderCurrencyCode = $orderCurrency + ->setLocale($defaultLocale) + ->getCode() + ; + } + + $lastOrderAmount = $lastOrder->getTotalAmount($tax_); + } + + $currentCustomer += [ + "order_TOTAL" => $total . " " . $defaultCurrencyCode, + "last_order_AMOUNT" => $lastOrderAmount === 0 ? "" : $lastOrderAmount . " " . $lastOrderCurrencyCode, + "last_order_DATE" => $formattedDate, + ]; + } + + $previousCustomerId = $currentCustomerId; + } + + return $results; + } + + protected function getAliases() + { + return [ + CustomerTableMap::REF => "ref", + CustomerTableMap::LASTNAME => "last_name", + CustomerTableMap::FIRSTNAME => "first_name", + CustomerTableMap::EMAIL => "email", + CustomerTableMap::DISCOUNT => "discount", + CustomerTableMap::CREATED_AT => "sign_up_date", + "title_TITLE" => "title", + "address_TITLE" => "address_title", + "address_LABEL" => "label", + "address_IS_DEFAULT" => "is_default_address", + "address_COMPANY" => "company", + "address_ADDRESS1" => "address1", + "address_ADDRESS2" => "address2", + "address_ADDRESS3" => "address3", + "address_ZIPCODE" => "zipcode", + "address_CITY" => "city", + "address_COUNTRY" => "country", + "address_PHONE" => "phone", + "address_CELLPHONE" => "cellphone", + "address_FIRST_NAME" => "address_first_name", + "address_LAST_NAME" => "address_last_name", + "newsletter_IS_REGISTRED" => "is_registered_to_newsletter", + "order_TOTAL" => "total_orders", + "last_order_AMOUNT" => "last_order_amount", + "last_order_DATE" => "last_order_date", + ]; + } + + public function getOrder() + { + return [ + "ref", + "title", + "last_name", + "first_name", + "email", + "discount", + "is_registered_to_newsletter", + "sign_up_date", + "total_orders", + "last_order_amount", + "last_order_date", + "label", + "address_title", + "address_first_name", + "address_last_name", + "company", + "address1", + "address2", + "address3", + "zipcode", + "city", + "country", + "phone", + "cellphone", + "is_default_address", + ]; + } +} diff --git a/core/lib/Thelia/Tests/ImportExport/Export/CustomerExportTest.php b/core/lib/Thelia/Tests/ImportExport/Export/CustomerExportTest.php new file mode 100644 index 000000000..52c7359f2 --- /dev/null +++ b/core/lib/Thelia/Tests/ImportExport/Export/CustomerExportTest.php @@ -0,0 +1,195 @@ + + */ +class CustomerExportTest extends \PHPUnit_Framework_TestCase +{ + public function testQuery() + { + new Translator(new Container()); + + $handler = new CustomerExport(new Container()); + + $lang = Lang::getDefaultLanguage(); + $data = $handler->buildData($lang); + + $keys = ["ref","title","last_name","first_name","email","label", + "discount","is_registered_to_newsletter","sign_up_date", + "total_orders","last_order_amount","last_order_date", + "address_first_name","address_last_name","company","address1", + "address2","address3","zipcode","city","country","phone", + "cellphone","is_default_address","address_title"]; + + sort($keys); + + $rawData = $data->getData(); + + $max = CustomerQuery::create()->count(); + /** + * 30 customers that has more than 1 addresses or enough + */ + if (30 < $max) { + $max = 30; + } + + for ($i = 0; $i < $max;) { + $row = $rawData[$i]; + + $rowKeys = array_keys($row); + sort($rowKeys); + + $this->assertEquals($rowKeys, $keys); + + $customer = CustomerQuery::create() + ->findOneByRef($row["ref"]) + ; + + $this->assertNotNull($customer); + + $this->assertEquals($customer->getFirstname(), $row["first_name"]); + $this->assertEquals($customer->getLastname(), $row["last_name"]); + $this->assertEquals($customer->getEmail(), $row["email"]); + $this->assertEquals($customer->getCreatedAt()->format($lang->getDatetimeFormat()), $row["sign_up_date"]); + $this->assertEquals($customer->getDiscount(), $row["discount"]); + + $title = CustomerTitleQuery::create()->findPk($customer->getTitleId()); + $this->assertEquals($title->getShort(), $row["title"]); + + $total = 0; + foreach ($customer->getOrders() as $order) { + $amount = $order->getTotalAmount($tax); + + if (0 < $rate = $order->getCurrencyRate()) { + $amount = round($amount / $rate, 2); + } + + $total += $amount; + } + + $defaultCurrencyCode = Currency::getDefaultCurrency()->getCode(); + $this->assertEquals($total . " " . $defaultCurrencyCode, $row["total_orders"]); + + $lastOrder = OrderQuery::create() + ->filterByCustomer($customer) + ->orderByCreatedAt(Criteria::DESC) + ->findOne() + ; + + if (null !== $lastOrder) { + $expectedPrice = $lastOrder->getTotalAmount($tax_) . " " . $lastOrder->getCurrency()->getCode(); + $expectedDate = $lastOrder->getCreatedAt()->format($lang->getDatetimeFormat()); + } else { + $expectedPrice = ""; + $expectedDate = ""; + } + + $this->assertEquals( + $expectedPrice, + $row["last_order_amount"] + ); + + $this->assertEquals( + $expectedDate, + $row["last_order_date"] + ); + + $newsletter = NewsletterQuery::create() + ->findOneByEmail($customer->getEmail()) + ; + + $this->assertEquals( + $newsletter === null ? 0 : 1, + $row["is_registered_to_newsletter"] + ); + + do { + $address = AddressQuery::create() + ->filterByCustomer($customer) + ->filterByAddress1($rawData[$i]["address1"]) + ->filterByAddress2($rawData[$i]["address2"]) + ->_if(empty($rawData[$i]["address2"])) + ->_or() + ->filterByAddress2(null, Criteria::ISNULL) + ->_endif() + ->filterByAddress3($rawData[$i]["address3"]) + ->_if(empty($rawData[$i]["address2"])) + ->_or() + ->filterByAddress2(null, Criteria::ISNULL) + ->_endif() + ->filterByFirstname($rawData[$i]["address_first_name"]) + ->filterByLastname($rawData[$i]["address_last_name"]) + ->filterByCountryId( + CountryI18nQuery::create() + ->filterByLocale($lang->getLocale()) + ->findOneByTitle($rawData[$i]["country"]) + ->getId() + ) + ->filterByCompany($rawData[$i]["company"]) + ->_if(empty($rawData[$i]["company"])) + ->_or() + ->filterByCompany(null, Criteria::ISNULL) + ->_endif() + ->filterByZipcode($rawData[$i]["zipcode"]) + ->filterByCity($rawData[$i]["city"]) + ->filterByIsDefault($rawData[$i]["is_default_address"]) + ->filterByCellphone($rawData[$i]["cellphone"]) + ->_if(empty($rawData[$i]["cellphone"])) + ->_or() + ->filterByCompany(null, Criteria::ISNULL) + ->_endif() + ->filterByPhone($rawData[$i]["phone"]) + ->filterByLabel($rawData[$i]["label"]) + ->filterByTitleId( + CustomerTitleI18nQuery::create() + ->filterByLocale($lang->getLocale()) + ->findOneByShort($rawData[$i]["address_title"]) + ->getId() + ) + ->find() + ; + + $this->assertEquals(1, $address->count()); + + $rowKeys = array_keys($rawData[$i]); + sort($rowKeys); + + $this->assertEquals($rowKeys, $keys); + + ++$i; + } while ( + isset($rawData[$i]["ref"]) && + $rawData[$i-1]["ref"] === $rawData[$i]["ref"] && + ++$max + ); + + } + } +} \ No newline at end of file