Completed
Push — master ( ce2dd0...10a6ac )
by Nicolaas
04:02
created

code/model/Order.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * @description:
5
 * The order class is a databound object for handling Orders within SilverStripe.
6
 * Note that it works closely with the ShoppingCart class, which accompanies the Order
7
 * until it has been paid for / confirmed by the user.
8
 *
9
 *
10
 * CONTENTS:
11
 * ----------------------------------------------
12
 * 1. CMS STUFF
13
 * 2. MAIN TRANSITION FUNCTIONS
14
 * 3. STATUS RELATED FUNCTIONS / SHORTCUTS
15
 * 4. LINKING ORDER WITH MEMBER AND ADDRESS
16
 * 5. CUSTOMER COMMUNICATION
17
 * 6. ITEM MANAGEMENT
18
 * 7. CRUD METHODS (e.g. canView, canEdit, canDelete, etc...)
19
 * 8. GET METHODS (e.g. Total, SubTotal, Title, etc...)
20
 * 9. TEMPLATE RELATED STUFF
21
 * 10. STANDARD SS METHODS (requireDefaultRecords, onBeforeDelete, etc...)
22
 * 11. DEBUG
23
 *
24
 * @authors: Nicolaas [at] Sunny Side Up .co.nz
25
 * @package: ecommerce
26
 * @sub-package: model
27
 * @inspiration: Silverstripe Ltd, Jeremy
28
 *
29
 * NOTE: This is the SQL for selecting orders in sequence of
30
 **/
