Version: current

      1 <?php
      2
      3 /**
      4  * Заказ
      5  *
      6  * @package cart
      7  * @author inxo, dvp
      8  */
      9 class Order extends DataObject {
     10
     11     static $db = array(
     12         'ClientName' => 'Text', // Имя клиента
     13         'Email' => 'Varchar', // Электропочта
     14         'Phone' => 'Varchar', // Телефон
     15         'ClientNotes' => 'Text', // Комментарий к заказу
     16         'IP' => 'Varchar',
     17         'HashLink' => 'Varchar',
     18
     19         'ItemsTotal' => 'CatalogPrice', // ??? Currency
     20         // ShippingTotal - from cart_shipping
     21         'Discount' => 'CatalogPrice', // Скидка применяемая при заказе
     22         'GrandTotal' => 'CatalogPrice', // ??? Currency
     23
     24         'AdminNotes' => 'Text', // Комментарий администратора
     25         'Status' => "Enum('Unpaid,Query,Paid,Processing,Sent,Complete,AdminCancelled,MemberCancelled')",
     26     );
     27
     28     static $has_one = array(
     29         'Member' => 'Member', // Пользователь заказчик
     30 //      'Payment' => 'Payment', - from cart_payment
     31 //      'ShippingMethod' => 'ShippingMethod', - from cart_shipping
     32 //      'Address' => 'Address', // Адрес клиента - cart_shipping
     33     );
     34
     35     static $has_many = array(
     36         'Items' => 'OrderItem', // Продукты
     37         'OrderLog' => 'Order_StatusLog', // Изменения статуса заказа
     38     );
     39
     40     static $defaults = array(
     41         'Status' => 'Unpaid',
     42     );
     43
     44     static $casting = array(
     45         'TotalPrice' => 'CatalogPrice',
     46         'StatusTitle' => 'Varchar',
     47     );
     48
     49     static $searchable_fields = array(
     50         'ID' => array(
     51             'field' => 'NumericField',
     52             'filter' => 'ExactMatchFilter'
     53         ),
     54         'ClientName' => array(
     55             'field' => 'TextField',
     56             'filter' => 'PartialMatchFilter',
     57         ),
     58         'Status' => array(
     59             'field' => 'DropdownField',
     60             'filter' => 'ExactMatchFilter',
     61         ),
     62         'LastEdited' => array(
     63             'field' => 'DateField',
     64             'filter' => 'GreaterThanFilter',
     65             "title" => 'Не раньше'
     66         ),
     67     );
     68
     69     static $default_sort = 'ID DESC';
     70     static $summary_fields = array('ID', 'ClientName', 'Created', 'StatusTitle', 'GrandTotal', 'Items');
     71
     72     static $required_fields = array('ClientName', 'Phone');
     73
     74     static $use_shipping = false;
     75     static $use_payments = false;
     76
     77     static $can_cancel_before_payment = true;
     78     static $can_cancel_before_processing = false;
     79     static $can_cancel_before_sending = false;
     80     static $can_cancel_after_sending = false;
     81
     82     /**
     83      * Изменение списка обязательных полей заказа
     84      *
     85      * @param array $val - новый список обязательных полей
     86      */
     87     static function set_required_fields($val) { self::$required_fields = $val; }
     88
     89     /**
     90      * Локализованное название статуса
     91      *
     92      * Локализация с помощью файла переводов, константы вида "Order.Status_{$status}"
     93      *
     94      * @param string $status - код статуса
     95      *
     96      * @return string
     97      */
     98     static function status_title($status) {
     99         return _t('Order.Status_' . $status, $status);
    100     }
    101
    102     static function get_by_hash($hash) {
    103         $hash = Convert::raw2sql($hash);
    104         return DataObject::get_one('Order', "HashLink='{$hash}'");
    105     }
    106
    107     function fieldLabels($includerelations = true) {
    108         $labels = parent::fieldLabels($includerelations);
    109
    110         $labels['ID'] = _t('Order.db_ID', 'Order ID');
    111         $labels['Created'] = _t('Order.db_Created', 'Created');
    112         $labels['LastEdited'] = _t('Order.db_LastEdited', 'Changed');
    113         $labels['StatusTitle'] = _t('Order.db_Status', 'Status');
    114         return $labels;
    115     }
    116
    117     /**
    118      * Список обязательных полей для формы заказа
    119      *
    120      * В расширении можно определить метод:
    121      * updateRequiredFields(array &$fields) - для изменения списка обязательных полей формы
    122      *
    123      * @return array
    124      */
    125     function getRequredFields() {
    126         $fields = self::$required_fields;
    127         $this->extend('updateRequiredFields', $fields);
    128         return $fields;
    129     }
    130
    131     /**
    132      * Список полей для шага контактной информации
    133      *
    134      * В расширении можно определить метод:
    135      * updateContactFields(FieldSet &$fields) - для изменения списка полей
    136      *
    137      * @return FieldSet
    138      */
    139     function getContactFields() {
    140         $fields = new FieldSet(
    141             $f = new TextField('ClientName', _t('OrderForm.ClientName', 'ClientName')),
    142             new PhoneField('Phone', _t('OrderForm.Phone', 'Phone')),
    143             new EmailField('Email', _t('OrderForm.Email', 'Email'))
    144         );
    145         $f->setAutocomplete('name');
    146         $this->extend('updateContactFields', $fields);
    147         return $fields;
    148     }
    149
    150     /**
    151      * Список полей для шага подтверждения
    152      *
    153      * В расширении можно определить метод:
    154      * updateSummaryFields(FieldSet &$fields) - для изменения списка полей формы
    155      *
    156      * @return FieldSet
    157      */
    158     function getSummaryFields() {
    159         $fields = new FieldSet(
    160             new TextareaField('ClientNotes', _t('OrderForm.ClientNotes', 'Notes'))
    161         );
    162         $siteConfig = SiteConfig::current_site_config();
    163         if ($siteConfig->hasMethod('SiteAgreementField') && ($rulesField = $siteConfig->SiteAgreementField())) {
    164             $fields->push($rulesField);
    165         }
    166         $this->extend('updateSummaryFields', $fields);
    167         return $fields;
    168     }
    169
    170     /**
    171      * Список полей для формы заказа в целом
    172      *
    173      * В расширении можно определить метод:
    174      * updateFrontendFields(FieldSet &$fields) - для изменения списка полей формы
    175      *
    176      * @return FieldSet
    177      */
    178     function getFrontendFields() {
    179         $fields = new FieldSet(
    180             new HeaderField('hdrContacts', _t('OrderForm.ContactsHeader', 'Contacts'), 2 , true)
    181         );
    182         foreach ($this->getContactFields() as $item) {
    183             $fields->push($item);
    184         }
    185
    186         $fields->push(new HeaderField('hdrSummary', _t('OrderForm.SummaryHeader', 'Summary'), 2 , true));
    187         foreach ($this->getSummaryFields() as $item) {
    188             $fields->push($item);
    189         }
    190         $siteConfig = SiteConfig::current_site_config();
    191         if ($siteConfig->hasMethod('SiteAgreementField') && ($rulesField = $siteConfig->SiteAgreementField())) {
    192             $fields->push($rulesField);
    193         }
    194         $this->extend('updateFrontendFields', $fields);
    195         return $fields;
    196     }
    197
    198     function getCMSFields() {
    199         DataObject::disableCMSFieldsExtensions();
    200         $fields = parent::getCMSFields();
    201         DataObject::enableCMSFieldsExtensions();
    202
    203         $fields->removeByName('HashLink');
    204         $fields->removeByName('IP');
    205
    206         // ClientName, Email, Phone, ClientNotes
    207         $fields->replaceField('ClientName', new TextField('ClientName', $this->fieldLabel('ClientName')));
    208         $fields->replaceField('Email', new EmailField('Email', $this->fieldLabel('Email')));
    209
    210         // ItemsTotal (RO), ShippingTotal (RO), Total (RO)
    211         foreach(array('ItemsTotal', 'GrandTotal') as $fieldName) {
    212             $fields->replaceField($fieldName, new ReadonlyField($fieldName, $this->fieldLabel($fieldName)));
    213         }
    214         foreach(array('Discount', 'ItemsTotal', 'GrandTotal') as $fieldName) {
    215             $f = $fields->dataFieldByName($fieldName);
    216             $f->setTitle(sprintf('%s (%s)', $f->Title(),  $this->Currency()));
    217         }
    218
    219         // TODO Shipping (RO?)
    220         // TODO Payment (RO?)
    221
    222         // AdminNotes, Status, StatusLog.Note
    223         $fields->addFieldToTab('Root.Main', new HeaderField('hdrManage', _t('Order.ManageHeader', 'Manage order')), 'AdminNotes');
    224
    225         $statusValues = $this->dbObject('Status')->enumValues();
    226         unset($statusValues['MemberCancelled']);
    227         foreach ($statusValues as $statusItem => $statusName) {
    228             $statusValues[$statusItem] = Order::status_title($statusItem);
    229         }
    230         $fields->replaceField('Status', new DropdownField('Status', $this->fieldLabel('Status'), $statusValues ));
    231         $fields->addFieldToTab('Root.Main', new TextareaField('Note', _t('Order_StatusLog.db_Note', 'Status change note')));
    232
    233         // Items (tab)
    234         $table = new ComplexTableField($this, 'Products', 'OrderItem', null, null, 'OrderID = ' . $this->ID);
    235         $table->setPermissions(array('add', 'edit', 'delete'));
    236         $table->setPopupCaption(_t('OrderAdmin.EditItem', 'Edit Item'));
    237         $fields->replaceField('Items', $table);
    238
    239         // OrderLog (tab, RO)
    240         $log = $fields->dataFieldByName('OrderLog');
    241         $log->setPermissions(array());
    242         $log->setPagesize(30);
    243
    244         // CHECK Member (tab, RO):
    245         $fields->removeByName('MemberID');
    246
    247         if ($this->MemberID && $this->Member()->ID) {
    248             $fields->findOrMakeTab('Root.Member', _t('Order.tab_Member', 'Client'));
    249             $fields->addFieldToTab('Root.Member', new HeaderField('hdrMember', _t('Order.MemberHeader', 'Client')));
    250             // Member:  Title, Email, Phone, Created, LastVisited, NumVisit, Discount,
    251             foreach (array('Title', 'Email', 'Phone', 'Created', 'LastVisited', 'NumVisit', 'Discount') as $name) {
    252                 $fields->addFieldToTab('Root.Member', new TextLiteralField('Member' . $name, $this->Member()->fieldLabel($name), $this->Member()->$name));
    253             }
    254
    255             // Member Order History
    256             $fields->addFieldToTab('Root.Member', new HeaderField('hdrStatistic', _t('Order.StatisticHeader', 'Order Statistic')));
    257
    258             $fields->addFieldToTab('Root.Member', new LiteralField(
    259                 'OrdersCount',
    260                 sprintf(_t('Order.OrdersCount', 'Orders total count: %d'), $this->Member()->Orders()->Count())
    261             ));
    262
    263             $fields->addFieldToTab('Root.Member', new LiteralField(
    264                 'OrdersSum',
    265                 sprintf(
    266                     _t('Order.OrdersSum', 'Orders total price: %.2f %s'),
    267                     array_sum($this->Member()->Orders()->column('GrandTotal')),
    268                     $this->Currency()
    269                 )
    270             ));
    271
    272             $orderFields = $this->summaryFields();
    273             unset($orderFields['ClientName']);
    274             unset($orderFields['Items']);
    275             $table = new ComplexTableField($this->Member(), 'Orders', 'Order', $orderFields, null, 'MemberID = ' . $this->MemberID);
    276             $table->setPermissions(array());
    277             $fields->addFieldToTab('Root.Member', $table);
    278         }
    279
    280         $this->extend('updateCMSFields', $fields);
    281         return $fields;
    282     }
    283
    284     function onBeforeWrite() {
    285         // новый заказ - запомним создателя
    286         if (!$this->isInDB()) {
    287             $this->MemberID = (Member::currentUserID()) ? Member::currentUserID() : 0;
    288             $this->IP = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
    289         }
    290
    291         if (!$this->HashLink) {
    292             $this->HashLink = md5(time() . serialize($this->data()) . rand());
    293         }
    294
    295         // если поменяли список товаров
    296         if ($this->getTotalPrice() != $this->ItemsTotal)
    297             $this->ItemsTotal = $this->getTotalPrice();
    298
    299         if ($this->isChanged('Discount') && $this->Discount < 0) {
    300             $this->Discount = 0;
    301         }
    302
    303         // пересчитываем всегда т.к. можем дополнить в расширениях
    304         // $this->GrandTotal = $this->ItemsTotal - $this->Discount;
    305
    306         // RNW
    307         // А вот теперь действительно пересчитываем.
    308         $this->GrandTotal = $this->calculateGrandTotal();
    309
    310         parent::onBeforeWrite();
    311     }
    312
    313     function onAfterWrite() {
    314         // новый заказ
    315         if ($this->isChanged('ID')) {
    316             $log = new Order_StatusLog();
    317             $log->Status = $this->StatusTitle();
    318             $log->Note = ($this->Note) ? $this->Note : _t('Order.OrderCreated', 'Odrder was created');
    319             $log->OrderID = $this->ID;
    320             $log->write();
    321         }
    322         // Если поменяли статус
    323         else if ($this->isChanged('Status')) {
    324             $log = new Order_StatusLog();
    325             $log->Status = $this->StatusTitle();
    326             $log->Note = ($this->Note) ? $this->Note : _t('Order.StatusChanged', 'Odrder was changed');
    327             $log->OrderID = $this->ID;
    328             $log->write();
    329
    330             // универсальные уведомления (модуль cart_notification)
    331             $changedFields = $this->getChangedFields();
    332             $note = $log->Note;
    333             $this->extend('OnAfterChangeStatus', Member::CurrentUser(), $changedFields['Status'], $note);
    334         }
    335         parent::onAfterWrite();
    336     }
    337
    338     function OnBeforeDelete() {
    339         foreach ($this->OrderLog() as $log) {
    340             $log->delete();
    341         }
    342         parent::OnBeforeDelete();
    343     }
    344
    345     /**
    346      * Возвращает минимальную сумму для заказа
    347      *
    348      * @param bool $real - возвращать число, а не объект CatalogPrice
    349      *
    350      * @return int
    351      */
    352     function MinTotalPrice($real=false) {
    353         if ($real) {
    354             return SiteConfig::current_site_config()->CartMinimalOrderPrice;
    355         }
    356         return DBField::create('CatalogPrice', SiteConfig::current_site_config()->CartMinimalOrderPrice);
    357     }
    358
    359     /**
    360      * Проверяет достаточна ли сумма заказа для оформления покупки
    361      *
    362      * @param int $price - сумма заказа [сумма в корзине]
    363      *
    364      * @return boolean
    365      */
    366     function CanCheckout($price=null) {
    367         if (!$this->exists())
    368             return false;
    369
    370         $minPrice = $this->MinTotalPrice(1);
    371         if ($minPrice == 0)
    372             return true;
    373
    374         if (!$price) {
    375             $price = $this->getTotalPrice();
    376         }
    377
    378         return ($price >= $minPrice);
    379     }
    380
    381     //Пересчет Общей стоимости заказа с учетом возможных его изменений в расширениях
    382     function calculateGrandTotal() {
    383         $itemsTotal = $this->getTotalPrice();
    384         $discount = $this->Discount;
    385         $grandTotal = $itemsTotal - $discount;
    386         $this->extend('updateGrandTotal', $grandTotal);
    387         return $grandTotal;
    388     }
    389
    390     //Геттер Общей стоимости заказа
    391     function getGrandTotal() {
    392         return (isset($this->record['GrandTotal']) && $this->record['GrandTotal']) ? $this->record['GrandTotal'] : $this->calculateGrandTotal();
    393     }
    394
    395     function exists() {
    396         if ($this->Items() && $this->Items()->Count() > 0) return true;
    397         parent::exists();
    398     }
    399
    400     /**
    401      * Валидация заказа
    402      *
    403      * Выполняется каждый раз при write() поэтому надо быть остороджным
    404      *
    405      * @return ValidationResult
    406      */
    407     function validate() {
    408         $result = new ValidationResult();
    409         foreach (self::getRequredFields() as $name) {
    410             if (!$this->hasValue($name)) {
    411                 $result->error(sprintf(_t('Order.FieldRequired', 'Field "%s" is required'), $this->fieldLabel($name)));
    412             }
    413         }
    414         if (!$this->Items() || $this->Items()->Count() == 0) {
    415             $result->error(_t('Order.ItemsRequired', 'Not items in cart'));
    416         }
    417         if ($result->valid()) {
    418             $this->extend('updateValidationResult', $result);
    419         }
    420         return $result;
    421     }
    422
    423     /**
    424      * Возвращает валюту заказа
    425      *
    426      * В настоящее время в заказе не хранится, а используется настройка валяюты каталога
    427      *
    428      * @return string
    429      */
    430     function Currency() {
    431         return SiteConfig::current_site_config()->CatalogCurrency;
    432     }
    433
    434     // Общая цена
    435     function getTotalPrice() {
    436         $ceil = SiteConfig::current_site_config()->CatalogDiscountCeil;
    437         $result = 0;
    438         if ($items = $this->Items()) {
    439             foreach ($items as $item) {
    440                 $price = $item->TotalPrice;
    441                 $result += ($ceil) ? ceil($price) : round($price, 2);
    442             }
    443         }
    444         return $result;
    445     }
    446
    447     // Общее кол-во товаров
    448     function getTotalQuantity() {
    449         $total = 0;
    450         if ($this->Items()) {
    451             foreach ($this->Items() as $item) {
    452                 $total += $item->getQuantity();
    453             }
    454         }
    455         return $total;
    456     }
    457
    458     // Локализованное название статуса
    459     function StatusTitle() {
    460         return self::status_title($this->Status);
    461     }
    462
    463     // Правильные числительные
    464     function ItemName() {
    465         return Convert::number2name($this->getTotalQuantity(), _t('Cart.ItemName1', 'product'), _t('Cart.ItemName2', 'products'), _t('Cart.ItemName3', 'products'));
    466     }
    467
    468     /**
    469      * Возвращает данные для ajax корзины.
    470      *
    471      * @param array $js
    472      */
    473     function updateForAjax(array &$js) {
    474         $js[] = array('total' => array(
    475                 'Quantity' => $this->getTotalQuantity(),
    476                 'TotalPrice' => $this->getTotalPrice(),
    477                 'ItemName' => $this->ItemName(),
    478                 ));
    479     }
    480
    481     public function canCancel() {
    482         switch ($this->Status) {
    483             case 'Unpaid':
    484             case 'Query':
    485                 return self::$can_cancel_before_payment;
    486             case 'Paid': return self::$can_cancel_before_processing;
    487             case 'Processing': return self::$can_cancel_before_sending;
    488             case 'Sent':
    489             case 'Complete':
    490                 return self::$can_cancel_after_sending;
    491             default : return false;
    492         }
    493     }
    494
    495     // Покупки или из базы или из корзины
    496     function Items() {
    497         if ($this->ID) {
    498             return $this->itemsFromDatabase();
    499         } else {
    500             $items = Cart::get_items();
    501             if ($items){
    502                 return $this->createItems($items);
    503             }
    504         }
    505     }
    506
    507     private function childClass() {
    508         $class = $this->stat('has_many');
    509         return $class['Items'];
    510     }
    511
    512     /**
    513      * Создаем список покупок
    514      *
    515      * @param array $items - список товаров
    516      * @param bool $write - сохранять ли его в БД
    517      *
    518      * @return DataObjectSet - список товаров в заказе
    519      */
    520     function createItems(array $items, $write = false) {
    521         if ($write) {
    522             foreach ($items as $item) {
    523                 $item->OrderID = $this->ID;
    524                 $item->write();
    525             }
    526         }
    527         return $write ? $this->itemsFromDatabase() : $this->createDataObjectSet($items);
    528     }
    529
    530     /**
    531      * Список товаров из БД
    532      *
    533      * @return DataObjectSet - список товаров в заказе
    534      */
    535     protected function itemsFromDatabase() {
    536         return DataObject::get('OrderItem', "\"OrderID\" = '$this->ID'");
    537     }
    538
    539     /**
    540      * Создает DataObjectSet из массива товаров из корзины
    541      *
    542      * @param array $items
    543      *
    544      * @return DataObjectSet - список товаров в заказе
    545      */
    546     protected function createDataObjectSet($items) {
    547         foreach ($items as $item) {
    548             $item->_id = $item->_productId;
    549         }
    550         return new DataObjectSet($items);
    551     }
    552
    553     function Link() {
    554         return ($this->MemberID && CartSiteConfig::CartRegistraionAvailable()) ? ProfilePage::find_link('order/' . $this->HashLink) : CheckoutPage::find_link('order/' . $this->HashLink);
    555     }
    556
    557     function PrintLink() {
    558         return ($this->MemberID && CartSiteConfig::CartRegistraionAvailable()) ? ProfilePage::find_link('printorder/' . $this->HashLink) : CheckoutPage::find_link('printorder/' . $this->HashLink);
    559     }
    560
    561     function CancelLink() {
    562         if ($this->canCancel()) {
    563             return ($this->MemberID && CartSiteConfig::CartRegistraionAvailable()) ? ProfilePage::find_link('cancel_order/' . $this->HashLink) : false;
    564         }
    565         return false;
    566     }
    567 }
    568
    569 /**
    570  * Запись об изменении статуса заказа
    571  *
    572  * @author inxo, dvp
    573  */
    574 class Order_StatusLog extends DataObject {
    575
    576     static $db = array(
    577         'Status' => 'Varchar(255)', // Новый статус
    578         'Note' => 'Text', // Комментарий к изменению
    579     );
    580
    581     static $has_one = array(
    582         'Creator' => 'Member',  // Автор изменения
    583         'Order' => 'Order', // Связанный заказ
    584     );
    585
    586     static $summary_fields = array(
    587         'Created', 'Status', 'Note'
    588     );
    589
    590     static $searchable_fields = array(
    591         'OrderID' => array(
    592             'field' => 'NumericField',
    593             'filter' => 'ExactMatchFilter'
    594         ),
    595     );
    596
    597     static $default_sort = 'ID';
    598
    599     function fieldLabels($includerelations = true) {
    600         $labels = parent::fieldLabels($includerelations);
    601         $labels['OrderID'] = _t('Order.db_ID', 'Order ID');
    602         $labels['Created'] = _t('Order_StatusLog.db_Created', 'Status time');
    603         return $labels;
    604     }
    605
    606     function getCMSFields() {
    607         $fields = parent::getCMSFields();
    608         $fields->replaceField('OrderID', new ReadonlyField('OrderID', $this->fieldLabel('Order'), $this->OrderID));
    609         return $fields;
    610     }
    611
    612     function canEdit() {
    613         return false;
    614     }
    615
    616     function canDelete() {
    617         return false;
    618     }
    619
    620     function onBeforeWrite() {
    621         if (!$this->ID) {
    622             $this->CreatorID = (Member::currentUserID()) ? Member::currentUserID() : 1;
    623         }
    624         parent::onBeforeWrite();
    625     }
    626
    627     function Link() {
    628         return $this->Order()->Link();
    629     }
    630
    631     function PrintLink() {
    632         return $this->Order()->PrintLink();
    633     }
    634 }
    635
    636 class Order_CancelForm extends Form {
    637
    638     function __construct($controller, $name, $orderID) {
    639         $fields = new FieldSet(
    640                         new HiddenField('OrderID', '', $orderID)
    641         );
    642         $actions = new FieldSet(
    643                         new FormAction('doCancel', 'Cancel Order')
    644         );
    645         parent::__construct($controller, $name, $fields, $actions);
    646     }
    647
    648     function doCancel($data, $form) {
    649         $SQL_data = Convert::raw2sql($data);
    650         $order = DataObject::get_by_id('Order', $SQL_data['OrderID']);
    651         $order->Status = 'MemberCancelled';
    652         $order->write();
    653         Director::redirectBack();
    654         return;
    655     }
    656 }
    657
    658

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -