Completed
Push — master ( 3b65eb...dc665d )
by Nicolaas
11:00 queued 02:51
created

code/model/Order.php (9 issues)

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);
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
                    $this->CMSEditLink('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
            $cancelledField = $fields->dataFieldByName('CancelledByID');
759
            $fields->removeByName('CancelledByID');
760
            $shopAdminAndCurrentCustomerArray = EcommerceRole::list_of_admins(true);
761
            if ($member && $member->exists()) {
762
                $shopAdminAndCurrentCustomerArray[$member->ID] = $member->getName();
763
            }
764
            if ($this->CancelledByID) {
765
                if ($cancellingMember = $this->CancelledBy()) {
766
                    $shopAdminAndCurrentCustomerArray[$this->CancelledByID] = $cancellingMember->getName();
767
                }
768
            }
769
            if ($this->canCancel()) {
770
                $fields->addFieldsToTab(
771
                    'Root.Cancellations',
772
                    array(
773
                        DropdownField::create(
774
                            'CancelledByID',
775
                            $cancelledField->Title(),
776
                            $shopAdminAndCurrentCustomerArray
777
                        )
778
                    )
779
                );
780
            } else {
781
                $cancelledBy = isset($shopAdminAndCurrentCustomerArray[$this->CancelledByID]) && $this->CancelledByID ? $shopAdminAndCurrentCustomerArray[$this->CancelledByID] : _t('Order.NOT_CANCELLED', 'not cancelled');
782
                $fields->addFieldsToTab(
783
                    'Root.Cancellations',
784
                    ReadonlyField::create(
785
                        'CancelledByDisplay',
786
                        $cancelledField->Title(),
787
                        $cancelledBy
788
789
                    )
790
                );
791
            }
792
            $fields->addFieldToTab('Root.Log', $this->getOrderStatusLogsTableField_Archived());
793
            $submissionLog = $this->SubmissionLog();
794
            if ($submissionLog) {
795
                $fields->addFieldToTab('Root.Log',
796
                    ReadonlyField::create(
797
                        'SequentialOrderNumber',
798
                        _t('Order.SEQUENTIALORDERNUMBER', 'Consecutive order number'),
799
                        $submissionLog->SequentialOrderNumber
800
                    )->setRightTitle('e.g. 1,2,3,4,5...')
801
                );
802
            }
803
        } else {
804
            $linkText = _t(
805
                'Order.LOAD_THIS_ORDER',
806
                'load this order'
807
            );
808
            $message = _t(
809
                'Order.NOSUBMITTEDYET',
810
                '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 .',
811
                array('link' => '<a href="'.$this->getRetrieveLink().'" data-popup="true">'.$linkText.'</a>')
812
            );
813
            $fields->addFieldToTab('Root.Next', new LiteralField('MainDetails', '<p>'.$message.'</p>'));
814
            $fields->addFieldToTab('Root.Items', $this->getOrderItemsField());
815
            $fields->addFieldToTab('Root.Extras', $this->getModifierTableField());
816
817
            //MEMBER STUFF
818
            $specialOptionsArray = array();
819
            if ($this->MemberID) {
820
                $specialOptionsArray[0] = _t('Order.SELECTCUSTOMER', '--- Remover Customer ---');
821
                $specialOptionsArray[$this->MemberID] = _t('Order.LEAVEWITHCURRENTCUSTOMER', '- Leave with current customer: ').$this->Member()->getTitle();
822
            } elseif ($currentMember) {
823
                $specialOptionsArray[0] = _t('Order.SELECTCUSTOMER', '--- Select Customers ---');
824
                $currentMemberID = $currentMember->ID;
825
                $specialOptionsArray[$currentMemberID] = _t('Order.ASSIGNTHISORDERTOME', '- Assign this order to me: ').$currentMember->getTitle();
826
            }
827
            //MEMBER FIELD!!!!!!!
828
            $memberArray = $specialOptionsArray + EcommerceRole::list_of_customers(true);
829
            $fields->addFieldToTab('Root.Next', new DropdownField('MemberID', _t('Order.SELECTCUSTOMER', 'Select Customer'), $memberArray), 'CustomerOrderNote');
830
            $memberArray = null;
831
        }
832
        $fields->addFieldToTab('Root.Addresses', new HeaderField('BillingAddressHeader', _t('Order.BILLINGADDRESS', 'Billing Address')));
833
834
        $fields->addFieldToTab('Root.Addresses', $this->getBillingAddressField());
835
836
        if (EcommerceConfig::get('OrderAddress', 'use_separate_shipping_address')) {
837
            $fields->addFieldToTab('Root.Addresses', new HeaderField('ShippingAddressHeader', _t('Order.SHIPPINGADDRESS', 'Shipping Address')));
838
            $fields->addFieldToTab('Root.Addresses', new CheckboxField('UseShippingAddress', _t('Order.USESEPERATEADDRESS', 'Use separate shipping address?')));
839
            if ($this->UseShippingAddress) {
840
                $fields->addFieldToTab('Root.Addresses', $this->getShippingAddressField());
841
            }
842
        }
843
        $currencies = EcommerceCurrency::get_list();
844
        if ($currencies && $currencies->count()) {
845
            $currencies = $currencies->map()->toArray();
846
            $fields->addFieldToTab('Root.Currency', new ReadOnlyField('ExchangeRate ', _t('Order.EXCHANGERATE', 'Exchange Rate'), $this->ExchangeRate));
847
            $fields->addFieldToTab('Root.Currency', $currencyField = new DropdownField('CurrencyUsedID', _t('Order.CurrencyUsed', 'Currency Used'), $currencies));
848
            if ($this->IsSubmitted()) {
849
                $fields->replaceField('CurrencyUsedID', $fields->dataFieldByName('CurrencyUsedID')->performReadonlyTransformation());
850
            }
851
        } else {
852
            $fields->addFieldToTab('Root.Currency', new LiteralField('CurrencyInfo', '<p>You can not change currencies, because no currencies have been created.</p>'));
853
            $fields->replaceField('CurrencyUsedID', $fields->dataFieldByName('CurrencyUsedID')->performReadonlyTransformation());
854
        }
855
        $fields->addFieldToTab('Root.Log', new ReadonlyField('Created', _t('Root.CREATED', 'Created')));
856
        $fields->addFieldToTab('Root.Log', new ReadonlyField('LastEdited', _t('Root.LASTEDITED', 'Last saved')));
857
        $this->extend('updateCMSFields', $fields);
858
859
        return $fields;
860
    }
861
862
    /**
863
     * Field to add and edit Order Items.
864
     *
865
     * @return GridField
866
     */
867
    protected function getOrderItemsField()
868
    {
869
        $gridFieldConfig = GridFieldConfigForOrderItems::create();
870
        $source = $this->OrderItems();
871
872
        return new GridField('OrderItems', _t('OrderItems.PLURALNAME', 'Order Items'), $source, $gridFieldConfig);
873
    }
874
875
    /**
876
     * Field to add and edit Modifiers.
877
     *
878
     * @return GridField
879
     */
880
    public function getModifierTableField()
881
    {
882
        $gridFieldConfig = GridFieldConfigForOrderItems::create();
883
        $source = $this->Modifiers();
884
885
        return new GridField('OrderModifiers', _t('OrderItems.PLURALNAME', 'Order Items'), $source, $gridFieldConfig);
886
    }
887
888
    /**
889
     *@return GridField
890
     **/
891
    protected function getBillingAddressField()
892
    {
893
        $this->CreateOrReturnExistingAddress('BillingAddress');
894
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
895
            new GridFieldToolbarHeader(),
896
            new GridFieldSortableHeader(),
897
            new GridFieldDataColumns(),
898
            new GridFieldPaginator(10),
899
            new GridFieldEditButton(),
900
            new GridFieldDetailForm()
901
        );
902
        //$source = $this->BillingAddress();
903
        $source = BillingAddress::get()->filter(array('OrderID' => $this->ID));
904
905
        return new GridField('BillingAddress', _t('BillingAddress.SINGULARNAME', 'Billing Address'), $source, $gridFieldConfig);
906
    }
907
908
    /**
909
     *@return GridField
910
     **/
911
    protected function getShippingAddressField()
912
    {
913
        $this->CreateOrReturnExistingAddress('ShippingAddress');
914
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
915
            new GridFieldToolbarHeader(),
916
            new GridFieldSortableHeader(),
917
            new GridFieldDataColumns(),
918
            new GridFieldPaginator(10),
919
            new GridFieldEditButton(),
920
            new GridFieldDetailForm()
921
        );
922
        //$source = $this->ShippingAddress();
923
        $source = ShippingAddress::get()->filter(array('OrderID' => $this->ID));
924
925
        return new GridField('ShippingAddress', _t('BillingAddress.SINGULARNAME', 'Shipping Address'), $source, $gridFieldConfig);
926
    }
927
928
    /**
929
     * Needs to be public because the OrderStep::getCMSFIelds accesses it.
930
     *
931
     * @param string    $sourceClass
932
     * @param string    $title
933
     *
934
     * @return GridField
935
     **/
936
    public function getOrderStatusLogsTableField(
937
        $sourceClass = 'OrderStatusLog',
938
        $title = ''
939
    ) {
940
        $gridFieldConfig = GridFieldConfig_RecordViewer::create()->addComponents(
941
            new GridFieldAddNewButton('toolbar-header-right'),
942
            new GridFieldDetailForm()
943
        );
944
        $title ? $title : $title = _t('OrderStatusLog.PLURALNAME', 'Order Status Logs');
945
        $source = $this->OrderStatusLogs()->Filter(array('ClassName' => $sourceClass));
946
        $gf = new GridField($sourceClass, $title, $source, $gridFieldConfig);
947
        $gf->setModelClass($sourceClass);
948
949
        return $gf;
950
    }
951
952
    /**
953
     * Needs to be public because the OrderStep::getCMSFIelds accesses it.
954
     *
955
     * @param string    $sourceClass
956
     * @param string    $title
957
     *
958
     * @return GridField
959
     **/
960
    public function getOrderStatusLogsTableFieldEditable(
961
        $sourceClass = 'OrderStatusLog',
962
        $title = ''
963
    ) {
964
        $gf = $this->getOrderStatusLogsTableField($sourceClass, $title);
965
        $gf->getConfig()->addComponents(
966
            new GridFieldEditButton()
967
        );
968
        return $gf;
969
    }
970
971
    /**
972
     * @param string    $sourceClass
973
     * @param string    $title
974
     * @param FieldList $fieldList          (Optional)
975
     * @param FieldList $detailedFormFields (Optional)
976
     *
977
     * @return GridField
978
     **/
979
    protected function getOrderStatusLogsTableField_Archived(
980
        $sourceClass = 'OrderStatusLog',
981
        $title = '',
982
        FieldList $fieldList = null,
983
        FieldList $detailedFormFields = null
984
    ) {
985
        $title ? $title : $title = _t('OrderLog.PLURALNAME', 'Order Log');
986
        $source = $this->OrderStatusLogs();
987
        if ($sourceClass != 'OrderStatusLog' && class_exists($sourceClass)) {
988
            $source = $source->filter(array('ClassName' => ClassInfo::subclassesFor($sourceClass)));
989
        }
990
        $gridField = GridField::create($sourceClass, $title, $source, $config = GridFieldConfig_RelationEditor::create());
991
        $config->removeComponentsByType('GridFieldAddExistingAutocompleter');
992
        $config->removeComponentsByType('GridFieldDeleteAction');
993
994
        return $gridField;
995
    }
996
997
    /**
998
     * @return GridField
999
     **/
1000
    public function getEmailsTableField()
1001
    {
1002
        $gridFieldConfig = GridFieldConfig_RecordViewer::create()->addComponents(
1003
            new GridFieldDetailForm()
1004
        );
1005
1006
        return new GridField('Emails', _t('Order.CUSTOMER_EMAILS', 'Customer Emails'), $this->Emails(), $gridFieldConfig);
1007
    }
1008
1009
    /**
1010
     * @return GridField
1011
     */
1012
    protected function getPaymentsField()
1013
    {
1014
        $gridFieldConfig = GridFieldConfig_RecordViewer::create()->addComponents(
1015
            new GridFieldDetailForm(),
1016
            new GridFieldEditButton()
1017
        );
1018
1019
        return new GridField('Payments', _t('Order.PAYMENTS', 'Payments'), $this->Payments(), $gridFieldConfig);
1020
    }
1021
1022
    /**
1023
     * @return OrderStepField
1024
     */
1025
    public function OrderStepField()
1026
    {
1027
        return OrderStepField::create($name = 'MyOrderStep', $this, Member::currentUser());
1028
    }
1029
1030
/*******************************************************
1031
   * 2. MAIN TRANSITION FUNCTIONS
1032
*******************************************************/
1033
1034
    /**
1035
     * init runs on start of a new Order (@see onAfterWrite)
1036
     * it adds all the modifiers to the orders and the starting OrderStep.
1037
     *
1038
     * @param bool $recalculate
1039
     *
1040
     * @return DataObject (Order)
1041
     **/
1042
    public function init($recalculate = false)
