Completed
Push — master ( 89474e...0940e8 )
by Nicolaas
03:40
created

code/model/Order.php (3 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
        'LastEdited' => true
117
    );
118
119
    /**
120
     * standard SS variable.
121
     *
122
     * @var string
123
     */
124
    private static $default_sort = [
125
        'LastEdited' => 'DESC',
126
        'ID' => 'DESC'
127
    ];
128
129
    /**
130
     * standard SS variable.
131
     *
132
     * @var array
133
     */
134
    private static $casting = array(
135
        'OrderEmail' => 'Varchar',
136
        'EmailLink' => 'Varchar',
137
        'PrintLink' => 'Varchar',
138
        'ShareLink' => 'Varchar',
139
        'FeedbackLink' => 'Varchar',
140
        'RetrieveLink' => 'Varchar',
141
        'Title' => 'Varchar',
142
        'Total' => 'Currency',
143
        'TotalAsMoney' => 'Money',
144
        'SubTotal' => 'Currency',
145
        'SubTotalAsMoney' => 'Money',
146
        'TotalPaid' => 'Currency',
147
        'TotalPaidAsMoney' => 'Money',
148
        'TotalOutstanding' => 'Currency',
149
        'TotalOutstandingAsMoney' => 'Money',
150
        'HasAlternativeCurrency' => 'Boolean',
151
        'TotalItems' => 'Double',
152
        'TotalItemsTimesQuantity' => 'Double',
153
        'IsCancelled' => 'Boolean',
154
        'IsPaidNice' => 'Varchar',
155
        'Country' => 'Varchar(3)', //This is the applicable country for the order - for tax purposes, etc....
156
        'FullNameCountry' => 'Varchar',
157
        'IsSubmitted' => 'Boolean',
158
        'CustomerStatus' => 'Varchar',
159
        'CanHaveShippingAddress' => 'Boolean',
160
    );
161
162
    /**
163
     * standard SS variable.
164
     *
165
     * @var string
166
     */
167
    private static $singular_name = 'Order';
168
    public function i18n_singular_name()
169
    {
170
        return _t('Order.ORDER', 'Order');
171
    }
172
173
    /**
174
     * standard SS variable.
175
     *
176
     * @var string
177
     */
178
    private static $plural_name = 'Orders';
179
    public function i18n_plural_name()
180
    {
181
        return _t('Order.ORDERS', 'Orders');
182
    }
183
184
    /**
185
     * Standard SS variable.
186
     *
187
     * @var string
188
     */
189
    private static $description = "A collection of items that together make up the 'Order'.  An order can be placed.";
190
191
    /**
192
     * Tells us if an order needs to be recalculated
193
     * can save one for each order...
194
     *
195
     * @var array
196
     */
197
    private static $_needs_recalculating = array();
198
199
    /**
200
     * @param bool (optional) $b
201
     * @param int (optional)  $orderID
202
     *
203
     * @return bool
204
     */
205
    public static function set_needs_recalculating($b = true, $orderID = 0)
206
    {
207
        self::$_needs_recalculating[$orderID] = $b;
208
    }
209
210
    /**
211
     * @param int (optional) $orderID
212
     *
213
     * @return bool
214
     */
215
    public static function get_needs_recalculating($orderID = 0)
216
    {
217
        return isset(self::$_needs_recalculating[$orderID]) ? self::$_needs_recalculating[$orderID] : false;
218
    }
219
220
    /**
221
     * Total Items : total items in cart
222
     * We start with -1 to easily identify if it has been run before.
223
     *
224
     * @var int
225
     */
226
    protected $totalItems = null;
227
228
    /**
229
     * Total Items : total items in cart
230
     * We start with -1 to easily identify if it has been run before.
231
     *
232
     * @var float
233
     */
234
    protected $totalItemsTimesQuantity = null;
235
236
    /**
237
     * Returns a set of modifier forms for use in the checkout order form,
238
     * Controller is optional, because the orderForm has its own default controller.
239
     *
240
     * This method only returns the Forms that should be included outside
241
     * the editable table... Forms within it can be called
242
     * from through the modifier itself.
243
     *
244
     * @param Controller $optionalController
245
     * @param Validator  $optionalValidator
246
     *
247
     * @return ArrayList (ModifierForms) | Null
248
     **/
249
    public function getModifierForms(Controller $optionalController = null, Validator $optionalValidator = null)
250
    {
251
        $arrayList = new ArrayList();
252
        $modifiers = $this->Modifiers();
253
        if ($modifiers->count()) {
254
            foreach ($modifiers as $modifier) {
255
                if ($modifier->ShowForm()) {
256
                    if ($form = $modifier->getModifierForm($optionalController, $optionalValidator)) {
257
                        $form->ShowFormInEditableOrderTable = $modifier->ShowFormInEditableOrderTable();
258
                        $form->ShowFormOutsideEditableOrderTable = $modifier->ShowFormOutsideEditableOrderTable();
259
                        $form->ModifierName = $modifier->ClassName;
260
                        $arrayList->push($form);
261
                    }
262
                }
263
            }
264
        }
265
        if ($arrayList->count()) {
266
            return $arrayList;
267
        } else {
268
            return;
269
        }
270
    }
271
272
    /**
273
     * This function returns the OrderSteps.
274
     *
275
     * @return ArrayList (OrderSteps)
276
     **/
277
    public static function get_order_status_options()
278
    {
279
        return OrderStep::get();
280
    }
281
282
    /**
283
     * Like the standard byID, but it checks whether we are allowed to view the order.
284
     *
285
     * @return: Order | Null
286
     **/
287
    public static function get_by_id_if_can_view($id)
288
    {
289
        $order = Order::get()->byID($id);
290
        if ($order && $order->canView()) {
291
            if ($order->IsSubmitted()) {
292
                // LITTLE HACK TO MAKE SURE WE SHOW THE LATEST INFORMATION!
293
                $order->tryToFinaliseOrder();
294
            }
295
296
            return $order;
297
        }
298
299
        return;
300
    }
301
302
    /**
303
     * returns a Datalist with the submitted order log included
304
     * this allows you to sort the orders by their submit dates.
305
     * You can retrieve this list and then add more to it (e.g. additional filters, additional joins, etc...).
306
     *
307
     * @param bool $onlySubmittedOrders - only include Orders that have already been submitted.
308
     * @param bool $includeCancelledOrders - only include Orders that have already been submitted.
309
     *
310
     * @return DataList (Orders)
311
     */
312
    public static function get_datalist_of_orders_with_submit_record($onlySubmittedOrders = true, $includeCancelledOrders = false)
313
    {
314
        if ($onlySubmittedOrders) {
315
            $submittedOrderStatusLogClassName = EcommerceConfig::get('OrderStatusLog', 'order_status_log_class_used_for_submitting_order');
316
            $list = Order::get()
317
                ->LeftJoin('OrderStatusLog', '"Order"."ID" = "OrderStatusLog"."OrderID"')
318
                ->LeftJoin($submittedOrderStatusLogClassName, '"OrderStatusLog"."ID" = "'.$submittedOrderStatusLogClassName.'"."ID"')
319
                ->Sort('OrderStatusLog.Created', 'ASC');
320
            $where = ' ("OrderStatusLog"."ClassName" = \''.$submittedOrderStatusLogClassName.'\') ';
321
        } else {
322
            $list = Order::get();
323
            $where = ' ("StatusID" > 0) ';
324
        }
325
        if ($includeCancelledOrders) {
326
            //do nothing...
327
        } else {
328
            $where .= ' AND ("CancelledByID" = 0 OR "CancelledByID" IS NULL)';
329
        }
330
        $list = $list->where($where);
331
332
        return $list;
333
    }
334
335
    /*******************************************************
336
       * 1. CMS STUFF
337
    *******************************************************/
338
339
    /**
340
     * fields that we remove from the parent::getCMSFields object set.
341
     *
342
     * @var array
343
     */
344
    protected $fieldsAndTabsToBeRemoved = array(
345
        'MemberID',
346
        'Attributes',
347
        'SessionID',
348
        'Emails',
349
        'BillingAddressID',
350
        'ShippingAddressID',
351
        'UseShippingAddress',
352
        'OrderStatusLogs',
353
        'Payments',
354
        'OrderDate',
355
        'ExchangeRate',
356
        'CurrencyUsedID',
357
        'StatusID',
358
        'Currency',
359
    );
360
361
    /**
362
     * STANDARD SILVERSTRIPE STUFF.
363
     **/
364
    private static $summary_fields = array(
365
        'Title' => 'Title',
366
        'Status.Title' => 'Next Step',
367
        'Member.Surname' => 'Name',
368
        'Member.Email' => 'Email',
369
        'TotalAsMoney.Nice' => 'Total',
370
        'TotalItemsTimesQuantity' => 'Units',
371
        'IsPaidNice' => 'Paid'
372
    );
373
374
    /**
375
     * STANDARD SILVERSTRIPE STUFF.
376
     *
377
     * @todo: how to translate this?
378
     **/