31
class Order extends DataObject implements EditableEcommerceObject
32
{
33
    /**
34
     * API Control.
35
     *
36
     * @var array
37
     */
38
    private static $api_access = array(
39
        'view' => array(
40
            'OrderEmail',
41
            'EmailLink',
42
            'PrintLink',
43
            'RetrieveLink',
44
            'ShareLink',
45
            'FeedbackLink',
46
            'Title',
47
            'Total',
48
            'SubTotal',
49
            'TotalPaid',
50
            'TotalOutstanding',
51
            'ExchangeRate',
52
            'CurrencyUsed',
53
            'TotalItems',
54
            'TotalItemsTimesQuantity',
55
            'IsCancelled',
56
            'Country',
57
            'FullNameCountry',
58
            'IsSubmitted',
59
            'CustomerStatus',
60
            'CanHaveShippingAddress',
61
            'CancelledBy',
62
            'CurrencyUsed',
63
            'BillingAddress',
64
            'UseShippingAddress',
65
            'ShippingAddress',
66
            'Status',
67
            'Attributes',
68
            'OrderStatusLogs',
69
            'MemberID',
70
        ),
71
    );
72
73
    /**
74
     * standard SS variable.
75
     *
76
     * @var array
77
     */
78
    private static $db = array(
79
        'SessionID' => 'Varchar(32)', //so that in the future we can link sessions with Orders.... One session can have several orders, but an order can onnly have one session
80
        'UseShippingAddress' => 'Boolean',
81
        'CustomerOrderNote' => 'Text',
82
        'ExchangeRate' => 'Double',
83
        //'TotalItems_Saved' => 'Double',
84
        //'TotalItemsTimesQuantity_Saved' => 'Double'
85
    );
86
87
    private static $has_one = array(
88
        'Member' => 'Member',
89
        'BillingAddress' => 'BillingAddress',
90
        'ShippingAddress' => 'ShippingAddress',
91
        'Status' => 'OrderStep',
92
        'CancelledBy' => 'Member',
93
        'CurrencyUsed' => 'EcommerceCurrency',
94
    );
95
96
    /**
97
     * standard SS variable.
98
     *
99
     * @var array
100
     */
101
    private static $has_many = array(
102
        'Attributes' => 'OrderAttribute',
103
        'OrderStatusLogs' => 'OrderStatusLog',
104
        'Payments' => 'EcommercePayment',
105
        'Emails' => 'OrderEmailRecord',
106
        'OrderProcessQueue' => 'OrderProcessQueue' //there is usually only one.
107
    );
108
109
    /**
110
     * standard SS variable.
111
     *
112
     * @var array
113
     */
114
    private static $indexes = array(
115
        'SessionID' => true,
116
    );
117
118
    /**
119
     * standard SS variable.
120
     *
121
     * @var string
122
     */
123
    private static $default_sort = '"LastEdited" DESC';
124
125
    /**
126
     * standard SS variable.
127
     *
128
     * @var array
129
     */
130
    private static $casting = array(
131
        'OrderEmail' => 'Varchar',
132
        'EmailLink' => 'Varchar',
133
        'PrintLink' => 'Varchar',
134
        'ShareLink' => 'Varchar',
135
        'FeedbackLink' => 'Varchar',
136
        'RetrieveLink' => 'Varchar',
137
        'Title' => 'Varchar',
138
        'Total' => 'Currency',
139
        'TotalAsMoney' => 'Money',
140
        'SubTotal' => 'Currency',
141
        'SubTotalAsMoney' => 'Money',
142
        'TotalPaid' => 'Currency',
143
        'TotalPaidAsMoney' => 'Money',
144
        'TotalOutstanding' => 'Currency',
145
        'TotalOutstandingAsMoney' => 'Money',
146
        'HasAlternativeCurrency' => 'Boolean',
147
        'TotalItems' => 'Double',
148
        'TotalItemsTimesQuantity' => 'Double',
149
        'IsCancelled' => 'Boolean',
150
        'IsPaidNice' => 'Boolean',
151
        'Country' => 'Varchar(3)', //This is the applicable country for the order - for tax purposes, etc....
152
        'FullNameCountry' => 'Varchar',
153
        'IsSubmitted' => 'Boolean',
154
        'CustomerStatus' => 'Varchar',
155
        'CanHaveShippingAddress' => 'Boolean',
156
    );
157
158
    /**
159
     * standard SS variable.
160
     *
161
     * @var string
162
     */
163
    private static $singular_name = 'Order';
164
    public function i18n_singular_name()
165
    {
166
        return _t('Order.ORDER', 'Order');
167
    }
168
169
    /**
170
     * standard SS variable.
171
     *
172
     * @var string
173
     */
174
    private static $plural_name = 'Orders';
175
    public function i18n_plural_name()
176
    {
177
        return _t('Order.ORDERS', 'Orders');
178
    }
179
180
    /**
181
     * Standard SS variable.
182
     *
183
     * @var string
184
     */
185
    private static $description = "A collection of items that together make up the 'Order'.  An order can be placed.";
186
187
    /**
188
     * Tells us if an order needs to be recalculated
189
     * can save one for each order...
190
     *
191
     * @var array
192
     */
193
    private static $_needs_recalculating = array();
194
195
    /**
196
     * @param bool (optional) $b
197
     * @param int (optional)  $orderID
198
     *
199
     * @return bool
200
     */
201
    public static function set_needs_recalculating($b = true, $orderID = 0)
202
    {
203
        self::$_needs_recalculating[$orderID] = $b;
204
    }
205
206
    /**
207
     * @param int (optional) $orderID
208
     *
209
     * @return bool
210
     */
211
    public static function get_needs_recalculating($orderID = 0)
212
    {
213
        return isset(self::$_needs_recalculating[$orderID]) ? self::$_needs_recalculating[$orderID] : false;
214
    }
215
216
    /**
217
     * Total Items : total items in cart
218
     * We start with -1 to easily identify if it has been run before.
219
     *
220
     * @var int
221
     */
222
    protected $totalItems = null;
223
224
    /**
225
     * Total Items : total items in cart
226
     * We start with -1 to easily identify if it has been run before.
227
     *
228
     * @var float
229
     */
230
    protected $totalItemsTimesQuantity = null;
231
232
    /**
233
     * Returns a set of modifier forms for use in the checkout order form,
234
     * Controller is optional, because the orderForm has its own default controller.
235
     *
236
     * This method only returns the Forms that should be included outside
237
     * the editable table... Forms within it can be called
238
     * from through the modifier itself.
239
     *
240
     * @param Controller $optionalController
241
     * @param Validator  $optionalValidator
242
     *
243
     * @return ArrayList (ModifierForms) | Null
244
     **/
245
    public function getModifierForms(Controller $optionalController = null, Validator $optionalValidator = null)
246
    {
247
        $arrayList = new ArrayList();
248
        $modifiers = $this->Modifiers();
249
        if ($modifiers->count()) {
250
            foreach ($modifiers as $modifier) {
251
                if ($modifier->ShowForm()) {
252
                    if ($form = $modifier->getModifierForm($optionalController, $optionalValidator)) {
253
                        $form->ShowFormInEditableOrderTable = $modifier->ShowFormInEditableOrderTable();
254
                        $form->ShowFormOutsideEditableOrderTable = $modifier->ShowFormOutsideEditableOrderTable();
255
                        $form->ModifierName = $modifier->ClassName;
256
                        $arrayList->push($form);
257
                    }
258
                }
259
            }
260
        }
261
        if ($arrayList->count()) {
262
            return $arrayList;
263
        } else {
264
            return;
265
        }
266
    }
267
268
    /**
269
     * This function returns the OrderSteps.
270
     *
271
     * @return ArrayList (OrderSteps)
272
     **/
273
    public static function get_order_status_options()
274
    {
275
        return OrderStep::get();
276
    }
277
278
    /**
279
     * Like the standard byID, but it checks whether we are allowed to view the order.
280
     *
281
     * @return: Order | Null
282
     **/
283
    public static function get_by_id_if_can_view($id)
284
    {
285
        $order = Order::get()->byID($id);
286
        if ($order && $order->canView()) {
287
            if ($order->IsSubmitted()) {
288
                // LITTLE HACK TO MAKE SURE WE SHOW THE LATEST INFORMATION!
289
                $order->tryToFinaliseOrder();
290
            }
291
292
            return $order;
293
        }
294
295
        return;
296
    }
297
298
    /**
299
     * returns a Datalist with the submitted order log included
300
     * this allows you to sort the orders by their submit dates.
301
     * You can retrieve this list and then add more to it (e.g. additional filters, additional joins, etc...).
302
     *
303
     * @param bool $onlySubmittedOrders - only include Orders that have already been submitted.
304
     * @param bool $includeCancelledOrders - only include Orders that have already been submitted.
305
     *
306
     * @return DataList (Orders)
307
     */
308
    public static function get_datalist_of_orders_with_submit_record($onlySubmittedOrders = true, $includeCancelledOrders = false)
309
    {
310
        if ($onlySubmittedOrders) {
311
            $submittedOrderStatusLogClassName = EcommerceConfig::get('OrderStatusLog', 'order_status_log_class_used_for_submitting_order');
312
            $list = Order::get()
313
                ->LeftJoin('OrderStatusLog', '"Order"."ID" = "OrderStatusLog"."OrderID"')
314
                ->LeftJoin($submittedOrderStatusLogClassName, '"OrderStatusLog"."ID" = "'.$submittedOrderStatusLogClassName.'"."ID"')
315
                ->Sort('OrderStatusLog.Created', 'ASC');
316
            $where = ' ("OrderStatusLog"."ClassName" = \''.$submittedOrderStatusLogClassName.'\') ';
317
        } else {
318
            $list = Order::get();
319
            $where = ' ("StatusID" > 0) ';
320
        }
321
        if ($includeCancelledOrders) {
322
            //do nothing...
323
        } else {
324
            $where .= ' AND ("CancelledByID" = 0 OR "CancelledByID" IS NULL)';
325
        }
326
        $list = $list->where($where);
327
328
        return $list;
329
    }
330
331
/*******************************************************
332
   * 1. CMS STUFF
333
*******************************************************/
334
335
    /**
336
     * fields that we remove from the parent::getCMSFields object set.
337
     *
338
     * @var array
339
     */
340
    protected $fieldsAndTabsToBeRemoved = array(
341
        'MemberID',
342
        'Attributes',
343
        'SessionID',
344
        'Emails',
345
        'BillingAddressID',
346
        'ShippingAddressID',
347
        'UseShippingAddress',
348
        'OrderStatusLogs',
349
        'Payments',
350
        'OrderDate',
351
        'ExchangeRate',
352
        'CurrencyUsedID',
353
        'StatusID',
354
        'Currency',
355
    );
356
357
    /**
358
     * STANDARD SILVERSTRIPE STUFF.
359
     **/
360
    private static $summary_fields = array(
361
        'Title' => 'Title',
362
        'Status.Title' => 'Next Step',
363
        'Member.Surname' => 'Name',
364
        'Member.Email' => 'Email',
365
        'TotalAsMoney.Nice' => 'Total',
366
        'TotalItemsTimesQuantity' => 'Units',
367
        'IsPaidNice' => 'Paid'
368
    );
369
370
    /**
371
     * STANDARD SILVERSTRIPE STUFF.
372
     *
373
     * @todo: how to translate this?
374
     **/
375
    private static $searchable_fields = array(
376
        'ID' => array(
377
            'field' => 'NumericField',
378
            'title' => 'Order Number',
379
        ),
380
        'MemberID' => array(
381
            'field' => 'TextField',
382
            'filter' => 'OrderFilters_MemberAndAddress',
383
            'title' => 'Customer Details',
384
        ),
385
        'Created' => array(
386
            'field' => 'TextField',
387
            'filter' => 'OrderFilters_AroundDateFilter',
388
            'title' => 'Date (e.g. Today, 1 jan 2007, or last week)',
389
        ),
390
        //make sure to keep the items below, otherwise they do not show in form
391
        'StatusID' => array(
392
            'filter' => 'OrderFilters_MultiOptionsetStatusIDFilter',
393
        ),
394
        'CancelledByID' => array(
395
            'filter' => 'OrderFilters_HasBeenCancelled',
396
            'title' => 'Cancelled by ...',
397
        ),
398
    );
399
400
    /**
401
     * Determine which properties on the DataObject are
402
     * searchable, and map them to their default {@link FormField}
403
     * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
404
     *
405
     * Some additional logic is included for switching field labels, based on
406
     * how generic or specific the field type is.
407
     *
408
     * Used by {@link SearchContext}.
409
     *
410
     * @param array $_params
411
     *                       'fieldClasses': Associative array of field names as keys and FormField classes as values
412
     *                       'restrictFields': Numeric array of a field name whitelist
413
     *
414
     * @return FieldList
415
     */
416
    public function scaffoldSearchFields($_params = null)
417
    {
418
        $fieldList = parent::scaffoldSearchFields($_params);
419
420
        //for sales to action only show relevant ones ...
421
        if(Controller::curr() && Controller::curr()->class === 'SalesAdmin') {
422
            $statusOptions = OrderStep::admin_manageable_steps();
423
        } else {
424
            $statusOptions = OrderStep::get();
425
        }
426
        if ($statusOptions && $statusOptions->count()) {
427
            $createdOrderStatusID = 0;
428
            $preSelected = array();
429
            $createdOrderStatus = $statusOptions->First();
430
            if ($createdOrderStatus) {
431
                $createdOrderStatusID = $createdOrderStatus->ID;
432
            }
433
            $arrayOfStatusOptions = clone $statusOptions->map('ID', 'Title');
434
            $arrayOfStatusOptionsFinal = array();
435
            if (count($arrayOfStatusOptions)) {
436
                foreach ($arrayOfStatusOptions as $key => $value) {
437
                    if (isset($_GET['q']['StatusID'][$key])) {
438
                        $preSelected[$key] = $key;
439
                    }
440
                    $count = Order::get()
441
                        ->Filter(array('StatusID' => intval($key)))
442
                        ->count();
443
                    if ($count < 1) {
444
                        //do nothing
445
                    } else {
446
                        $arrayOfStatusOptionsFinal[$key] = $value." ($count)";
447
                    }
448
                }
449
            }
450
            $statusField = new CheckboxSetField(
451
                'StatusID',
452
                Injector::inst()->get('OrderStep')->i18n_singular_name(),
453
                $arrayOfStatusOptionsFinal,
454
                $preSelected
455
            );
456
            $fieldList->push($statusField);
457
        }
458
        $fieldList->push(new DropdownField('CancelledByID', 'Cancelled', array(-1 => '(Any)', 1 => 'yes', 0 => 'no')));
459
460
        //allow changes
461
        $this->extend('scaffoldSearchFields', $fieldList, $_params);
462
463
        return $fieldList;
464
    }
465
466
    /**
467
     * link to edit the record.
468
     *
469
     * @param string | Null $action - e.g. edit
470
     *
471
     * @return string
472
     */
473
    public function CMSEditLink($action = null)
474
    {
475
        return CMSEditLinkAPI::find_edit_link_for_object($this, $action, 'sales-advanced');
476
    }
477
478
    /**
479
     * STANDARD SILVERSTRIPE STUFF
480
     * broken up into submitted and not (yet) submitted.
481
     **/
482
    public function getCMSFields()
483
    {
484
        $fields = $this->scaffoldFormFields(array(
485
            // Don't allow has_many/many_many relationship editing before the record is first saved
486
            'includeRelations' => false,
487
            'tabbed' => true,
488
            'ajaxSafe' => true
489
        ));
490
        $fields->insertBefore(
491
            Tab::create(
492
                'Next',
493
                _t('Order.NEXT_TAB', 'Action')
494
            ),
495
            'Main'
496
        );
497
        $fields->addFieldsToTab(
498
            'Root',
499
            array(
500
                Tab::create(
501
                    "Items",
502
                    _t('Order.ITEMS_TAB', 'Items')
503
                ),
504
                Tab::create(
505
                    "Extras",
506
                    _t('Order.MODIFIERS_TAB', 'Adjustments')
507
                ),
508
                Tab::create(
509
                    'Emails',
510
                    _t('Order.EMAILS_TAB', 'Emails')
511
                ),
512
                Tab::create(
513
                    'Payments',
514
                    _t('Order.PAYMENTS_TAB', 'Payment')
515
                ),
516
                Tab::create(
517
                    'Account',
518
                    _t('Order.ACCOUNT_TAB', 'Account')
519
                ),
520
                Tab::create(
521
                    'Currency',
522
                    _t('Order.CURRENCY_TAB', 'Currency')
523
                ),
524
                Tab::create(
525
                    'Addresses',
526
                    _t('Order.ADDRESSES_TAB', 'Addresses')
527
                ),
528
                Tab::create(
529
                    'Log',
530
                    _t('Order.LOG_TAB', 'Notes')
531
                ),
532
                Tab::create(
533
                    'Cancellations',
534
                    _t('Order.CANCELLATION_TAB', 'Cancel')
535
                ),
536
            )
537
        );
538
        //as we are no longer using the parent:;getCMSFields
539
        // we had to add the updateCMSFields hook.
540
        $this->extend('updateCMSFields', $fields);
541
        $currentMember = Member::currentUser();
542
        if (!$this->exists() || !$this->StatusID) {
543
            $firstStep = OrderStep::get()->First();
544
            $this->StatusID = $firstStep->ID;
545
            $this->write();
546
        }
547
        $submitted = $this->IsSubmitted() ? true : false;
548
        if ($submitted) {
549
            //TODO
550
            //Having trouble here, as when you submit the form (for example, a payment confirmation)
551
            //as the step moves forward, meaning the fields generated are incorrect, causing an error
552
            //"I can't handle sub-URLs of a Form object." generated by the RequestHandler.
553
            //Therefore we need to try reload the page so that it will be requesting the correct URL to generate the correct fields for the current step
554
            //Or something similar.
555
            //why not check if the URL == $this->CMSEditLink()
556
            //and only tryToFinaliseOrder if this is true....
557
            if ($_SERVER['REQUEST_URI'] == $this->CMSEditLink() || $_SERVER['REQUEST_URI'] == $this->CMSEditLink('edit')) {
558
                $this->tryToFinaliseOrder();
559
            }
560
        } else {
561
            $this->init(true);
562
            $this->calculateOrderAttributes(true);
563
            Session::set('EcommerceOrderGETCMSHack', $this->ID);
564
        }
565
        if ($submitted) {
566
            $this->fieldsAndTabsToBeRemoved[] = 'CustomerOrderNote';
567
        } else {
568
            $this->fieldsAndTabsToBeRemoved[] = 'Emails';
569
        }
570
        foreach ($this->fieldsAndTabsToBeRemoved as $field) {
571
            $fields->removeByName($field);
572
        }
573
        $orderSummaryConfig = GridFieldConfig_Base::create();
574
        $orderSummaryConfig->removeComponentsByType('GridFieldToolbarHeader');
575
        // $orderSummaryConfig->removeComponentsByType('GridFieldSortableHeader');
576
        $orderSummaryConfig->removeComponentsByType('GridFieldFilterHeader');
577
        $orderSummaryConfig->removeComponentsByType('GridFieldPageCount');
578
        $orderSummaryConfig->removeComponentsByType('GridFieldPaginator');
579
        $nextFieldArray = array(
580
            LiteralField::create('CssFix', '<style>#Root_Next h2.form-control {padding: 0!important; margin: 0!important; padding-top: 4em!important;}</style>'),
581
            HeaderField::create('MyOrderStepHeader', _t('Order.CURRENT_STATUS', '1. Current Status')),
582
            $this->OrderStepField(),
583
            GridField::create(
584
                'OrderSummary',
585
                _t('Order.CURRENT_STATUS', 'Summary'),
586
                ArrayList::create(array($this)),
587
                $orderSummaryConfig
588
            )
589
        );
590
        $keyNotes = OrderStatusLog::get()->filter(
591
            array(
592
                'OrderID' => $this->ID,
593
                'ClassName' => 'OrderStatusLog'
594
            )
595
        );
596
        if ($keyNotes->count()) {
597
            $notesSummaryConfig = GridFieldConfig_RecordViewer::create();
598
            $notesSummaryConfig->removeComponentsByType('GridFieldToolbarHeader');
599
            $notesSummaryConfig->removeComponentsByType('GridFieldFilterHeader');
600
            // $orderSummaryConfig->removeComponentsByType('GridFieldSortableHeader');
601
            $notesSummaryConfig->removeComponentsByType('GridFieldPageCount');
602
            $notesSummaryConfig->removeComponentsByType('GridFieldPaginator');
603
            $nextFieldArray = array_merge(
604
                $nextFieldArray,
605
                array(
606
                    HeaderField::create('KeyNotesHeader', _t('Order.KEY_NOTES_HEADER', 'Key Notes')),
607
                    GridField::create(
608
                        'OrderStatusLogSummary',
609
                        _t('Order.CURRENT_KEY_NOTES', 'Key Notes'),
610
                        $keyNotes,
611
                        $notesSummaryConfig
612
                    )
613
                )
614
            );
615
        }
616
        $nextFieldArray = array_merge(
617
            $nextFieldArray,
618
            array(
619
                EcommerceCMSButtonField::create(
620
                    'AddNoteButton',
621
                    '/admin/sales/Order/EditForm/field/Order/item/' . $this->ID . '/ItemEditForm/field/OrderStatusLog/item/new',
622
                    _t('Order.ADD_NOTE', 'Add Note')
623
                )
624
            )
625
        );
626
        $nextFieldArray = array_merge(
627
            $nextFieldArray,
628
            array(
629
630
            )
631
        );
632
633
         //is the member is a shop admin they can always view it
634
635
        if (EcommerceRole::current_member_can_process_orders(Member::currentUser())) {
636
            $lastStep = OrderStep::get()->Last();
637
            if($this->StatusID != $lastStep->ID) {
638
                $queueObjectSingleton = Injector::inst()->get('OrderProcessQueue');
639
                if ($myQueueObject = $queueObjectSingleton->getQueueObject($this)) {
640
                    $myQueueObjectField = GridField::create(
641
                        'MyQueueObjectField',
642
                        _t('Order.QUEUE_DETAILS', 'Queue Details'),
643
                        $this->OrderProcessQueue(),
644
                        GridFieldConfig_RecordEditor::create()
645
                    );
646
                } else {
647
                    $myQueueObjectField = LiteralField::create('MyQueueObjectField', '<p>'._t('Order.NOT_QUEUED','This order is not queued for future processing.').'</p>');
648
                }
649
                $nextFieldArray = array_merge(
650
                    $nextFieldArray,
651
                    array(
652
                        HeaderField::create('OrderStepNextStepHeader', _t('Order.ACTION_NEXT_STEP', '2. Action Next Step')),
653
                        $myQueueObjectField,
654
                        HeaderField::create('ActionNextStepManually', _t('Order.MANUAL_STATUS_CHANGE', '3. Move Order Along')),
655
                        LiteralField::create('OrderStepNextStepHeaderExtra', '<p>'._t('Order.NEEDTOREFRESH', 'Once you have made any changes to the order then you will have to refresh below or save it to move it along.').'</p>'),
656
                        EcommerceCMSButtonField::create(
657
                            'StatusIDExplanation',
658
                            $this->CMSEditLink(),
659
                            _t('Order.REFRESH', 'refresh now')
660
                        )
661
                    )
662
                );
663
            }
664
        }
665
        $fields->addFieldsToTab(
666
            'Root.Next',
667
            $nextFieldArray
668
        );
669
670
        $this->MyStep()->addOrderStepFields($fields, $this);
671
672
        if ($submitted) {
673
            $permaLinkLabel = _t('Order.PERMANENT_LINK', 'Customer Link');
674
            $html = '<p>'.$permaLinkLabel.': <a href="'.$this->getRetrieveLink().'">'.$this->getRetrieveLink().'</a></p>';
675
            $shareLinkLabel = _t('Order.SHARE_LINK', 'Share Link');
676
            $html .= '<p>'.$shareLinkLabel.': <a href="'.$this->getShareLink().'">'.$this->getShareLink().'</a></p>';
677
            $feedbackLinkLabel = _t('Order.FEEDBACK_LINK', 'Feedback Link');
678
            $html .= '<p>'.$feedbackLinkLabel.': <a href="'.$this->getFeedbackLink().'">'.$this->getFeedbackLink().'</a></p>';
679
            $js = "window.open(this.href, 'payment', 'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1,width=800,height=600'); return false;";
680
            $link = $this->getPrintLink();
681
            $label = _t('Order.PRINT_INVOICE', 'invoice');
682
            $linkHTML = '<a href="'.$link.'" onclick="'.$js.'">'.$label.'</a>';
683
            $linkHTML .= ' | ';
684
            $link = $this->getPackingSlipLink();
685
            $label = _t('Order.PRINT_PACKING_SLIP', 'packing slip');
686
            $labelPrint = _t('Order.PRINT', 'Print');
687
            $linkHTML .= '<a href="'.$link.'" onclick="'.$js.'">'.$label.'</a>';
688
            $html .= '<h3>';
689
            $html .= $labelPrint.': '.$linkHTML;
690
            $html .= '</h3>';
691
692
            $fields->addFieldToTab(
693
                'Root.Main',
694
                LiteralField::create('getPrintLinkANDgetPackingSlipLink', $html)
695
            );
696
697
            //add order here as well.
698
            $fields->addFieldToTab(
699
                'Root.Main',
700
                new LiteralField(
701
                    'MainDetails',
702
                    '<iframe src="'.$this->getPrintLink().'" width="100%" height="2500" style="border: 5px solid #2e7ead; border-radius: 2px;"></iframe>')
703
            );
704
            $fields->addFieldsToTab(
705
                'Root.Items',
706
                array(
707
                    GridField::create(
708
                        'Items_Sold',
709
                        'Items Sold',
710
                        $this->Items(),
711
                        new GridFieldConfig_RecordViewer
712
                    )
713
                )
714
            );
715
            $fields->addFieldsToTab(
716
                'Root.Extras',
717
                array(
718
                    GridField::create(
719
                        'Modifications',
720
                        'Price (and other) adjustments',
721
                        $this->Modifiers(),
722
                        new GridFieldConfig_RecordViewer
723
                    )
724
                )
725
            );
726
            $fields->addFieldsToTab(
727
                'Root.Emails',
728
                array(
729
                    $this->getEmailsTableField()
730
                )
731
            );
732
            $fields->addFieldsToTab(
733
                'Root.Payments',
734
                array(
735
                    $this->getPaymentsField(),
736
                    new ReadOnlyField('TotalPaidNice', _t('Order.TOTALPAID', 'Total Paid'), $this->TotalPaidAsCurrencyObject()->Nice()),
737
                    new ReadOnlyField('TotalOutstandingNice', _t('Order.TOTALOUTSTANDING', 'Total Outstanding'), $this->getTotalOutstandingAsMoney()->Nice())
738
                )
739
            );
740
            if ($this->canPay()) {
741
                $link = EcommercePaymentController::make_payment_link($this->ID);
742
                $js = "window.open(this.href, 'payment', 'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1,width=800,height=600'); return false;";
743
                $header = _t('Order.MAKEPAYMENT', 'make payment');
744
                $label = _t('Order.MAKEADDITIONALPAYMENTNOW', 'make additional payment now');
745
                $linkHTML = '<a href="'.$link.'" onclick="'.$js.'">'.$label.'</a>';
746
                $fields->addFieldToTab('Root.Payments', new HeaderField('MakeAdditionalPaymentHeader', $header, 3));
747
                $fields->addFieldToTab('Root.Payments', new LiteralField('MakeAdditionalPayment', $linkHTML));
748
            }
749
            //member
750
            $member = $this->Member();
751
            if ($member && $member->exists()) {
752
                $fields->addFieldToTab('Root.Account', new LiteralField('MemberDetails', $member->getEcommerceFieldsForCMS()));
753
            } else {
754
                $fields->addFieldToTab('Root.Account', new LiteralField('MemberDetails',
755
                    '<p>'._t('Order.NO_ACCOUNT', 'There is no --- account --- associated with this order').'</p>'
756
                ));
757
            }
758
            if($this->getFeedbackLink()) {
759
                $fields->addFieldToTab(
760
                    'Root.Account',
761
                    GridField::create(
762
                        'OrderFeedback',
763
                        Injector::inst()->get('OrderFeedback')->singular_name(),
764
                        OrderFeedback::get()->filter(array('OrderID' => $this->ID)),
765
                        GridFieldConfig_RecordViewer::create()
766
                    )
767
                );
768
            }
769
            $cancelledField = $fields->dataFieldByName('CancelledByID');
770
            $fields->removeByName('CancelledByID');
771
            $shopAdminAndCurrentCustomerArray = EcommerceRole::list_of_admins(true);
772
            if ($member && $member->exists()) {
773
                $shopAdminAndCurrentCustomerArray[$member->ID] = $member->getName();
774
            }
775
            if ($this->CancelledByID) {
776
                if ($cancellingMember = $this->CancelledBy()) {
777
                    $shopAdminAndCurrentCustomerArray[$this->CancelledByID] = $cancellingMember->getName();
778
                }
779
            }
780
            if ($this->canCancel()) {
781
                $fields->addFieldsToTab(
782
                    'Root.Cancellations',
783
                    array(
784
                        DropdownField::create(
785
                            'CancelledByID',
786
                            $cancelledField->Title(),
787
                            $shopAdminAndCurrentCustomerArray
788
                        )
789
                    )
790
                );
791
            } else {
792
                $cancelledBy = isset($shopAdminAndCurrentCustomerArray[$this->CancelledByID]) && $this->CancelledByID ? $shopAdminAndCurrentCustomerArray[$this->CancelledByID] : _t('Order.NOT_CANCELLED', 'not cancelled');
793
                $fields->addFieldsToTab(
794
                    'Root.Cancellations',
795
                    ReadonlyField::create(
796
                        'CancelledByDisplay',
797
                        $cancelledField->Title(),
798
                        $cancelledBy
799
800
                    )
801
                );
802
            }
803
            $fields->addFieldToTab('Root.Log', $this->getOrderStatusLogsTableField_Archived());
804
            $submissionLog = $this->SubmissionLog();
805
            if ($submissionLog) {
806
                $fields->addFieldToTab('Root.Log',
807
                    ReadonlyField::create(
808
                        'SequentialOrderNumber',
809
                        _t('Order.SEQUENTIALORDERNUMBER', 'Consecutive order number'),
810
                        $submissionLog->SequentialOrderNumber
811
                    )->setRightTitle('e.g. 1,2,3,4,5...')
812
                );
813
            }
814
        } else {
815
            $linkText = _t(
816
                'Order.LOAD_THIS_ORDER',
817
                'load this order'
818
            );
819
            $message = _t(
820
                'Order.NOSUBMITTEDYET',
821
                'No details are shown here as this order has not been submitted yet. You can {link} to submit it... NOTE: For this, you will be logged in as the customer and logged out as (shop)admin .',
822
                array('link' => '<a href="'.$this->getRetrieveLink().'" data-popup="true">'.$linkText.'</a>')
823
            );
824
            $fields->addFieldToTab('Root.Next', new LiteralField('MainDetails', '<p>'.$message.'</p>'));
825
            $fields->addFieldToTab('Root.Items', $this->getOrderItemsField());
826
            $fields->addFieldToTab('Root.Extras', $this->getModifierTableField());
827
828
            //MEMBER STUFF
829
            $specialOptionsArray = array();
830
            if ($this->MemberID) {
831
                $specialOptionsArray[0] = _t('Order.SELECTCUSTOMER', '--- Remover Customer ---');
832
                $specialOptionsArray[$this->MemberID] = _t('Order.LEAVEWITHCURRENTCUSTOMER', '- Leave with current customer: ').$this->Member()->getTitle();
833
            } elseif ($currentMember) {
834
                $specialOptionsArray[0] = _t('Order.SELECTCUSTOMER', '--- Select Customers ---');
835
                $currentMemberID = $currentMember->ID;
836
                $specialOptionsArray[$currentMemberID] = _t('Order.ASSIGNTHISORDERTOME', '- Assign this order to me: ').$currentMember->getTitle();
837
            }
838
            //MEMBER FIELD!!!!!!!
839
            $memberArray = $specialOptionsArray + EcommerceRole::list_of_customers(true);
840
            $fields->addFieldToTab('Root.Next', new DropdownField('MemberID', _t('Order.SELECTCUSTOMER', 'Select Customer'), $memberArray), 'CustomerOrderNote');
841
            $memberArray = null;
842
        }
843
        $fields->addFieldToTab('Root.Addresses', new HeaderField('BillingAddressHeader', _t('Order.BILLINGADDRESS', 'Billing Address')));
844
845
        $fields->addFieldToTab('Root.Addresses', $this->getBillingAddressField());
846
847
        if (EcommerceConfig::get('OrderAddress', 'use_separate_shipping_address')) {
848
            $fields->addFieldToTab('Root.Addresses', new HeaderField('ShippingAddressHeader', _t('Order.SHIPPINGADDRESS', 'Shipping Address')));
849
            $fields->addFieldToTab('Root.Addresses', new CheckboxField('UseShippingAddress', _t('Order.USESEPERATEADDRESS', 'Use separate shipping address?')));
850
            if ($this->UseShippingAddress) {
851
                $fields->addFieldToTab('Root.Addresses', $this->getShippingAddressField());
852
            }
853
        }
854
        $currencies = EcommerceCurrency::get_list();
855
        if ($currencies && $currencies->count()) {
856
            $currencies = $currencies->map()->toArray();
857
            $fields->addFieldToTab('Root.Currency', new ReadOnlyField('ExchangeRate ', _t('Order.EXCHANGERATE', 'Exchange Rate'), $this->ExchangeRate));
858
            $fields->addFieldToTab('Root.Currency', $currencyField = new DropdownField('CurrencyUsedID', _t('Order.CurrencyUsed', 'Currency Used'), $currencies));
859
            if ($this->IsSubmitted()) {
860
                $fields->replaceField('CurrencyUsedID', $fields->dataFieldByName('CurrencyUsedID')->performReadonlyTransformation());
861
            }
862
        } else {
863
            $fields->addFieldToTab('Root.Currency', new LiteralField('CurrencyInfo', '<p>You can not change currencies, because no currencies have been created.</p>'));
864
            $fields->replaceField('CurrencyUsedID', $fields->dataFieldByName('CurrencyUsedID')->performReadonlyTransformation());
865
        }
866
        $fields->addFieldToTab('Root.Log', new ReadonlyField('Created', _t('Root.CREATED', 'Created')));
867
        $fields->addFieldToTab('Root.Log', new ReadonlyField('LastEdited', _t('Root.LASTEDITED', 'Last saved')));
868
        $this->extend('updateCMSFields', $fields);
869
870
        return $fields;
871
    }
872
873
    /**
874
     * Field to add and edit Order Items.
875
     *
876
     * @return GridField
877
     */
878
    protected function getOrderItemsField()
879
    {
880
        $gridFieldConfig = GridFieldConfigForOrderItems::create();
881
        $source = $this->OrderItems();
882
883
        return new GridField('OrderItems', _t('OrderItems.PLURALNAME', 'Order Items'), $source, $gridFieldConfig);
884
    }
885
886
    /**
887
     * Field to add and edit Modifiers.
888
     *
889
     * @return GridField
890
     */
891
    public function getModifierTableField()
892
    {
893
        $gridFieldConfig = GridFieldConfigForOrderItems::create();
894
        $source = $this->Modifiers();
895
896
        return new GridField('OrderModifiers', _t('OrderItems.PLURALNAME', 'Order Items'), $source, $gridFieldConfig);
897
    }
898
899
    /**
900
     *@return GridField
901
     **/
902
    protected function getBillingAddressField()
903
    {
904
        $this->CreateOrReturnExistingAddress('BillingAddress');
905
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
906
            new GridFieldToolbarHeader(),
907
            new GridFieldSortableHeader(),
908
            new GridFieldDataColumns(),
909
            new GridFieldPaginator(10),
910
            new GridFieldEditButton(),
911
            new GridFieldDetailForm()
912
        );
913
        //$source = $this->BillingAddress();
914
        $source = BillingAddress::get()->filter(array('OrderID' => $this->ID));
915
916
        return new GridField('BillingAddress', _t('BillingAddress.SINGULARNAME', 'Billing Address'), $source, $gridFieldConfig);
917
    }