1043
    {
1044
        if ($this->IsSubmitted()) {
1045
            user_error('Can not init an order that has been submitted', E_USER_NOTICE);
1046
        } else {
1047
            //to do: check if shop is open....
1048
            if ($this->StatusID || $recalculate) {
1049
                if (!$this->StatusID) {
1050
                    $createdOrderStatus = OrderStep::get()->First();
1051
                    if (!$createdOrderStatus) {
1052
                        user_error('No ordersteps have been created', E_USER_WARNING);
1053
                    }
1054
                    $this->StatusID = $createdOrderStatus->ID;
1055
                }
1056
                $createdModifiersClassNames = array();
1057
                $modifiersAsArrayList = new ArrayList();
1058
                $modifiers = $this->modifiersFromDatabase($includingRemoved = true);
1059
                if ($modifiers->count()) {
1060
                    foreach ($modifiers as $modifier) {
1061
                        $modifiersAsArrayList->push($modifier);
1062
                    }
1063
                }
1064
                if ($modifiersAsArrayList->count()) {
1065
                    foreach ($modifiersAsArrayList as $modifier) {
1066
                        $createdModifiersClassNames[$modifier->ID] = $modifier->ClassName;
1067
                    }
1068
                } else {
1069
                }
1070
                $modifiersToAdd = EcommerceConfig::get('Order', 'modifiers');
1071
                if (is_array($modifiersToAdd) && count($modifiersToAdd) > 0) {
1072
                    foreach ($modifiersToAdd as $numericKey => $className) {
1073
                        if (!in_array($className, $createdModifiersClassNames)) {
1074
                            if (class_exists($className)) {
1075
                                $modifier = new $className();
1076
                                //only add the ones that should be added automatically
1077
                                if (!$modifier->DoNotAddAutomatically()) {
1078
                                    if (is_a($modifier, 'OrderModifier')) {
1079
                                        $modifier->OrderID = $this->ID;
1080
                                        $modifier->Sort = $numericKey;
1081
                                        //init method includes a WRITE
1082
                                        $modifier->init();
1083
                                        //IMPORTANT - add as has_many relationship  (Attributes can be a modifier OR an OrderItem)
1084
                                        $this->Attributes()->add($modifier);
1085
                                        $modifiersAsArrayList->push($modifier);
1086
                                    }
1087
                                }
1088
                            } else {
1089
                                user_error('reference to a non-existing class: '.$className.' in modifiers', E_USER_NOTICE);
1090
                            }
1091
                        }
1092
                    }
1093
                }
1094
                $this->extend('onInit', $this);
1095
                //careful - this will call "onAfterWrite" again
1096
                $this->write();
1097
            }
1098
        }
1099
1100
        return $this;
1101
    }
1102
1103
    /**
1104
     * @var array
1105
     */
1106
    private static $_try_to_finalise_order_is_running = array();
1107
1108
    /**
1109
     * Goes through the order steps and tries to "apply" the next status to the order.
1110
     *
1111
     * @param bool $runAgain
1112
     * @param bool $fromOrderQueue - is it being called from the OrderProcessQueue (or similar)
1113
     **/
1114
    public function tryToFinaliseOrder($runAgain = false, $fromOrderQueue = false)
1115
    {
1116
        if (empty(self::$_try_to_finalise_order_is_running[$this->ID]) || $runAgain) {
1117
            self::$_try_to_finalise_order_is_running[$this->ID] = true;
1118
1119
            //if the order has been cancelled then we do not process it ...
1120
            if ($this->CancelledByID) {
1121
                $this->Archive(true);
1122
1123
                return;
1124
            }
1125
            // if it is in the queue it has to run from the queue tasks
1126
            // if it ruins from the queue tasks then it has to be one currently processing.
1127
            $queueObjectSingleton = Injector::inst()->get('OrderProcessQueue');
1128
            if ($myQueueObject = $queueObjectSingleton->getQueueObject($this)) {
1129
                if($fromOrderQueue) {
1130
                    if ( ! $myQueueObject->InProcess) {
1131
                        return;
1132
                    }
1133
                } else {
1134
                    return;
1135
                }
1136
            }
1137
            //a little hack to make sure we do not rely on a stored value
1138
            //of "isSubmitted"
1139
            $this->_isSubmittedTempVar = -1;
1140
            //status of order is being progressed
1141
            $nextStatusID = $this->doNextStatus();
1142
            if ($nextStatusID) {
1143
                $nextStatusObject = OrderStep::get()->byID($nextStatusID);
1144
                if ($nextStatusObject) {
1145
                    $delay = $nextStatusObject->CalculatedDeferTimeInSeconds($this);
1146
                    if ($delay > 0) {
1147
                        if ($nextStatusObject->DeferFromSubmitTime) {
1148
                            $delay = $delay - $this->SecondsSinceBeingSubmitted();
1149
                            if ($delay < 0) {
1150
                                $delay = 0;
1151
                            }
1152
                        }
1153
                        $queueObjectSingleton->AddOrderToQueue(
1154
                            $this,
1155
                            $delay
1156
                        );
1157
                    } else {
1158
                        //status has been completed, so it can be released
1159
                        self::$_try_to_finalise_order_is_running[$this->ID] = false;
1160
                        $this->tryToFinaliseOrder($runAgain, $fromOrderQueue);
1161
                    }
1162
                }
1163
            }
1164
        }
1165
    }
1166
1167
    /**
1168
     * Goes through the order steps and tries to "apply" the next step
1169
     * Step is updated after the other one is completed...
1170
     *
1171
     * @return int (StatusID or false if the next status can not be "applied")
1172
     **/
1173
    public function doNextStatus()
1174
    {
1175
        if ($this->MyStep()->initStep($this)) {
1176
            if ($this->MyStep()->doStep($this)) {
1177
                if ($nextOrderStepObject = $this->MyStep()->nextStep($this)) {
1178
                    $this->StatusID = $nextOrderStepObject->ID;
1179
                    $this->write();
1180
1181
                    return $this->StatusID;
1182
                }
1183
            }
1184
        }
1185
1186
        return 0;
1187
    }
1188
1189
    /**
1190
     * cancel an order.
1191
     *
1192
     * @param Member $member - (optional) the user cancelling the order
1193
     * @param string $reason - (optional) the reason the order is cancelled
1194
     *
1195
     * @return OrderStatusLog_Cancel
1196
     */
1197
    public function Cancel($member = null, $reason = '')
1198
    {
1199
        if($member && $member instanceof Member) {
1200
            //we have a valid member
1201
        } else {
1202
            $member = EcommerceRole::get_default_shop_admin_user();
1203
        }
1204
        if($member) {
1205
            //archive and write
1206
            $this->Archive($avoidWrites = true);
1207
            if($avoidWrites) {
1208
                DB::query('Update "Order" SET CancelledByID = '.$member->ID.' WHERE ID = '.$this->ID.' LIMIT 1;');
1209
            } else {
1210
                $this->CancelledByID = $member->ID;
1211
                $this->write();
1212
            }
1213
            //create log ...
1214
            $log = OrderStatusLog_Cancel::create();
1215
            $log->AuthorID = $member->ID;
1216
            $log->OrderID = $this->ID;
1217
            $log->Note = $reason;
1218
            if ($member->IsShopAdmin()) {
1219
                $log->InternalUseOnly = true;
1220
            }
1221
            $log->write();
1222
            //remove from queue ...
1223
            $queueObjectSingleton = Injector::inst()->get('OrderProcessQueue');
1224
            $ordersinQueue = $queueObjectSingleton->removeOrderFromQueue($this);
1225
            $this->extend('doCancel', $member, $log);
1226
1227
            return $log;
1228
        }
1229
    }
1230
1231
    /**
1232
     * returns true if successful.
1233
     *
1234
     * @param bool $avoidWrites
1235
     *
1236
     * @return bool
1237
     */
1238
    public function Archive($avoidWrites = true)