379
    private static $searchable_fields = array(
380
        'ID' => array(
381
            'field' => 'NumericField',
382
            'title' => 'Order Number',
383
        ),
384
        'MemberID' => array(
385
            'field' => 'TextField',
386
            'filter' => 'OrderFilters_MemberAndAddress',
387
            'title' => 'Customer Details',
388
        ),
389
        'Created' => array(
390
            'field' => 'TextField',
391
            'filter' => 'OrderFilters_AroundDateFilter',
392
            'title' => 'Date (e.g. Today, 1 jan 2007, or last week)',
393
        ),
394
        //make sure to keep the items below, otherwise they do not show in form
395
        'StatusID' => array(
396
            'filter' => 'OrderFilters_MultiOptionsetStatusIDFilter',
397
        ),
398
        'CancelledByID' => array(
399
            'filter' => 'OrderFilters_HasBeenCancelled',
400
            'title' => 'Cancelled by ...',
401
        ),
402
    );
403
404
    /**
405
     * Determine which properties on the DataObject are
406
     * searchable, and map them to their default {@link FormField}
407
     * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
408
     *
409
     * Some additional logic is included for switching field labels, based on
410
     * how generic or specific the field type is.
411
     *
412
     * Used by {@link SearchContext}.
413
     *
414
     * @param array $_params
415
     *                       'fieldClasses': Associative array of field names as keys and FormField classes as values
416
     *                       'restrictFields': Numeric array of a field name whitelist
417
     *
418
     * @return FieldList
419
     */
420
    public function scaffoldSearchFields($_params = null)
421
    {
422
        $fieldList = parent::scaffoldSearchFields($_params);
423
424
        //for sales to action only show relevant ones ...
425
        if (Controller::curr() && Controller::curr()->class === 'SalesAdmin') {
426
            $statusOptions = OrderStep::admin_manageable_steps();
427
        } else {
428
            $statusOptions = OrderStep::get();
429
        }
430
        if ($statusOptions && $statusOptions->count()) {
431
            $createdOrderStatusID = 0;
432
            $preSelected = array();
433
            $createdOrderStatus = $statusOptions->First();
434
            if ($createdOrderStatus) {
435
                $createdOrderStatusID = $createdOrderStatus->ID;
436
            }
437
            $arrayOfStatusOptions = clone $statusOptions->map('ID', 'Title');
438
            $arrayOfStatusOptionsFinal = array();
439
            if (count($arrayOfStatusOptions)) {
440
                foreach ($arrayOfStatusOptions as $key => $value) {
441
                    if (isset($_GET['q']['StatusID'][$key])) {
442
                        $preSelected[$key] = $key;
443
                    }
444
                    $count = Order::get()
445
                        ->Filter(array('StatusID' => intval($key)))
446
                        ->count();
447
                    if ($count < 1) {
448
                        //do nothing
449
                    } else {
450
                        $arrayOfStatusOptionsFinal[$key] = $value." ($count)";
451
                    }
452
                }
453
            }
454
            $statusField = new CheckboxSetField(
455
                'StatusID',
456
                Injector::inst()->get('OrderStep')->i18n_singular_name(),
457
                $arrayOfStatusOptionsFinal,
458
                $preSelected
459
            );
460
            $fieldList->push($statusField);
461
        }
462
        $fieldList->push(new DropdownField('CancelledByID', 'Cancelled', array(-1 => '(Any)', 1 => 'yes', 0 => 'no')));
463
464
        //allow changes
465
        $this->extend('scaffoldSearchFields', $fieldList, $_params);
466
467
        return $fieldList;
468
    }
469
470
    /**
471
     * link to edit the record.
472
     *
473
     * @param string | Null $action - e.g. edit
474
     *
475
     * @return string
476
     */
477
    public function CMSEditLink($action = null)
478
    {
479
        return CMSEditLinkAPI::find_edit_link_for_object($this, $action, 'sales-advanced');
480
    }
481
482
    /**
483
     * STANDARD SILVERSTRIPE STUFF
484
     * broken up into submitted and not (yet) submitted.
485
     **/
486
    public function getCMSFields()