918
919
    /**
920
     *@return GridField
921
     **/
922
    protected function getShippingAddressField()
923
    {
924
        $this->CreateOrReturnExistingAddress('ShippingAddress');
925
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
926
            new GridFieldToolbarHeader(),
927
            new GridFieldSortableHeader(),
928
            new GridFieldDataColumns(),
929
            new GridFieldPaginator(10),
930
            new GridFieldEditButton(),
931
            new GridFieldDetailForm()
932
        );
933
        //$source = $this->ShippingAddress();
934
        $source = ShippingAddress::get()->filter(array('OrderID' => $this->ID));
935
936
        return new GridField('ShippingAddress', _t('BillingAddress.SINGULARNAME', 'Shipping Address'), $source, $gridFieldConfig);
937
    }
938
939
    /**
940
     * Needs to be public because the OrderStep::getCMSFIelds accesses it.
941
     *
942
     * @param string    $sourceClass
943
     * @param string    $title
944
     *
945
     * @return GridField
946
     **/
947
    public function getOrderStatusLogsTableField(
948
        $sourceClass = 'OrderStatusLog',
949
        $title = ''
950
    ) {
951
        $gridFieldConfig = GridFieldConfig_RecordViewer::create()->addComponents(
952
            new GridFieldAddNewButton('toolbar-header-right'),
953
            new GridFieldDetailForm()
954
        );
955
        $title ? $title : $title = _t('OrderStatusLog.PLURALNAME', 'Order Status Logs');
956
        $source = $this->OrderStatusLogs()->Filter(array('ClassName' => $sourceClass));
957
        $gf = new GridField($sourceClass, $title, $source, $gridFieldConfig);
958
        $gf->setModelClass($sourceClass);
959
960
        return $gf;
961
    }
962
963
    /**
964
     * Needs to be public because the OrderStep::getCMSFIelds accesses it.
965
     *
966
     * @param string    $sourceClass
967
     * @param string    $title
968
     *
969
     * @return GridField
970
     **/
971
    public function getOrderStatusLogsTableFieldEditable(
972
        $sourceClass = 'OrderStatusLog',
973
        $title = ''
974
    ) {
975
        $gf = $this->getOrderStatusLogsTableField($sourceClass, $title);
976
        $gf->getConfig()->addComponents(
977
            new GridFieldEditButton()
978
        );
979
        return $gf;
980
    }
981
982
    /**
983
     * @param string    $sourceClass
984
     * @param string    $title
985
     * @param FieldList $fieldList          (Optional)
986
     * @param FieldList $detailedFormFields (Optional)
987
     *
988
     * @return GridField
989
     **/
990
    protected function getOrderStatusLogsTableField_Archived(
991
        $sourceClass = 'OrderStatusLog',
992
        $title = '',
993
        FieldList $fieldList = null,
994
        FieldList $detailedFormFields = null
995
    ) {
996
        $title ? $title : $title = _t('OrderLog.PLURALNAME', 'Order Log');
997
        $source = $this->OrderStatusLogs();
998
        if ($sourceClass != 'OrderStatusLog' && class_exists($sourceClass)) {
999
            $source = $source->filter(array('ClassName' => ClassInfo::subclassesFor($sourceClass)));
1000
        }
1001
        $gridField = GridField::create($sourceClass, $title, $source, $config = GridFieldConfig_RelationEditor::create());
1002
        $config->removeComponentsByType('GridFieldAddExistingAutocompleter');
1003
        $config->removeComponentsByType('GridFieldDeleteAction');
1004
1005
        return $gridField;
1006
    }
1007
1008
    /**
1009
     * @return GridField
1010
     **/
1011
    public function getEmailsTableField()
1012
    {
1013
        $gridFieldConfig = GridFieldConfig_RecordViewer::create()->addComponents(
1014
            new GridFieldDetailForm()
1015
        );
1016
1017
        return new GridField('Emails', _t('Order.CUSTOMER_EMAILS', 'Customer Emails'), $this->Emails(), $gridFieldConfig);
1018
    }
1019
1020
    /**
1021
     * @return GridField
1022
     */
1023
    protected function getPaymentsField()
1024
    {
1025
        $gridFieldConfig = GridFieldConfig_RecordViewer::create()->addComponents(
1026
            new GridFieldDetailForm(),
1027
            new GridFieldEditButton()
1028
        );
1029
1030
        return new GridField('Payments', _t('Order.PAYMENTS', 'Payments'), $this->Payments(), $gridFieldConfig);
1031
    }
1032
1033
    /**
1034
     * @return OrderStepField
1035
     */
1036
    public function OrderStepField()
1037
    {
1038
        return OrderStepField::create($name = 'MyOrderStep', $this, Member::currentUser());
1039
    }
1040
1041
/*******************************************************
1042
   * 2. MAIN TRANSITION FUNCTIONS
1043
*******************************************************/
1044
1045
    /**
1046
     * init runs on start of a new Order (@see onAfterWrite)
1047
     * it adds all the modifiers to the orders and the starting OrderStep.
1048
     *
1049
     * @param bool $recalculate
1050
     *
1051
     * @return DataObject (Order)
1052
     **/
1053
    public function init($recalculate = false)
1054
    {
1055
        if ($this->IsSubmitted()) {
1056
            user_error('Can not init an order that has been submitted', E_USER_NOTICE);
1057
        } else {
1058
            //to do: check if shop is open....
1059
            if ($this->StatusID || $recalculate) {
1060
                if (!$this->StatusID) {
1061
                    $createdOrderStatus = OrderStep::get()->First();
1062
                    if (!$createdOrderStatus) {
1063
                        user_error('No ordersteps have been created', E_USER_WARNING);
1064
                    }
1065
                    $this->StatusID = $createdOrderStatus->ID;
1066
                }
1067
                $createdModifiersClassNames = array();
1068
                $modifiersAsArrayList = new ArrayList();
1069
                $modifiers = $this->modifiersFromDatabase($includingRemoved = true);
1070
                if ($modifiers->count()) {
1071
                    foreach ($modifiers as $modifier) {
1072
                        $modifiersAsArrayList->push($modifier);
1073
                    }
1074
                }
1075
                if ($modifiersAsArrayList->count()) {
1076
                    foreach ($modifiersAsArrayList as $modifier) {
1077
                        $createdModifiersClassNames[$modifier->ID] = $modifier->ClassName;
1078
                    }
1079
                } else {
1080
                }
1081
                $modifiersToAdd = EcommerceConfig::get('Order', 'modifiers');
1082
                if (is_array($modifiersToAdd) && count($modifiersToAdd) > 0) {
1083
                    foreach ($modifiersToAdd as $numericKey => $className) {
1084
                        if (!in_array($className, $createdModifiersClassNames)) {
1085
                            if (class_exists($className)) {
1086
                                $modifier = new $className();
1087
                                //only add the ones that should be added automatically
1088
                                if (!$modifier->DoNotAddAutomatically()) {
1089
                                    if (is_a($modifier, 'OrderModifier')) {
1090
                                        $modifier->OrderID = $this->ID;
1091
                                        $modifier->Sort = $numericKey;
1092
                                        //init method includes a WRITE
1093
                                        $modifier->init();
1094
                                        //IMPORTANT - add as has_many relationship  (Attributes can be a modifier OR an OrderItem)
1095
                                        $this->Attributes()->add($modifier);
1096
                                        $modifiersAsArrayList->push($modifier);
1097
                                    }
1098
                                }
1099
                            } else {
1100
                                user_error('reference to a non-existing class: '.$className.' in modifiers', E_USER_NOTICE);
1101
                            }
1102
                        }
1103
                    }
1104
                }
1105
                $this->extend('onInit', $this);
1106
                //careful - this will call "onAfterWrite" again
1107
                $this->write();
1108
            }
1109
        }
1110
1111
        return $this;
1112
    }
1113
1114
    /**
1115
     * @var array
1116
     */
1117
    private static $_try_to_finalise_order_is_running = array();
1118
1119
    /**
1120
     * Goes through the order steps and tries to "apply" the next status to the order.
1121
     *
1122
     * @param bool $runAgain
1123
     * @param bool $fromOrderQueue - is it being called from the OrderProcessQueue (or similar)
1124
     *
1125
     * @return null
1126
     **/
1127
    public function tryToFinaliseOrder($runAgain = false, $fromOrderQueue = false)
1128
    {
1129
        if (empty(self::$_try_to_finalise_order_is_running[$this->ID]) || $runAgain) {
1130
            self::$_try_to_finalise_order_is_running[$this->ID] = true;
1131
1132
            //if the order has been cancelled then we do not process it ...
1133
            if ($this->CancelledByID) {
0 ignored issues
show
The property CancelledByID does not exist on object<Order>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1134
                $this->Archive(true);
1135
1136
                return;
1137
            }
1138
            // if it is in the queue it has to run from the queue tasks
1139
            // if it ruins from the queue tasks then it has to be one currently processing.
1140
            $queueObjectSingleton = Injector::inst()->get('OrderProcessQueue');
1141
            if ($myQueueObject = $queueObjectSingleton->getQueueObject($this)) {
1142
                if($fromOrderQueue) {
1143
                    if ( ! $myQueueObject->InProcess) {
1144
1145
                        return;
1146
                    }
1147
                } else {
1148
1149
                    return;
1150
                }
1151
            }
1152
            //a little hack to make sure we do not rely on a stored value
1153
            //of "isSubmitted"
1154
            $this->_isSubmittedTempVar = -1;
1155
            //status of order is being progressed
1156
            $nextStatusID = $this->doNextStatus();
1157
            if ($nextStatusID) {
1158
                $nextStatusObject = OrderStep::get()->byID($nextStatusID);
1159
                if ($nextStatusObject) {
1160
                    $delay = $nextStatusObject->CalculatedDeferTimeInSeconds($this);
1161
                    if ($delay > 0) {
1162
                        //adjust delay time from seconds since being submitted
1163
                        if ($nextStatusObject->DeferFromSubmitTime) {
1164
                            $delay = $delay - $this->SecondsSinceBeingSubmitted();
1165
                            if ($delay < 0) {
1166
                                $delay = 0;
1167
                            }
1168
                        }
1169
                        $queueObjectSingleton->AddOrderToQueue(
1170
                            $this,
1171
                            $delay
1172
                        );
1173
                    } else {
1174
                        //status has been completed, so it can be released
1175
                        self::$_try_to_finalise_order_is_running[$this->ID] = false;
1176
                        $this->tryToFinaliseOrder($runAgain, $fromOrderQueue);
1177
                    }
1178
                }
1179
            }
1180
        }
1181
    }
1182
1183
    /**
1184
     * Goes through the order steps and tries to "apply" the next step
1185
     * Step is updated after the other one is completed...
1186
     *
1187
     * @return int (StatusID or false if the next status can not be "applied")
1188
     **/
1189
    public function doNextStatus()
1190
    {
1191
        if ($this->MyStep()->initStep($this)) {
1192
            if ($this->MyStep()->doStep($this)) {
1193
                if ($nextOrderStepObject = $this->MyStep()->nextStep($this)) {
1194
                    $this->StatusID = $nextOrderStepObject->ID;
1195
                    $this->write();
1196
1197
                    return $this->StatusID;
1198
                }
1199
            }
1200
        }
1201
1202
        return 0;
1203
    }
1204
1205
    /**
1206
     * cancel an order.
1207
     *
1208
     * @param Member $member - (optional) the user cancelling the order
1209
     * @param string $reason - (optional) the reason the order is cancelled
1210
     *
1211
     * @return OrderStatusLog_Cancel
1212
     */
1213
    public function Cancel($member = null, $reason = '')
1214
    {
1215
        if($member && $member instanceof Member) {
1216
            //we have a valid member
1217
        } else {
1218
            $member = EcommerceRole::get_default_shop_admin_user();
1219
        }
1220
        if($member) {
1221
            //archive and write
1222
            $this->Archive($avoidWrites = true);
1223
            if($avoidWrites) {
1224
                DB::query('Update "Order" SET CancelledByID = '.$member->ID.' WHERE ID = '.$this->ID.' LIMIT 1;');
1225
            } else {
1226
                $this->CancelledByID = $member->ID;
1227
                $this->write();
1228
            }
1229
            //create log ...
1230
            $log = OrderStatusLog_Cancel::create();
1231
            $log->AuthorID = $member->ID;
1232
            $log->OrderID = $this->ID;
1233
            $log->Note = $reason;
1234
            if ($member->IsShopAdmin()) {
1235
                $log->InternalUseOnly = true;
1236
            }
1237
            $log->write();
1238
            //remove from queue ...
1239
            $queueObjectSingleton = Injector::inst()->get('OrderProcessQueue');
1240
            $ordersinQueue = $queueObjectSingleton->removeOrderFromQueue($this);
1241
            $this->extend('doCancel', $member, $log);
1242
1243
            return $log;
1244
        }
1245
    }
1246
1247
    /**
1248
     * returns true if successful.
1249
     *
1250
     * @param bool $avoidWrites
1251
     *
1252
     * @return bool
1253
     */
1254
    public function Archive($avoidWrites = true)