1239
    {
1240
        $lastOrderStep = OrderStep::get()->Last();
1241
        if ($lastOrderStep) {
1242
            if ($avoidWrites) {
1243
                DB::query('
1244
                    UPDATE "Order"
1245
                    SET "Order"."StatusID" = '.$lastOrderStep->ID.'
1246
                    WHERE "Order"."ID" = '.$this->ID.'
1247
                    LIMIT 1
1248
                ');
1249
1250
                return true;
1251
            } else {
1252
                $this->StatusID = $lastOrderStep->ID;
1253
                $this->write();
1254
1255
                return true;
1256
            }
1257
        }
1258
1259
        return false;
1260
    }
1261
1262
/*******************************************************
1263
   * 3. STATUS RELATED FUNCTIONS / SHORTCUTS
1264
*******************************************************/
1265
1266
    /**
1267
     * Avoids caching of $this->Status().
1268
     *
1269
     * @return DataObject (current OrderStep)
1270
     */
1271
    public function MyStep()
1272
    {
1273
        $step = null;
1274
        if ($this->StatusID) {
1275
            $step = OrderStep::get()->byID($this->StatusID);
1276
        }
1277
        if (!$step) {
1278
            $step = OrderStep::get()->First(); //TODO: this could produce strange results
1279
        }
1280
        if (!$step) {
1281
            $step = OrderStep_Created::create();
1282
        }
1283
        if (!$step) {
1284
            user_error('You need an order step in your Database.');
1285
        }
1286
1287
        return $step;
1288
    }
1289
1290
    /**
1291
     * Return the OrderStatusLog that is relevant to the Order status.
1292
     *
1293
     * @return OrderStatusLog
1294
     */
1295
    public function RelevantLogEntry()
1296
    {
1297
        return $this->MyStep()->RelevantLogEntry($this);
1298
    }
1299
1300
    /**
1301
     * @return OrderStep (current OrderStep that can be seen by customer)
1302
     */
1303
    public function CurrentStepVisibleToCustomer()
1304
    {
1305
        $obj = $this->MyStep();
1306
        if ($obj->HideStepFromCustomer) {
1307
            $obj = OrderStep::get()->where('"OrderStep"."Sort" < '.$obj->Sort.' AND "HideStepFromCustomer" = 0')->Last();
1308
            if (!$obj) {
1309
                $obj = OrderStep::get()->First();
1310
            }
1311
        }
1312
1313
        return $obj;
1314
    }
1315
1316
    /**
1317
     * works out if the order is still at the first OrderStep.
1318
     *
1319
     * @return bool
1320
     */
1321
    public function IsFirstStep()
1322
    {
1323
        $firstStep = OrderStep::get()->First();
1324
        $currentStep = $this->MyStep();
1325
        if ($firstStep && $currentStep) {
1326
            if ($firstStep->ID == $currentStep->ID) {
1327
                return true;
1328
            }
1329
        }
1330
1331
        return false;
1332
    }
1333
1334
    /**
1335
     * Is the order still being "edited" by the customer?
1336
     *
1337
     * @return bool
1338
     */
1339
    public function IsInCart()
1340
    {
1341
        return (bool) $this->IsSubmitted() ? false : true;
1342
    }
1343
1344
    /**
1345
     * The order has "passed" the IsInCart phase.
1346
     *
1347
     * @return bool
1348
     */
1349
    public function IsPastCart()
1350
    {
1351
        return (bool) $this->IsInCart() ? false : true;
1352
    }
1353
1354
    /**
1355
     * Are there still steps the order needs to go through?
1356
     *
1357
     * @return bool
1358
     */
1359
    public function IsUncomplete()
1360
    {
1361
        return (bool) $this->MyStep()->ShowAsUncompletedOrder;
1362
    }
1363
1364
    /**
1365
     * Is the order in the :"processing" phaase.?
1366
     *
1367
     * @return bool
1368
     */
1369
    public function IsProcessing()
1370
    {
1371
        return (bool) $this->MyStep()->ShowAsInProcessOrder;
1372
    }
1373
1374
    /**
1375
     * Is the order completed?
1376
     *
1377
     * @return bool
1378
     */
1379
    public function IsCompleted()
1380
    {
1381
        return (bool) $this->MyStep()->ShowAsCompletedOrder;
1382
    }
1383
1384
    /**
1385
     * Has the order been paid?
1386
     * TODO: why do we check if there is a total at all?
1387
     *
1388
     * @return bool
1389
     */
1390
    public function IsPaid()
1391
    {
1392
        if ($this->IsSubmitted()) {
1393
            return (bool) (($this->Total() >= 0) && ($this->TotalOutstanding() <= 0));
1394
        }
1395
1396
        return false;
1397
    }
1398
    /**
1399
     * Has the order been paid?
1400
     * TODO: why do we check if there is a total at all?
1401
     *
1402
     * @return Boolean (object)
1403
     */
1404
    public function IsPaidNice()
1405
    {
1406
        return  DBField::create_field('Boolean', $this->IsPaid());
1407
    }
1408
1409
    /**
1410
     * Has the order been paid?
1411
     * TODO: why do we check if there is a total at all?
1412
     *
1413
     * @return bool
1414
     */
1415
    public function PaymentIsPending()
1416
    {
1417
        if ($this->IsSubmitted()) {
1418
            if ($this->IsPaid()) {
1419
                //do nothing;
1420
            } elseif (($payments = $this->Payments()) && $payments->count()) {
1421
                foreach ($payments as $payment) {
1422
                    if ('Pending' == $payment->Status) {
1423
                        return true;
1424
                    }
1425
                }
1426
            }
1427
        }
1428
1429
        return false;
1430
    }
1431
1432
    /**
1433
     * shows payments that are meaningfull
1434
     * if the order has been paid then only show successful payments.
1435
     *
1436
     * @return DataList
1437
     */
1438
    public function RelevantPayments()
1439
    {
1440
        if ($this->IsPaid()) {
1441
            return $this->Payments("\"Status\" = 'Success'");
1442
            //EcommercePayment::get()->
1443
            //	filter(array("OrderID" => $this->ID, "Status" => "Success"));
1444
        } else {
1445
            return $this->Payments();
1446
        }
1447
    }
1448
1449
    /**
1450
     * Has the order been cancelled?
1451
     *
1452
     * @return bool
1453
     */
1454
    public function IsCancelled()
1455
    {
1456
        return $this->getIsCancelled();
1457
    }
1458
    public function getIsCancelled()
1459
    {
1460
        return $this->CancelledByID ? true : false;
1461
    }
1462
1463
    /**
1464
     * Has the order been cancelled by the customer?
1465
     *
1466
     * @return bool
1467
     */
1468
    public function IsCustomerCancelled()
1469
    {
1470
        if ($this->MemberID > 0 && $this->MemberID == $this->IsCancelledID) {
1471
            return true;
1472
        }
1473
1474
        return false;
1475
    }
1476
1477
    /**
1478
     * Has the order been cancelled by the  administrator?
1479
     *
1480
     * @return bool
1481
     */
1482
    public function IsAdminCancelled()
1483
    {
1484
        if ($this->IsCancelled()) {
1485
            if (!$this->IsCustomerCancelled()) {
1486
                $admin = Member::get()->byID($this->CancelledByID);
1487
                if ($admin) {
1488
                    if ($admin->IsShopAdmin()) {
1489
                        return true;
1490
                    }
1491
                }
1492
            }
1493
        }
1494
1495
        return false;
1496
    }
1497
1498
    /**
1499
     * Is the Shop Closed for business?
1500
     *
1501
     * @return bool
1502
     */
1503
    public function ShopClosed()
1504
    {
1505
        return EcomConfig()->ShopClosed;
1506
    }
1507
1508
/*******************************************************
1509
   * 4. LINKING ORDER WITH MEMBER AND ADDRESS
1510
*******************************************************/
1511
1512
    /**
1513
     * Returns a member linked to the order.
1514
     * If a member is already linked, it will return the existing member.
1515
     * Otherwise it will return a new Member.
1516
     *
1517
     * Any new member is NOT written, because we dont want to create a new member unless we have to!
1518
     * We will not add a member to the order unless a new one is created in the checkout
1519
     * OR the member is logged in / logs in.
1520
     *
1521
     * Also note that if a new member is created, it is not automatically written
1522
     *
1523
     * @param bool $forceCreation - if set to true then the member will always be saved in the database.
1524
     *
1525
     * @return Member
1526
     **/
1527
    public function CreateOrReturnExistingMember($forceCreation = false)
1528
    {
1529
        if ($this->IsSubmitted()) {
1530
            return $this->Member();
1531
        }
1532
        if ($this->MemberID) {
1533
            $member = $this->Member();
1534
        } elseif ($member = Member::currentUser()) {
1535
            if (!$member->IsShopAdmin()) {
1536
                $this->MemberID = $member->ID;
1537
                $this->write();
1538
            }
1539
        }
1540
        $member = $this->Member();
1541
        if (!$member) {
1542
            $member = new Member();
1543
        }
1544
        if ($member && $forceCreation) {
1545
            $member->write();
1546
        }
1547
1548
        return $member;
1549
    }
1550
1551
    /**
1552
     * Returns either the existing one or a new Order Address...
1553
     * All Orders will have a Shipping and Billing address attached to it.
1554
     * Method used to retrieve object e.g. for $order->BillingAddress(); "BillingAddress" is the method name you can use.
1555
     * If the method name is the same as the class name then dont worry about providing one.
1556
     *
1557
     * @param string $className             - ClassName of the Address (e.g. BillingAddress or ShippingAddress)
1558
     * @param string $alternativeMethodName - method to retrieve Address
1559
     **/
1560
    public function CreateOrReturnExistingAddress($className = 'BillingAddress', $alternativeMethodName = '')
1561
    {
1562
        if ($this->exists()) {
1563
            $methodName = $className;
1564
            if ($alternativeMethodName) {
1565
                $methodName = $alternativeMethodName;
1566
            }
1567
            if ($this->IsSubmitted()) {
1568
                return $this->$methodName();
1569
            }
1570
            $variableName = $className.'ID';
1571
            $address = null;
1572
            if ($this->$variableName) {
1573
                $address = $this->$methodName();
1574
            }
1575
            if (!$address) {
1576
                $address = new $className();
1577
                if ($member = $this->CreateOrReturnExistingMember()) {
1578
                    if ($member->exists()) {
1579
                        $address->FillWithLastAddressFromMember($member, $write = false);
1580
                    }
1581
                }
1582
            }
1583
            if ($address) {
1584
                if (!$address->exists()) {
1585
                    $address->write();
1586
                }
1587
                if ($address->OrderID != $this->ID) {
1588
                    $address->OrderID = $this->ID;
1589
                    $address->write();
1590
                }
1591
                if ($this->$variableName != $address->ID) {
1592
                    if (!$this->IsSubmitted()) {
1593
                        $this->$variableName = $address->ID;
1594
                        $this->write();
1595
                    }
1596
                }
1597
1598
                return $address;
1599
            }
1600
        }
1601
1602
        return;
1603
    }
1604
1605
    /**
1606
     * Sets the country in the billing and shipping address.
1607
     *
1608
     * @param string $countryCode            - code for the country e.g. NZ
1609
     * @param bool   $includeBillingAddress
1610
     * @param bool   $includeShippingAddress
1611
     **/
1612
    public function SetCountryFields($countryCode, $includeBillingAddress = true, $includeShippingAddress = true)
1613
    {
1614
        if ($this->IsSubmitted()) {
1615
            user_error('Can not change country in submitted order', E_USER_NOTICE);
1616
        } else {
1617
            if ($includeBillingAddress) {
1618
                if ($billingAddress = $this->CreateOrReturnExistingAddress('BillingAddress')) {
1619
                    $billingAddress->SetCountryFields($countryCode);
1620
                }
1621
            }
1622
            if (EcommerceConfig::get('OrderAddress', 'use_separate_shipping_address')) {
1623
                if ($includeShippingAddress) {
1624
                    if ($shippingAddress = $this->CreateOrReturnExistingAddress('ShippingAddress')) {
1625
                        $shippingAddress->SetCountryFields($countryCode);
1626
                    }
1627
                }
1628
            }
1629
        }
1630
    }
1631
1632
    /**
1633
     * Sets the region in the billing and shipping address.
1634
     *
1635
     * @param int $regionID - ID for the region to be set
1636
     **/
1637
    public function SetRegionFields($regionID)
1638
    {
1639
        if ($this->IsSubmitted()) {
1640
            user_error('Can not change country in submitted order', E_USER_NOTICE);
1641
        } else {
1642
            if ($billingAddress = $this->CreateOrReturnExistingAddress('BillingAddress')) {
1643
                $billingAddress->SetRegionFields($regionID);
1644
            }
1645
            if ($this->CanHaveShippingAddress()) {
1646
                if ($shippingAddress = $this->CreateOrReturnExistingAddress('ShippingAddress')) {
1647
                    $shippingAddress->SetRegionFields($regionID);
1648
                }
1649
            }
1650
        }
1651
    }
1652
1653
    /**
1654
     * Stores the preferred currency of the order.
1655
     * IMPORTANTLY we store the exchange rate for future reference...
1656
     *
1657
     * @param EcommerceCurrency $currency
1658
     */
1659
    public function UpdateCurrency($newCurrency)
1660
    {
1661
        if ($this->IsSubmitted()) {
1662
            user_error('Can not set the currency after the order has been submitted', E_USER_NOTICE);
1663
        } else {
1664
            if (! is_a($newCurrency, Object::getCustomClass('EcommerceCurrency'))) {
1665
                $newCurrency = EcommerceCurrency::default_currency();
1666
            }
1667
            $this->CurrencyUsedID = $newCurrency->ID;
1668
            $this->ExchangeRate = $newCurrency->getExchangeRate();
1669
            $this->write();
1670
        }
1671
    }
1672
1673
    /**
1674
     * alias for UpdateCurrency.
1675
     *
1676
     * @param EcommerceCurrency $currency
1677
     */
1678
    public function SetCurrency($currency)
1679
    {
1680
        $this->UpdateCurrency($currency);
1681
    }
1682
1683
/*******************************************************
1684
   * 5. CUSTOMER COMMUNICATION
1685
*******************************************************/
1686
1687
    /**
1688
     * Send the invoice of the order by email.
1689
     *
1690
     * @param string $emailClassName     (optional) class used to send email
1691
     * @param string $subject            (optional) subject for the email
1692
     * @param string $message            (optional) the main message in the email
1693
     * @param bool   $resend             (optional) send the email even if it has been sent before
1694
     * @param bool   $adminOnlyOrToEmail (optional) sends the email to the ADMIN ONLY, if you provide an email, it will go to the email...
1695
     *
1696
     * @return bool TRUE on success, FALSE on failure
1697
     */
1698
    public function sendEmail(
1699
        $emailClassName = 'Order_InvoiceEmail',
1700
        $subject = '',
1701
        $message = '',
1702
        $resend = false,
1703
        $adminOnlyOrToEmail = false
1704
    ) {
1705
        return $this->prepareAndSendEmail(
1706
            $emailClassName,
1707
            $subject,
1708
            $message,
1709
            $resend,
1710
            $adminOnlyOrToEmail
1711
        );
1712
    }
1713
1714
    /**
1715
     * Sends a message to the shop admin ONLY and not to the customer
1716
     * This can be used by ordersteps and orderlogs to notify the admin of any potential problems.
1717
     *
1718
     * @param string         $emailClassName       - (optional) template to be used ...
1719
     * @param string         $subject              - (optional) subject for the email
1720
     * @param string         $message              - (optional) message to be added with the email
1721
     * @param bool           $resend               - (optional) can it be sent twice?
1722
     * @param bool | string  $adminOnlyOrToEmail   - (optional) sends the email to the ADMIN ONLY, if you provide an email, it will go to the email...
1723
     *
1724
     * @return bool TRUE for success, FALSE for failure (not tested)
1725
     */
1726
    public function sendAdminNotification(
1727
        $emailClassName = 'Order_ErrorEmail',
1728
        $subject = '',
1729
        $message = '',
1730
        $resend = false,
1731
        $adminOnlyOrToEmail = true
1732
    ) {
1733
        return $this->prepareAndSendEmail(
1734
            $emailClassName,
1735
            $subject,
1736
            $message,
1737
            $resend,
1738
            $adminOnlyOrToEmail
1739
        );
1740
    }
1741
1742
    /**
1743
     * returns the order formatted as an email.
1744
     *
1745
     * @param string $emailClassName - template to use.
1746
     * @param string $subject        - (optional) the subject (which can be used as title in email)
1747
     * @param string $message        - (optional) the additional message
1748
     *
1749
     * @return string (html)
1750
     */
1751
    public function renderOrderInEmailFormat(
1752
        $emailClassName,
1753
        $subject = '',
1754
        $message = ''
1755
    )
1756
    {
1757
        $arrayData = $this->createReplacementArrayForEmail($subject, $message);
1758
        Config::nest();
1759
        Config::inst()->update('SSViewer', 'theme_enabled', true);
1760
        $html = $arrayData->renderWith($emailClassName);
1761
        Config::unnest();
1762
1763
        return Order_Email::emogrify_html($html);
1764
    }
1765
1766
    /**
1767
     * Send a mail of the order to the client (and another to the admin).
1768
     *
1769
     * @param string         $emailClassName       - (optional) template to be used ...
1770
     * @param string         $subject              - (optional) subject for the email
1771
     * @param string         $message              - (optional) message to be added with the email
1772
     * @param bool           $resend               - (optional) can it be sent twice?
1773
     * @param bool | string  $adminOnlyOrToEmail   - (optional) sends the email to the ADMIN ONLY, if you provide an email, it will go to the email...
1774
     *
1775
     * @return bool TRUE for success, FALSE for failure (not tested)
1776
     */
1777
    protected function prepareAndSendEmail(
1778
        $emailClassName,
1779
        $subject,
1780
        $message,
1781
        $resend = false,
1782
        $adminOnlyOrToEmail = false
1783
    ) {
1784
        $arrayData = $this->createReplacementArrayForEmail($subject, $message);
1785
        $from = Order_Email::get_from_email();
1786
        //why are we using this email and NOT the member.EMAIL?
1787
        //for historical reasons????
1788
        if ($adminOnlyOrToEmail) {
1789
            if (filter_var($adminOnlyOrToEmail, FILTER_VALIDATE_EMAIL)) {
1790
                $to = $adminOnlyOrToEmail;
1791
                // invalid e-mail address
1792
            } else {
1793
                $to = Order_Email::get_from_email();
1794
            }
1795
        } else {
1796
            $to = $this->getOrderEmail();
1797
        }
1798
        if ($from && $to) {
1799
            $email = new $emailClassName();
1800
            if (!(is_a($email, Object::getCustomClass('Email')))) {
1801
                user_error('No correct email class provided.', E_USER_ERROR);
1802
            }
1803
            $email->setFrom($from);
1804
            $email->setTo($to);
1805
            //we take the subject from the Array Data, just in case it has been adjusted.
1806
            $email->setSubject($arrayData->getField('Subject'));
1807
            //we also see if a CC and a BCC have been added
1808
            ;
1809
            if ($cc = $arrayData->getField('CC')) {
1810
                $email->setCc($cc);
1811
            }
1812
            if ($bcc = $arrayData->getField('BCC')) {
1813
                $email->setBcc($bcc);
1814
            }
1815
            $email->populateTemplate($arrayData);
1816
            // This might be called from within the CMS,
1817
            // so we need to restore the theme, just in case
1818
            // templates within the theme exist
1819
            Config::nest();
1820
            Config::inst()->update('SSViewer', 'theme_enabled', true);
1821
            $email->setOrder($this);
1822
            $email->setResend($resend);
1823
            $result = $email->send(null);
1824
            Config::unnest();
1825
            if (Director::isDev()) {
1826
                return true;
1827
            } else {
1828
                return $result;
1829
            }
1830
        }
1831
1832
        return false;
1833
    }
1834
1835
    /**
1836
     * returns the Data that can be used in the body of an order Email
1837
     * we add the subject here so that the subject, for example, can be added to the <title>
1838
     * of the email template.
1839
     * we add the subject here so that the subject, for example, can be added to the <title>
1840
     * of the email template.
1841
     *
1842
     * @param string $subject  - (optional) subject for email
1843
     * @param string $message  - (optional) the additional message
1844
     *
1845
     * @return ArrayData
1846
     *                   - Subject - EmailSubject
1847
     *                   - Message - specific message for this order
1848
     *                   - Message - custom message
1849
     *                   - OrderStepMessage - generic message for step
1850
     *                   - Order
1851
     *                   - EmailLogo
1852
     *                   - ShopPhysicalAddress
1853
     *                   - CurrentDateAndTime
1854
     *                   - BaseURL
1855
     *                   - CC
1856
     *                   - BCC
1857
     */
1858
    protected function createReplacementArrayForEmail($subject = '', $message = '')
1859
    {
1860
        $step = $this->MyStep();
1861
        $config = $this->EcomConfig();
1862
        $replacementArray = array();
1863
        //set subject
1864
        if ( ! $subject) {
1865
            $subject = $step->EmailSubject;
1866
        }
1867
        if( ! $message) {
1868
            $message = $step->CustomerMessage;
1869
        }
1870
        $subject = str_replace('[OrderNumber]', $this->ID, $subject);
1871
        //set other variables
1872
        $replacementArray['Subject'] = $subject;
1873
        $replacementArray['To'] = '';
1874
        $replacementArray['CC'] = '';
1875
        $replacementArray['BCC'] = '';
1876
        $replacementArray['OrderStepMessage'] = $message;
1877
        $replacementArray['Order'] = $this;
1878
        $replacementArray['EmailLogo'] = $config->EmailLogo();
1879
        $replacementArray['ShopPhysicalAddress'] = $config->ShopPhysicalAddress;
1880
        $replacementArray['CurrentDateAndTime'] = DBField::create_field('SS_Datetime', 'Now');
1881
        $replacementArray['BaseURL'] = Director::baseURL();
1882
        $arrayData = ArrayData::create($replacementArray);
1883
        $this->extend('updateReplacementArrayForEmail', $arrayData);
1884
1885
        return $arrayData;
1886
    }
1887
1888
/*******************************************************
1889
   * 6. ITEM MANAGEMENT
1890
*******************************************************/
1891
1892
    /**
1893
     * returns a list of Order Attributes by type.
1894
     *
1895
     * @param array | String $types
1896
     *
1897
     * @return ArrayList
1898
     */
1899
    public function getOrderAttributesByType($types)
1900
    {
1901
        if (!is_array($types) && is_string($types)) {
1902
            $types = array($types);
1903
        }
1904
        if (!is_array($al)) {
1905
            user_error('wrong parameter (types) provided in Order::getOrderAttributesByTypes');
1906
        }
1907
        $al = new ArrayList();
1908
        $items = $this->Items();
1909
        foreach ($items as $item) {
1910
            if (in_array($item->OrderAttributeType(), $types)) {
1911
                $al->push($item);
1912
            }
1913
        }
1914
        $modifiers = $this->Modifiers();
1915
        foreach ($modifiers as $modifier) {
1916
            if (in_array($modifier->OrderAttributeType(), $types)) {
1917
                $al->push($modifier);
1918
            }
1919
        }
1920
1921
        return $al;
1922
    }
1923
1924
    /**
1925
     * Returns the items of the order.
1926
     * Items are the order items (products) and NOT the modifiers (discount, tax, etc...).
1927
     *
1928
     * N. B. this method returns Order Items
1929
     * also see Buaybles
1930
1931
     *
1932
     * @param string filter - where statement to exclude certain items OR ClassName (e.g. 'TaxModifier')
1933
     *
1934
     * @return DataList (OrderItems)
1935
     */
1936
    public function Items($filterOrClassName = '')
1937
    {
1938
        if (!$this->exists()) {
1939
            $this->write();
1940
        }
1941
1942
        return $this->itemsFromDatabase($filterOrClassName);
1943
    }
1944
1945
    /**
1946
     * @alias function of Items
1947
     *
1948
     * N. B. this method returns Order Items
1949
     * also see Buaybles
1950
     *
1951
     * @param string filter - where statement to exclude certain items.
1952
     * @alias for Items
1953
     * @return DataList (OrderItems)
1954
     */
1955
    public function OrderItems($filterOrClassName = '')
1956
    {
1957
        return $this->Items($filterOrClassName);
1958
    }
1959
1960
    /**
1961
     * returns the buyables asscoiated with the order items.
1962
     *
1963
     * NB. this method retursn buyables
1964
     *
1965
     * @param string filter - where statement to exclude certain items.
1966
     *
1967
     * @return ArrayList (Buyables)
1968
     */
1969
    public function Buyables($filterOrClassName = '')
1970
    {
1971
        $items = $this->Items($filterOrClassName);
1972
        $arrayList = new ArrayList();
1973
        foreach ($items as $item) {
1974
            $arrayList->push($item->Buyable());
1975
        }
1976
1977
        return $arrayList;
1978
    }
1979
1980
    /**
1981
     * Return all the {@link OrderItem} instances that are
1982
     * available as records in the database.
1983
     *
1984
     * @param string filter - where statement to exclude certain items,
1985
     *   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)
1986
     *
1987
     * @return DataList (OrderItems)
1988
     */
1989
    protected function itemsFromDatabase($filterOrClassName = '')
1990
    {
1991
        $className = 'OrderItem';
1992
        $extrafilter = '';
1993
        if ($filterOrClassName) {
1994
            if (class_exists($filterOrClassName)) {
1995
                $className = $filterOrClassName;
1996
            } else {
1997
                $extrafilter = " AND $filterOrClassName";
1998
            }
1999
        }
2000
2001
        return $className::get()->filter(array('OrderID' => $this->ID))->where($extrafilter);
2002
    }
2003
2004
    /**
2005
     * @alias for Modifiers
2006
     *
2007
     * @return DataList (OrderModifiers)
2008
     */
2009
    public function OrderModifiers()
2010
    {
2011
        return $this->Modifiers();
2012
    }
2013
2014
    /**
2015
     * Returns the modifiers of the order, if it hasn't been saved yet
2016
     * it returns the modifiers from session, if it has, it returns them
2017
     * from the DB entry. ONLY USE OUTSIDE ORDER.
2018
     *
2019
     * @param string filter - where statement to exclude certain items OR ClassName (e.g. 'TaxModifier')
2020
     *
2021
     * @return DataList (OrderModifiers)
2022
     */
2023
    public function Modifiers($filterOrClassName = '')
2024
    {
2025
        return $this->modifiersFromDatabase($filterOrClassName);
2026
    }
2027
2028
    /**
2029
     * Get all {@link OrderModifier} instances that are
2030
     * available as records in the database.
2031
     * NOTE: includes REMOVED Modifiers, so that they do not get added again...
2032
     *
2033
     * @param string filter - where statement to exclude certain items OR ClassName (e.g. 'TaxModifier')
2034
     *
2035
     * @return DataList (OrderModifiers)
2036
     */
2037
    protected function modifiersFromDatabase($filterOrClassName = '')
2038
    {
2039
        $className = 'OrderModifier';
2040
        $extrafilter = '';
2041
        if ($filterOrClassName) {
2042
            if (class_exists($filterOrClassName)) {
2043
                $className = $filterOrClassName;
2044
            } else {
2045
                $extrafilter = " AND $filterOrClassName";
2046
            }
2047
        }
2048
2049
        return $className::get()->where('"OrderAttribute"."OrderID" = '.$this->ID." $extrafilter");
2050
    }
2051
2052
    /**
2053
     * Calculates and updates all the order attributes.
2054
     *
2055
     * @param bool $recalculate - run it, even if it has run already
2056
     */
2057
    public function calculateOrderAttributes($recalculate = false)
2058
    {
2059
        if ($this->IsSubmitted()) {
2060
            //submitted orders are NEVER recalculated.
2061
            //they are set in stone.
2062
        } elseif (self::get_needs_recalculating($this->ID) || $recalculate) {
2063
            if ($this->StatusID || $this->TotalItems()) {
2064
                $this->ensureCorrectExchangeRate();
2065
                $this->calculateOrderItems($recalculate);
2066
                $this->calculateModifiers($recalculate);
2067
                $this->extend('onCalculateOrder');
2068
            }
2069
        }
2070
    }
2071
2072
    /**
2073
     * Calculates and updates all the product items.
2074
     *
2075
     * @param bool $recalculate - run it, even if it has run already
2076
     */
2077
    protected function calculateOrderItems($recalculate = false)
2078
    {
2079
        //check if order has modifiers already
2080
        //check /re-add all non-removable ones
2081
        //$start = microtime();
2082
        $orderItems = $this->itemsFromDatabase();
2083
        if ($orderItems->count()) {
2084
            foreach ($orderItems as $orderItem) {
2085
                if ($orderItem) {
2086
                    $orderItem->runUpdate($recalculate);
2087
                }
2088
            }
2089
        }
2090
        $this->extend('onCalculateOrderItems', $orderItems);
2091
    }
2092
2093
    /**
2094
     * Calculates and updates all the modifiers.
2095
     *
2096
     * @param bool $recalculate - run it, even if it has run already
2097
     */
2098
    protected function calculateModifiers($recalculate = false)
2099
    {
2100
        $createdModifiers = $this->modifiersFromDatabase();
2101
        if ($createdModifiers->count()) {
2102
            foreach ($createdModifiers as $modifier) {
2103
                if ($modifier) {
2104
                    $modifier->runUpdate($recalculate);
2105
                }
2106
            }
2107
        }
2108
        $this->extend('onCalculateModifiers', $createdModifiers);
2109
    }
2110
2111
    /**
2112
     * Returns the subtotal of the modifiers for this order.
2113
     * If a modifier appears in the excludedModifiers array, it is not counted.
2114
     *
2115
     * @param string|array $excluded               - Class(es) of modifier(s) to ignore in the calculation.
2116
     * @param bool         $stopAtExcludedModifier - when this flag is TRUE, we stop adding the modifiers when we reach an excluded modifier.
2117
     *
2118
     * @return float
2119
     */
2120
    public function ModifiersSubTotal($excluded = null, $stopAtExcludedModifier = false)
2121
    {
2122
        $total = 0;
2123
        $modifiers = $this->Modifiers();
2124
        if ($modifiers->count()) {
2125
            foreach ($modifiers as $modifier) {
2126
                if (!$modifier->IsRemoved()) { //we just double-check this...
2127
                    if (is_array($excluded) && in_array($modifier->ClassName, $excluded)) {
2128
                        if ($stopAtExcludedModifier) {
2129
                            break;
2130
                        }
2131
                        //do the next modifier
2132
                        continue;
2133
                    } elseif (is_string($excluded) && ($modifier->ClassName == $excluded)) {
2134
                        if ($stopAtExcludedModifier) {
2135
                            break;
2136
                        }
2137
                        //do the next modifier
2138
                        continue;
2139
                    }
2140
                    $total += $modifier->CalculationTotal();
2141
                }
2142
            }
2143
        }
2144
2145
        return $total;
2146
    }
2147
2148
    /**
2149
     * returns a modifier that is an instanceof the classname
2150
     * it extends.
2151
     *
2152
     * @param string $className: class name for the modifier
2153
     *
2154
     * @return DataObject (OrderModifier)
2155
     **/
2156
    public function RetrieveModifier($className)
2157
    {
2158
        $modifiers = $this->Modifiers();
2159
        if ($modifiers->count()) {
2160
            foreach ($modifiers as $modifier) {
2161
                if (is_a($modifier, Object::getCustomClass($className))) {
2162
                    return $modifier;
2163
                }
2164
            }
2165
        }
2166
    }
2167
2168
/*******************************************************
2169
   * 7. CRUD METHODS (e.g. canView, canEdit, canDelete, etc...)
2170
*******************************************************/
2171
2172
    /**
2173
     * @param Member $member
2174
     *
2175
     * @return DataObject (Member)
2176
     **/
2177
     //TODO: please comment why we make use of this function
2178
    protected function getMemberForCanFunctions(Member $member = null)
2179
    {
2180
        if (!$member) {
2181
            $member = Member::currentUser();
2182
        }
2183
        if (!$member) {
2184
            $member = new Member();
2185
            $member->ID = 0;
2186
        }
2187
2188
        return $member;
2189
    }
2190
2191
    /**
2192
     * @param Member $member
2193
     *
2194
     * @return bool
2195
     **/
2196
    public function canCreate($member = null)
2197
    {
2198
        $member = $this->getMemberForCanFunctions($member);
2199
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>, but the function expects a object<Member>|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2200
        if ($extended !== null) {
2201
            return $extended;
2202
        }
2203
        if ($member->exists()) {
2204
            return $member->IsShopAdmin();
2205
        }
2206
    }
2207
2208
    /**
2209
     * Standard SS method - can the current member view this order?
2210
     *
2211
     * @param Member $member
2212
     *
2213
     * @return bool
2214
     **/
2215
    public function canView($member = null)
2216
    {
2217
        if (!$this->exists()) {
2218
            return true;
2219
        }
2220
        $member = $this->getMemberForCanFunctions($member);
2221
        //check if this has been "altered" in any DataExtension
2222
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>, but the function expects a object<Member>|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2223
        if ($extended !== null) {
2224
            return $extended;
2225
        }
2226
        //is the member is a shop admin they can always view it
2227
        if (EcommerceRole::current_member_is_shop_admin($member)) {
2228
            return true;
2229
        }
2230
2231
        //is the member is a shop assistant they can always view it
2232
        if (EcommerceRole::current_member_is_shop_assistant($member)) {
2233
            return true;
2234
        }
2235
        //if the current member OWNS the order, (s)he can always view it.
2236
        if ($member->exists() && $this->MemberID == $member->ID) {
2237
            return true;
2238
        }
2239
        //it is the current order
2240
        if ($this->IsInSession()) {
2241
            //we do some additional CHECKS for session hackings!
2242
            if ($member->exists() && $this->MemberID) {
2243
                //can't view the order of another member!
2244
                //shop admin exemption is already captured.
2245
                //this is always true
2246
                if ($this->MemberID != $member->ID) {
2247
                    return false;
2248
                }
2249
            } else {
2250
                //order belongs to someone, but current user is NOT logged in...
2251
                //this is allowed!
2252
                //the reason it is allowed is because we want to be able to
2253
                //add order to non-existing member
2254
                return true;
2255
            }
2256
        }
2257
2258
        return false;
2259
    }
2260
2261
    /**
2262
     * @param Member $member optional
2263
     * @return bool
2264
     */
2265
    public function canOverrideCanView($member = null)
2266
    {
2267
        if ($this->canView($member)) {
2268
            //can view overrides any concerns
2269
            return true;
2270
        } else {
2271
            $tsOrder = strtotime($this->LastEdited);
2272
            $tsNow = time();
2273
            $minutes = EcommerceConfig::get('Order', 'minutes_an_order_can_be_viewed_without_logging_in');
2274
            if ($minutes && ((($tsNow - $tsOrder) / 60) < $minutes)) {
2275
2276
                //has the order been edited recently?
2277
                return true;
2278
            } elseif ($orderStep = $this->MyStep()) {
2279
2280
                // order is being processed ...
2281
                return $orderStep->canOverrideCanViewForOrder($this, $member);
2282
            }
2283
        }
2284
        return false;
2285
    }
2286
2287
    /**
2288
     * @return bool
2289
     */
2290
    public function IsInSession()
2291
    {
2292
        $orderInSession = ShoppingCart::session_order();
2293
2294
        return $orderInSession && $this->ID && $this->ID == $orderInSession->ID;
2295
    }
2296
2297
    /**
2298
     * returns a pseudo random part of the session id.
2299
     *
2300
     * @param int $size
2301
     *
2302
     * @return string
2303
     */
2304
    public function LessSecureSessionID($size = 7, $start = null)
2305
    {
2306
        if (!$start || $start < 0 || $start > (32 - $size)) {
2307
            $start = 0;
2308
        }
2309
2310
        return substr($this->SessionID, $start, $size);
2311
    }
2312
    /**
2313
     *
2314
     * @param Member (optional) $member
2315
     *
2316
     * @return bool
2317
     **/
2318
    public function canViewAdminStuff($member = null)
2319
    {
2320
        $member = $this->getMemberForCanFunctions($member);
2321
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>, but the function expects a object<Member>|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2322
        if ($extended !== null) {
2323
            return $extended;
2324
        }
2325
        if (Permission::checkMember($member, Config::inst()->get('EcommerceRole', 'admin_permission_code'))) {
2326
            return true;
2327
        }
2328
    }
2329
2330
    /**
2331
     * if we set canEdit to false then we
2332
     * can not see the child records
2333
     * Basically, you can edit when you can view and canEdit (even as a customer)
2334
     * Or if you are a Shop Admin you can always edit.
2335
     * Otherwise it is false...
2336
     *
2337
     * @param Member $member
2338
     *
2339
     * @return bool
2340
     **/
2341
    public function canEdit($member = null)
2342
    {
2343
        $member = $this->getMemberForCanFunctions($member);
2344
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>, but the function expects a object<Member>|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2345
        if ($extended !== null) {
2346
            return $extended;
2347
        }
2348
        if ($this->canView($member) && $this->MyStep()->CustomerCanEdit) {
2349
            return true;
2350
        }
2351
        if (Permission::checkMember($member, Config::inst()->get('EcommerceRole', 'admin_permission_code'))) {
2352
            return true;
2353
        }
2354
        //is the member is a shop assistant they can always view it
2355
        if (EcommerceRole::current_member_is_shop_assistant($member)) {
2356
            return true;
2357
        }
2358
        return false;
2359
    }
2360
2361
    /**
2362
     * is the order ready to go through to the
2363
     * checkout process.
2364
     *
2365
     * This method checks all the order items and order modifiers
2366
     * If any of them need immediate attention then this is done
2367
     * first after which it will go through to the checkout page.
2368
     *
2369
     * @param Member (optional) $member
2370
     *
2371
     * @return bool
2372
     **/
2373
    public function canCheckout(Member $member = null)
2374
    {
2375
        $member = $this->getMemberForCanFunctions($member);
2376
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>, but the function expects a object<Member>|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2377
        if ($extended !== null) {
2378
            return $extended;
2379
        }
2380
        $submitErrors = $this->SubmitErrors();
2381
        if ($submitErrors && $submitErrors->count()) {
2382
            return false;
2383
        }
2384
2385
        return true;
2386
    }
2387
2388
    /**
2389
     * Can the order be submitted?
2390
     * this method can be used to stop an order from being submitted
2391
     * due to something not being completed or done.
2392
     *
2393
     * @see Order::SubmitErrors
2394
     *
2395
     * @param Member $member
2396
     *
2397
     * @return bool
2398
     **/
2399
    public function canSubmit(Member $member = null)
2400
    {
2401
        $member = $this->getMemberForCanFunctions($member);
2402
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>, but the function expects a object<Member>|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2403
        if ($extended !== null) {
2404
            return $extended;
2405
        }
2406
        if ($this->IsSubmitted()) {
2407
            return false;
2408
        }
2409
        $submitErrors = $this->SubmitErrors();
2410
        if ($submitErrors && $submitErrors->count()) {
2411
            return false;
2412
        }
2413
2414
        return true;
2415
    }
2416
2417
    /**
2418
     * Can a payment be made for this Order?
2419
     *
2420
     * @param Member $member
2421
     *
2422
     * @return bool
2423
     **/
2424
    public function canPay(Member $member = null)
2425
    {
2426
        $member = $this->getMemberForCanFunctions($member);
2427
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>, but the function expects a object<Member>|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2428
        if ($extended !== null) {
2429
            return $extended;
2430
        }
2431
        if ($this->IsPaid() || $this->IsCancelled() || $this->PaymentIsPending()) {
2432
            return false;
2433
        }
2434
2435
        return $this->MyStep()->CustomerCanPay;
2436
    }
2437
2438
    /**
2439
     * Can the given member cancel this order?
2440
     *
2441
     * @param Member $member
2442
     *
2443
     * @return bool
2444
     **/
2445
    public function canCancel(Member $member = null)
2446
    {
2447
        //if it is already cancelled it can not be cancelled again
2448
        if ($this->CancelledByID) {
2449
            return false;
2450
        }
2451
        $member = $this->getMemberForCanFunctions($member);
2452
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>, but the function expects a object<Member>|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2453
        if ($extended !== null) {
2454
            return $extended;
2455
        }
2456
        if (EcommerceRole::current_member_can_process_orders($member)) {
2457
            return true;
2458
        }
2459
2460
        return $this->MyStep()->CustomerCanCancel && $this->canView($member);
2461
    }
2462
2463
    /**
2464
     * @param Member $member
2465
     *
2466
     * @return bool
2467
     **/
2468
    public function canDelete($member = null)
2469
    {
2470
        $member = $this->getMemberForCanFunctions($member);
2471
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>, but the function expects a object<Member>|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2472
        if ($extended !== null) {
2473
            return $extended;
2474
        }
2475
        if ($this->IsSubmitted()) {
2476
            return false;
2477
        }
2478
        if (Permission::checkMember($member, Config::inst()->get('EcommerceRole', 'admin_permission_code'))) {
2479
            return true;
2480
        }
2481
2482
        return false;
2483
    }
2484
2485
    /**
2486
     * Returns all the order logs that the current member can view
2487
     * i.e. some order logs can only be viewed by the admin (e.g. suspected fraud orderlog).
2488
     *
2489
     * @return ArrayList (OrderStatusLogs)
2490
     **/
2491
    public function CanViewOrderStatusLogs()
2492
    {
2493
        $canViewOrderStatusLogs = new ArrayList();
2494
        $logs = $this->OrderStatusLogs();
2495
        foreach ($logs as $log) {
2496
            if ($log->canView()) {
2497
                $canViewOrderStatusLogs->push($log);
2498
            }
2499
        }
2500
2501
        return $canViewOrderStatusLogs;
2502
    }
2503
2504
    /**
2505
     * returns all the logs that can be viewed by the customer.
2506
     *
2507
     * @return ArrayList (OrderStausLogs)
2508
     */
2509
    public function CustomerViewableOrderStatusLogs()
2510
    {
2511
        $customerViewableOrderStatusLogs = new ArrayList();
2512
        $logs = $this->OrderStatusLogs();
2513
        if ($logs) {
2514
            foreach ($logs as $log) {
2515
                if (!$log->InternalUseOnly) {
2516
                    $customerViewableOrderStatusLogs->push($log);
2517
                }
2518
            }
2519
        }
2520
2521
        return $customerViewableOrderStatusLogs;
2522
    }
2523
2524
/*******************************************************
2525
   * 8. GET METHODS (e.g. Total, SubTotal, Title, etc...)
2526
*******************************************************/
2527
2528
    /**
2529
     * returns the email to be used for customer communication.
2530
     *
2531
     * @return string
2532
     */
2533
    public function OrderEmail()
2534
    {
2535
        return $this->getOrderEmail();
2536
    }
2537
    public function getOrderEmail()
2538
    {
2539
        $email = '';
2540
        if ($this->BillingAddressID && $this->BillingAddress()) {
2541
            $email = $this->BillingAddress()->Email;
2542
        }
2543
        if (! $email) {
2544
            if ($this->MemberID && $this->Member()) {
2545
                $email = $this->Member()->Email;
2546
            }
2547
        }
2548
        $extendedEmail = $this->extend('updateOrderEmail', $email);
2549
        if ($extendedEmail !== null && is_array($extendedEmail) && count($extendedEmail)) {
2550
            $email = implode(';', $extendedEmail);
2551
        }
2552
2553
        return $email;
2554
    }
2555
2556
    /**
2557
     * Returns true if there is a prink or email link.
2558
     *
2559
     * @return bool
2560
     */
2561
    public function HasPrintOrEmailLink()
2562
    {
2563
        return $this->EmailLink() || $this->PrintLink();
2564
    }
2565
2566
    /**
2567
     * returns the absolute link to the order that can be used in the customer communication (email).
2568
     *
2569
     * @return string
2570
     */
2571
    public function EmailLink($type = 'Order_StatusEmail')
2572
    {
2573
        return $this->getEmailLink();
2574
    }
2575
    public function getEmailLink($type = 'Order_StatusEmail')
2576
    {
2577
        if (!isset($_REQUEST['print'])) {
2578
            if ($this->IsSubmitted()) {
2579
                return Director::AbsoluteURL(OrderConfirmationPage::get_email_link($this->ID, $this->MyStep()->getEmailClassName(), $actuallySendEmail = true));
2580
            }
2581
        }
2582
    }
2583
2584
    /**
2585
     * returns the absolute link to the order for printing.
2586
     *
2587
     * @return string
2588
     */
2589
    public function PrintLink()
2590
    {
2591
        return $this->getPrintLink();
2592
    }
2593
    public function getPrintLink()
2594
    {
2595
        if (!isset($_REQUEST['print'])) {
2596
            if ($this->IsSubmitted()) {
2597
                return Director::AbsoluteURL(OrderConfirmationPage::get_order_link($this->ID)).'?print=1';
2598
            }
2599
        }
2600
    }
2601
2602
    /**
2603
     * returns the absolute link to the order for printing.
2604
     *
2605
     * @return string
2606
     */
2607
    public function PackingSlipLink()
2608
    {
2609
        return $this->getPackingSlipLink();
2610
    }
2611
    public function getPackingSlipLink()
2612
    {
2613
        if ($this->IsSubmitted()) {
2614
            return Director::AbsoluteURL(OrderConfirmationPage::get_order_link($this->ID)).'?packingslip=1';
2615
        }
2616
    }
2617
2618
    /**
2619
     * returns the absolute link that the customer can use to retrieve the email WITHOUT logging in.
2620
     *
2621
     * @return string
2622
     */
2623
    public function RetrieveLink()
2624
    {
2625
        return $this->getRetrieveLink();
2626
    }
2627
2628
    public function getRetrieveLink()
2629
    {
2630
        //important to recalculate!
2631
        if ($this->IsSubmitted($recalculate = true)) {
2632
            //add session ID if not added yet...
2633
            if (!$this->SessionID) {
2634
                $this->write();
2635
            }
2636
2637
            return Director::AbsoluteURL(OrderConfirmationPage::find_link()).'retrieveorder/'.$this->SessionID.'/'.$this->ID.'/';
2638
        } else {
2639
            return Director::AbsoluteURL('/shoppingcart/loadorder/'.$this->ID.'/');
2640
        }
2641
    }
2642
2643
    public function ShareLink()
2644
    {
2645
        return $this->getShareLink();
2646
    }
2647
2648
    public function getShareLink()
2649
    {
2650
        $orderItems = $this->itemsFromDatabase();
2651
        $action = 'share';
2652
        $array = array();
2653
        foreach ($orderItems as $orderItem) {
2654
            $array[] = implode(
2655
                ',',
2656
                array(
2657
                    $orderItem->BuyableClassName,
2658
                    $orderItem->BuyableID,
2659
                    $orderItem->Quantity
2660
                )
2661
            );
2662
        }
2663
2664
        return Director::AbsoluteURL(CartPage::find_link($action.'/'.implode('-', $array)));
2665
    }
2666
2667
    /**
2668
     * @alias for getFeedbackLink
2669
     * @return string
2670
     */
2671
    public function FeedbackLink()
2672
    {
2673
        return $this->getFeedbackLink();
2674
    }
2675
2676
    /**
2677
     * @return string | null
2678
     */
2679
    public function getFeedbackLink()
2680
    {
2681
        $orderConfirmationPage = OrderConfirmationPage::get()->first();
2682
        if($orderConfirmationPage->IsFeedbackEnabled) {
2683
2684
            return Director::AbsoluteURL($this->getRetrieveLink()).'#OrderForm_Feedback_FeedbackForm';
2685
        }
2686
    }
2687
2688
    /**
2689
     * link to delete order.
2690
     *
2691
     * @return string
2692
     */
2693
    public function DeleteLink()
2694
    {
2695
        return $this->getDeleteLink();
2696
    }
2697
    public function getDeleteLink()
2698
    {
2699
        if ($this->canDelete()) {
2700
            return ShoppingCart_Controller::delete_order_link($this->ID);
2701
        } else {
2702
            return '';
2703
        }
2704
    }
2705
2706
    /**
2707
     * link to copy order.
2708
     *
2709
     * @return string
2710
     */
2711
    public function CopyOrderLink()
2712
    {
2713
        return $this->getCopyOrderLink();
2714
    }
2715
    public function getCopyOrderLink()
2716
    {
2717
        if ($this->canView() && $this->IsSubmitted()) {
2718
            return ShoppingCart_Controller::copy_order_link($this->ID);
2719
        } else {
2720
            return '';
2721
        }
2722
    }
2723
2724
    /**
2725
     * A "Title" for the order, which summarises the main details (date, and customer) in a string.
2726
     *
2727
     * @param string $dateFormat  - e.g. "D j M Y, G:i T"
2728
     * @param bool   $includeName - e.g. by Mr Johnson
2729
     *
2730
     * @return string
2731
     **/
2732
    public function Title($dateFormat = null, $includeName = false)
2733
    {
2734
        return $this->getTitle($dateFormat, $includeName);
2735
    }
2736
    public function getTitle($dateFormat = null, $includeName = false)
2737
    {
2738
        if ($this->exists()) {
2739
            if ($dateFormat === null) {
2740
                $dateFormat = EcommerceConfig::get('Order', 'date_format_for_title');
2741
            }
2742
            if ($includeName === null) {
2743
                $includeName = EcommerceConfig::get('Order', 'include_customer_name_in_title');
2744
            }
2745
            $title = $this->i18n_singular_name()." #".number_format($this->ID);
2746
            if ($dateFormat) {
2747
                if ($submissionLog = $this->SubmissionLog()) {
2748
                    $dateObject = $submissionLog->dbObject('Created');
2749
                    $placed = _t('Order.PLACED', 'placed');
2750
                } else {
2751
                    $dateObject = $this->dbObject('Created');
2752
                    $placed = _t('Order.STARTED', 'started');
2753
                }
2754
                $title .= ' - '.$placed.' '.$dateObject->Format($dateFormat);
2755
            }
2756
            $name = '';
2757
            if ($this->CancelledByID) {
2758
                $name = ' - '._t('Order.CANCELLED', 'CANCELLED');
2759
            }
2760
            if ($includeName) {
2761
                $by = _t('Order.BY', 'by');
2762
                if (!$name) {
2763
                    if ($this->BillingAddressID) {
2764
                        if ($billingAddress = $this->BillingAddress()) {
2765
                            $name = ' - '.$by.' '.$billingAddress->Prefix.' '.$billingAddress->FirstName.' '.$billingAddress->Surname;
2766
                        }
2767
                    }
2768
                }
2769
                if (!$name) {
2770
                    if ($this->MemberID) {
2771
                        if ($member = $this->Member()) {
2772
                            if ($member->exists()) {
2773
                                if ($memberName = $member->getName()) {
2774
                                    if (!trim($memberName)) {
2775
                                        $memberName = _t('Order.ANONYMOUS', 'anonymous');
2776
                                    }
2777
                                    $name = ' - '.$by.' '.$memberName;
2778
                                }
2779
                            }
2780
                        }
2781
                    }
2782
                }
2783
            }
2784
            $title .= $name;
2785
        } else {
2786
            $title = _t('Order.NEW', 'New').' '.$this->i18n_singular_name();
2787
        }
2788
        $extendedTitle = $this->extend('updateTitle', $title);
2789
        if ($extendedTitle !== null && is_array($extendedTitle) && count($extendedTitle)) {
2790
            $title = implode('; ', $extendedTitle);
2791
        }
2792
2793
        return $title;
2794
    }
2795
2796
    /**
2797
     * Returns the subtotal of the items for this order.
2798
     *
2799
     * @return float
2800
     */
2801
    public function SubTotal()
2802
    {
2803
        return $this->getSubTotal();
2804
    }
2805
    public function getSubTotal()
2806
    {
2807
        $result = 0;
2808
        $items = $this->Items();
2809
        if ($items->count()) {
2810
            foreach ($items as $item) {
2811
                if (is_a($item, Object::getCustomClass('OrderAttribute'))) {
2812
                    $result += $item->Total();
2813
                }
2814
            }
2815
        }
2816
2817
        return $result;
2818
    }
2819
2820
    /**
2821
     * @return Currency (DB Object)
2822
     **/
2823
    public function SubTotalAsCurrencyObject()
2824
    {
2825
        return DBField::create_field('Currency', $this->SubTotal());
2826
    }
2827
2828
    /**
2829
     * @return Money
2830
     **/
2831
    public function SubTotalAsMoney()
2832
    {
2833
        return $this->getSubTotalAsMoney();
2834
    }
2835
    public function getSubTotalAsMoney()
2836
    {
2837
        return EcommerceCurrency::get_money_object_from_order_currency($this->SubTotal(), $this);
2838
    }
2839
2840
    /**
2841
     * @param string|array $excluded               - Class(es) of modifier(s) to ignore in the calculation.
2842
     * @param bool         $stopAtExcludedModifier - when this flag is TRUE, we stop adding the modifiers when we reach an excluded modifier.
2843
     *
2844
     * @return Currency (DB Object)
2845
     **/
2846
    public function ModifiersSubTotalAsCurrencyObject($excluded = null, $stopAtExcludedModifier = false)
2847
    {
2848
        return DBField::create_field('Currency', $this->ModifiersSubTotal($excluded, $stopAtExcludedModifier));
2849
    }
2850
2851
    /**
2852
     * @param string|array $excluded               - Class(es) of modifier(s) to ignore in the calculation.
2853
     * @param bool         $stopAtExcludedModifier - when this flag is TRUE, we stop adding the modifiers when we reach an excluded modifier.
2854
     *
2855
     * @return Money (DB Object)
2856
     **/
2857
    public function ModifiersSubTotalAsMoneyObject($excluded = null, $stopAtExcludedModifier = false)
2858
    {
2859
        return EcommerceCurrency::get_money_object_from_order_currency($this->ModifiersSubTotal($excluded, $stopAtExcludedModifier), $this);
2860
    }
2861
2862
    /**
2863
     * Returns the total cost of an order including the additional charges or deductions of its modifiers.
2864
     *
2865
     * @return float
2866
     */
2867
    public function Total()
2868
    {
2869
        return $this->getTotal();
2870
    }
2871
    public function getTotal()
2872
    {
2873
        return $this->SubTotal() + $this->ModifiersSubTotal();
2874
    }
2875
2876
    /**
2877
     * @return Currency (DB Object)
2878
     **/
2879
    public function TotalAsCurrencyObject()
2880
    {
2881
        return DBField::create_field('Currency', $this->Total());
2882
    }
2883
2884
    /**
2885
     * @return Money
2886
     **/
2887
    public function TotalAsMoney()
2888
    {
2889
        return $this->getTotalAsMoney();
2890
    }
2891
    public function getTotalAsMoney()
2892
    {
2893
        return EcommerceCurrency::get_money_object_from_order_currency($this->Total(), $this);
2894
    }
2895
2896
    /**
2897
     * Checks to see if any payments have been made on this order
2898
     * and if so, subracts the payment amount from the order.
2899
     *
2900
     * @return float
2901
     **/
2902
    public function TotalOutstanding()
2903
    {
2904
        return $this->getTotalOutstanding();
2905
    }
2906
    public function getTotalOutstanding()
2907
    {
2908
        if ($this->IsSubmitted()) {
2909
            $total = $this->Total();
2910
            $paid = $this->TotalPaid();
2911
            $outstanding = $total - $paid;
2912
            $maxDifference = EcommerceConfig::get('Order', 'maximum_ignorable_sales_payments_difference');
2913
            if (abs($outstanding) < $maxDifference) {
2914
                $outstanding = 0;
2915
            }
2916
2917
            return floatval($outstanding);
2918
        } else {
2919
            return 0;
2920
        }
2921
    }
2922
2923
    /**
2924
     * @return Currency (DB Object)
2925
     **/
2926
    public function TotalOutstandingAsCurrencyObject()
2927
    {
2928
        return DBField::create_field('Currency', $this->TotalOutstanding());
2929
    }
2930
2931
    /**
2932
     * @return Money
2933
     **/
2934
    public function TotalOutstandingAsMoney()
2935
    {
2936
        return $this->getTotalOutstandingAsMoney();
2937
    }
2938
    public function getTotalOutstandingAsMoney()
2939
    {
2940
        return EcommerceCurrency::get_money_object_from_order_currency($this->TotalOutstanding(), $this);
2941
    }
2942
2943
    /**
2944
     * @return float
2945
     */
2946
    public function TotalPaid()
2947
    {
2948
        return $this->getTotalPaid();
2949
    }
2950
    public function getTotalPaid()
2951
    {
2952
        $paid = 0;
2953
        if ($payments = $this->Payments()) {
2954
            foreach ($payments as $payment) {
2955
                if ($payment->Status == 'Success') {
2956
                    $paid += $payment->Amount->getAmount();
2957
                }
2958
            }
2959
        }
2960
        $reverseExchange = 1;
2961
        if ($this->ExchangeRate && $this->ExchangeRate != 1) {
2962
            $reverseExchange = 1 / $this->ExchangeRate;
2963
        }
2964
2965
        return $paid * $reverseExchange;
2966
    }
2967
2968
    /**
2969
     * @return Currency (DB Object)
2970
     **/
2971
    public function TotalPaidAsCurrencyObject()
2972
    {
2973
        return DBField::create_field('Currency', $this->TotalPaid());
2974
    }
2975
2976
    /**
2977
     * @return Money
2978
     **/
2979
    public function TotalPaidAsMoney()
2980
    {
2981
        return $this->getTotalPaidAsMoney();
2982
    }
2983
    public function getTotalPaidAsMoney()
2984
    {
2985
        return EcommerceCurrency::get_money_object_from_order_currency($this->TotalPaid(), $this);
2986
    }
2987
2988
    /**
2989
     * returns the total number of OrderItems (not modifiers).
2990
     * This is meant to run as fast as possible to quickly check
2991
     * if there is anything in the cart.
2992
     *
2993
     * @param bool $recalculate - do we need to recalculate (value is retained during lifetime of Object)
2994
     *
2995
     * @return int
2996
     **/
2997
    public function TotalItems($recalculate = false)
2998
    {
2999
        return $this->getTotalItems($recalculate);
3000
    }
3001
    public function getTotalItems($recalculate = false)
3002
    {
3003
        if ($this->totalItems === null || $recalculate) {
3004
            $this->totalItems = OrderItem::get()
3005
                ->where('"OrderAttribute"."OrderID" = '.$this->ID.' AND "OrderItem"."Quantity" > 0')
3006
                ->count();
3007
        }
3008
3009
        return $this->totalItems;
3010
    }
3011
3012
    /**
3013
     * Little shorthand.
3014
     *
3015
     * @param bool $recalculate
3016
     *
3017
     * @return bool
3018
     **/
3019
    public function MoreThanOneItemInCart($recalculate = false)
3020
    {
3021
        return $this->TotalItems($recalculate) > 1 ? true : false;
3022
    }
3023
3024
    /**
3025
     * returns the total number of OrderItems (not modifiers) times their respectective quantities.
3026
     *
3027
     * @param bool $recalculate - force recalculation
3028
     *
3029
     * @return float
3030
     **/
3031
    public function TotalItemsTimesQuantity($recalculate = false)
3032
    {
3033
        return $this->getTotalItemsTimesQuantity($recalculate);
3034
    }
3035
    public function getTotalItemsTimesQuantity($recalculate = false)
3036
    {
3037
        if ($this->totalItemsTimesQuantity === null || $recalculate) {
3038
            //to do, why do we check if you can edit ????
3039
            $this->totalItemsTimesQuantity = DB::query('
3040
                SELECT SUM("OrderItem"."Quantity")
3041
                FROM "OrderItem"
3042
                    INNER JOIN "OrderAttribute" ON "OrderAttribute"."ID" = "OrderItem"."ID"
3043
                WHERE
3044
                    "OrderAttribute"."OrderID" = '.$this->ID.'
3045
                    AND "OrderItem"."Quantity" > 0'
3046
            )->value();
3047
        }
3048
3049
        return $this->totalItemsTimesQuantity - 0;
3050
    }
3051
3052
    /**
3053
     *
3054
     * @return string (country code)
3055
     **/
3056
    public function Country()
3057
    {
3058
        return $this->getCountry();
3059
    }
3060
3061
    /**
3062
    * Returns the country code for the country that applies to the order.
3063
    * @alias  for getCountry
3064
    *
3065
    * @return string - country code e.g. NZ
3066
     */
3067
    public function getCountry()
3068
    {
3069
        $countryCodes = array(
3070
            'Billing' => '',
3071
            'Shipping' => '',
3072
        );
3073
        $code = null;
3074
        if ($this->BillingAddressID) {
3075
            $billingAddress = BillingAddress::get()->byID($this->BillingAddressID);
3076
            if ($billingAddress) {
3077
                if ($billingAddress->Country) {
3078
                    $countryCodes['Billing'] = $billingAddress->Country;
3079
                }
3080
            }
3081
        }
3082
        if ($this->ShippingAddressID && $this->UseShippingAddress) {
3083
            $shippingAddress = ShippingAddress::get()->byID($this->ShippingAddressID);
3084
            if ($shippingAddress) {
3085
                if ($shippingAddress->ShippingCountry) {
3086
                    $countryCodes['Shipping'] = $shippingAddress->ShippingCountry;
3087
                }
3088
            }
3089
        }
3090
        if (
3091
            (EcommerceConfig::get('OrderAddress', 'use_shipping_address_for_main_region_and_country') && $countryCodes['Shipping'])
3092
            ||
3093
            (!$countryCodes['Billing'] && $countryCodes['Shipping'])
3094
        ) {
3095
            $code = $countryCodes['Shipping'];
3096
        } elseif ($countryCodes['Billing']) {
3097
            $code = $countryCodes['Billing'];
3098
        } else {
3099
            $code = EcommerceCountry::get_country_from_ip();
3100
        }
3101
3102
        return $code;
3103
    }
3104
3105
    /**
3106
     * @alias for getFullNameCountry
3107
     *
3108
     * @return string - country name
3109
     **/
3110
    public function FullNameCountry()
3111
    {
3112
        return $this->getFullNameCountry();
3113
    }
3114
3115
    /**
3116
     * returns name of coutry.
3117
     *
3118
     * @return string - country name
3119
     **/
3120
    public function getFullNameCountry()
3121
    {
3122
        return EcommerceCountry::find_title($this->Country());
3123
    }
3124
3125
    /**
3126
     * @alis for getExpectedCountryName
3127
     * @return string - country name
3128
     **/
3129
    public function ExpectedCountryName()
3130
    {
3131
        return $this->getExpectedCountryName();
3132
    }
3133
3134
    /**
3135
     * returns name of coutry that we expect the customer to have
3136
     * This takes into consideration more than just what has been entered
3137
     * for example, it looks at GEO IP.
3138
     *
3139
     * @todo: why do we dont return a string IF there is only one item.
3140
     *
3141
     * @return string - country name
3142
     **/
3143
    public function getExpectedCountryName()
3144
    {
3145
        return EcommerceCountry::find_title(EcommerceCountry::get_country(false, $this->ID));
3146
    }
3147
3148
    /**
3149
     * return the title of the fixed country (if any).
3150
     *
3151
     * @return string | empty string
3152
     **/
3153
    public function FixedCountry()
3154
    {
3155
        return $this->getFixedCountry();
3156
    }
3157
    public function getFixedCountry()
3158
    {
3159
        $code = EcommerceCountry::get_fixed_country_code();
3160
        if ($code) {
3161
            return EcommerceCountry::find_title($code);
3162
        }
3163
3164
        return '';
3165
    }
3166
3167
    /**
3168
     * Returns the region that applies to the order.
3169
     * we check both billing and shipping, in case one of them is empty.
3170
     *
3171
     * @return DataObject | Null (EcommerceRegion)
3172
     **/
3173
    public function Region()
3174
    {
3175
        return $this->getRegion();
3176
    }
3177
    public function getRegion()
3178
    {
3179
        $regionIDs = array(
3180
            'Billing' => 0,
3181
            'Shipping' => 0,
3182
        );
3183
        if ($this->BillingAddressID) {
3184
            if ($billingAddress = $this->BillingAddress()) {
3185
                if ($billingAddress->RegionID) {
3186
                    $regionIDs['Billing'] = $billingAddress->RegionID;
3187
                }
3188
            }
3189
        }
3190
        if ($this->CanHaveShippingAddress()) {
3191
            if ($this->ShippingAddressID) {
3192
                if ($shippingAddress = $this->ShippingAddress()) {
3193
                    if ($shippingAddress->ShippingRegionID) {
3194
                        $regionIDs['Shipping'] = $shippingAddress->ShippingRegionID;
3195
                    }
3196
                }
3197
            }
3198
        }
3199
        if (count($regionIDs)) {
3200
            //note the double-check with $this->CanHaveShippingAddress() and get_use_....
3201
            if ($this->CanHaveShippingAddress() && EcommerceConfig::get('OrderAddress', 'use_shipping_address_for_main_region_and_country') && $regionIDs['Shipping']) {
3202
                return EcommerceRegion::get()->byID($regionIDs['Shipping']);
3203
            } else {
3204
                return EcommerceRegion::get()->byID($regionIDs['Billing']);
3205
            }
3206
        } else {
3207
            return EcommerceRegion::get()->byID(EcommerceRegion::get_region_from_ip());
3208
        }
3209
    }
3210
3211
    /**
3212
     * Casted variable
3213
     * Currency is not the same as the standard one?
3214
     *
3215
     * @return bool
3216
     **/
3217
    public function HasAlternativeCurrency()
3218
    {
3219
        return $this->getHasAlternativeCurrency();
3220
    }
3221
    public function getHasAlternativeCurrency()
3222
    {
3223
        if ($currency = $this->CurrencyUsed()) {
3224
            if ($currency->IsDefault()) {
3225
                return false;
3226
            } else {
3227
                return true;
3228
            }
3229
        } else {
3230
            return false;
3231
        }
3232
    }
3233
3234
    /**
3235
     * Makes sure exchange rate is updated and maintained before order is submitted
3236
     * This method is public because it could be called from a shopping Cart Object.
3237
     **/
3238
    public function EnsureCorrectExchangeRate()
3239
    {
3240
        if (!$this->IsSubmitted()) {
3241
            $oldExchangeRate = $this->ExchangeRate;
3242
            if ($currency = $this->CurrencyUsed()) {
3243
                if ($currency->IsDefault()) {
3244
                    $this->ExchangeRate = 0;
3245
                } else {
3246
                    $this->ExchangeRate = $currency->getExchangeRate();
3247
                }
3248
            } else {
3249
                $this->ExchangeRate = 0;
3250
            }
3251
            if ($this->ExchangeRate != $oldExchangeRate) {
3252
                $this->write();
3253
            }
3254
        }
3255
    }
3256
3257
    /**
3258
     * speeds up processing by storing the IsSubmitted value
3259
     * we start with -1 to know if it has been requested before.
3260
     *
3261
     * @var bool
3262
     */
3263
    protected $_isSubmittedTempVar = -1;
3264
3265
    /**
3266
     * Casted variable - has the order been submitted?
3267
     * alias
3268
     * @param bool $recalculate
3269
     *
3270
     * @return bool
3271
     **/
3272
    public function IsSubmitted($recalculate = true)
3273
    {
3274
        return $this->getIsSubmitted($recalculate);
3275
    }
3276
3277
    /**
3278
     * Casted variable - has the order been submitted?
3279
     *
3280
     * @param bool $recalculate
3281
     *
3282
     * @return bool
3283
     **/
3284
    public function getIsSubmitted($recalculate = false)
3285
    {
3286
        if ($this->_isSubmittedTempVar === -1 || $recalculate) {
3287
            if ($this->SubmissionLog()) {
3288
                $this->_isSubmittedTempVar = true;
3289
            } else {
3290
                $this->_isSubmittedTempVar = false;
3291
            }
3292
        }
3293
3294
        return $this->_isSubmittedTempVar;
3295
    }
3296
3297
    /**
3298
     *
3299
     *
3300
     * @return bool
3301
     */
3302
    public function IsArchived()
3303
    {
3304
        $lastStep = OrderStep::get()->Last();
3305
        if ($lastStep) {
3306
            if ($lastStep->ID == $this->StatusID) {
3307
                return true;
3308
            }
3309
        }
3310
        return false;
3311
    }
3312
3313
    /**
3314
     * Submission Log for this Order (if any).
3315
     *
3316
     * @return Submission Log (OrderStatusLog_Submitted) | Null
3317
     **/
3318
    public function SubmissionLog()
3319
    {
3320
        $className = EcommerceConfig::get('OrderStatusLog', 'order_status_log_class_used_for_submitting_order');
3321
3322
        return $className::get()
3323
            ->Filter(array('OrderID' => $this->ID))
3324
            ->Last();
3325
    }
3326
3327
    /**
3328
     * @return int
3329
     */
3330
    public function SecondsSinceBeingSubmitted()
3331
    {
3332
        if ($submissionLog = $this->SubmissionLog()) {
3333
            return time() - strtotime($submissionLog->Created);
3334
        } else {
3335
            return 0;
3336
        }
3337
    }
3338
3339
    /**
3340
     * if the order can not be submitted,
3341
     * then the reasons why it can not be submitted
3342
     * will be returned by this method.
3343
     *
3344
     * @see Order::canSubmit
3345
     *
3346
     * @return ArrayList | null
3347
     */
3348
    public function SubmitErrors()
3349
    {
3350
        $al = null;
3351
        $extendedSubmitErrors = $this->extend('updateSubmitErrors');
3352
        if ($extendedSubmitErrors !== null && is_array($extendedSubmitErrors) && count($extendedSubmitErrors)) {
3353
            $al = ArrayList::create();
3354
            foreach ($extendedSubmitErrors as $returnResultArray) {
3355
                foreach ($returnResultArray as $issue) {
3356
                    if ($issue) {
3357
                        $al->push(ArrayData::create(array("Title" => $issue)));
3358
                    }
3359
                }
3360
            }
3361
        }
3362
        return $al;
3363
    }
3364
3365
    /**
3366
     * Casted variable - has the order been submitted?
3367
     *
3368
     * @param bool $withDetail
3369
     *
3370
     * @return string
3371
     **/
3372
    public function CustomerStatus($withDetail = true)
3373
    {
3374
        return $this->getCustomerStatus($withDetail);
3375
    }
3376
    public function getCustomerStatus($withDetail = true)
3377
    {
3378
        $str = '';
3379
        if ($this->MyStep()->ShowAsUncompletedOrder) {
3380
            $str = _t('Order.UNCOMPLETED', 'Uncompleted');
3381
        } elseif ($this->MyStep()->ShowAsInProcessOrder) {
3382
            $str = _t('Order.IN_PROCESS', 'In Process');
3383
        } elseif ($this->MyStep()->ShowAsCompletedOrder) {
3384
            $str = _t('Order.COMPLETED', 'Completed');
3385
        }
3386
        if ($withDetail) {
3387
            if (!$this->HideStepFromCustomer) {
3388
                $str .= ' ('.$this->MyStep()->Name.')';
3389
            }
3390
        }
3391
3392
        return $str;
3393
    }
3394
3395
    /**
3396
     * Casted variable - does the order have a potential shipping address?
3397
     *
3398
     * @return bool
3399
     **/
3400
    public function CanHaveShippingAddress()
3401
    {
3402
        return $this->getCanHaveShippingAddress();
3403
    }
3404
    public function getCanHaveShippingAddress()
3405
    {
3406
        return EcommerceConfig::get('OrderAddress', 'use_separate_shipping_address');
3407
    }
3408
3409
    /**
3410
     * returns the link to view the Order
3411
     * WHY NOT CHECKOUT PAGE: first we check for cart page.
3412
     *
3413
     * @return CartPage | Null
3414
     */
3415
    public function DisplayPage()
3416
    {
3417
        if ($this->MyStep() && $this->MyStep()->AlternativeDisplayPage()) {
3418
            $page = $this->MyStep()->AlternativeDisplayPage();
3419
        } elseif ($this->IsSubmitted()) {
3420
            $page = OrderConfirmationPage::get()->First();
3421
        } else {
3422
            $page = CartPage::get()
3423
                ->Filter(array('ClassName' => 'CartPage'))
3424
                ->First();
3425
            if (!$page) {
3426
                $page = CheckoutPage::get()->First();
3427
            }
3428
        }
3429
3430
        return $page;
3431
    }
3432
3433
    /**
3434
     * returns the link to view the Order
3435
     * WHY NOT CHECKOUT PAGE: first we check for cart page.
3436
     * If a cart page has been created then we refer through to Cart Page.
3437
     * Otherwise it will default to the checkout page.
3438
     *
3439
     * @param string $action - any action that should be added to the link.
3440
     *
3441
     * @return String(URLSegment)
3442
     */
3443
    public function Link($action = null)
3444
    {
3445
        $page = $this->DisplayPage();
3446
        if ($page) {
3447
            return $page->getOrderLink($this->ID, $action);
3448
        } else {
3449
            user_error('A Cart / Checkout Page + an Order Confirmation Page needs to be setup for the e-commerce module to work.', E_USER_NOTICE);
3450
            $page = ErrorPage::get()
3451
                ->Filter(array('ErrorCode' => '404'))
3452
                ->First();
3453
            if ($page) {
3454
                return $page->Link();
3455
            }
3456
        }
3457
    }
3458
3459
    /**
3460
     * Returns to link to access the Order's API.
3461
     *
3462
     * @param string $version
3463
     * @param string $extension
3464
     *
3465
     * @return String(URL)
3466
     */
3467
    public function APILink($version = 'v1', $extension = 'xml')
3468
    {
3469
        return Director::AbsoluteURL("/api/ecommerce/$version/Order/".$this->ID."/.$extension");
3470
    }
3471
3472
    /**
3473
     * returns the link to finalise the Order.
3474
     *
3475
     * @return String(URLSegment)
3476
     */
3477
    public function CheckoutLink()
3478
    {
3479
        $page = CheckoutPage::get()->First();
3480
        if ($page) {
3481
            return $page->Link();
3482
        } else {
3483
            $page = ErrorPage::get()
3484
                ->Filter(array('ErrorCode' => '404'))
3485
                ->First();
3486
            if ($page) {
3487
                return $page->Link();
3488
            }
3489
        }
3490
    }
3491
3492
    /**
3493
     * Converts the Order into HTML, based on the Order Template.
3494
     *
3495
     * @return HTML Object
3496
     **/
3497
    public function ConvertToHTML()
3498
    {
3499
        Config::nest();
3500
        Config::inst()->update('SSViewer', 'theme_enabled', true);
3501
        $html = $this->renderWith('Order');
3502
        Config::unnest();
3503
        $html = preg_replace('/(\s)+/', ' ', $html);
3504
3505
        return DBField::create_field('HTMLText', $html);
3506
    }
3507
3508
    /**
3509
     * Converts the Order into a serialized string
3510
     * TO DO: check if this works and check if we need to use special sapphire serialization code.
3511
     *
3512
     * @return string - serialized object
3513
     **/
3514
    public function ConvertToString()
3515
    {
3516
        return serialize($this->addHasOneAndHasManyAsVariables());
3517
    }
3518
3519
    /**
3520
     * Converts the Order into a JSON object
3521
     * TO DO: check if this works and check if we need to use special sapphire JSON code.
3522
     *
3523
     * @return string -  JSON
3524
     **/
3525
    public function ConvertToJSON()
3526
    {
3527
        return json_encode($this->addHasOneAndHasManyAsVariables());
3528
    }
3529
3530
    /**
3531
     * returns itself wtih more data added as variables.
3532
     * We add has_one and has_many as variables like this: $this->MyHasOne_serialized = serialize($this->MyHasOne()).
3533
     *
3534
     * @return Order - with most important has one and has many items included as variables.
3535
     **/
3536
    protected function addHasOneAndHasManyAsVariables()
3537
    {
3538
        $object = clone $this;
3539
        $object->Member_serialized = serialize($this->Member());
3540
        $object->BillingAddress_serialized = serialize($this->BillingAddress());
3541
        $object->ShippingAddress_serialized = serialize($this->ShippingAddress());
3542
        $object->Attributes_serialized = serialize($this->Attributes());
3543
        $object->OrderStatusLogs_serialized = serialize($this->OrderStatusLogs());
3544
        $object->Payments_serialized = serialize($this->Payments());
3545
        $object->Emails_serialized = serialize($this->Emails());
3546
3547
        return $object;
3548
    }
3549
3550
/*******************************************************
3551
   * 9. TEMPLATE RELATED STUFF
3552
*******************************************************/
3553
3554
    /**
3555
     * returns the instance of EcommerceConfigAjax for use in templates.
3556
     * In templates, it is used like this:
3557
     * $EcommerceConfigAjax.TableID.
3558
     *
3559
     * @return EcommerceConfigAjax
3560
     **/
3561
    public function AJAXDefinitions()
3562
    {
3563
        return EcommerceConfigAjax::get_one($this);
3564
    }
3565
3566
    /**
3567
     * returns the instance of EcommerceDBConfig.
3568
     *
3569
     * @return EcommerceDBConfig
3570
     **/
3571
    public function EcomConfig()
3572
    {
3573
        return EcommerceDBConfig::current_ecommerce_db_config();
3574
    }
3575
3576
    /**
3577
     * Collects the JSON data for an ajax return of the cart.
3578
     *
3579
     * @param array $js
3580
     *
3581
     * @return array (for use in AJAX for JSON)
3582
     **/
3583
    public function updateForAjax(array $js)
3584
    {
3585
        $function = EcommerceConfig::get('Order', 'ajax_subtotal_format');
3586
        if (is_array($function)) {
3587
            list($function, $format) = $function;
3588
        }
3589
        $subTotal = $this->$function();
3590
        if (isset($format)) {
3591
            $subTotal = $subTotal->$format();
3592
            unset($format);
3593
        }
3594
        $function = EcommerceConfig::get('Order', 'ajax_total_format');
3595
        if (is_array($function)) {
3596
            list($function, $format) = $function;
3597
        }
3598
        $total = $this->$function();
3599
        if (isset($format)) {
3600
            $total = $total->$format();
3601
        }
3602
        $ajaxObject = $this->AJAXDefinitions();
3603
        $js[] = array(
3604
            't' => 'id',
3605
            's' => $ajaxObject->TableSubTotalID(),
3606
            'p' => 'innerHTML',
3607
            'v' => $subTotal,
3608
        );
3609
        $js[] = array(
3610
            't' => 'id',
3611
            's' => $ajaxObject->TableTotalID(),
3612
            'p' => 'innerHTML',
3613
            'v' => $total,
3614
        );
3615
        $js[] = array(
3616
            't' => 'class',
3617
            's' => $ajaxObject->TotalItemsClassName(),
3618
            'p' => 'innerHTML',
3619
            'v' => $this->TotalItems($recalculate = true),
3620
        );
3621
        $js[] = array(
3622
            't' => 'class',
3623
            's' => $ajaxObject->TotalItemsTimesQuantityClassName(),
3624
            'p' => 'innerHTML',
3625
            'v' => $this->TotalItemsTimesQuantity(),
3626
        );
3627
        $js[] = array(
3628
            't' => 'class',
3629
            's' => $ajaxObject->ExpectedCountryClassName(),
3630
            'p' => 'innerHTML',
3631
            'v' => $this->ExpectedCountryName(),
3632
        );
3633
3634
        return $js;
3635
    }
3636
3637
    /**
3638
     * @ToDO: move to more appropriate class
3639
     *
3640
     * @return float
3641
     **/
3642
    public function SubTotalCartValue()
3643
    {
3644
        return $this->SubTotal;
3645
    }
3646
3647
/*******************************************************
3648
   * 10. STANDARD SS METHODS (requireDefaultRecords, onBeforeDelete, etc...)
3649
*******************************************************/
3650
3651
    /**
3652
     *standard SS method.
3653
     **/
3654
    public function populateDefaults()
3655
    {
3656
        parent::populateDefaults();
3657
    }
3658
3659
    public function onBeforeWrite()
3660
    {
3661
        parent::onBeforeWrite();
3662
        if (! $this->getCanHaveShippingAddress()) {
3663
            $this->UseShippingAddress = false;
3664
        }
3665
        if (!$this->CurrencyUsedID) {
3666
            $this->CurrencyUsedID = EcommerceCurrency::default_currency_id();
3667
        }
3668
        if (!$this->SessionID) {
3669
            $generator = Injector::inst()->create('RandomGenerator');
3670
            $token = $generator->randomToken('sha1');
3671
            $this->SessionID = substr($token, 0, 32);
3672
        }
3673
    }
3674
3675
    /**
3676
     * standard SS method
3677
     * adds the ability to update order after writing it.
3678
     **/
3679
    public function onAfterWrite()
3680
    {
3681
        parent::onAfterWrite();
3682
        //crucial!
3683
        self::set_needs_recalculating(true, $this->ID);
3684
        // quick double-check
3685
        if ($this->IsCancelled() && ! $this->IsArchived()) {
3686
            $this->Archive($avoidWrites = true);
3687
        }
3688
        if ($this->IsSubmitted($recalculate = true)) {
3689
            //do nothing
3690
        } else {
3691
            if ($this->StatusID) {
3692
                $this->calculateOrderAttributes($recalculate = false);
3693
                if (EcommerceRole::current_member_is_shop_admin()) {
3694
                    if (isset($_REQUEST['SubmitOrderViaCMS'])) {
3695
                        $this->tryToFinaliseOrder();
3696
                        //just in case it writes again...
3697
                        unset($_REQUEST['SubmitOrderViaCMS']);
3698
                    }
3699
                }
3700
            }
3701
        }
3702
    }
3703
3704
    /**
3705
     *standard SS method.
3706
     *
3707
     * delete attributes, statuslogs, and payments
3708
     * THIS SHOULD NOT BE USED AS ORDERS SHOULD BE CANCELLED NOT DELETED
3709
     */
3710
    public function onBeforeDelete()
3711
    {
3712
        parent::onBeforeDelete();
3713
        if ($attributes = $this->Attributes()) {
3714
            foreach ($attributes as $attribute) {
3715
                $attribute->delete();
3716
                $attribute->destroy();
3717
            }
3718
        }
3719
3720
        //THE REST WAS GIVING ERRORS - POSSIBLY DUE TO THE FUNNY RELATIONSHIP (one-one, two times...)
3721
        /*
3722
        if($billingAddress = $this->BillingAddress()) {
3723
            if($billingAddress->exists()) {
3724
                $billingAddress->delete();
3725
                $billingAddress->destroy();
3726
            }
3727
        }
3728
        if($shippingAddress = $this->ShippingAddress()) {
3729
            if($shippingAddress->exists()) {
3730
                $shippingAddress->delete();
3731
                $shippingAddress->destroy();
3732
            }
3733
        }
3734
3735
        if($statuslogs = $this->OrderStatusLogs()){
3736
            foreach($statuslogs as $log){
3737
                $log->delete();
3738
                $log->destroy();
3739
            }
3740
        }
3741
        if($payments = $this->Payments()){
3742
            foreach($payments as $payment){
3743
                $payment->delete();
3744
                $payment->destroy();
3745
            }
3746
        }
3747
        if($emails = $this->Emails()) {
3748
            foreach($emails as $email){
3749
                $email->delete();
3750
                $email->destroy();
3751
            }
3752
        }
3753
        */
3754
    }
3755
3756
/*******************************************************
3757
   * 11. DEBUG
3758
*******************************************************/
3759
3760
    /**
3761
     * Debug helper method.
3762
     * Can be called from /shoppingcart/debug/.
3763
     *
3764
     * @return string
3765
     */
3766
    public function debug()
3767
    {
3768
        $this->calculateOrderAttributes(true);
3769
3770
        return EcommerceTaskDebugCart::debug_object($this);
3771
    }
3772
}
3773