487
    {
488
        $fields = $this->scaffoldFormFields(array(
489
            // Don't allow has_many/many_many relationship editing before the record is first saved
490
            'includeRelations' => false,
491
            'tabbed' => true,
492
            'ajaxSafe' => true
493
        ));
494
        $fields->insertBefore(
495
            Tab::create(
496
                'Next',
497
                _t('Order.NEXT_TAB', 'Action')
498
            ),
499
            'Main'
500
        );
501
        $fields->addFieldsToTab(
502
            'Root',
503
            array(
504
                Tab::create(
505
                    "Items",
506
                    _t('Order.ITEMS_TAB', 'Items')
507
                ),
508
                Tab::create(
509
                    "Extras",
510
                    _t('Order.MODIFIERS_TAB', 'Adjustments')
511
                ),
512
                Tab::create(
513
                    'Emails',
514
                    _t('Order.EMAILS_TAB', 'Emails')
515
                ),
516
                Tab::create(
517
                    'Payments',
518
                    _t('Order.PAYMENTS_TAB', 'Payment')
519
                ),
520
                Tab::create(
521
                    'Account',
522
                    _t('Order.ACCOUNT_TAB', 'Account')
523
                ),
524
                Tab::create(
525
                    'Currency',
526
                    _t('Order.CURRENCY_TAB', 'Currency')
527
                ),
528
                Tab::create(
529
                    'Addresses',
530
                    _t('Order.ADDRESSES_TAB', 'Addresses')
531
                ),
532
                Tab::create(
533
                    'Log',
534
                    _t('Order.LOG_TAB', 'Notes')
535
                ),
536
                Tab::create(
537
                    'Cancellations',
538
                    _t('Order.CANCELLATION_TAB', 'Cancel')
539
                ),
540
            )
541
        );
542
        //as we are no longer using the parent:;getCMSFields
543
        // we had to add the updateCMSFields hook.
544
        $this->extend('updateCMSFields', $fields);
545
        $currentMember = Member::currentUser();
546
        if (!$this->exists() || !$this->StatusID) {
547
            $firstStep = DataObject::get_one('OrderStep');
548
            $this->StatusID = $firstStep->ID;
549
            $this->write();
550
        }
551
        $submitted = $this->IsSubmitted() ? true : false;
552
        if ($submitted) {
553
            //TODO
554
            //Having trouble here, as when you submit the form (for example, a payment confirmation)
555
            //as the step moves forward, meaning the fields generated are incorrect, causing an error
556
            //"I can't handle sub-URLs of a Form object." generated by the RequestHandler.
557
            //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
558
            //Or something similar.
559
            //why not check if the URL == $this->CMSEditLink()
560
            //and only tryToFinaliseOrder if this is true....
561
            if ($_SERVER['REQUEST_URI'] == $this->CMSEditLink() || $_SERVER['REQUEST_URI'] == $this->CMSEditLink('edit')) {
562
                $this->tryToFinaliseOrder();
563
            }
564
        } else {
565
            $this->init(true);
566
            $this->calculateOrderAttributes(true);
567
            Session::set('EcommerceOrderGETCMSHack', $this->ID);
568
        }
569
        if ($submitted) {
570
            $this->fieldsAndTabsToBeRemoved[] = 'CustomerOrderNote';
571
        } else {
572
            $this->fieldsAndTabsToBeRemoved[] = 'Emails';
573
        }
574
        foreach ($this->fieldsAndTabsToBeRemoved as $field) {
575
            $fields->removeByName($field);
576
        }
577
        $orderSummaryConfig = GridFieldConfig_Base::create();
578
        $orderSummaryConfig->removeComponentsByType('GridFieldToolbarHeader');
579
        // $orderSummaryConfig->removeComponentsByType('GridFieldSortableHeader');
580
        $orderSummaryConfig->removeComponentsByType('GridFieldFilterHeader');
581
        $orderSummaryConfig->removeComponentsByType('GridFieldPageCount');
582
        $orderSummaryConfig->removeComponentsByType('GridFieldPaginator');
583
        $nextFieldArray = array(
584
            LiteralField::create('CssFix', '<style>#Root_Next h2.form-control {padding: 0!important; margin: 0!important; padding-top: 4em!important;}</style>'),
585
            HeaderField::create('MyOrderStepHeader', _t('Order.CURRENT_STATUS', '1. Current Status')),
586
            $this->OrderStepField(),
587
            GridField::create(
588
                'OrderSummary',
589
                _t('Order.CURRENT_STATUS', 'Summary'),
590
                ArrayList::create(array($this)),
591
                $orderSummaryConfig
592
            )
593
        );
594
        $keyNotes = OrderStatusLog::get()->filter(
595
            array(
596
                'OrderID' => $this->ID,
597
                'ClassName' => 'OrderStatusLog'
598
            )
599
        );
600
        if ($keyNotes->count()) {
601
            $notesSummaryConfig = GridFieldConfig_RecordViewer::create();
602
            $notesSummaryConfig->removeComponentsByType('GridFieldToolbarHeader');
603
            $notesSummaryConfig->removeComponentsByType('GridFieldFilterHeader');
604
            // $orderSummaryConfig->removeComponentsByType('GridFieldSortableHeader');
605
            $notesSummaryConfig->removeComponentsByType('GridFieldPageCount');
606
            $notesSummaryConfig->removeComponentsByType('GridFieldPaginator');
607
            $nextFieldArray = array_merge(
608
                $nextFieldArray,
609
                array(
610
                    HeaderField::create('KeyNotesHeader', _t('Order.KEY_NOTES_HEADER', 'Key Notes')),
611
                    GridField::create(
612
                        'OrderStatusLogSummary',
613
                        _t('Order.CURRENT_KEY_NOTES', 'Key Notes'),
614
                        $keyNotes,
615
                        $notesSummaryConfig
616
                    )
617
                )
618
            );
619
        }
620
        $nextFieldArray = array_merge(
621
            $nextFieldArray,
622
            array(
623
                EcommerceCMSButtonField::create(
624
                    'AddNoteButton',
625
                    '/admin/sales/Order/EditForm/field/Order/item/' . $this->ID . '/ItemEditForm/field/OrderStatusLog/item/new',
626
                    _t('Order.ADD_NOTE', 'Add Note')
627
                )
628
            )
629
        );
630
        $nextFieldArray = array_merge(
631
            $nextFieldArray,
632
            array(
633
634
            )
635
        );
636
637
        //is the member is a shop admin they can always view it
638
639
        if (EcommerceRole::current_member_can_process_orders(Member::currentUser())) {
640
            $lastStep = OrderStep::last_order_step();
641
            if ($this->StatusID != $lastStep->ID) {
642
                $queueObjectSingleton = Injector::inst()->get('OrderProcessQueue');
643
                if ($myQueueObject = $queueObjectSingleton->getQueueObject($this)) {
644
                    $myQueueObjectField = GridField::create(
645
                        'MyQueueObjectField',
646
                        _t('Order.QUEUE_DETAILS', 'Queue Details'),
647
                        $this->OrderProcessQueue(),
648
                        GridFieldConfig_RecordEditor::create()
649
                    );
650
                } else {
651
                    $myQueueObjectField = LiteralField::create('MyQueueObjectField', '<p>'._t('Order.NOT_QUEUED', 'This order is not queued for future processing.').'</p>');
652
                }
653
                $nextFieldArray = array_merge(
654
                    $nextFieldArray,
655
                    array(
656
                        HeaderField::create('OrderStepNextStepHeader', _t('Order.ACTION_NEXT_STEP', '2. Action Next Step')),
657
                        $myQueueObjectField,
658
                        HeaderField::create('ActionNextStepManually', _t('Order.MANUAL_STATUS_CHANGE', '3. Move Order Along')),
659
                        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>'),
660
                        EcommerceCMSButtonField::create(
661
                            'StatusIDExplanation',
662
                            $this->CMSEditLink(),
663
                            _t('Order.REFRESH', 'refresh now')
664
                        )
665
                    )
666
                );
667
            }
668
        }
669
        $fields->addFieldsToTab(
670
            'Root.Next',
671
            $nextFieldArray
672
        );
673
674
        $this->MyStep()->addOrderStepFields($fields, $this);
675
676
        if ($submitted) {
677
            $permaLinkLabel = _t('Order.PERMANENT_LINK', 'Customer Link');
678
            $html = '<p>'.$permaLinkLabel.': <a href="'.$this->getRetrieveLink().'">'.$this->getRetrieveLink().'</a></p>';
679
            $shareLinkLabel = _t('Order.SHARE_LINK', 'Share Link');
680
            $html .= '<p>'.$shareLinkLabel.': <a href="'.$this->getShareLink().'">'.$this->getShareLink().'</a></p>';
681
            $feedbackLinkLabel = _t('Order.FEEDBACK_LINK', 'Feedback Link');
682
            $html .= '<p>'.$feedbackLinkLabel.': <a href="'.$this->getFeedbackLink().'">'.$this->getFeedbackLink().'</a></p>';
683
            $js = "window.open(this.href, 'payment', 'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1,width=800,height=600'); return false;";
684
            $link = $this->getPrintLink();
685
            $label = _t('Order.PRINT_INVOICE', 'invoice');
686
            $linkHTML = '<a href="'.$link.'" onclick="'.$js.'">'.$label.'</a>';
687
            $linkHTML .= ' | ';
688
            $link = $this->getPackingSlipLink();
689
            $label = _t('Order.PRINT_PACKING_SLIP', 'packing slip');
690
            $labelPrint = _t('Order.PRINT', 'Print');
691
            $linkHTML .= '<a href="'.$link.'" onclick="'.$js.'">'.$label.'</a>';
692
            $html .= '<h3>';
693
            $html .= $labelPrint.': '.$linkHTML;
694
            $html .= '</h3>';
695
696
            $fields->addFieldToTab(
697
                'Root.Main',
698
                LiteralField::create('getPrintLinkANDgetPackingSlipLink', $html)
699
            );
700
701
            //add order here as well.
702
            $fields->addFieldToTab(
703
                'Root.Main',
704
                new LiteralField(
705
                    'MainDetails',
706
                    '<iframe src="'.$this->getPrintLink().'" width="100%" height="2500" style="border: 5px solid #2e7ead; border-radius: 2px;"></iframe>'
707
                )
708
            );
709
            $fields->addFieldsToTab(
710
                'Root.Items',
711
                array(
712
                    GridField::create(
713
                        'Items_Sold',
714
                        'Items Sold',
715
                        $this->Items(),
716
                        new GridFieldConfig_RecordViewer
717
                    )
718
                )
719
            );
720
            $fields->addFieldsToTab(
721
                'Root.Extras',
722
                array(
723
                    GridField::create(
724
                        'Modifications',
725
                        'Price (and other) adjustments',
726
                        $this->Modifiers(),
727
                        new GridFieldConfig_RecordViewer
728
                    )
729
                )
730
            );
731
            $fields->addFieldsToTab(
732
                'Root.Emails',
733
                array(
734
                    $this->getEmailsTableField()
735
                )
736
            );
737
            $fields->addFieldsToTab(
738
                'Root.Payments',
739
                array(
740
                    $this->getPaymentsField(),
741
                    new ReadOnlyField('TotalPaidNice', _t('Order.TOTALPAID', 'Total Paid'), $this->TotalPaidAsCurrencyObject()->Nice()),
742
                    new ReadOnlyField('TotalOutstandingNice', _t('Order.TOTALOUTSTANDING', 'Total Outstanding'), $this->getTotalOutstandingAsMoney()->Nice())
743
                )
744
            );
745
            if ($this->canPay()) {
746
                $link = EcommercePaymentController::make_payment_link($this->ID);
747
                $js = "window.open(this.href, 'payment', 'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1,width=800,height=600'); return false;";
748
                $header = _t('Order.MAKEPAYMENT', 'make payment');
749
                $label = _t('Order.MAKEADDITIONALPAYMENTNOW', 'make additional payment now');
750
                $linkHTML = '<a href="'.$link.'" onclick="'.$js.'">'.$label.'</a>';
751
                $fields->addFieldToTab('Root.Payments', new HeaderField('MakeAdditionalPaymentHeader', $header, 3));
752
                $fields->addFieldToTab('Root.Payments', new LiteralField('MakeAdditionalPayment', $linkHTML));
753
            }
754
            //member
755
            $member = $this->Member();
756
            if ($member && $member->exists()) {
757
                $fields->addFieldToTab('Root.Account', new LiteralField('MemberDetails', $member->getEcommerceFieldsForCMS()));
758
            } else {
759
                $fields->addFieldToTab('Root.Account', new LiteralField(
760
                    'MemberDetails',
761
                    '<p>'._t('Order.NO_ACCOUNT', 'There is no --- account --- associated with this order').'</p>'
762
                ));
763
            }
764
            if ($this->getFeedbackLink()) {
765
                $fields->addFieldToTab(
766
                    'Root.Account',
767
                    GridField::create(
768
                        'OrderFeedback',
769
                        Injector::inst()->get('OrderFeedback')->singular_name(),
770
                        OrderFeedback::get()->filter(array('OrderID' => $this->ID)),
771
                        GridFieldConfig_RecordViewer::create()
772
                    )
773
                );
774
            }
775
            $cancelledField = $fields->dataFieldByName('CancelledByID');
776
            $fields->removeByName('CancelledByID');
777
            $shopAdminAndCurrentCustomerArray = EcommerceRole::list_of_admins(true);
778
            if ($member && $member->exists()) {
779
                $shopAdminAndCurrentCustomerArray[$member->ID] = $member->getName();
780
            }
781
            if ($this->CancelledByID) {
782
                if ($cancellingMember = $this->CancelledBy()) {
783
                    $shopAdminAndCurrentCustomerArray[$this->CancelledByID] = $cancellingMember->getName();
784
                }
785
            }
786
            if ($this->canCancel()) {
787
                $fields->addFieldsToTab(
788
                    'Root.Cancellations',
789
                    array(
790
                        DropdownField::create(
791
                            'CancelledByID',
792
                            $cancelledField->Title(),
793
                            $shopAdminAndCurrentCustomerArray
794
                        )
795
                    )
796
                );
797
            } else {
798
                $cancelledBy = isset($shopAdminAndCurrentCustomerArray[$this->CancelledByID]) && $this->CancelledByID ? $shopAdminAndCurrentCustomerArray[$this->CancelledByID] : _t('Order.NOT_CANCELLED', 'not cancelled');
0 ignored issues
show
The property CancelledByID does not exist on object<Order>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
799
                $fields->addFieldsToTab(
800
                    'Root.Cancellations',
801
                    ReadonlyField::create(
802
                        'CancelledByDisplay',
803
                        $cancelledField->Title(),
804
                        $cancelledBy
805
806
                    )
807
                );
808
            }
809
            $fields->addFieldToTab('Root.Log', $this->getOrderStatusLogsTableField_Archived());
810
            $submissionLog = $this->SubmissionLog();
811
            if ($submissionLog) {
812
                $fields->addFieldToTab(
813
                    'Root.Log',
814
                    ReadonlyField::create(
815
                        'SequentialOrderNumber',
816
                        _t('Order.SEQUENTIALORDERNUMBER', 'Consecutive order number'),
817
                        $submissionLog->SequentialOrderNumber
818
                    )->setRightTitle('e.g. 1,2,3,4,5...')
819
                );
820
            }
821
        } else {
822
            $linkText = _t(
823
                'Order.LOAD_THIS_ORDER',
824
                'load this order'
825
            );
826
            $message = _t(
827
                'Order.NOSUBMITTEDYET',
828
                '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 .',
829
                array('link' => '<a href="'.$this->getRetrieveLink().'" data-popup="true">'.$linkText.'</a>')
830
            );
831
            $fields->addFieldToTab('Root.Next', new LiteralField('MainDetails', '<p>'.$message.'</p>'));
832
            $fields->addFieldToTab('Root.Items', $this->getOrderItemsField());
833
            $fields->addFieldToTab('Root.Extras', $this->getModifierTableField());
834
835
            //MEMBER STUFF
836
            $specialOptionsArray = array();
837
            if ($this->MemberID) {
838
                $specialOptionsArray[0] = _t('Order.SELECTCUSTOMER', '--- Remover Customer ---');
839
                $specialOptionsArray[$this->MemberID] = _t('Order.LEAVEWITHCURRENTCUSTOMER', '- Leave with current customer: ').$this->Member()->getTitle();
840
            } elseif ($currentMember) {
841
                $specialOptionsArray[0] = _t('Order.SELECTCUSTOMER', '--- Select Customers ---');
842
                $currentMemberID = $currentMember->ID;
843
                $specialOptionsArray[$currentMemberID] = _t('Order.ASSIGNTHISORDERTOME', '- Assign this order to me: ').$currentMember->getTitle();
844
            }
845
            //MEMBER FIELD!!!!!!!
846
            $memberArray = $specialOptionsArray + EcommerceRole::list_of_customers(true);
847
            $fields->addFieldToTab('Root.Next', new DropdownField('MemberID', _t('Order.SELECTCUSTOMER', 'Select Customer'), $memberArray), 'CustomerOrderNote');
848
            $memberArray = null;
849
        }
850
        $fields->addFieldToTab('Root.Addresses', new HeaderField('BillingAddressHeader', _t('Order.BILLINGADDRESS', 'Billing Address')));
851
852
        $fields->addFieldToTab('Root.Addresses', $this->getBillingAddressField());
853
854
        if (EcommerceConfig::get('OrderAddress', 'use_separate_shipping_address')) {
855
            $fields->addFieldToTab('Root.Addresses', new HeaderField('ShippingAddressHeader', _t('Order.SHIPPINGADDRESS', 'Shipping Address')));
856
            $fields->addFieldToTab('Root.Addresses', new CheckboxField('UseShippingAddress', _t('Order.USESEPERATEADDRESS', 'Use separate shipping address?')));
857
            if ($this->UseShippingAddress) {
858
                $fields->addFieldToTab('Root.Addresses', $this->getShippingAddressField());
859
            }
860
        }
861
        $currencies = EcommerceCurrency::get_list();
862
        if ($currencies && $currencies->count()) {
863
            $currencies = $currencies->map()->toArray();
864
            $fields->addFieldToTab('Root.Currency', new ReadOnlyField('ExchangeRate ', _t('Order.EXCHANGERATE', 'Exchange Rate'), $this->ExchangeRate));
865
            $fields->addFieldToTab('Root.Currency', $currencyField = new DropdownField('CurrencyUsedID', _t('Order.CurrencyUsed', 'Currency Used'), $currencies));
866
            if ($this->IsSubmitted()) {
867
                $fields->replaceField('CurrencyUsedID', $fields->dataFieldByName('CurrencyUsedID')->performReadonlyTransformation());
868
            }
869
        } else {
870
            $fields->addFieldToTab('Root.Currency', new LiteralField('CurrencyInfo', '<p>You can not change currencies, because no currencies have been created.</p>'));
871
            $fields->replaceField('CurrencyUsedID', $fields->dataFieldByName('CurrencyUsedID')->performReadonlyTransformation());
872
        }
873
        $fields->addFieldToTab('Root.Log', new ReadonlyField('Created', _t('Root.CREATED', 'Created')));
874
        $fields->addFieldToTab('Root.Log', new ReadonlyField('LastEdited', _t('Root.LASTEDITED', 'Last saved')));
875
        $this->extend('updateCMSFields', $fields);
876
877
        return $fields;
878
    }