1255
    {
1256
        $lastOrderStep = OrderStep::get()->Last();
1257
        if ($lastOrderStep) {
1258
            if ($avoidWrites) {
1259
                DB::query('
1260
                    UPDATE "Order"
1261
                    SET "Order"."StatusID" = '.$lastOrderStep->ID.'
1262
                    WHERE "Order"."ID" = '.$this->ID.'
1263
                    LIMIT 1
1264
                ');
1265
1266
                return true;
1267
            } else {
1268
                $this->StatusID = $lastOrderStep->ID;
1269
                $this->write();
1270
1271
                return true;
1272
            }
1273
        }
1274
1275
        return false;
1276
    }
1277
1278
/*******************************************************
1279
   * 3. STATUS RELATED FUNCTIONS / SHORTCUTS
1280
*******************************************************/
1281
1282
    /**
1283
     * Avoids caching of $this->Status().
1284
     *
1285
     * @return DataObject (current OrderStep)
1286
     */
1287
    public function MyStep()
1288
    {
1289
        $step = null;
1290
        if ($this->StatusID) {
1291
            $step = OrderStep::get()->byID($this->StatusID);
1292
        }
1293
        if (!$step) {
1294
            $step = OrderStep::get()->First(); //TODO: this could produce strange results
1295
        }
1296
        if (!$step) {
1297
            $step = OrderStep_Created::create();
1298
        }
1299
        if (!$step) {
1300
            user_error('You need an order step in your Database.');
1301
        }
1302
1303
        return $step;
1304
    }
1305
1306
    /**
1307
     * Return the OrderStatusLog that is relevant to the Order status.
1308
     *
1309
     * @return OrderStatusLog
1310
     */
1311
    public function RelevantLogEntry()
1312
    {
1313
        return $this->MyStep()->RelevantLogEntry($this);
1314
    }
1315
1316
    /**
1317
     * @return OrderStep (current OrderStep that can be seen by customer)
1318
     */
1319
    public function CurrentStepVisibleToCustomer()
1320
    {
1321
        $obj = $this->MyStep();
1322
        if ($obj->HideStepFromCustomer) {
1323
            $obj = OrderStep::get()->where('"OrderStep"."Sort" < '.$obj->Sort.' AND "HideStepFromCustomer" = 0')->Last();
1324
            if (!$obj) {
1325
                $obj = OrderStep::get()->First();
1326
            }
1327
        }
1328
1329
        return $obj;
1330
    }
1331
1332
    /**
1333
     * works out if the order is still at the first OrderStep.
1334
     *
1335
     * @return bool
1336
     */
1337
    public function IsFirstStep()
1338
    {
1339
        $firstStep = OrderStep::get()->First();
1340
        $currentStep = $this->MyStep();
1341
        if ($firstStep && $currentStep) {
1342
            if ($firstStep->ID == $currentStep->ID) {
1343
                return true;
1344
            }
1345
        }
1346
1347
        return false;
1348
    }
1349
1350
    /**
1351
     * Is the order still being "edited" by the customer?
1352
     *
1353
     * @return bool
1354
     */
1355
    public function IsInCart()
1356
    {
1357
        return (bool) $this->IsSubmitted() ? false : true;
1358
    }
1359
1360
    /**
1361
     * The order has "passed" the IsInCart phase.
1362
     *
1363
     * @return bool
1364
     */
1365
    public function IsPastCart()
1366
    {
1367
        return (bool) $this->IsInCart() ? false : true;
1368
    }
1369
1370
    /**
1371
     * Are there still steps the order needs to go through?
1372
     *
1373
     * @return bool
1374
     */
1375
    public function IsUncomplete()
1376
    {
1377
        return (bool) $this->MyStep()->ShowAsUncompletedOrder;
1378
    }
1379
1380
    /**
1381
     * Is the order in the :"processing" phaase.?
1382
     *
1383
     * @return bool
1384
     */
1385
    public function IsProcessing()
1386
    {
1387
        return (bool) $this->MyStep()->ShowAsInProcessOrder;
1388
    }
1389
1390
    /**
1391
     * Is the order completed?
1392
     *
1393
     * @return bool
1394
     */
1395
    public function IsCompleted()
1396
    {
1397
        return (bool) $this->MyStep()->ShowAsCompletedOrder;
1398
    }
1399
1400
    /**
1401
     * Has the order been paid?
1402
     * TODO: why do we check if there is a total at all?
1403
     *
1404
     * @return bool
1405
     */
1406
    public function IsPaid()
1407
    {
1408
        if ($this->IsSubmitted()) {
1409
            return (bool) (($this->Total() >= 0) && ($this->TotalOutstanding() <= 0));
1410
        }
1411
1412
        return false;
1413
    }
1414
    /**
1415
     * Has the order been paid?
1416
     * TODO: why do we check if there is a total at all?
1417
     *
1418
     * @return Boolean (object)
1419
     */
1420
    public function IsPaidNice()
1421
    {
1422
        return  DBField::create_field('Boolean', $this->IsPaid());
1423
    }
1424
1425
    /**
1426
     * Has the order been paid?
1427
     * TODO: why do we check if there is a total at all?
1428
     *
1429
     * @return bool
1430
     */
1431
    public function PaymentIsPending()
1432
    {
1433
        if ($this->IsSubmitted()) {
1434
            if ($this->IsPaid()) {
1435
                //do nothing;
1436
            } elseif (($payments = $this->Payments()) && $payments->count()) {
1437
                foreach ($payments as $payment) {
1438
                    if ('Pending' == $payment->Status) {
1439
                        return true;
1440
                    }
1441
                }
1442
            }
1443
        }
1444
1445
        return false;
1446
    }
1447
1448
    /**
1449
     * shows payments that are meaningfull
1450
     * if the order has been paid then only show successful payments.
1451
     *
1452
     * @return DataList
1453
     */
1454
    public function RelevantPayments()
1455
    {
1456
        if ($this->IsPaid()) {
1457
            return $this->Payments("\"Status\" = 'Success'");
1458
            //EcommercePayment::get()->
1459
            //	filter(array("OrderID" => $this->ID, "Status" => "Success"));
1460
        } else {
1461
            return $this->Payments();
1462
        }
1463
    }
1464
1465
    /**
1466
     * Has the order been cancelled?
1467
     *
1468
     * @return bool
1469
     */
1470
    public function IsCancelled()
1471
    {
1472
        return $this->getIsCancelled();
1473
    }
1474
    public function getIsCancelled()
1475
    {
1476
        return $this->CancelledByID ? true : false;
1477
    }
1478
1479
    /**
1480
     * Has the order been cancelled by the customer?
1481
     *
1482
     * @return bool
1483
     */
1484
    public function IsCustomerCancelled()
1485
    {
1486
        if ($this->MemberID > 0 && $this->MemberID == $this->IsCancelledID) {
1487
            return true;
1488
        }
1489
1490
        return false;
1491
    }
1492
1493
    /**
1494
     * Has the order been cancelled by the  administrator?
1495
     *
1496
     * @return bool
1497
     */
1498
    public function IsAdminCancelled()
1499
    {
1500
        if ($this->IsCancelled()) {
1501
            if (!$this->IsCustomerCancelled()) {
1502
                $admin = Member::get()->byID($this->CancelledByID);
1503
                if ($admin) {
1504
                    if ($admin->IsShopAdmin()) {
1505
                        return true;
1506
                    }
1507
                }
1508
            }
1509
        }
1510
1511
        return false;
1512
    }
1513
1514
    /**
1515
     * Is the Shop Closed for business?
1516
     *
1517
     * @return bool
1518
     */
1519
    public function ShopClosed()
1520
    {
1521
        return EcomConfig()->ShopClosed;
1522
    }
1523
1524
/*******************************************************
1525
   * 4. LINKING ORDER WITH MEMBER AND ADDRESS
1526
*******************************************************/
1527
1528
    /**
1529
     * Returns a member linked to the order.
1530
     * If a member is already linked, it will return the existing member.
1531
     * Otherwise it will return a new Member.
1532
     *
1533
     * Any new member is NOT written, because we dont want to create a new member unless we have to!
1534
     * We will not add a member to the order unless a new one is created in the checkout
1535
     * OR the member is logged in / logs in.
1536
     *
1537
     * Also note that if a new member is created, it is not automatically written
1538
     *
1539
     * @param bool $forceCreation - if set to true then the member will always be saved in the database.
1540
     *
1541
     * @return Member
1542
     **/
1543
    public function CreateOrReturnExistingMember($forceCreation = false)
1544
    {
1545
        if ($this->IsSubmitted()) {
1546
            return $this->Member();
1547
        }
1548
        if ($this->MemberID) {
1549
            $member = $this->Member();
1550
        } elseif ($member = Member::currentUser()) {
1551
            if (!$member->IsShopAdmin()) {
1552
                $this->MemberID = $member->ID;
1553
                $this->write();
1554
            }
1555
        }
1556
        $member = $this->Member();
1557
        if (!$member) {
1558
            $member = new Member();
1559
        }
1560
        if ($member && $forceCreation) {
1561
            $member->write();
1562
        }
1563
1564
        return $member;
1565
    }
1566
1567
    /**
1568
     * Returns either the existing one or a new Order Address...
1569
     * All Orders will have a Shipping and Billing address attached to it.
1570
     * Method used to retrieve object e.g. for $order->BillingAddress(); "BillingAddress" is the method name you can use.
1571
     * If the method name is the same as the class name then dont worry about providing one.
1572
     *
1573
     * @param string $className             - ClassName of the Address (e.g. BillingAddress or ShippingAddress)
1574
     * @param string $alternativeMethodName - method to retrieve Address
1575
     **/
1576
    public function CreateOrReturnExistingAddress($className = 'BillingAddress', $alternativeMethodName = '')
1577
    {
1578
        if ($this->exists()) {
1579
            $methodName = $className;
1580
            if ($alternativeMethodName) {
1581
                $methodName = $alternativeMethodName;
1582
            }
1583
            if ($this->IsSubmitted()) {
1584
                return $this->$methodName();
1585
            }
1586
            $variableName = $className.'ID';
1587
            $address = null;
1588
            if ($this->$variableName) {
1589
                $address = $this->$methodName();
1590
            }
1591
            if (!$address) {
1592
                $address = new $className();
1593
                if ($member = $this->CreateOrReturnExistingMember()) {
1594
                    if ($member->exists()) {
1595
                        $address->FillWithLastAddressFromMember($member, $write = false);
1596
                    }
1597
                }
1598
            }
1599
            if ($address) {
1600
                if (!$address->exists()) {
1601
                    $address->write();
1602
                }
1603
                if ($address->OrderID != $this->ID) {
1604
                    $address->OrderID = $this->ID;
1605
                    $address->write();
1606
                }
1607
                if ($this->$variableName != $address->ID) {
1608
                    if (!$this->IsSubmitted()) {
1609
                        $this->$variableName = $address->ID;
1610
                        $this->write();
1611
                    }
1612
                }
1613
1614
                return $address;
1615
            }
1616
        }
1617
1618
        return;
1619
    }
1620
1621
    /**
1622
     * Sets the country in the billing and shipping address.
1623
     *
1624
     * @param string $countryCode            - code for the country e.g. NZ
1625
     * @param bool   $includeBillingAddress
1626
     * @param bool   $includeShippingAddress
1627
     **/
1628
    public function SetCountryFields($countryCode, $includeBillingAddress = true, $includeShippingAddress = true)
1629
    {
1630
        if ($this->IsSubmitted()) {
1631
            user_error('Can not change country in submitted order', E_USER_NOTICE);
1632
        } else {
1633
            if ($includeBillingAddress) {
1634
                if ($billingAddress = $this->CreateOrReturnExistingAddress('BillingAddress')) {
1635
                    $billingAddress->SetCountryFields($countryCode);
1636
                }
1637
            }
1638
            if (EcommerceConfig::get('OrderAddress', 'use_separate_shipping_address')) {
1639
                if ($includeShippingAddress) {
1640
                    if ($shippingAddress = $this->CreateOrReturnExistingAddress('ShippingAddress')) {
1641
                        $shippingAddress->SetCountryFields($countryCode);
1642
                    }
1643
                }
1644
            }
1645
        }
1646
    }
1647
1648
    /**
1649
     * Sets the region in the billing and shipping address.
1650
     *
1651
     * @param int $regionID - ID for the region to be set
1652
     **/
1653
    public function SetRegionFields($regionID)
1654
    {
1655
        if ($this->IsSubmitted()) {
1656
            user_error('Can not change country in submitted order', E_USER_NOTICE);
1657
        } else {
1658
            if ($billingAddress = $this->CreateOrReturnExistingAddress('BillingAddress')) {
1659
                $billingAddress->SetRegionFields($regionID);
1660
            }
1661
            if ($this->CanHaveShippingAddress()) {
1662
                if ($shippingAddress = $this->CreateOrReturnExistingAddress('ShippingAddress')) {
1663
                    $shippingAddress->SetRegionFields($regionID);
1664
                }
1665
            }
1666
        }
1667
    }
1668
1669
    /**
1670
     * Stores the preferred currency of the order.
1671
     * IMPORTANTLY we store the exchange rate for future reference...
1672
     *
1673
     * @param EcommerceCurrency $currency
1674
     */
1675
    public function UpdateCurrency($newCurrency)
1676
    {
1677
        if ($this->IsSubmitted()) {
1678
            user_error('Can not set the currency after the order has been submitted', E_USER_NOTICE);
1679
        } else {
1680
            if (! is_a($newCurrency, Object::getCustomClass('EcommerceCurrency'))) {
1681
                $newCurrency = EcommerceCurrency::default_currency();
1682
            }
1683
            $this->CurrencyUsedID = $newCurrency->ID;
1684
            $this->ExchangeRate = $newCurrency->getExchangeRate();
1685
            $this->write();
1686
        }
1687
    }
1688
1689
    /**
1690
     * alias for UpdateCurrency.
1691
     *
1692
     * @param EcommerceCurrency $currency
1693
     */
1694
    public function SetCurrency($currency)
1695
    {
1696
        $this->UpdateCurrency($currency);
1697
    }
1698
1699
/*******************************************************
1700
   * 5. CUSTOMER COMMUNICATION
1701
*******************************************************/
1702
1703
    /**
1704
     * Send the invoice of the order by email.
1705
     *
1706
     * @param string $emailClassName     (optional) class used to send email
1707
     * @param string $subject            (optional) subject for the email
1708
     * @param string $message            (optional) the main message in the email
1709
     * @param bool   $resend             (optional) send the email even if it has been sent before
1710
     * @param bool   $adminOnlyOrToEmail (optional) sends the email to the ADMIN ONLY, if you provide an email, it will go to the email...
1711
     *
1712
     * @return bool TRUE on success, FALSE on failure
1713
     */
1714
    public function sendEmail(
1715
        $emailClassName = 'Order_InvoiceEmail',
1716
        $subject = '',
1717
        $message = '',
1718
        $resend = false,
1719
        $adminOnlyOrToEmail = false
1720
    ) {
1721
        return $this->prepareAndSendEmail(
1722
            $emailClassName,
1723
            $subject,
1724
            $message,
1725
            $resend,
1726
            $adminOnlyOrToEmail
1727
        );
1728
    }
1729
1730
    /**
1731
     * Sends a message to the shop admin ONLY and not to the customer
1732
     * This can be used by ordersteps and orderlogs to notify the admin of any potential problems.
1733
     *
1734
     * @param string         $emailClassName       - (optional) template to be used ...
1735
     * @param string         $subject              - (optional) subject for the email
1736
     * @param string         $message              - (optional) message to be added with the email
1737
     * @param bool           $resend               - (optional) can it be sent twice?
1738
     * @param bool | string  $adminOnlyOrToEmail   - (optional) sends the email to the ADMIN ONLY, if you provide an email, it will go to the email...
1739
     *
1740
     * @return bool TRUE for success, FALSE for failure (not tested)
1741
     */
1742
    public function sendAdminNotification(
1743
        $emailClassName = 'Order_ErrorEmail',
1744
        $subject = '',
1745
        $message = '',
1746
        $resend = false,
1747
        $adminOnlyOrToEmail = true
1748
    ) {
1749
        return $this->prepareAndSendEmail(
1750
            $emailClassName,
1751
            $subject,
1752
            $message,
1753
            $resend,
1754
            $adminOnlyOrToEmail
1755
        );
1756
    }
1757
1758
    /**
1759
     * returns the order formatted as an email.
1760
     *
1761
     * @param string $emailClassName - template to use.
1762
     * @param string $subject        - (optional) the subject (which can be used as title in email)
1763
     * @param string $message        - (optional) the additional message
1764
     *
1765
     * @return string (html)
1766
     */
1767
    public function renderOrderInEmailFormat(
1768
        $emailClassName,
1769
        $subject = '',
1770
        $message = ''
1771
    )
1772
    {
1773
        $arrayData = $this->createReplacementArrayForEmail($subject, $message);
1774
        Config::nest();
1775
        Config::inst()->update('SSViewer', 'theme_enabled', true);
1776
        $html = $arrayData->renderWith($emailClassName);
1777
        Config::unnest();
1778
1779
        return Order_Email::emogrify_html($html);
1780
    }
1781
1782
    /**
1783
     * Send a mail of the order to the client (and another to the admin).
1784
     *
1785
     * @param string         $emailClassName       - (optional) template to be used ...
1786
     * @param string         $subject              - (optional) subject for the email
1787
     * @param string         $message              - (optional) message to be added with the email
1788
     * @param bool           $resend               - (optional) can it be sent twice?
1789
     * @param bool | string  $adminOnlyOrToEmail   - (optional) sends the email to the ADMIN ONLY, if you provide an email, it will go to the email...
1790
     *
1791
     * @return bool TRUE for success, FALSE for failure (not tested)
1792
     */
1793
    protected function prepareAndSendEmail(
1794
        $emailClassName,
1795
        $subject,
1796
        $message,
1797
        $resend = false,
1798
        $adminOnlyOrToEmail = false
1799
    ) {
1800
        $arrayData = $this->createReplacementArrayForEmail($subject, $message);
1801
        $from = Order_Email::get_from_email();
1802
        //why are we using this email and NOT the member.EMAIL?
1803
        //for historical reasons????
1804
        if ($adminOnlyOrToEmail) {
1805
            if (filter_var($adminOnlyOrToEmail, FILTER_VALIDATE_EMAIL)) {
1806
                $to = $adminOnlyOrToEmail;
1807
                // invalid e-mail address
1808
            } else {
1809
                $to = Order_Email::get_from_email();
1810
            }
1811
        } else {
1812
            $to = $this->getOrderEmail();
1813
        }
1814
        if ($from && $to) {
1815
            $email = new $emailClassName();
1816
            if (!(is_a($email, Object::getCustomClass('Email')))) {
1817
                user_error('No correct email class provided.', E_USER_ERROR);
1818
            }
1819
            $email->setFrom($from);
1820
            $email->setTo($to);
1821
            //we take the subject from the Array Data, just in case it has been adjusted.
1822
            $email->setSubject($arrayData->getField('Subject'));
1823
            //we also see if a CC and a BCC have been added
1824
            ;
1825
            if ($cc = $arrayData->getField('CC')) {
1826
                $email->setCc($cc);
1827
            }
1828
            if ($bcc = $arrayData->getField('BCC')) {
1829
                $email->setBcc($bcc);
1830
            }
1831
            $email->populateTemplate($arrayData);
1832
            // This might be called from within the CMS,
1833
            // so we need to restore the theme, just in case
1834
            // templates within the theme exist
1835
            Config::nest();
1836
            Config::inst()->update('SSViewer', 'theme_enabled', true);
1837
            $email->setOrder($this);
1838
            $email->setResend($resend);
1839
            $result = $email->send(null);
1840
            Config::unnest();
1841
            if (Director::isDev()) {
1842
                return true;
1843
            } else {
1844
                return $result;
1845
            }
1846
        }
1847
1848
        return false;
1849
    }
1850
1851
    /**
1852
     * returns the Data that can be used in the body of an order Email
1853
     * we add the subject here so that the subject, for example, can be added to the <title>
1854
     * of the email template.
1855
     * we add the subject here so that the subject, for example, can be added to the <title>
1856
     * of the email template.
1857
     *
1858
     * @param string $subject  - (optional) subject for email
1859
     * @param string $message  - (optional) the additional message
1860
     *
1861
     * @return ArrayData
1862
     *                   - Subject - EmailSubject
1863
     *                   - Message - specific message for this order
1864
     *                   - Message - custom message
1865
     *                   - OrderStepMessage - generic message for step
1866
     *                   - Order
1867
     *                   - EmailLogo
1868
     *                   - ShopPhysicalAddress
1869
     *                   - CurrentDateAndTime
1870
     *                   - BaseURL
1871
     *                   - CC
1872
     *                   - BCC
1873
     */
1874
    protected function createReplacementArrayForEmail($subject = '', $message = '')
1875
    {
1876
        $step = $this->MyStep();
1877
        $config = $this->EcomConfig();
1878
        $replacementArray = array();
1879
        //set subject
1880
        if ( ! $subject) {
1881
            $subject = $step->EmailSubject;
1882
        }
1883
        if( ! $message) {
1884
            $message = $step->CustomerMessage;
1885
        }
1886
        $subject = str_replace('[OrderNumber]', $this->ID, $subject);
1887
        //set other variables
1888
        $replacementArray['Subject'] = $subject;
1889
        $replacementArray['To'] = '';
1890
        $replacementArray['CC'] = '';
1891
        $replacementArray['BCC'] = '';
1892
        $replacementArray['OrderStepMessage'] = $message;
1893
        $replacementArray['Order'] = $this;
1894
        $replacementArray['EmailLogo'] = $config->EmailLogo();
1895
        $replacementArray['ShopPhysicalAddress'] = $config->ShopPhysicalAddress;
1896
        $replacementArray['CurrentDateAndTime'] = DBField::create_field('SS_Datetime', 'Now');
1897
        $replacementArray['BaseURL'] = Director::baseURL();
1898
        $arrayData = ArrayData::create($replacementArray);
1899
        $this->extend('updateReplacementArrayForEmail', $arrayData);
1900
1901
        return $arrayData;
1902
    }
1903
1904
/*******************************************************
1905
   * 6. ITEM MANAGEMENT
1906
*******************************************************/
1907
1908
    /**
1909
     * returns a list of Order Attributes by type.
1910
     *
1911
     * @param array | String $types
1912
     *
1913
     * @return ArrayList
1914
     */
1915
    public function getOrderAttributesByType($types)
1916
    {
1917
        if (!is_array($types) && is_string($types)) {
1918
            $types = array($types);
1919
        }
1920
        if (!is_array($al)) {
1921
            user_error('wrong parameter (types) provided in Order::getOrderAttributesByTypes');
1922
        }
1923
        $al = new ArrayList();
1924
        $items = $this->Items();
1925
        foreach ($items as $item) {
1926
            if (in_array($item->OrderAttributeType(), $types)) {
1927
                $al->push($item);
1928
            }
1929
        }
1930
        $modifiers = $this->Modifiers();
1931
        foreach ($modifiers as $modifier) {
1932
            if (in_array($modifier->OrderAttributeType(), $types)) {
1933
                $al->push($modifier);
1934
            }
1935
        }
1936
1937
        return $al;
1938
    }
1939
1940
    /**
1941
     * Returns the items of the order.
1942
     * Items are the order items (products) and NOT the modifiers (discount, tax, etc...).
1943
     *
1944
     * N. B. this method returns Order Items
1945
     * also see Buaybles
1946
1947
     *
1948
     * @param string filter - where statement to exclude certain items OR ClassName (e.g. 'TaxModifier')
1949
     *
1950
     * @return DataList (OrderItems)
1951
     */
1952
    public function Items($filterOrClassName = '')
1953
    {
1954
        if (!$this->exists()) {
1955
            $this->write();
1956
        }
1957
1958
        return $this->itemsFromDatabase($filterOrClassName);
1959
    }
1960
1961
    /**
1962
     * @alias function of Items
1963
     *
1964
     * N. B. this method returns Order Items
1965
     * also see Buaybles
1966
     *
1967
     * @param string filter - where statement to exclude certain items.
1968
     * @alias for Items
1969
     * @return DataList (OrderItems)
1970
     */
1971
    public function OrderItems($filterOrClassName = '')
1972
    {
1973
        return $this->Items($filterOrClassName);
1974
    }
1975
1976
    /**
1977
     * returns the buyables asscoiated with the order items.
1978
     *
1979
     * NB. this method retursn buyables
1980
     *
1981
     * @param string filter - where statement to exclude certain items.
1982
     *
1983
     * @return ArrayList (Buyables)
1984
     */
1985
    public function Buyables($filterOrClassName = '')
1986
    {
1987
        $items = $this->Items($filterOrClassName);
1988
        $arrayList = new ArrayList();
1989
        foreach ($items as $item) {
1990
            $arrayList->push($item->Buyable());
1991
        }
1992
1993
        return $arrayList;
1994
    }
1995
1996
    /**
1997
     * Return all the {@link OrderItem} instances that are
1998
     * available as records in the database.
1999
     *
2000
     * @param string filter - where statement to exclude certain items,
2001
     *   you can also pass a classname (e.g. MyOrderItem), in which case only this class will be returned (and any class extending your given class)
2002
     *
2003
     * @return DataList (OrderItems)
2004
     */
2005
    protected function itemsFromDatabase($filterOrClassName = '')
2006
    {
2007
        $className = 'OrderItem';
2008
        $extrafilter = '';
2009
        if ($filterOrClassName) {
2010
            if (class_exists($filterOrClassName)) {
2011
                $className = $filterOrClassName;
2012
            } else {
2013
                $extrafilter = " AND $filterOrClassName";
2014
            }
2015
        }
2016
2017
        return $className::get()->filter(array('OrderID' => $this->ID))->where($extrafilter);
2018
    }
2019
2020
    /**
2021
     * @alias for Modifiers
2022
     *
2023
     * @return DataList (OrderModifiers)
2024
     */
2025
    public function OrderModifiers()
2026
    {
2027
        return $this->Modifiers();
2028
    }
2029
2030
    /**
2031
     * Returns the modifiers of the order, if it hasn't been saved yet
2032
     * it returns the modifiers from session, if it has, it returns them
2033
     * from the DB entry. ONLY USE OUTSIDE ORDER.
2034
     *
2035
     * @param string filter - where statement to exclude certain items OR ClassName (e.g. 'TaxModifier')
2036
     *
2037
     * @return DataList (OrderModifiers)
2038
     */
2039
    public function Modifiers($filterOrClassName = '')
2040
    {
2041
        return $this->modifiersFromDatabase($filterOrClassName);
2042
    }
2043
2044
    /**
2045
     * Get all {@link OrderModifier} instances that are
2046
     * available as records in the database.
2047
     * NOTE: includes REMOVED Modifiers, so that they do not get added again...
2048
     *
2049
     * @param string filter - where statement to exclude certain items OR ClassName (e.g. 'TaxModifier')
2050
     *
2051
     * @return DataList (OrderModifiers)
2052
     */
2053
    protected function modifiersFromDatabase($filterOrClassName = '')
2054
    {
2055
        $className = 'OrderModifier';
2056
        $extrafilter = '';
2057
        if ($filterOrClassName) {
2058
            if (class_exists($filterOrClassName)) {
2059
                $className = $filterOrClassName;
2060
            } else {
2061
                $extrafilter = " AND $filterOrClassName";
2062
            }
2063
        }
2064
2065
        return $className::get()->where('"OrderAttribute"."OrderID" = '.$this->ID." $extrafilter");
2066
    }
2067
2068
    /**
2069
     * Calculates and updates all the order attributes.
2070
     *
2071
     * @param bool $recalculate - run it, even if it has run already
2072
     */
2073
    public function calculateOrderAttributes($recalculate = false)
2074
    {
2075
        if ($this->IsSubmitted()) {
2076
            //submitted orders are NEVER recalculated.
2077
            //they are set in stone.
2078
        } elseif (self::get_needs_recalculating($this->ID) || $recalculate) {
2079
            if ($this->StatusID || $this->TotalItems()) {
2080
                $this->ensureCorrectExchangeRate();
2081
                $this->calculateOrderItems($recalculate);
2082
                $this->calculateModifiers($recalculate);
2083
                $this->extend('onCalculateOrder');
2084
            }
2085
        }
2086
    }
2087
2088
    /**
2089
     * Calculates and updates all the product items.
2090
     *
2091
     * @param bool $recalculate - run it, even if it has run already
2092
     */
2093
    protected function calculateOrderItems($recalculate = false)
2094
    {
2095
        //check if order has modifiers already
2096
        //check /re-add all non-removable ones
2097
        //$start = microtime();
2098
        $orderItems = $this->itemsFromDatabase();
2099
        if ($orderItems->count()) {
2100
            foreach ($orderItems as $orderItem) {
2101
                if ($orderItem) {
2102
                    $orderItem->runUpdate($recalculate);
2103
                }
2104
            }
2105
        }
2106
        $this->extend('onCalculateOrderItems', $orderItems);
2107
    }
2108
2109
    /**
2110
     * Calculates and updates all the modifiers.
2111
     *
2112
     * @param bool $recalculate - run it, even if it has run already
2113
     */
2114
    protected function calculateModifiers($recalculate = false)
2115
    {
2116
        $createdModifiers = $this->modifiersFromDatabase();
2117
        if ($createdModifiers->count()) {
2118
            foreach ($createdModifiers as $modifier) {
2119
                if ($modifier) {
2120
                    $modifier->runUpdate($recalculate);
2121
                }
2122
            }
2123
        }
2124
        $this->extend('onCalculateModifiers', $createdModifiers);
2125
    }
2126
2127
    /**
2128
     * Returns the subtotal of the modifiers for this order.
2129
     * If a modifier appears in the excludedModifiers array, it is not counted.
2130
     *
2131
     * @param string|array $excluded               - Class(es) of modifier(s) to ignore in the calculation.
2132
     * @param bool         $stopAtExcludedModifier - when this flag is TRUE, we stop adding the modifiers when we reach an excluded modifier.
2133
     *
2134
     * @return float
2135
     */
2136
    public function ModifiersSubTotal($excluded = null, $stopAtExcludedModifier = false)
2137
    {
2138
        $total = 0;
2139
        $modifiers = $this->Modifiers();
2140
        if ($modifiers->count()) {
2141
            foreach ($modifiers as $modifier) {
2142
                if (!$modifier->IsRemoved()) { //we just double-check this...
2143
                    if (is_array($excluded) && in_array($modifier->ClassName, $excluded)) {
2144
                        if ($stopAtExcludedModifier) {
2145
                            break;
2146
                        }
2147
                        //do the next modifier
2148
                        continue;
2149
                    } elseif (is_string($excluded) && ($modifier->ClassName == $excluded)) {
2150
                        if ($stopAtExcludedModifier) {
2151
                            break;
2152
                        }
2153
                        //do the next modifier
2154
                        continue;
2155
                    }
2156
                    $total += $modifier->CalculationTotal();
2157
                }
2158
            }
2159
        }
2160
2161
        return $total;
2162
    }
2163
2164
    /**
2165
     * returns a modifier that is an instanceof the classname
2166
     * it extends.
2167
     *
2168
     * @param string $className: class name for the modifier
2169
     *
2170
     * @return DataObject (OrderModifier)
2171
     **/
2172
    public function RetrieveModifier($className)
2173
    {
2174
        $modifiers = $this->Modifiers();
2175
        if ($modifiers->count()) {
2176
            foreach ($modifiers as $modifier) {
2177
                if (is_a($modifier, Object::getCustomClass($className))) {
2178
                    return $modifier;
2179
                }
2180
            }
2181
        }
2182
    }
2183
2184
/*******************************************************
2185
   * 7. CRUD METHODS (e.g. canView, canEdit, canDelete, etc...)
2186
*******************************************************/
2187
2188
    /**
2189
     * @param Member $member
2190
     *
2191
     * @return DataObject (Member)
2192
     **/
2193
     //TODO: please comment why we make use of this function
2194
    protected function getMemberForCanFunctions(Member $member = null)
2195
    {
2196
        if (!$member) {
2197
            $member = Member::currentUser();
2198
        }
2199
        if (!$member) {
2200
            $member = new Member();
2201
            $member->ID = 0;
2202
        }
2203
2204
        return $member;
2205
    }
2206
2207
    /**
2208
     * @param Member $member
2209
     *
2210
     * @return bool
2211
     **/
2212
    public function canCreate($member = null)
2213
    {
2214
        $member = $this->getMemberForCanFunctions($member);
2215
        $extended = $this->extendedCan(__FUNCTION__, $member);
2216
        if ($extended !== null) {
2217
            return $extended;
2218
        }
2219
        if ($member->exists()) {
2220
            return $member->IsShopAdmin();
2221
        }
2222
    }
2223
2224
    /**
2225
     * Standard SS method - can the current member view this order?
2226
     *
2227
     * @param Member $member
2228
     *
2229
     * @return bool
2230
     **/
2231
    public function canView($member = null)
2232
    {
2233
        if (!$this->exists()) {
2234
            return true;
2235
        }
2236
        $member = $this->getMemberForCanFunctions($member);
2237
        //check if this has been "altered" in any DataExtension
2238
        $extended = $this->extendedCan(__FUNCTION__, $member);
2239
        if ($extended !== null) {
2240
            return $extended;
2241
        }
2242
        //is the member is a shop admin they can always view it
2243
        if (EcommerceRole::current_member_is_shop_admin($member)) {
2244
            return true;
2245
        }
2246
2247
        //is the member is a shop assistant they can always view it
2248
        if (EcommerceRole::current_member_is_shop_assistant($member)) {
2249
            return true;
2250
        }
2251
        //if the current member OWNS the order, (s)he can always view it.
2252
        if ($member->exists() && $this->MemberID == $member->ID) {
2253
            return true;
2254
        }
2255
        //it is the current order
2256
        if ($this->IsInSession()) {
2257
            //we do some additional CHECKS for session hackings!
2258
            if ($member->exists() && $this->MemberID) {
2259
                //can't view the order of another member!
2260
                //shop admin exemption is already captured.
2261
                //this is always true
2262
                if ($this->MemberID != $member->ID) {
2263
                    return false;
2264
                }
2265
            } else {
2266
                //order belongs to someone, but current user is NOT logged in...
2267
                //this is allowed!
2268
                //the reason it is allowed is because we want to be able to
2269
                //add order to non-existing member
2270
                return true;
2271
            }
2272
        }
2273
2274
        return false;
2275
    }
2276
2277
    /**
2278
     * @param Member $member optional
2279
     * @return bool
2280
     */
2281
    public function canOverrideCanView($member = null)
2282
    {
2283
        if ($this->canView($member)) {
2284
            //can view overrides any concerns
2285
            return true;
2286
        } else {
2287
            $tsOrder = strtotime($this->LastEdited);
2288
            $tsNow = time();
2289
            $minutes = EcommerceConfig::get('Order', 'minutes_an_order_can_be_viewed_without_logging_in');
2290
            if ($minutes && ((($tsNow - $tsOrder) / 60) < $minutes)) {
2291
2292
                //has the order been edited recently?
2293
                return true;
2294
            } elseif ($orderStep = $this->MyStep()) {
2295
2296
                // order is being processed ...
2297
                return $orderStep->canOverrideCanViewForOrder($this, $member);
2298
            }
2299
        }
2300
        return false;
2301
    }
2302
2303
    /**
2304
     * @return bool
2305
     */
2306
    public function IsInSession()
2307
    {
2308
        $orderInSession = ShoppingCart::session_order();
2309
2310
        return $orderInSession && $this->ID && $this->ID == $orderInSession->ID;
2311
    }
2312
2313
    /**
2314
     * returns a pseudo random part of the session id.
2315
     *
2316
     * @param int $size
2317
     *
2318
     * @return string
2319
     */
2320
    public function LessSecureSessionID($size = 7, $start = null)
2321
    {
2322
        if (!$start || $start < 0 || $start > (32 - $size)) {
2323
            $start = 0;
2324
        }
2325
2326
        return substr($this->SessionID, $start, $size);
2327
    }
2328
    /**
2329
     *
2330
     * @param Member (optional) $member
2331
     *
2332
     * @return bool
2333
     **/
2334
    public function canViewAdminStuff($member = null)
2335
    {
2336
        $member = $this->getMemberForCanFunctions($member);
2337
        $extended = $this->extendedCan(__FUNCTION__, $member);
2338
        if ($extended !== null) {
2339
            return $extended;
2340
        }
2341
        if (Permission::checkMember($member, Config::inst()->get('EcommerceRole', 'admin_permission_code'))) {
2342
            return true;
2343
        }
2344
    }
2345
2346
    /**
2347
     * if we set canEdit to false then we
2348
     * can not see the child records
2349
     * Basically, you can edit when you can view and canEdit (even as a customer)
2350
     * Or if you are a Shop Admin you can always edit.
2351
     * Otherwise it is false...
2352
     *
2353
     * @param Member $member
2354
     *
2355
     * @return bool
2356
     **/
2357
    public function canEdit($member = null)
2358
    {
2359
        $member = $this->getMemberForCanFunctions($member);
2360
        $extended = $this->extendedCan(__FUNCTION__, $member);
2361
        if ($extended !== null) {
2362
            return $extended;
2363
        }
2364
        if ($this->canView($member) && $this->MyStep()->CustomerCanEdit) {
2365
            return true;
2366
        }
2367
        if (Permission::checkMember($member, Config::inst()->get('EcommerceRole', 'admin_permission_code'))) {
2368
            return true;
2369
        }
2370
        //is the member is a shop assistant they can always view it
2371
        if (EcommerceRole::current_member_is_shop_assistant($member)) {
2372
            return true;
2373
        }
2374
        return false;
2375
    }
2376
2377
    /**
2378
     * is the order ready to go through to the
2379
     * checkout process.
2380
     *
2381
     * This method checks all the order items and order modifiers
2382
     * If any of them need immediate attention then this is done
2383
     * first after which it will go through to the checkout page.
2384
     *
2385
     * @param Member (optional) $member
2386
     *
2387
     * @return bool
2388
     **/
2389
    public function canCheckout(Member $member = null)
2390
    {
2391
        $member = $this->getMemberForCanFunctions($member);
2392
        $extended = $this->extendedCan(__FUNCTION__, $member);
2393
        if ($extended !== null) {
2394
            return $extended;
2395
        }
2396
        $submitErrors = $this->SubmitErrors();
2397
        if ($submitErrors && $submitErrors->count()) {
2398
            return false;
2399
        }
2400
2401
        return true;
2402
    }
2403
2404
    /**
2405
     * Can the order be submitted?
2406
     * this method can be used to stop an order from being submitted
2407
     * due to something not being completed or done.
2408
     *
2409
     * @see Order::SubmitErrors
2410
     *
2411
     * @param Member $member
2412
     *
2413
     * @return bool
2414
     **/
2415
    public function canSubmit(Member $member = null)
2416
    {
2417
        $member = $this->getMemberForCanFunctions($member);
2418
        $extended = $this->extendedCan(__FUNCTION__, $member);
2419
        if ($extended !== null) {
2420
            return $extended;
2421
        }
2422
        if ($this->IsSubmitted()) {
2423
            return false;
2424
        }
2425
        $submitErrors = $this->SubmitErrors();
2426
        if ($submitErrors && $submitErrors->count()) {
2427
            return false;
2428
        }
2429
2430
        return true;
2431
    }
2432
2433
    /**
2434
     * Can a payment be made for this Order?
2435
     *
2436
     * @param Member $member
2437
     *
2438
     * @return bool
2439
     **/
2440
    public function canPay(Member $member = null)
2441
    {
2442
        $member = $this->getMemberForCanFunctions($member);
2443
        $extended = $this->extendedCan(__FUNCTION__, $member);
2444
        if ($extended !== null) {
2445
            return $extended;
2446
        }
2447
        if ($this->IsPaid() || $this->IsCancelled() || $this->PaymentIsPending()) {
2448
            return false;
2449
        }
2450
2451
        return $this->MyStep()->CustomerCanPay;
2452
    }
2453
2454
    /**
2455
     * Can the given member cancel this order?
2456
     *
2457
     * @param Member $member
2458
     *
2459
     * @return bool
2460
     **/
2461
    public function canCancel(Member $member = null)
2462
    {
2463
        //if it is already cancelled it can not be cancelled again
2464
        if ($this->CancelledByID) {
2465
            return false;
2466
        }
2467
        $member = $this->getMemberForCanFunctions($member);
2468
        $extended = $this->extendedCan(__FUNCTION__, $member);
2469
        if ($extended !== null) {
2470
            return $extended;
2471
        }
2472
        if (EcommerceRole::current_member_can_process_orders($member)) {
2473
            return true;
2474
        }
2475
2476
        return $this->MyStep()->CustomerCanCancel && $this->canView($member);
2477
    }
2478
2479
    /**
2480
     * @param Member $member
2481
     *
2482
     * @return bool
2483
     **/
2484
    public function canDelete($member = null)
2485
    {
2486
        $member = $this->getMemberForCanFunctions($member);
2487
        $extended = $this->extendedCan(__FUNCTION__, $member);
2488
        if ($extended !== null) {
2489
            return $extended;
2490
        }
2491
        if ($this->IsSubmitted()) {
2492
            return false;
2493
        }
2494
        if (Permission::checkMember($member, Config::inst()->get('EcommerceRole', 'admin_permission_code'))) {
2495
            return true;
2496
        }
2497
2498
        return false;
2499
    }
2500
2501
    /**
2502
     * Returns all the order logs that the current member can view
2503
     * i.e. some order logs can only be viewed by the admin (e.g. suspected fraud orderlog).
2504
     *
2505
     * @return ArrayList (OrderStatusLogs)
2506
     **/
2507
    public function CanViewOrderStatusLogs()
2508
    {
2509
        $canViewOrderStatusLogs = new ArrayList();
2510
        $logs = $this->OrderStatusLogs();
2511
        foreach ($logs as $log) {
2512
            if ($log->canView()) {
2513
                $canViewOrderStatusLogs->push($log);
2514
            }
2515
        }
2516
2517
        return $canViewOrderStatusLogs;
2518
    }
2519
2520
    /**
2521
     * returns all the logs that can be viewed by the customer.
2522
     *
2523
     * @return ArrayList (OrderStausLogs)
2524
     */
2525
    public function CustomerViewableOrderStatusLogs()
2526
    {
2527
        $customerViewableOrderStatusLogs = new ArrayList();
2528
        $logs = $this->OrderStatusLogs();
2529
        if ($logs) {
2530
            foreach ($logs as $log) {
2531
                if (!$log->InternalUseOnly) {
2532
                    $customerViewableOrderStatusLogs->push($log);
2533
                }
2534
            }
2535
        }
2536
2537
        return $customerViewableOrderStatusLogs;
2538
    }
2539
2540
/*******************************************************
2541
   * 8. GET METHODS (e.g. Total, SubTotal, Title, etc...)
2542
*******************************************************/
2543
2544
    /**
2545
     * returns the email to be used for customer communication.
2546
     *
2547
     * @return string
2548
     */
2549
    public function OrderEmail()
2550
    {
2551
        return $this->getOrderEmail();
2552
    }
2553
    public function getOrderEmail()
2554
    {
2555
        $email = '';
2556
        if ($this->BillingAddressID && $this->BillingAddress()) {
2557
            $email = $this->BillingAddress()->Email;
2558
        }
2559
        if (! $email) {
2560
            if ($this->MemberID && $this->Member()) {
2561
                $email = $this->Member()->Email;
2562
            }
2563
        }
2564
        $extendedEmail = $this->extend('updateOrderEmail', $email);
2565
        if ($extendedEmail !== null && is_array($extendedEmail) && count($extendedEmail)) {
2566
            $email = implode(';', $extendedEmail);
2567
        }
2568
2569
        return $email;
2570
    }
2571
2572
    /**
2573
     * Returns true if there is a prink or email link.
2574
     *
2575
     * @return bool
2576
     */
2577
    public function HasPrintOrEmailLink()
2578
    {
2579
        return $this->EmailLink() || $this->PrintLink();
2580
    }
2581
2582
    /**
2583
     * returns the absolute link to the order that can be used in the customer communication (email).
2584
     *
2585
     * @return string
2586
     */
2587
    public function EmailLink($type = 'Order_StatusEmail')
2588
    {
2589
        return $this->getEmailLink();
2590
    }
2591
    public function getEmailLink($type = 'Order_StatusEmail')
2592
    {
2593
        if (!isset($_REQUEST['print'])) {
2594
            if ($this->IsSubmitted()) {
2595
                return Director::AbsoluteURL(OrderConfirmationPage::get_email_link($this->ID, $this->MyStep()->getEmailClassName(), $actuallySendEmail = true));
2596
            }
2597
        }
2598
    }
2599
2600
    /**
2601
     * returns the absolute link to the order for printing.
2602
     *
2603
     * @return string
2604
     */
2605
    public function PrintLink()
2606
    {
2607
        return $this->getPrintLink();
2608
    }
2609
    public function getPrintLink()
2610
    {
2611
        if (!isset($_REQUEST['print'])) {
2612
            if ($this->IsSubmitted()) {
2613
                return Director::AbsoluteURL(OrderConfirmationPage::get_order_link($this->ID)).'?print=1';
2614
            }
2615
        }
2616
    }
2617
2618
    /**
2619
     * returns the absolute link to the order for printing.
2620
     *
2621
     * @return string
2622
     */
2623
    public function PackingSlipLink()
2624
    {
2625
        return $this->getPackingSlipLink();
2626
    }
2627
    public function getPackingSlipLink()
2628
    {
2629
        if ($this->IsSubmitted()) {
2630
            return Director::AbsoluteURL(OrderConfirmationPage::get_order_link($this->ID)).'?packingslip=1';
2631
        }
2632
    }
2633
2634
    /**
2635
     * returns the absolute link that the customer can use to retrieve the email WITHOUT logging in.
2636
     *
2637
     * @return string
2638
     */
2639
    public function RetrieveLink()
2640
    {
2641
        return $this->getRetrieveLink();
2642
    }
2643
2644
    public function getRetrieveLink()
2645
    {
2646
        //important to recalculate!
2647
        if ($this->IsSubmitted($recalculate = true)) {
2648
            //add session ID if not added yet...
2649
            if (!$this->SessionID) {
2650
                $this->write();
2651
            }
2652
2653
            return Director::AbsoluteURL(OrderConfirmationPage::find_link()).'retrieveorder/'.$this->SessionID.'/'.$this->ID.'/';
2654
        } else {
2655
            return Director::AbsoluteURL('/shoppingcart/loadorder/'.$this->ID.'/');
2656
        }
2657
    }
2658
2659
    public function ShareLink()
2660
    {
2661
        return $this->getShareLink();
2662
    }
2663
2664
    public function getShareLink()
2665
    {
2666
        $orderItems = $this->itemsFromDatabase();
2667
        $action = 'share';
2668
        $array = array();
2669
        foreach ($orderItems as $orderItem) {
2670
            $array[] = implode(
2671
                ',',
2672
                array(
2673
                    $orderItem->BuyableClassName,
2674
                    $orderItem->BuyableID,
2675
                    $orderItem->Quantity
2676
                )
2677
            );
2678
        }
2679
2680
        return Director::AbsoluteURL(CartPage::find_link($action.'/'.implode('-', $array)));
2681
    }
2682
2683
    /**
2684
     * @alias for getFeedbackLink
2685
     * @return string
2686
     */
2687
    public function FeedbackLink()
2688
    {
2689
        return $this->getFeedbackLink();
2690
    }
2691
2692
    /**
2693
     * @return string | null
2694
     */
2695
    public function getFeedbackLink()
2696
    {
2697
        $orderConfirmationPage = OrderConfirmationPage::get()->first();
2698
        if($orderConfirmationPage->IsFeedbackEnabled) {
2699
2700
            return Director::AbsoluteURL($this->getRetrieveLink()).'#OrderForm_Feedback_FeedbackForm';
2701
        }
2702
    }
2703
2704
    /**
2705
     * link to delete order.
2706
     *
2707
     * @return string
2708
     */
2709
    public function DeleteLink()
2710
    {
2711
        return $this->getDeleteLink();
2712
    }
2713
    public function getDeleteLink()
2714
    {
2715
        if ($this->canDelete()) {
2716
            return ShoppingCart_Controller::delete_order_link($this->ID);
2717
        } else {
2718
            return '';
2719
        }
2720
    }
2721
2722
    /**
2723
     * link to copy order.
2724
     *
2725
     * @return string
2726
     */
2727
    public function CopyOrderLink()
2728
    {
2729
        return $this->getCopyOrderLink();
2730
    }
2731
    public function getCopyOrderLink()
2732
    {
2733
        if ($this->canView() && $this->IsSubmitted()) {
2734
            return ShoppingCart_Controller::copy_order_link($this->ID);
2735
        } else {
2736
            return '';
2737
        }
2738
    }
2739
2740
    /**
2741
     * A "Title" for the order, which summarises the main details (date, and customer) in a string.
2742
     *
2743
     * @param string $dateFormat  - e.g. "D j M Y, G:i T"
2744
     * @param bool   $includeName - e.g. by Mr Johnson
2745
     *
2746
     * @return string
2747
     **/
2748
    public function Title($dateFormat = null, $includeName = false)
2749
    {
2750
        return $this->getTitle($dateFormat, $includeName);
2751
    }
2752
    public function getTitle($dateFormat = null, $includeName = false)
2753
    {
2754
        if ($this->exists()) {
2755
            if ($dateFormat === null) {
2756
                $dateFormat = EcommerceConfig::get('Order', 'date_format_for_title');
2757
            }
2758
            if ($includeName === null) {
2759
                $includeName = EcommerceConfig::get('Order', 'include_customer_name_in_title');
2760
            }
2761
            $title = $this->i18n_singular_name()." #".number_format($this->ID);
2762
            if ($dateFormat) {
2763
                if ($submissionLog = $this->SubmissionLog()) {
2764
                    $dateObject = $submissionLog->dbObject('Created');
2765
                    $placed = _t('Order.PLACED', 'placed');
2766
                } else {
2767
                    $dateObject = $this->dbObject('Created');
2768
                    $placed = _t('Order.STARTED', 'started');
2769
                }
2770
                $title .= ' - '.$placed.' '.$dateObject->Format($dateFormat);
2771
            }
2772
            $name = '';
2773
            if ($this->CancelledByID) {
2774
                $name = ' - '._t('Order.CANCELLED', 'CANCELLED');
2775
            }
2776
            if ($includeName) {
2777
                $by = _t('Order.BY', 'by');
2778
                if (!$name) {
2779
                    if ($this->BillingAddressID) {
2780
                        if ($billingAddress = $this->BillingAddress()) {
2781
                            $name = ' - '.$by.' '.$billingAddress->Prefix.' '.$billingAddress->FirstName.' '.$billingAddress->Surname;
2782
                        }
2783
                    }
2784
                }
2785
                if (!$name) {
2786
                    if ($this->MemberID) {
2787
                        if ($member = $this->Member()) {
2788
                            if ($member->exists()) {
2789
                                if ($memberName = $member->getName()) {
2790
                                    if (!trim($memberName)) {
2791
                                        $memberName = _t('Order.ANONYMOUS', 'anonymous');
2792
                                    }
2793
                                    $name = ' - '.$by.' '.$memberName;
2794
                                }
2795
                            }
2796
                        }
2797
                    }
2798
                }
2799
            }
2800
            $title .= $name;
2801
        } else {
2802
            $title = _t('Order.NEW', 'New').' '.$this->i18n_singular_name();
2803
        }
2804
        $extendedTitle = $this->extend('updateTitle', $title);
2805
        if ($extendedTitle !== null && is_array($extendedTitle) && count($extendedTitle)) {
2806
            $title = implode('; ', $extendedTitle);
2807
        }
2808
2809
        return $title;
2810
    }
2811
2812
    /**
2813
     * Returns the subtotal of the items for this order.
2814
     *
2815
     * @return float
2816
     */
2817
    public function SubTotal()
2818
    {
2819
        return $this->getSubTotal();
2820
    }
2821
    public function getSubTotal()
2822
    {
2823
        $result = 0;
2824
        $items = $this->Items();
2825
        if ($items->count()) {
2826
            foreach ($items as $item) {
2827
                if (is_a($item, Object::getCustomClass('OrderAttribute'))) {
2828
                    $result += $item->Total();
2829
                }
2830
            }
2831
        }
2832
2833
        return $result;
2834
    }
2835
2836
    /**
2837
     * @return Currency (DB Object)
2838
     **/
2839
    public function SubTotalAsCurrencyObject()
2840
    {
2841
        return DBField::create_field('Currency', $this->SubTotal());
2842
    }
2843
2844
    /**
2845
     * @return Money
2846
     **/
2847
    public function SubTotalAsMoney()
2848
    {
2849
        return $this->getSubTotalAsMoney();
2850
    }
2851
    public function getSubTotalAsMoney()
2852
    {
2853
        return EcommerceCurrency::get_money_object_from_order_currency($this->SubTotal(), $this);
2854
    }
2855
2856
    /**
2857
     * @param string|array $excluded               - Class(es) of modifier(s) to ignore in the calculation.
2858
     * @param bool         $stopAtExcludedModifier - when this flag is TRUE, we stop adding the modifiers when we reach an excluded modifier.
2859
     *
2860
     * @return Currency (DB Object)
2861
     **/
2862
    public function ModifiersSubTotalAsCurrencyObject($excluded = null, $stopAtExcludedModifier = false)
2863
    {
2864
        return DBField::create_field('Currency', $this->ModifiersSubTotal($excluded, $stopAtExcludedModifier));
2865
    }
2866
2867
    /**
2868
     * @param string|array $excluded               - Class(es) of modifier(s) to ignore in the calculation.
2869
     * @param bool         $stopAtExcludedModifier - when this flag is TRUE, we stop adding the modifiers when we reach an excluded modifier.
2870
     *
2871
     * @return Money (DB Object)
2872
     **/
2873
    public function ModifiersSubTotalAsMoneyObject($excluded = null, $stopAtExcludedModifier = false)
2874
    {
2875
        return EcommerceCurrency::get_money_object_from_order_currency($this->ModifiersSubTotal($excluded, $stopAtExcludedModifier), $this);
2876
    }
2877
2878
    /**
2879
     * Returns the total cost of an order including the additional charges or deductions of its modifiers.
2880
     *
2881
     * @return float
2882
     */
2883
    public function Total()
2884
    {
2885
        return $this->getTotal();
2886
    }
2887
    public function getTotal()
2888
    {
2889
        return $this->SubTotal() + $this->ModifiersSubTotal();
2890
    }
2891
2892
    /**
2893
     * @return Currency (DB Object)
2894
     **/
2895
    public function TotalAsCurrencyObject()
2896
    {
2897
        return DBField::create_field('Currency', $this->Total());
2898
    }
2899
2900
    /**
2901
     * @return Money
2902
     **/
2903
    public function TotalAsMoney()
2904
    {
2905
        return $this->getTotalAsMoney();
2906
    }
2907
    public function getTotalAsMoney()
2908
    {
2909
        return EcommerceCurrency::get_money_object_from_order_currency($this->Total(), $this);
2910
    }
2911
2912
    /**
2913
     * Checks to see if any payments have been made on this order
2914
     * and if so, subracts the payment amount from the order.
2915
     *
2916
     * @return float
2917
     **/
2918
    public function TotalOutstanding()
2919
    {
2920
        return $this->getTotalOutstanding();
2921
    }
2922
    public function getTotalOutstanding()
2923
    {
2924
        if ($this->IsSubmitted()) {
2925
            $total = $this->Total();
2926
            $paid = $this->TotalPaid();
2927
            $outstanding = $total - $paid;
2928
            $maxDifference = EcommerceConfig::get('Order', 'maximum_ignorable_sales_payments_difference');
2929
            if (abs($outstanding) < $maxDifference) {
2930
                $outstanding = 0;
2931
            }
2932
2933
            return floatval($outstanding);
2934
        } else {
2935
            return 0;
2936
        }
2937
    }
2938
2939
    /**
2940
     * @return Currency (DB Object)
2941
     **/
2942
    public function TotalOutstandingAsCurrencyObject()
2943
    {
2944
        return DBField::create_field('Currency', $this->TotalOutstanding());
2945
    }
2946
2947
    /**
2948
     * @return Money
2949
     **/
2950
    public function TotalOutstandingAsMoney()
2951
    {
2952
        return $this->getTotalOutstandingAsMoney();
2953
    }
2954
    public function getTotalOutstandingAsMoney()
2955
    {
2956
        return EcommerceCurrency::get_money_object_from_order_currency($this->TotalOutstanding(), $this);
2957
    }
2958
2959
    /**
2960
     * @return float
2961
     */
2962
    public function TotalPaid()
2963
    {
2964
        return $this->getTotalPaid();
2965
    }
2966
    public function getTotalPaid()
2967
    {
2968
        $paid = 0;
2969
        if ($payments = $this->Payments()) {
2970
            foreach ($payments as $payment) {
2971
                if ($payment->Status == 'Success') {
2972
                    $paid += $payment->Amount->getAmount();
2973
                }
2974
            }
2975
        }
2976
        $reverseExchange = 1;
2977
        if ($this->ExchangeRate && $this->ExchangeRate != 1) {
2978
            $reverseExchange = 1 / $this->ExchangeRate;
2979
        }
2980
2981
        return $paid * $reverseExchange;
2982
    }
2983
2984
    /**
2985
     * @return Currency (DB Object)
2986
     **/
2987
    public function TotalPaidAsCurrencyObject()
2988
    {
2989
        return DBField::create_field('Currency', $this->TotalPaid());
2990
    }
2991
2992
    /**
2993
     * @return Money
2994
     **/
2995
    public function TotalPaidAsMoney()
2996
    {
2997
        return $this->getTotalPaidAsMoney();
2998
    }
2999
    public function getTotalPaidAsMoney()
3000
    {
3001
        return EcommerceCurrency::get_money_object_from_order_currency($this->TotalPaid(), $this);
3002
    }
3003
3004
    /**
3005
     * returns the total number of OrderItems (not modifiers).
3006
     * This is meant to run as fast as possible to quickly check
3007
     * if there is anything in the cart.
3008
     *
3009
     * @param bool $recalculate - do we need to recalculate (value is retained during lifetime of Object)
3010
     *
3011
     * @return int
3012
     **/
3013
    public function TotalItems($recalculate = false)
3014
    {
3015
        return $this->getTotalItems($recalculate);
3016
    }
3017
    public function getTotalItems($recalculate = false)
3018
    {
3019
        if ($this->totalItems === null || $recalculate) {
3020
            $this->totalItems = OrderItem::get()
3021
                ->where('"OrderAttribute"."OrderID" = '.$this->ID.' AND "OrderItem"."Quantity" > 0')
3022
                ->count();
3023
        }
3024
3025
        return $this->totalItems;
3026
    }
3027
3028
    /**
3029
     * Little shorthand.
3030
     *
3031
     * @param bool $recalculate
3032
     *
3033
     * @return bool
3034
     **/
3035
    public function MoreThanOneItemInCart($recalculate = false)
3036
    {
3037
        return $this->TotalItems($recalculate) > 1 ? true : false;
3038
    }
3039
3040
    /**
3041
     * returns the total number of OrderItems (not modifiers) times their respectective quantities.
3042
     *
3043
     * @param bool $recalculate - force recalculation
3044
     *
3045
     * @return float
3046
     **/
3047
    public function TotalItemsTimesQuantity($recalculate = false)
3048
    {
3049
        return $this->getTotalItemsTimesQuantity($recalculate);
3050
    }
3051
    public function getTotalItemsTimesQuantity($recalculate = false)
3052
    {
3053
        if ($this->totalItemsTimesQuantity === null || $recalculate) {
3054
            //to do, why do we check if you can edit ????
3055
            $this->totalItemsTimesQuantity = DB::query('
3056
                SELECT SUM("OrderItem"."Quantity")
3057
                FROM "OrderItem"
3058
                    INNER JOIN "OrderAttribute" ON "OrderAttribute"."ID" = "OrderItem"."ID"
3059
                WHERE
3060
                    "OrderAttribute"."OrderID" = '.$this->ID.'
3061
                    AND "OrderItem"."Quantity" > 0'
3062
            )->value();
3063
        }
3064
3065
        return $this->totalItemsTimesQuantity - 0;
3066
    }
3067
3068
    /**
3069
     *
3070
     * @return string (country code)
3071
     **/
3072
    public function Country()
3073
    {
3074
        return $this->getCountry();
3075
    }
3076
3077
    /**
3078
    * Returns the country code for the country that applies to the order.
3079
    * @alias  for getCountry
3080
    *
3081
    * @return string - country code e.g. NZ
3082
     */
3083
    public function getCountry()
3084
    {
3085
        $countryCodes = array(
3086
            'Billing' => '',
3087
            'Shipping' => '',
3088
        );
3089
        $code = null;
3090
        if ($this->BillingAddressID) {
3091
            $billingAddress = BillingAddress::get()->byID($this->BillingAddressID);
3092
            if ($billingAddress) {
3093
                if ($billingAddress->Country) {
3094
                    $countryCodes['Billing'] = $billingAddress->Country;
3095
                }
3096
            }
3097
        }
3098
        if ($this->ShippingAddressID && $this->UseShippingAddress) {
3099
            $shippingAddress = ShippingAddress::get()->byID($this->ShippingAddressID);
3100
            if ($shippingAddress) {
3101
                if ($shippingAddress->ShippingCountry) {
3102
                    $countryCodes['Shipping'] = $shippingAddress->ShippingCountry;
3103
                }
3104
            }
3105
        }
3106
        if (
3107
            (EcommerceConfig::get('OrderAddress', 'use_shipping_address_for_main_region_and_country') && $countryCodes['Shipping'])
3108
            ||
3109
            (!$countryCodes['Billing'] && $countryCodes['Shipping'])
3110
        ) {
3111
            $code = $countryCodes['Shipping'];
3112
        } elseif ($countryCodes['Billing']) {
3113
            $code = $countryCodes['Billing'];
3114
        } else {
3115
            $code = EcommerceCountry::get_country_from_ip();
3116
        }
3117
3118
        return $code;
3119
    }
3120
3121
    /**
3122
     * @alias for getFullNameCountry
3123
     *
3124
     * @return string - country name
3125
     **/
3126
    public function FullNameCountry()
3127
    {
3128
        return $this->getFullNameCountry();
3129
    }
3130
3131
    /**
3132
     * returns name of coutry.
3133
     *
3134
     * @return string - country name
3135
     **/
3136
    public function getFullNameCountry()
3137
    {
3138
        return EcommerceCountry::find_title($this->Country());
3139
    }
3140
3141
    /**
3142
     * @alis for getExpectedCountryName
3143
     * @return string - country name
3144
     **/
3145
    public function ExpectedCountryName()
3146
    {
3147
        return $this->getExpectedCountryName();
3148
    }
3149
3150
    /**
3151
     * returns name of coutry that we expect the customer to have
3152
     * This takes into consideration more than just what has been entered
3153
     * for example, it looks at GEO IP.
3154
     *
3155
     * @todo: why do we dont return a string IF there is only one item.
3156
     *
3157
     * @return string - country name
3158
     **/
3159
    public function getExpectedCountryName()
3160
    {
3161
        return EcommerceCountry::find_title(EcommerceCountry::get_country(false, $this->ID));
3162
    }
3163
3164
    /**
3165
     * return the title of the fixed country (if any).
3166
     *
3167
     * @return string | empty string
3168
     **/
3169
    public function FixedCountry()
3170
    {
3171
        return $this->getFixedCountry();
3172
    }
3173
    public function getFixedCountry()
3174
    {
3175
        $code = EcommerceCountry::get_fixed_country_code();
3176
        if ($code) {
3177
            return EcommerceCountry::find_title($code);
3178
        }
3179
3180
        return '';
3181
    }
3182
3183
    /**
3184
     * Returns the region that applies to the order.
3185
     * we check both billing and shipping, in case one of them is empty.
3186
     *
3187
     * @return DataObject | Null (EcommerceRegion)
3188
     **/
3189
    public function Region()
3190
    {
3191
        return $this->getRegion();
3192
    }
3193
    public function getRegion()
3194
    {
3195
        $regionIDs = array(
3196
            'Billing' => 0,
3197
            'Shipping' => 0,
3198
        );
3199
        if ($this->BillingAddressID) {
3200
            if ($billingAddress = $this->BillingAddress()) {
3201
                if ($billingAddress->RegionID) {
3202
                    $regionIDs['Billing'] = $billingAddress->RegionID;
3203
                }
3204
            }
3205
        }
3206
        if ($this->CanHaveShippingAddress()) {
3207
            if ($this->ShippingAddressID) {
3208
                if ($shippingAddress = $this->ShippingAddress()) {
3209
                    if ($shippingAddress->ShippingRegionID) {
3210
                        $regionIDs['Shipping'] = $shippingAddress->ShippingRegionID;
3211
                    }
3212
                }
3213
            }
3214
        }
3215
        if (count($regionIDs)) {
3216
            //note the double-check with $this->CanHaveShippingAddress() and get_use_....
3217
            if ($this->CanHaveShippingAddress() && EcommerceConfig::get('OrderAddress', 'use_shipping_address_for_main_region_and_country') && $regionIDs['Shipping']) {
3218
                return EcommerceRegion::get()->byID($regionIDs['Shipping']);
3219
            } else {
3220
                return EcommerceRegion::get()->byID($regionIDs['Billing']);
3221
            }
3222
        } else {
3223
            return EcommerceRegion::get()->byID(EcommerceRegion::get_region_from_ip());
3224
        }
3225
    }
3226
3227
    /**
3228
     * Casted variable
3229
     * Currency is not the same as the standard one?
3230
     *
3231
     * @return bool
3232
     **/
3233
    public function HasAlternativeCurrency()
3234
    {
3235
        return $this->getHasAlternativeCurrency();
3236
    }
3237
    public function getHasAlternativeCurrency()
3238
    {
3239
        if ($currency = $this->CurrencyUsed()) {
3240
            if ($currency->IsDefault()) {
3241
                return false;
3242
            } else {
3243
                return true;
3244
            }
3245
        } else {
3246
            return false;
3247
        }
3248
    }
3249
3250
    /**
3251
     * Makes sure exchange rate is updated and maintained before order is submitted
3252
     * This method is public because it could be called from a shopping Cart Object.
3253
     **/
3254
    public function EnsureCorrectExchangeRate()
3255
    {
3256
        if (!$this->IsSubmitted()) {
3257
            $oldExchangeRate = $this->ExchangeRate;
3258
            if ($currency = $this->CurrencyUsed()) {
3259
                if ($currency->IsDefault()) {
3260
                    $this->ExchangeRate = 0;
3261
                } else {
3262
                    $this->ExchangeRate = $currency->getExchangeRate();
3263
                }
3264
            } else {
3265
                $this->ExchangeRate = 0;
3266
            }
3267
            if ($this->ExchangeRate != $oldExchangeRate) {
3268
                $this->write();
3269
            }
3270
        }
3271
    }
3272
3273
    /**
3274
     * speeds up processing by storing the IsSubmitted value
3275
     * we start with -1 to know if it has been requested before.
3276
     *
3277
     * @var bool
3278
     */
3279
    protected $_isSubmittedTempVar = -1;
3280
3281
    /**
3282
     * Casted variable - has the order been submitted?
3283
     * alias
3284
     * @param bool $recalculate
3285
     *
3286
     * @return bool
3287
     **/
3288
    public function IsSubmitted($recalculate = true)
3289
    {
3290
        return $this->getIsSubmitted($recalculate);
3291
    }
3292
3293
    /**
3294
     * Casted variable - has the order been submitted?
3295
     *
3296
     * @param bool $recalculate
3297
     *
3298
     * @return bool
3299
     **/
3300
    public function getIsSubmitted($recalculate = false)
3301
    {
3302
        if ($this->_isSubmittedTempVar === -1 || $recalculate) {
3303
            if ($this->SubmissionLog()) {
3304
                $this->_isSubmittedTempVar = true;
3305
            } else {
3306
                $this->_isSubmittedTempVar = false;
3307
            }
3308
        }
3309
3310
        return $this->_isSubmittedTempVar;
3311
    }
3312
3313
    /**
3314
     *
3315
     *
3316
     * @return bool
3317
     */
3318
    public function IsArchived()
3319
    {
3320
        $lastStep = OrderStep::get()->Last();
3321
        if ($lastStep) {
3322
            if ($lastStep->ID == $this->StatusID) {
3323
                return true;
3324
            }
3325
        }
3326
        return false;
3327
    }
3328
3329
    /**
3330
     * Submission Log for this Order (if any).
3331
     *
3332
     * @return Submission Log (OrderStatusLog_Submitted) | Null
3333
     **/
3334
    public function SubmissionLog()
3335
    {
3336
        $className = EcommerceConfig::get('OrderStatusLog', 'order_status_log_class_used_for_submitting_order');
3337
3338
        return $className::get()
3339
            ->Filter(array('OrderID' => $this->ID))
3340
            ->Last();
3341
    }
3342
3343
    /**
3344
     * Submission Log for this Order (if any).
3345
     *
3346
     * @return DateTime
3347
     **/
3348
    public function OrderDate()
3349
    {
3350
        $object = $this->SubmissionLog();
3351
        if($object) {
3352
            $created = $object->Created;
3353
        } else {
3354
            $created = $this->LastEdited;
3355
        }
3356
3357
        return DBField::create_field('SS_Datetime', $created);
3358
    }
3359
3360
    /**
3361
     * @return int
3362
     */
3363
    public function SecondsSinceBeingSubmitted()
3364
    {
3365
        if ($submissionLog = $this->SubmissionLog()) {
3366
            return time() - strtotime($submissionLog->Created);
3367
        } else {
3368
            return 0;
3369
        }
3370
    }
3371
3372
    /**
3373
     * if the order can not be submitted,
3374
     * then the reasons why it can not be submitted
3375
     * will be returned by this method.
3376
     *
3377
     * @see Order::canSubmit
3378
     *
3379
     * @return ArrayList | null
3380
     */
3381
    public function SubmitErrors()
3382
    {
3383
        $al = null;
3384
        $extendedSubmitErrors = $this->extend('updateSubmitErrors');
3385
        if ($extendedSubmitErrors !== null && is_array($extendedSubmitErrors) && count($extendedSubmitErrors)) {
3386
            $al = ArrayList::create();
3387
            foreach ($extendedSubmitErrors as $returnResultArray) {
3388
                foreach ($returnResultArray as $issue) {
3389
                    if ($issue) {
3390
                        $al->push(ArrayData::create(array("Title" => $issue)));
3391
                    }
3392
                }
3393
            }
3394
        }
3395
        return $al;
3396
    }
3397
3398
    /**
3399
     * Casted variable - has the order been submitted?
3400
     *
3401
     * @param bool $withDetail
3402
     *
3403
     * @return string
3404
     **/
3405
    public function CustomerStatus($withDetail = true)
3406
    {
3407
        return $this->getCustomerStatus($withDetail);
3408
    }
3409
    public function getCustomerStatus($withDetail = true)
3410
    {
3411
        $str = '';
3412
        if ($this->MyStep()->ShowAsUncompletedOrder) {
3413
            $str = _t('Order.UNCOMPLETED', 'Uncompleted');
3414
        } elseif ($this->MyStep()->ShowAsInProcessOrder) {
3415
            $str = _t('Order.IN_PROCESS', 'In Process');
3416
        } elseif ($this->MyStep()->ShowAsCompletedOrder) {
3417
            $str = _t('Order.COMPLETED', 'Completed');
3418
        }
3419
        if ($withDetail) {
3420
            if (!$this->HideStepFromCustomer) {
3421
                $str .= ' ('.$this->MyStep()->Name.')';
3422
            }
3423
        }
3424
3425
        return $str;
3426
    }
3427
3428
    /**
3429
     * Casted variable - does the order have a potential shipping address?
3430
     *
3431
     * @return bool
3432
     **/
3433
    public function CanHaveShippingAddress()
3434
    {
3435
        return $this->getCanHaveShippingAddress();
3436
    }
3437
    public function getCanHaveShippingAddress()
3438
    {
3439
        return EcommerceConfig::get('OrderAddress', 'use_separate_shipping_address');
3440
    }
3441
3442
    /**
3443
     * returns the link to view the Order
3444
     * WHY NOT CHECKOUT PAGE: first we check for cart page.
3445
     *
3446
     * @return CartPage | Null
3447
     */
3448
    public function DisplayPage()
3449
    {
3450
        if ($this->MyStep() && $this->MyStep()->AlternativeDisplayPage()) {
3451
            $page = $this->MyStep()->AlternativeDisplayPage();
3452
        } elseif ($this->IsSubmitted()) {
3453
            $page = OrderConfirmationPage::get()->First();
3454
        } else {
3455
            $page = CartPage::get()
3456
                ->Filter(array('ClassName' => 'CartPage'))
3457
                ->First();
3458
            if (!$page) {
3459
                $page = CheckoutPage::get()->First();
3460
            }
3461
        }
3462
3463
        return $page;
3464
    }
3465
3466
    /**
3467
     * returns the link to view the Order
3468
     * WHY NOT CHECKOUT PAGE: first we check for cart page.
3469
     * If a cart page has been created then we refer through to Cart Page.
3470
     * Otherwise it will default to the checkout page.
3471
     *
3472
     * @param string $action - any action that should be added to the link.
3473
     *
3474
     * @return String(URLSegment)
3475
     */
3476
    public function Link($action = null)
3477
    {
3478
        $page = $this->DisplayPage();
3479
        if ($page) {
3480
            return $page->getOrderLink($this->ID, $action);
3481
        } else {
3482
            user_error('A Cart / Checkout Page + an Order Confirmation Page needs to be setup for the e-commerce module to work.', E_USER_NOTICE);
3483
            $page = ErrorPage::get()
3484
                ->Filter(array('ErrorCode' => '404'))
3485
                ->First();
3486
            if ($page) {
3487
                return $page->Link();
3488
            }
3489
        }
3490
    }
3491
3492
    /**
3493
     * Returns to link to access the Order's API.
3494
     *
3495
     * @param string $version
3496
     * @param string $extension
3497
     *
3498
     * @return String(URL)
3499
     */
3500
    public function APILink($version = 'v1', $extension = 'xml')
3501
    {
3502
        return Director::AbsoluteURL("/api/ecommerce/$version/Order/".$this->ID."/.$extension");
3503
    }
3504
3505
    /**
3506
     * returns the link to finalise the Order.
3507
     *
3508
     * @return String(URLSegment)
3509
     */
3510
    public function CheckoutLink()
3511
    {
3512
        $page = CheckoutPage::get()->First();
3513
        if ($page) {
3514
            return $page->Link();
3515
        } else {
3516
            $page = ErrorPage::get()
3517
                ->Filter(array('ErrorCode' => '404'))
3518
                ->First();
3519
            if ($page) {
3520
                return $page->Link();
3521
            }
3522
        }
3523
    }
3524
3525
    /**
3526
     * Converts the Order into HTML, based on the Order Template.
3527
     *
3528
     * @return HTML Object
3529
     **/
3530
    public function ConvertToHTML()
3531
    {
3532
        Config::nest();
3533
        Config::inst()->update('SSViewer', 'theme_enabled', true);
3534
        $html = $this->renderWith('Order');
3535
        Config::unnest();
3536
        $html = preg_replace('/(\s)+/', ' ', $html);
3537
3538
        return DBField::create_field('HTMLText', $html);
3539
    }
3540
3541
    /**
3542
     * Converts the Order into a serialized string
3543
     * TO DO: check if this works and check if we need to use special sapphire serialization code.
3544
     *
3545
     * @return string - serialized object
3546
     **/
3547
    public function ConvertToString()
3548
    {
3549
        return serialize($this->addHasOneAndHasManyAsVariables());
3550
    }
3551
3552
    /**
3553
     * Converts the Order into a JSON object
3554
     * TO DO: check if this works and check if we need to use special sapphire JSON code.
3555
     *
3556
     * @return string -  JSON
3557
     **/
3558
    public function ConvertToJSON()
3559
    {
3560
        return json_encode($this->addHasOneAndHasManyAsVariables());
3561
    }
3562
3563
    /**
3564
     * returns itself wtih more data added as variables.
3565
     * We add has_one and has_many as variables like this: $this->MyHasOne_serialized = serialize($this->MyHasOne()).
3566
     *
3567
     * @return Order - with most important has one and has many items included as variables.
3568
     **/
3569
    protected function addHasOneAndHasManyAsVariables()
3570
    {
3571
        $object = clone $this;
3572
        $object->Member_serialized = serialize($this->Member());
3573
        $object->BillingAddress_serialized = serialize($this->BillingAddress());
3574
        $object->ShippingAddress_serialized = serialize($this->ShippingAddress());
3575
        $object->Attributes_serialized = serialize($this->Attributes());
3576
        $object->OrderStatusLogs_serialized = serialize($this->OrderStatusLogs());
3577
        $object->Payments_serialized = serialize($this->Payments());
3578
        $object->Emails_serialized = serialize($this->Emails());
3579
3580
        return $object;
3581
    }
3582
3583
/*******************************************************
3584
   * 9. TEMPLATE RELATED STUFF
3585
*******************************************************/
3586
3587
    /**
3588
     * returns the instance of EcommerceConfigAjax for use in templates.
3589
     * In templates, it is used like this:
3590
     * $EcommerceConfigAjax.TableID.
3591
     *
3592
     * @return EcommerceConfigAjax
3593
     **/
3594
    public function AJAXDefinitions()
3595
    {
3596
        return EcommerceConfigAjax::get_one($this);
3597
    }
3598
3599
    /**
3600
     * returns the instance of EcommerceDBConfig.
3601
     *
3602
     * @return EcommerceDBConfig
3603
     **/
3604
    public function EcomConfig()
3605
    {
3606
        return EcommerceDBConfig::current_ecommerce_db_config();
3607
    }
3608
3609
    /**
3610
     * Collects the JSON data for an ajax return of the cart.
3611
     *
3612
     * @param array $js
3613
     *
3614
     * @return array (for use in AJAX for JSON)
3615
     **/
3616
    public function updateForAjax(array $js)
3617
    {
3618
        $function = EcommerceConfig::get('Order', 'ajax_subtotal_format');
3619
        if (is_array($function)) {
3620
            list($function, $format) = $function;
3621
        }
3622
        $subTotal = $this->$function();
3623
        if (isset($format)) {
3624
            $subTotal = $subTotal->$format();
3625
            unset($format);
3626
        }
3627
        $function = EcommerceConfig::get('Order', 'ajax_total_format');
3628
        if (is_array($function)) {
3629
            list($function, $format) = $function;
3630
        }
3631
        $total = $this->$function();
3632
        if (isset($format)) {
3633
            $total = $total->$format();
3634
        }
3635
        $ajaxObject = $this->AJAXDefinitions();
3636
        $js[] = array(
3637
            't' => 'id',
3638
            's' => $ajaxObject->TableSubTotalID(),
3639
            'p' => 'innerHTML',
3640
            'v' => $subTotal,
3641
        );
3642
        $js[] = array(
3643
            't' => 'id',
3644
            's' => $ajaxObject->TableTotalID(),
3645
            'p' => 'innerHTML',
3646
            'v' => $total,
3647
        );
3648
        $js[] = array(
3649
            't' => 'class',
3650
            's' => $ajaxObject->TotalItemsClassName(),
3651
            'p' => 'innerHTML',
3652
            'v' => $this->TotalItems($recalculate = true),
3653
        );
3654
        $js[] = array(
3655
            't' => 'class',
3656
            's' => $ajaxObject->TotalItemsTimesQuantityClassName(),
3657
            'p' => 'innerHTML',
3658
            'v' => $this->TotalItemsTimesQuantity(),
3659
        );
3660
        $js[] = array(
3661
            't' => 'class',
3662
            's' => $ajaxObject->ExpectedCountryClassName(),
3663
            'p' => 'innerHTML',
3664
            'v' => $this->ExpectedCountryName(),
3665
        );
3666
3667
        return $js;
3668
    }
3669
3670
    /**
3671
     * @ToDO: move to more appropriate class
3672
     *
3673
     * @return float
3674
     **/
3675
    public function SubTotalCartValue()
3676
    {
3677
        return $this->SubTotal;
3678
    }
3679
3680
/*******************************************************
3681
   * 10. STANDARD SS METHODS (requireDefaultRecords, onBeforeDelete, etc...)
3682
*******************************************************/
3683
3684
    /**
3685
     *standard SS method.
3686
     **/
3687
    public function populateDefaults()
3688
    {
3689
        parent::populateDefaults();
3690
    }
3691
3692
    public function onBeforeWrite()
3693
    {
3694
        parent::onBeforeWrite();
3695
        if (! $this->getCanHaveShippingAddress()) {
3696
            $this->UseShippingAddress = false;
3697
        }
3698
        if (!$this->CurrencyUsedID) {
3699
            $this->CurrencyUsedID = EcommerceCurrency::default_currency_id();
3700
        }
3701
        if (!$this->SessionID) {
3702
            $generator = Injector::inst()->create('RandomGenerator');
3703
            $token = $generator->randomToken('sha1');
3704
            $this->SessionID = substr($token, 0, 32);
3705
        }
3706
    }
3707
3708
    /**
3709
     * standard SS method
3710
     * adds the ability to update order after writing it.
3711
     **/
3712
    public function onAfterWrite()
3713
    {
3714
        parent::onAfterWrite();
3715
        //crucial!
3716
        self::set_needs_recalculating(true, $this->ID);
3717
        // quick double-check
3718
        if ($this->IsCancelled() && ! $this->IsArchived()) {
3719
            $this->Archive($avoidWrites = true);
3720
        }
3721
        if ($this->IsSubmitted($recalculate = true)) {
3722
            //do nothing
3723
        } else {
3724
            if ($this->StatusID) {
3725
                $this->calculateOrderAttributes($recalculate = false);
3726
                if (EcommerceRole::current_member_is_shop_admin()) {
3727
                    if (isset($_REQUEST['SubmitOrderViaCMS'])) {
3728
                        $this->tryToFinaliseOrder();
3729
                        //just in case it writes again...
3730
                        unset($_REQUEST['SubmitOrderViaCMS']);
3731
                    }
3732
                }
3733
            }
3734
        }
3735
    }
3736
3737
    /**
3738
     *standard SS method.
3739
     *
3740
     * delete attributes, statuslogs, and payments
3741
     * THIS SHOULD NOT BE USED AS ORDERS SHOULD BE CANCELLED NOT DELETED
3742
     */
3743
    public function onBeforeDelete()
3744
    {
3745
        parent::onBeforeDelete();
3746
        if ($attributes = $this->Attributes()) {
3747
            foreach ($attributes as $attribute) {
3748
                $attribute->delete();
3749
                $attribute->destroy();
3750
            }
3751
        }
3752
3753
        //THE REST WAS GIVING ERRORS - POSSIBLY DUE TO THE FUNNY RELATIONSHIP (one-one, two times...)
3754
        /*
3755
        if($billingAddress = $this->BillingAddress()) {
3756
            if($billingAddress->exists()) {
3757
                $billingAddress->delete();
3758
                $billingAddress->destroy();
3759
            }
3760
        }
3761
        if($shippingAddress = $this->ShippingAddress()) {
3762
            if($shippingAddress->exists()) {
3763
                $shippingAddress->delete();
3764
                $shippingAddress->destroy();
3765
            }
3766
        }
3767
3768
        if($statuslogs = $this->OrderStatusLogs()){
3769
            foreach($statuslogs as $log){
3770
                $log->delete();
3771
                $log->destroy();
3772
            }
3773
        }
3774
        if($payments = $this->Payments()){
3775
            foreach($payments as $payment){
3776
                $payment->delete();
3777
                $payment->destroy();
3778
            }
3779
        }
3780
        if($emails = $this->Emails()) {
3781
            foreach($emails as $email){
3782
                $email->delete();
3783
                $email->destroy();
3784
            }
3785
        }
3786
        */
3787
    }
3788
3789
/*******************************************************
3790
   * 11. DEBUG
3791
*******************************************************/
3792
3793
    /**
3794
     * Debug helper method.
3795
     * Can be called from /shoppingcart/debug/.
3796
     *
3797
     * @return string
3798
     */
3799
    public function debug()
3800
    {
3801
        $this->calculateOrderAttributes(true);
3802
3803
        return EcommerceTaskDebugCart::debug_object($this);
3804
    }
3805
}
3806