879
880
    /**
881
     * Field to add and edit Order Items.
882
     *
883
     * @return GridField
884
     */
885
    protected function getOrderItemsField()
886
    {
887
        $gridFieldConfig = GridFieldConfigForOrderItems::create();
888
        $source = $this->OrderItems();
889
890
        return new GridField('OrderItems', _t('OrderItems.PLURALNAME', 'Order Items'), $source, $gridFieldConfig);
891
    }
892
893
    /**
894
     * Field to add and edit Modifiers.
895
     *
896
     * @return GridField
897
     */
898
    public function getModifierTableField()
899
    {
900
        $gridFieldConfig = GridFieldConfigForOrderItems::create();
901
        $source = $this->Modifiers();
902
903
        return new GridField('OrderModifiers', _t('OrderItems.PLURALNAME', 'Order Items'), $source, $gridFieldConfig);
904
    }
905
906
    /**
907
     *@return GridField
908
     **/
909
    protected function getBillingAddressField()
910
    {
911
        $this->CreateOrReturnExistingAddress('BillingAddress');
912
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
913
            new GridFieldToolbarHeader(),
914
            new GridFieldSortableHeader(),
915
            new GridFieldDataColumns(),
916
            new GridFieldPaginator(10),
917
            new GridFieldEditButton(),
918
            new GridFieldDetailForm()
919
        );
920
        //$source = $this->BillingAddress();
921
        $source = BillingAddress::get()->filter(array('OrderID' => $this->ID));
922
923
        return new GridField('BillingAddress', _t('BillingAddress.SINGULARNAME', 'Billing Address'), $source, $gridFieldConfig);
924
    }
925
926
    /**
927
     *@return GridField
928
     **/
929
    protected function getShippingAddressField()
930
    {
931
        $this->CreateOrReturnExistingAddress('ShippingAddress');
932
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
933
            new GridFieldToolbarHeader(),
934
            new GridFieldSortableHeader(),
935
            new GridFieldDataColumns(),
936
            new GridFieldPaginator(10),
937
            new GridFieldEditButton(),
938
            new GridFieldDetailForm()
939
        );
940
        //$source = $this->ShippingAddress();
941
        $source = ShippingAddress::get()->filter(array('OrderID' => $this->ID));
942
943
        return new GridField('ShippingAddress', _t('BillingAddress.SINGULARNAME', 'Shipping Address'), $source, $gridFieldConfig);
944
    }
945
946
    /**
947
     * Needs to be public because the OrderStep::getCMSFIelds accesses it.
948
     *
949
     * @param string    $sourceClass
950
     * @param string    $title
951
     *
952
     * @return GridField
953
     **/
954
    public function getOrderStatusLogsTableField(
955
        $sourceClass = 'OrderStatusLog',
956
        $title = ''
957
    ) {
958
        $gridFieldConfig = GridFieldConfig_RecordViewer::create()->addComponents(
959
            new GridFieldAddNewButton('toolbar-header-right'),
960
            new GridFieldDetailForm()
961
        );
962
        $title ? $title : $title = _t('OrderStatusLog.PLURALNAME', 'Order Status Logs');
963
        $source = $this->OrderStatusLogs()->Filter(array('ClassName' => $sourceClass));
964
        $gf = new GridField($sourceClass, $title, $source, $gridFieldConfig);
965
        $gf->setModelClass($sourceClass);
966
967
        return $gf;
968
    }
969
970
    /**
971
     * Needs to be public because the OrderStep::getCMSFIelds accesses it.
972
     *
973
     * @param string    $sourceClass
974
     * @param string    $title
975
     *
976
     * @return GridField
977
     **/
978
    public function getOrderStatusLogsTableFieldEditable(
979
        $sourceClass = 'OrderStatusLog',
980
        $title = ''
981
    ) {
982
        $gf = $this->getOrderStatusLogsTableField($sourceClass, $title);
983
        $gf->getConfig()->addComponents(
984
            new GridFieldEditButton()
985
        );
986
        return $gf;
987
    }
988
989
    /**
990
     * @param string    $sourceClass
991
     * @param string    $title
992
     * @param FieldList $fieldList          (Optional)
993
     * @param FieldList $detailedFormFields (Optional)
994
     *
995
     * @return GridField
996
     **/
997
    protected function getOrderStatusLogsTableField_Archived(
998
        $sourceClass = 'OrderStatusLog',
999
        $title = '',
1000
        FieldList $fieldList = null,
1001
        FieldList $detailedFormFields = null
1002
    ) {
1003
        $title ? $title : $title = _t('OrderLog.PLURALNAME', 'Order Log');
1004
        $source = $this->OrderStatusLogs();
0 ignored issues
show
The method OrderStatusLogs() does not exist on Order. Did you maybe mean getOrderStatusLogsTableField()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

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