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

code/model/Order.php (27 issues)

Upgrade to new PHP Analysis Engine

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

1
<?php
2
3
/**
4
 * @description:
5
 * The order class is a databound object for handling Orders within SilverStripe.
6
 * Note that it works closely with the ShoppingCart class, which accompanies the Order
7
 * until it has been paid for / confirmed by the user.
8
 *
9
 *
10
 * CONTENTS:
11
 * ----------------------------------------------
12
 * 1. CMS STUFF
13
 * 2. MAIN TRANSITION FUNCTIONS
14
 * 3. STATUS RELATED FUNCTIONS / SHORTCUTS
15
 * 4. LINKING ORDER WITH MEMBER AND ADDRESS
16
 * 5. CUSTOMER COMMUNICATION
17
 * 6. ITEM MANAGEMENT
18
 * 7. CRUD METHODS (e.g. canView, canEdit, canDelete, etc...)
19
 * 8. GET METHODS (e.g. Total, SubTotal, Title, etc...)
20
 * 9. TEMPLATE RELATED STUFF
21
 * 10. STANDARD SS METHODS (requireDefaultRecords, onBeforeDelete, etc...)
22
 * 11. DEBUG
23
 *
24
 * @authors: Nicolaas [at] Sunny Side Up .co.nz
25
 * @package: ecommerce
26
 * @sub-package: model
27
 * @inspiration: Silverstripe Ltd, Jeremy
28
 *
29
 * NOTE: This is the SQL for selecting orders in sequence of
30
 **/
31
class Order extends DataObject implements EditableEcommerceObject
32
{
33
    /**
34
     * API Control.
35
     *
36
     * @var array
37
     */
38
    private static $api_access = array(
39
        'view' => array(
40
            'OrderEmail',
41
            'EmailLink',
42
            'PrintLink',
43
            'RetrieveLink',
44
            'ShareLink',
45
            'FeedbackLink',
46
            'Title',
47
            'Total',
48
            'SubTotal',
49
            'TotalPaid',
50
            'TotalOutstanding',
51
            'ExchangeRate',
52
            'CurrencyUsed',
53
            'TotalItems',
54
            'TotalItemsTimesQuantity',
55
            'IsCancelled',
56
            'Country',
57
            'FullNameCountry',
58
            'IsSubmitted',
59
            'CustomerStatus',
60
            'CanHaveShippingAddress',
61
            'CancelledBy',
62
            'CurrencyUsed',
63
            'BillingAddress',
64
            'UseShippingAddress',
65
            'ShippingAddress',
66
            'Status',
67
            'Attributes',
68
            'OrderStatusLogs',
69
            'MemberID',
70
        ),
71
    );
72
73
    /**
74
     * standard SS variable.
75
     *
76
     * @var array
77
     */
78
    private static $db = array(
79
        'SessionID' => 'Varchar(32)', //so that in the future we can link sessions with Orders.... One session can have several orders, but an order can onnly have one session
80
        'UseShippingAddress' => 'Boolean',
81
        'CustomerOrderNote' => 'Text',
82
        'ExchangeRate' => 'Double',
83
        //'TotalItems_Saved' => 'Double',
84
        //'TotalItemsTimesQuantity_Saved' => 'Double'
85
    );
86
87
    private static $has_one = array(
88
        'Member' => 'Member',
89
        'BillingAddress' => 'BillingAddress',
90
        'ShippingAddress' => 'ShippingAddress',
91
        'Status' => 'OrderStep',
92
        'CancelledBy' => 'Member',
93
        'CurrencyUsed' => 'EcommerceCurrency',
94
    );
95
96
    /**
97
     * standard SS variable.
98
     *
99
     * @var array
100
     */
101
    private static $has_many = array(
102
        'Attributes' => 'OrderAttribute',
103
        'OrderStatusLogs' => 'OrderStatusLog',
104
        'Payments' => 'EcommercePayment',
105
        'Emails' => 'OrderEmailRecord',
106
        'OrderProcessQueue' => 'OrderProcessQueue' //there is usually only one.
107
    );
108
109
    /**
110
     * standard SS variable.
111
     *
112
     * @var array
113
     */
114
    private static $indexes = array(
115
        'SessionID' => true,
116
    );
117
118
    /**
119
     * standard SS variable.
120
     *
121
     * @var string
122
     */
123
    private static $default_sort = '"LastEdited" DESC';
124
125
    /**
126
     * standard SS variable.
127
     *
128
     * @var array
129
     */
130
    private static $casting = array(
131
        'OrderEmail' => 'Varchar',
132
        'EmailLink' => 'Varchar',
133
        'PrintLink' => 'Varchar',
134
        'ShareLink' => 'Varchar',
135
        'FeedbackLink' => 'Varchar',
136
        'RetrieveLink' => 'Varchar',
137
        'Title' => 'Varchar',
138
        'Total' => 'Currency',
139
        'TotalAsMoney' => 'Money',
140
        'SubTotal' => 'Currency',
141
        'SubTotalAsMoney' => 'Money',
142
        'TotalPaid' => 'Currency',
143
        'TotalPaidAsMoney' => 'Money',
144
        'TotalOutstanding' => 'Currency',
145
        'TotalOutstandingAsMoney' => 'Money',
146
        'HasAlternativeCurrency' => 'Boolean',
147
        'TotalItems' => 'Double',
148
        'TotalItemsTimesQuantity' => 'Double',
149
        'IsCancelled' => 'Boolean',
150
        'IsPaidNice' => 'Boolean',
151
        'Country' => 'Varchar(3)', //This is the applicable country for the order - for tax purposes, etc....
152
        'FullNameCountry' => 'Varchar',
153
        'IsSubmitted' => 'Boolean',
154
        'CustomerStatus' => 'Varchar',
155
        'CanHaveShippingAddress' => 'Boolean',
156
    );
157
158
    /**
159
     * standard SS variable.
160
     *
161
     * @var string
162
     */
163
    private static $singular_name = 'Order';
164
    public function i18n_singular_name()
165
    {
166
        return _t('Order.ORDER', 'Order');
167
    }
168
169
    /**
170
     * standard SS variable.
171
     *
172
     * @var string
173
     */
174
    private static $plural_name = 'Orders';
175
    public function i18n_plural_name()
176
    {
177
        return _t('Order.ORDERS', 'Orders');
178
    }
179
180
    /**
181
     * Standard SS variable.
182
     *
183
     * @var string
184
     */
185
    private static $description = "A collection of items that together make up the 'Order'.  An order can be placed.";
186
187
    /**
188
     * Tells us if an order needs to be recalculated
189
     * can save one for each order...
190
     *
191
     * @var array
192
     */
193
    private static $_needs_recalculating = array();
194
195
    /**
196
     * @param bool (optional) $b
197
     * @param int (optional)  $orderID
198
     *
199
     * @return bool
200
     */
201
    public static function set_needs_recalculating($b = true, $orderID = 0)
202
    {
203
        self::$_needs_recalculating[$orderID] = $b;
204
    }
205
206
    /**
207
     * @param int (optional) $orderID
208
     *
209
     * @return bool
210
     */
211
    public static function get_needs_recalculating($orderID = 0)
212
    {
213
        return isset(self::$_needs_recalculating[$orderID]) ? self::$_needs_recalculating[$orderID] : false;
214
    }
215
216
    /**
217
     * Total Items : total items in cart
218
     * We start with -1 to easily identify if it has been run before.
219
     *
220
     * @var int
221
     */
222
    protected $totalItems = null;
223
224
    /**
225
     * Total Items : total items in cart
226
     * We start with -1 to easily identify if it has been run before.
227
     *
228
     * @var float
229
     */
230
    protected $totalItemsTimesQuantity = null;
231
232
    /**
233
     * Returns a set of modifier forms for use in the checkout order form,
234
     * Controller is optional, because the orderForm has its own default controller.
235
     *
236
     * This method only returns the Forms that should be included outside
237
     * the editable table... Forms within it can be called
238
     * from through the modifier itself.
239
     *
240
     * @param Controller $optionalController
241
     * @param Validator  $optionalValidator
242
     *
243
     * @return ArrayList (ModifierForms) | Null
244
     **/
245
    public function getModifierForms(Controller $optionalController = null, Validator $optionalValidator = null)
246
    {
247
        $arrayList = new ArrayList();
248
        $modifiers = $this->Modifiers();
249
        if ($modifiers->count()) {
250
            foreach ($modifiers as $modifier) {
251
                if ($modifier->ShowForm()) {
252
                    if ($form = $modifier->getModifierForm($optionalController, $optionalValidator)) {
253
                        $form->ShowFormInEditableOrderTable = $modifier->ShowFormInEditableOrderTable();
254
                        $form->ShowFormOutsideEditableOrderTable = $modifier->ShowFormOutsideEditableOrderTable();
255
                        $form->ModifierName = $modifier->ClassName;
256
                        $arrayList->push($form);
257
                    }
258
                }
259
            }
260
        }
261
        if ($arrayList->count()) {
262
            return $arrayList;
263
        } else {
264
            return;
265
        }
266
    }
267
268
    /**
269
     * This function returns the OrderSteps.
270
     *
271
     * @return ArrayList (OrderSteps)
272
     **/
273
    public static function get_order_status_options()
274
    {
275
        return OrderStep::get();
276
    }
277
278
    /**
279
     * Like the standard byID, but it checks whether we are allowed to view the order.
280
     *
281
     * @return: Order | Null
282
     **/
283
    public static function get_by_id_if_can_view($id)
284
    {
285
        $order = Order::get()->byID($id);
286
        if ($order && $order->canView()) {
287
            if ($order->IsSubmitted()) {
288
                // LITTLE HACK TO MAKE SURE WE SHOW THE LATEST INFORMATION!
289
                $order->tryToFinaliseOrder();
290
            }
291
292
            return $order;
293
        }
294
295
        return;
296
    }
297
298
    /**
299
     * returns a Datalist with the submitted order log included
300
     * this allows you to sort the orders by their submit dates.
301
     * You can retrieve this list and then add more to it (e.g. additional filters, additional joins, etc...).
302
     *
303
     * @param bool $onlySubmittedOrders - only include Orders that have already been submitted.
304
     * @param bool $includeCancelledOrders - only include Orders that have already been submitted.
305
     *
306
     * @return DataList (Orders)
307
     */
308
    public static function get_datalist_of_orders_with_submit_record($onlySubmittedOrders = true, $includeCancelledOrders = false)
309
    {
310
        if ($onlySubmittedOrders) {
311
            $submittedOrderStatusLogClassName = EcommerceConfig::get('OrderStatusLog', 'order_status_log_class_used_for_submitting_order');
312
            $list = Order::get()
313
                ->LeftJoin('OrderStatusLog', '"Order"."ID" = "OrderStatusLog"."OrderID"')
314
                ->LeftJoin($submittedOrderStatusLogClassName, '"OrderStatusLog"."ID" = "'.$submittedOrderStatusLogClassName.'"."ID"')
315
                ->Sort('OrderStatusLog.Created', 'ASC');
316
            $where = ' ("OrderStatusLog"."ClassName" = \''.$submittedOrderStatusLogClassName.'\') ';
317
        } else {
318
            $list = Order::get();
319
            $where = ' ("StatusID" > 0) ';
320
        }
321
        if ($includeCancelledOrders) {
322
            //do nothing...
323
        } else {
324
            $where .= ' AND ("CancelledByID" = 0 OR "CancelledByID" IS NULL)';
325
        }
326
        $list = $list->where($where);
327
328
        return $list;
329
    }
330
331
/*******************************************************
332
   * 1. CMS STUFF
333
*******************************************************/
334
335
    /**
336
     * fields that we remove from the parent::getCMSFields object set.
337
     *
338
     * @var array
339
     */
340
    protected $fieldsAndTabsToBeRemoved = array(
341
        'MemberID',
342
        'Attributes',
343
        'SessionID',
344
        'Emails',
345
        'BillingAddressID',
346
        'ShippingAddressID',
347
        'UseShippingAddress',
348
        'OrderStatusLogs',
349
        'Payments',
350
        'OrderDate',
351
        'ExchangeRate',
352
        'CurrencyUsedID',
353
        'StatusID',
354
        'Currency',
355
    );
356
357
    /**
358
     * STANDARD SILVERSTRIPE STUFF.
359
     **/
360
    private static $summary_fields = array(
361
        'Title' => 'Title',
362
        'Status.Title' => 'Next Step',
363
        'Member.Surname' => 'Name',
364
        'Member.Email' => 'Email',
365
        'TotalAsMoney.Nice' => 'Total',
366
        'TotalItemsTimesQuantity' => 'Units',
367
        'IsPaidNice' => 'Paid'
368
    );
369
370
    /**
371
     * STANDARD SILVERSTRIPE STUFF.
372
     *
373
     * @todo: how to translate this?
374
     **/
375
    private static $searchable_fields = array(
376
        'ID' => array(
377
            'field' => 'NumericField',
378
            'title' => 'Order Number',
379
        ),
380
        'MemberID' => array(
381
            'field' => 'TextField',
382
            'filter' => 'OrderFilters_MemberAndAddress',
383
            'title' => 'Customer Details',
384
        ),
385
        'Created' => array(
386
            'field' => 'TextField',
387
            'filter' => 'OrderFilters_AroundDateFilter',
388
            'title' => 'Date (e.g. Today, 1 jan 2007, or last week)',
389
        ),
390
        //make sure to keep the items below, otherwise they do not show in form
391
        'StatusID' => array(
392
            'filter' => 'OrderFilters_MultiOptionsetStatusIDFilter',
393
        ),
394
        'CancelledByID' => array(
395
            'filter' => 'OrderFilters_HasBeenCancelled',
396
            'title' => 'Cancelled by ...',
397
        ),
398
    );
399
400
    /**
401
     * Determine which properties on the DataObject are
402
     * searchable, and map them to their default {@link FormField}
403
     * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
404
     *
405
     * Some additional logic is included for switching field labels, based on
406
     * how generic or specific the field type is.
407
     *
408
     * Used by {@link SearchContext}.
409
     *
410
     * @param array $_params
411
     *                       'fieldClasses': Associative array of field names as keys and FormField classes as values
412
     *                       'restrictFields': Numeric array of a field name whitelist
413
     *
414
     * @return FieldList
415
     */
416
    public function scaffoldSearchFields($_params = null)
417
    {
418
        $fieldList = parent::scaffoldSearchFields($_params);
419
420
        //for sales to action only show relevant ones ...
421
        if(Controller::curr() && Controller::curr()->class === 'SalesAdmin') {
422
            $statusOptions = OrderStep::admin_manageable_steps();
423
        } else {
424
            $statusOptions = OrderStep::get();
425
        }
426
        if ($statusOptions && $statusOptions->count()) {
427
            $createdOrderStatusID = 0;
428
            $preSelected = array();
429
            $createdOrderStatus = $statusOptions->First();
430
            if ($createdOrderStatus) {
431
                $createdOrderStatusID = $createdOrderStatus->ID;
432
            }
433
            $arrayOfStatusOptions = clone $statusOptions->map('ID', 'Title');
434
            $arrayOfStatusOptionsFinal = array();
435
            if (count($arrayOfStatusOptions)) {
436
                foreach ($arrayOfStatusOptions as $key => $value) {
437
                    if (isset($_GET['q']['StatusID'][$key])) {
438
                        $preSelected[$key] = $key;
439
                    }
440
                    $count = Order::get()
441
                        ->Filter(array('StatusID' => intval($key)))
442
                        ->count();
443
                    if ($count < 1) {
444
                        //do nothing
445
                    } else {
446
                        $arrayOfStatusOptionsFinal[$key] = $value." ($count)";
447
                    }
448
                }
449
            }
450
            $statusField = new CheckboxSetField(
451
                'StatusID',
452
                Injector::inst()->get('OrderStep')->i18n_singular_name(),
453
                $arrayOfStatusOptionsFinal,
454
                $preSelected
455
            );
456
            $fieldList->push($statusField);
457
        }
458
        $fieldList->push(new DropdownField('CancelledByID', 'Cancelled', array(-1 => '(Any)', 1 => 'yes', 0 => 'no')));
459
460
        //allow changes
461
        $this->extend('scaffoldSearchFields', $fieldList, $_params);
462
463
        return $fieldList;
464
    }
465
466
    /**
467
     * link to edit the record.
468
     *
469
     * @param string | Null $action - e.g. edit
470
     *
471
     * @return string
472
     */
473
    public function CMSEditLink($action = null)
474
    {
475
        return CMSEditLinkAPI::find_edit_link_for_object($this, $action);
476
    }
477
478
    /**
479
     * STANDARD SILVERSTRIPE STUFF
480
     * broken up into submitted and not (yet) submitted.
481
     **/
482
    public function getCMSFields()
483
    {
484
        $fields = $this->scaffoldFormFields(array(
485
            // Don't allow has_many/many_many relationship editing before the record is first saved
486
            'includeRelations' => false,
487
            'tabbed' => true,
488
            'ajaxSafe' => true
489
        ));
490
        $fields->insertBefore(
491
            Tab::create(
492
                'Next',
493
                _t('Order.NEXT_TAB', 'Action')
494
            ),
495
            'Main'
496
        );
497
        $fields->addFieldsToTab(
498
            'Root',
499
            array(
500
                Tab::create(
501
                    "Items",
502
                    _t('Order.ITEMS_TAB', 'Items')
503
                ),
504
                Tab::create(
505
                    "Extras",
506
                    _t('Order.MODIFIERS_TAB', 'Adjustments')
507
                ),
508
                Tab::create(
509
                    'Emails',
510
                    _t('Order.EMAILS_TAB', 'Emails')
511
                ),
512
                Tab::create(
513
                    'Payments',
514
                    _t('Order.PAYMENTS_TAB', 'Payment')
515
                ),
516
                Tab::create(
517
                    'Account',
518
                    _t('Order.ACCOUNT_TAB', 'Account')
519
                ),
520
                Tab::create(
521
                    'Currency',
522
                    _t('Order.CURRENCY_TAB', 'Currency')
523
                ),
524
                Tab::create(
525
                    'Addresses',
526
                    _t('Order.ADDRESSES_TAB', 'Addresses')
527
                ),
528
                Tab::create(
529
                    'Log',
530
                    _t('Order.LOG_TAB', 'Notes')
531
                ),
532
                Tab::create(
533
                    'Cancellations',
534
                    _t('Order.CANCELLATION_TAB', 'Cancel')
535
                ),
536
            )
537
        );
538
        //as we are no longer using the parent:;getCMSFields
539
        // we had to add the updateCMSFields hook.
540
        $this->extend('updateCMSFields', $fields);
541
        $currentMember = Member::currentUser();
542
        if (!$this->exists() || !$this->StatusID) {
543
            $firstStep = OrderStep::get()->First();
544
            $this->StatusID = $firstStep->ID;
545
            $this->write();
546
        }
547
        $submitted = $this->IsSubmitted() ? true : false;
548
        if ($submitted) {
549
            //TODO
550
            //Having trouble here, as when you submit the form (for example, a payment confirmation)
551
            //as the step moves forward, meaning the fields generated are incorrect, causing an error
552
            //"I can't handle sub-URLs of a Form object." generated by the RequestHandler.
553
            //Therefore we need to try reload the page so that it will be requesting the correct URL to generate the correct fields for the current step
554
            //Or something similar.
555
            //why not check if the URL == $this->CMSEditLink()
556
            //and only tryToFinaliseOrder if this is true....
557
            if ($_SERVER['REQUEST_URI'] == $this->CMSEditLink() || $_SERVER['REQUEST_URI'] == $this->CMSEditLink('edit')) {
558
                $this->tryToFinaliseOrder();
559
            }
560
        } else {
561
            $this->init(true);
562
            $this->calculateOrderAttributes(true);
563
            Session::set('EcommerceOrderGETCMSHack', $this->ID);
564
        }
565
        if ($submitted) {
566
            $this->fieldsAndTabsToBeRemoved[] = 'CustomerOrderNote';
567
        } else {
568
            $this->fieldsAndTabsToBeRemoved[] = 'Emails';
569
        }
570
        foreach ($this->fieldsAndTabsToBeRemoved as $field) {
571
            $fields->removeByName($field);
572
        }
573
        $orderSummaryConfig = GridFieldConfig_Base::create();
574
        $orderSummaryConfig->removeComponentsByType('GridFieldToolbarHeader');
575
        // $orderSummaryConfig->removeComponentsByType('GridFieldSortableHeader');
576
        $orderSummaryConfig->removeComponentsByType('GridFieldFilterHeader');
577
        $orderSummaryConfig->removeComponentsByType('GridFieldPageCount');
578
        $orderSummaryConfig->removeComponentsByType('GridFieldPaginator');
579
        $nextFieldArray = array(
580
            LiteralField::create('CssFix', '<style>#Root_Next h2.form-control {padding: 0!important; margin: 0!important; padding-top: 4em!important;}</style>'),
581
            HeaderField::create('MyOrderStepHeader', _t('Order.CURRENT_STATUS', '1. Current Status')),
582
            $this->OrderStepField(),
583
            GridField::create(
584
                'OrderSummary',
585
                _t('Order.CURRENT_STATUS', 'Summary'),
586
                ArrayList::create(array($this)),
587
                $orderSummaryConfig
588
            )
589
        );
590
        $keyNotes = OrderStatusLog::get()->filter(
591
            array(
592
                'OrderID' => $this->ID,
593
                'ClassName' => 'OrderStatusLog'
594
            )
595
        );
596
        if ($keyNotes->count()) {
597
            $notesSummaryConfig = GridFieldConfig_RecordViewer::create();
598
            $notesSummaryConfig->removeComponentsByType('GridFieldToolbarHeader');
599
            $notesSummaryConfig->removeComponentsByType('GridFieldFilterHeader');
600
            // $orderSummaryConfig->removeComponentsByType('GridFieldSortableHeader');
601
            $notesSummaryConfig->removeComponentsByType('GridFieldPageCount');
602
            $notesSummaryConfig->removeComponentsByType('GridFieldPaginator');
603
            $nextFieldArray = array_merge(
604
                $nextFieldArray,
605
                array(
606
                    HeaderField::create('KeyNotesHeader', _t('Order.KEY_NOTES_HEADER', 'Key Notes')),
607
                    GridField::create(
608
                        'OrderStatusLogSummary',
609
                        _t('Order.CURRENT_KEY_NOTES', 'Key Notes'),
610
                        $keyNotes,
611
                        $notesSummaryConfig
612
                    )
613
                )
614
            );
615
        }
616
        $nextFieldArray = array_merge(
617
            $nextFieldArray,
618
            array(
619
                EcommerceCMSButtonField::create(
620
                    'AddNoteButton',
621
                    $this->CMSEditLink('ItemEditForm/field/OrderStatusLog/item/new'),
622
                    _t('Order.ADD_NOTE', 'Add Note')
623
                )
624
            )
625
        );
626
        $nextFieldArray = array_merge(
627
            $nextFieldArray,
628
            array(
629
630
            )
631
        );
632
633
         //is the member is a shop admin they can always view it
634
635
        if (EcommerceRole::current_member_can_process_orders(Member::currentUser())) {
636
            $lastStep = OrderStep::get()->Last();
637
            if($this->StatusID != $lastStep->ID) {
638
                $queueObjectSingleton = Injector::inst()->get('OrderProcessQueue');
639
                if ($myQueueObject = $queueObjectSingleton->getQueueObject($this)) {
640
                    $myQueueObjectField = GridField::create(
641
                        'MyQueueObjectField',
642
                        _t('Order.QUEUE_DETAILS', 'Queue Details'),
643
                        $this->OrderProcessQueue(),
644
                        GridFieldConfig_RecordEditor::create()
645
                    );
646
                } else {
647
                    $myQueueObjectField = LiteralField::create('MyQueueObjectField', '<p>'._t('Order.NOT_QUEUED','This order is not queued for future processing.').'</p>');
648
                }
649
                $nextFieldArray = array_merge(
650
                    $nextFieldArray,
651
                    array(
652
                        HeaderField::create('OrderStepNextStepHeader', _t('Order.ACTION_NEXT_STEP', '2. Action Next Step')),
653
                        $myQueueObjectField,
654
                        HeaderField::create('ActionNextStepManually', _t('Order.MANUAL_STATUS_CHANGE', '3. Move Order Along')),
655
                        LiteralField::create('OrderStepNextStepHeaderExtra', '<p>'._t('Order.NEEDTOREFRESH', 'Once you have made any changes to the order then you will have to refresh below or save it to move it along.').'</p>'),
656
                        EcommerceCMSButtonField::create(
657
                            'StatusIDExplanation',
658
                            $this->CMSEditLink(),
659
                            _t('Order.REFRESH', 'refresh now')
660
                        )
661
                    )
662
                );
663
            }
664
        }
665
        $fields->addFieldsToTab(
666
            'Root.Next',
667
            $nextFieldArray
668
        );
669
670
        $this->MyStep()->addOrderStepFields($fields, $this);
671
672
        if ($submitted) {
673
            $permaLinkLabel = _t('Order.PERMANENT_LINK', 'Customer Link');
674
            $html = '<p>'.$permaLinkLabel.': <a href="'.$this->getRetrieveLink().'">'.$this->getRetrieveLink().'</a></p>';
675
            $shareLinkLabel = _t('Order.SHARE_LINK', 'Share Link');
676
            $html .= '<p>'.$shareLinkLabel.': <a href="'.$this->getShareLink().'">'.$this->getShareLink().'</a></p>';
677
            $feedbackLinkLabel = _t('Order.FEEDBACK_LINK', 'Feedback Link');
678
            $html .= '<p>'.$feedbackLinkLabel.': <a href="'.$this->getFeedbackLink().'">'.$this->getFeedbackLink().'</a></p>';
679
            $js = "window.open(this.href, 'payment', 'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1,width=800,height=600'); return false;";
680
            $link = $this->getPrintLink();
681
            $label = _t('Order.PRINT_INVOICE', 'invoice');
682
            $linkHTML = '<a href="'.$link.'" onclick="'.$js.'">'.$label.'</a>';
683
            $linkHTML .= ' | ';
684
            $link = $this->getPackingSlipLink();
685
            $label = _t('Order.PRINT_PACKING_SLIP', 'packing slip');
686
            $labelPrint = _t('Order.PRINT', 'Print');
687
            $linkHTML .= '<a href="'.$link.'" onclick="'.$js.'">'.$label.'</a>';
688
            $html .= '<h3>';
689
            $html .= $labelPrint.': '.$linkHTML;
690
            $html .= '</h3>';
691
692
            $fields->addFieldToTab(
693
                'Root.Main',
694
                LiteralField::create('getPrintLinkANDgetPackingSlipLink', $html)
695
            );
696
697
            //add order here as well.
698
            $fields->addFieldToTab(
699
                'Root.Main',
700
                new LiteralField(
701
                    'MainDetails',
702
                    '<iframe src="'.$this->getPrintLink().'" width="100%" height="2500" style="border: 5px solid #2e7ead; border-radius: 2px;"></iframe>')
703
            );
704
            $fields->addFieldsToTab(
705
                'Root.Items',
706
                array(
707
                    GridField::create(
708
                        'Items_Sold',
709
                        'Items Sold',
710
                        $this->Items(),
711
                        new GridFieldConfig_RecordViewer
712
                    )
713
                )
714
            );
715
            $fields->addFieldsToTab(
716
                'Root.Extras',
717
                array(
718
                    GridField::create(
719
                        'Modifications',
720
                        'Price (and other) adjustments',
721
                        $this->Modifiers(),
722
                        new GridFieldConfig_RecordViewer
723
                    )
724
                )
725
            );
726
            $fields->addFieldsToTab(
727
                'Root.Emails',
728
                array(
729
                    $this->getEmailsTableField()
730
                )
731
            );
732
            $fields->addFieldsToTab(
733
                'Root.Payments',
734
                array(
735
                    $this->getPaymentsField(),
736
                    new ReadOnlyField('TotalPaidNice', _t('Order.TOTALPAID', 'Total Paid'), $this->TotalPaidAsCurrencyObject()->Nice()),
737
                    new ReadOnlyField('TotalOutstandingNice', _t('Order.TOTALOUTSTANDING', 'Total Outstanding'), $this->getTotalOutstandingAsMoney()->Nice())
738
                )
739
            );
740
            if ($this->canPay()) {
741
                $link = EcommercePaymentController::make_payment_link($this->ID);
742
                $js = "window.open(this.href, 'payment', 'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1,width=800,height=600'); return false;";
743
                $header = _t('Order.MAKEPAYMENT', 'make payment');
744
                $label = _t('Order.MAKEADDITIONALPAYMENTNOW', 'make additional payment now');
745
                $linkHTML = '<a href="'.$link.'" onclick="'.$js.'">'.$label.'</a>';
746
                $fields->addFieldToTab('Root.Payments', new HeaderField('MakeAdditionalPaymentHeader', $header, 3));
747
                $fields->addFieldToTab('Root.Payments', new LiteralField('MakeAdditionalPayment', $linkHTML));
748
            }
749
            //member
750
            $member = $this->Member();
751
            if ($member && $member->exists()) {
752
                $fields->addFieldToTab('Root.Account', new LiteralField('MemberDetails', $member->getEcommerceFieldsForCMS()));
753
            } else {
754
                $fields->addFieldToTab('Root.Account', new LiteralField('MemberDetails',
755
                    '<p>'._t('Order.NO_ACCOUNT', 'There is no --- account --- associated with this order').'</p>'
756
                ));
757
            }
758
            $cancelledField = $fields->dataFieldByName('CancelledByID');
759
            $fields->removeByName('CancelledByID');
760
            $shopAdminAndCurrentCustomerArray = EcommerceRole::list_of_admins(true);
761
            if ($member && $member->exists()) {
762
                $shopAdminAndCurrentCustomerArray[$member->ID] = $member->getName();
763
            }
764
            if ($this->CancelledByID) {
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...
765
                if ($cancellingMember = $this->CancelledBy()) {
766
                    $shopAdminAndCurrentCustomerArray[$this->CancelledByID] = $cancellingMember->getName();
767
                }
768
            }
769
            if ($this->canCancel()) {
770
                $fields->addFieldsToTab(
771
                    'Root.Cancellations',
772
                    array(
773
                        DropdownField::create(
774
                            'CancelledByID',
775
                            $cancelledField->Title(),
776
                            $shopAdminAndCurrentCustomerArray
777
                        )
778
                    )
779
                );
780
            } else {
781
                $cancelledBy = isset($shopAdminAndCurrentCustomerArray[$this->CancelledByID]) && $this->CancelledByID ? $shopAdminAndCurrentCustomerArray[$this->CancelledByID] : _t('Order.NOT_CANCELLED', 'not cancelled');
782
                $fields->addFieldsToTab(
783
                    'Root.Cancellations',
784
                    ReadonlyField::create(
785
                        'CancelledByDisplay',
786
                        $cancelledField->Title(),
787
                        $cancelledBy
788
789
                    )
790
                );
791
            }
792
            $fields->addFieldToTab('Root.Log', $this->getOrderStatusLogsTableField_Archived());
793
            $submissionLog = $this->SubmissionLog();
794
            if ($submissionLog) {
795
                $fields->addFieldToTab('Root.Log',
796
                    ReadonlyField::create(
797
                        'SequentialOrderNumber',
798
                        _t('Order.SEQUENTIALORDERNUMBER', 'Consecutive order number'),
799
                        $submissionLog->SequentialOrderNumber
800
                    )->setRightTitle('e.g. 1,2,3,4,5...')
801
                );
802
            }
803
        } else {
804
            $linkText = _t(
805
                'Order.LOAD_THIS_ORDER',
806
                'load this order'
807
            );
808
            $message = _t(
809
                'Order.NOSUBMITTEDYET',
810
                'No details are shown here as this order has not been submitted yet. You can {link} to submit it... NOTE: For this, you will be logged in as the customer and logged out as (shop)admin .',
811
                array('link' => '<a href="'.$this->getRetrieveLink().'" data-popup="true">'.$linkText.'</a>')
812
            );
813
            $fields->addFieldToTab('Root.Next', new LiteralField('MainDetails', '<p>'.$message.'</p>'));
814
            $fields->addFieldToTab('Root.Items', $this->getOrderItemsField());
815
            $fields->addFieldToTab('Root.Extras', $this->getModifierTableField());
816
817
            //MEMBER STUFF
818
            $specialOptionsArray = array();
819
            if ($this->MemberID) {
820
                $specialOptionsArray[0] = _t('Order.SELECTCUSTOMER', '--- Remover Customer ---');
821
                $specialOptionsArray[$this->MemberID] = _t('Order.LEAVEWITHCURRENTCUSTOMER', '- Leave with current customer: ').$this->Member()->getTitle();
822
            } elseif ($currentMember) {
823
                $specialOptionsArray[0] = _t('Order.SELECTCUSTOMER', '--- Select Customers ---');
824
                $currentMemberID = $currentMember->ID;
825
                $specialOptionsArray[$currentMemberID] = _t('Order.ASSIGNTHISORDERTOME', '- Assign this order to me: ').$currentMember->getTitle();
826
            }
827
            //MEMBER FIELD!!!!!!!
828
            $memberArray = $specialOptionsArray + EcommerceRole::list_of_customers(true);
829
            $fields->addFieldToTab('Root.Next', new DropdownField('MemberID', _t('Order.SELECTCUSTOMER', 'Select Customer'), $memberArray), 'CustomerOrderNote');
830
            $memberArray = null;
831
        }
832
        $fields->addFieldToTab('Root.Addresses', new HeaderField('BillingAddressHeader', _t('Order.BILLINGADDRESS', 'Billing Address')));
833
834
        $fields->addFieldToTab('Root.Addresses', $this->getBillingAddressField());
835
836
        if (EcommerceConfig::get('OrderAddress', 'use_separate_shipping_address')) {
837
            $fields->addFieldToTab('Root.Addresses', new HeaderField('ShippingAddressHeader', _t('Order.SHIPPINGADDRESS', 'Shipping Address')));
838
            $fields->addFieldToTab('Root.Addresses', new CheckboxField('UseShippingAddress', _t('Order.USESEPERATEADDRESS', 'Use separate shipping address?')));
839
            if ($this->UseShippingAddress) {
840
                $fields->addFieldToTab('Root.Addresses', $this->getShippingAddressField());
841
            }
842
        }
843
        $currencies = EcommerceCurrency::get_list();
844
        if ($currencies && $currencies->count()) {
845
            $currencies = $currencies->map()->toArray();
846
            $fields->addFieldToTab('Root.Currency', new ReadOnlyField('ExchangeRate ', _t('Order.EXCHANGERATE', 'Exchange Rate'), $this->ExchangeRate));
847
            $fields->addFieldToTab('Root.Currency', $currencyField = new DropdownField('CurrencyUsedID', _t('Order.CurrencyUsed', 'Currency Used'), $currencies));
848
            if ($this->IsSubmitted()) {
849
                $fields->replaceField('CurrencyUsedID', $fields->dataFieldByName('CurrencyUsedID')->performReadonlyTransformation());
850
            }
851
        } else {
852
            $fields->addFieldToTab('Root.Currency', new LiteralField('CurrencyInfo', '<p>You can not change currencies, because no currencies have been created.</p>'));
853
            $fields->replaceField('CurrencyUsedID', $fields->dataFieldByName('CurrencyUsedID')->performReadonlyTransformation());
854
        }
855
        $fields->addFieldToTab('Root.Log', new ReadonlyField('Created', _t('Root.CREATED', 'Created')));
856
        $fields->addFieldToTab('Root.Log', new ReadonlyField('LastEdited', _t('Root.LASTEDITED', 'Last saved')));
857
        $this->extend('updateCMSFields', $fields);
858
859
        return $fields;
860
    }
861
862
    /**
863
     * Field to add and edit Order Items.
864
     *
865
     * @return GridField
866
     */
867
    protected function getOrderItemsField()
868
    {
869
        $gridFieldConfig = GridFieldConfigForOrderItems::create();
870
        $source = $this->OrderItems();
871
872
        return new GridField('OrderItems', _t('OrderItems.PLURALNAME', 'Order Items'), $source, $gridFieldConfig);
873
    }
874
875
    /**
876
     * Field to add and edit Modifiers.
877
     *
878
     * @return GridField
879
     */
880
    public function getModifierTableField()
881
    {
882
        $gridFieldConfig = GridFieldConfigForOrderItems::create();
883
        $source = $this->Modifiers();
884
885
        return new GridField('OrderModifiers', _t('OrderItems.PLURALNAME', 'Order Items'), $source, $gridFieldConfig);
886
    }
887
888
    /**
889
     *@return GridField
890
     **/
891
    protected function getBillingAddressField()
892
    {
893
        $this->CreateOrReturnExistingAddress('BillingAddress');
894
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
895
            new GridFieldToolbarHeader(),
896
            new GridFieldSortableHeader(),
897
            new GridFieldDataColumns(),
898
            new GridFieldPaginator(10),
899
            new GridFieldEditButton(),
900
            new GridFieldDetailForm()
901
        );
902
        //$source = $this->BillingAddress();
903
        $source = BillingAddress::get()->filter(array('OrderID' => $this->ID));
904
905
        return new GridField('BillingAddress', _t('BillingAddress.SINGULARNAME', 'Billing Address'), $source, $gridFieldConfig);
906
    }
907
908
    /**
909
     *@return GridField
910
     **/
911
    protected function getShippingAddressField()
912
    {
913
        $this->CreateOrReturnExistingAddress('ShippingAddress');
914
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
915
            new GridFieldToolbarHeader(),
916
            new GridFieldSortableHeader(),
917
            new GridFieldDataColumns(),
918
            new GridFieldPaginator(10),
919
            new GridFieldEditButton(),
920
            new GridFieldDetailForm()
921
        );
922
        //$source = $this->ShippingAddress();
923
        $source = ShippingAddress::get()->filter(array('OrderID' => $this->ID));
924
925
        return new GridField('ShippingAddress', _t('BillingAddress.SINGULARNAME', 'Shipping Address'), $source, $gridFieldConfig);
926
    }
927
928
    /**
929
     * Needs to be public because the OrderStep::getCMSFIelds accesses it.
930
     *
931
     * @param string    $sourceClass
932
     * @param string    $title
933
     *
934
     * @return GridField
935
     **/
936
    public function getOrderStatusLogsTableField(
937
        $sourceClass = 'OrderStatusLog',
938
        $title = ''
939
    ) {
940
        $gridFieldConfig = GridFieldConfig_RecordViewer::create()->addComponents(
941
            new GridFieldAddNewButton('toolbar-header-right'),
942
            new GridFieldDetailForm()
943
        );
944
        $title ? $title : $title = _t('OrderStatusLog.PLURALNAME', 'Order Status Logs');
945
        $source = $this->OrderStatusLogs()->Filter(array('ClassName' => $sourceClass));
946
        $gf = new GridField($sourceClass, $title, $source, $gridFieldConfig);
947
        $gf->setModelClass($sourceClass);
948
949
        return $gf;
950
    }
951
952
    /**
953
     * Needs to be public because the OrderStep::getCMSFIelds accesses it.
954
     *
955
     * @param string    $sourceClass
956
     * @param string    $title
957
     *
958
     * @return GridField
959
     **/
960
    public function getOrderStatusLogsTableFieldEditable(
961
        $sourceClass = 'OrderStatusLog',
962
        $title = ''
963
    ) {
964
        $gf = $this->getOrderStatusLogsTableField($sourceClass, $title);
965
        $gf->getConfig()->addComponents(
966
            new GridFieldEditButton()
967
        );
968
        return $gf;
969
    }
970
971
    /**
972
     * @param string    $sourceClass
973
     * @param string    $title
974
     * @param FieldList $fieldList          (Optional)
975
     * @param FieldList $detailedFormFields (Optional)
976
     *
977
     * @return GridField
978
     **/
979
    protected function getOrderStatusLogsTableField_Archived(
980
        $sourceClass = 'OrderStatusLog',
981
        $title = '',
982
        FieldList $fieldList = null,
983
        FieldList $detailedFormFields = null
984
    ) {
985
        $title ? $title : $title = _t('OrderLog.PLURALNAME', 'Order Log');
986
        $source = $this->OrderStatusLogs();
987
        if ($sourceClass != 'OrderStatusLog' && class_exists($sourceClass)) {
988
            $source = $source->filter(array('ClassName' => ClassInfo::subclassesFor($sourceClass)));
989
        }
990
        $gridField = GridField::create($sourceClass, $title, $source, $config = GridFieldConfig_RelationEditor::create());
991
        $config->removeComponentsByType('GridFieldAddExistingAutocompleter');
992
        $config->removeComponentsByType('GridFieldDeleteAction');
993
994
        return $gridField;
995
    }
996
997
    /**
998
     * @return GridField
999
     **/
1000
    public function getEmailsTableField()
1001
    {
1002
        $gridFieldConfig = GridFieldConfig_RecordViewer::create()->addComponents(
1003
            new GridFieldDetailForm()
1004
        );
1005
1006
        return new GridField('Emails', _t('Order.CUSTOMER_EMAILS', 'Customer Emails'), $this->Emails(), $gridFieldConfig);
1007
    }
1008
1009
    /**
1010
     * @return GridField
1011
     */
1012
    protected function getPaymentsField()
1013
    {
1014
        $gridFieldConfig = GridFieldConfig_RecordViewer::create()->addComponents(
1015
            new GridFieldDetailForm(),
1016
            new GridFieldEditButton()
1017
        );
1018
1019
        return new GridField('Payments', _t('Order.PAYMENTS', 'Payments'), $this->Payments(), $gridFieldConfig);
1020
    }
1021
1022
    /**
1023
     * @return OrderStepField
1024
     */
1025
    public function OrderStepField()
1026
    {
1027
        return OrderStepField::create($name = 'MyOrderStep', $this, Member::currentUser());
1028
    }
1029
1030
/*******************************************************
1031
   * 2. MAIN TRANSITION FUNCTIONS
1032
*******************************************************/
1033
1034
    /**
1035
     * init runs on start of a new Order (@see onAfterWrite)
1036
     * it adds all the modifiers to the orders and the starting OrderStep.
1037
     *
1038
     * @param bool $recalculate
1039
     *
1040
     * @return DataObject (Order)
1041
     **/
1042
    public function init($recalculate = false)
1043
    {
1044
        if ($this->IsSubmitted()) {
1045
            user_error('Can not init an order that has been submitted', E_USER_NOTICE);
1046
        } else {
1047
            //to do: check if shop is open....
1048
            if ($this->StatusID || $recalculate) {
1049
                if (!$this->StatusID) {
1050
                    $createdOrderStatus = OrderStep::get()->First();
1051
                    if (!$createdOrderStatus) {
1052
                        user_error('No ordersteps have been created', E_USER_WARNING);
1053
                    }
1054
                    $this->StatusID = $createdOrderStatus->ID;
1055
                }
1056
                $createdModifiersClassNames = array();
1057
                $modifiersAsArrayList = new ArrayList();
1058
                $modifiers = $this->modifiersFromDatabase($includingRemoved = true);
1059
                if ($modifiers->count()) {
1060
                    foreach ($modifiers as $modifier) {
1061
                        $modifiersAsArrayList->push($modifier);
1062
                    }
1063
                }
1064
                if ($modifiersAsArrayList->count()) {
1065
                    foreach ($modifiersAsArrayList as $modifier) {
1066
                        $createdModifiersClassNames[$modifier->ID] = $modifier->ClassName;
1067
                    }
1068
                } else {
1069
                }
1070
                $modifiersToAdd = EcommerceConfig::get('Order', 'modifiers');
1071
                if (is_array($modifiersToAdd) && count($modifiersToAdd) > 0) {
1072
                    foreach ($modifiersToAdd as $numericKey => $className) {
1073
                        if (!in_array($className, $createdModifiersClassNames)) {
1074
                            if (class_exists($className)) {
1075
                                $modifier = new $className();
1076
                                //only add the ones that should be added automatically
1077
                                if (!$modifier->DoNotAddAutomatically()) {
1078
                                    if (is_a($modifier, 'OrderModifier')) {
1079
                                        $modifier->OrderID = $this->ID;
1080
                                        $modifier->Sort = $numericKey;
1081
                                        //init method includes a WRITE
1082
                                        $modifier->init();
1083
                                        //IMPORTANT - add as has_many relationship  (Attributes can be a modifier OR an OrderItem)
1084
                                        $this->Attributes()->add($modifier);
1085
                                        $modifiersAsArrayList->push($modifier);
1086
                                    }
1087
                                }
1088
                            } else {
1089
                                user_error('reference to a non-existing class: '.$className.' in modifiers', E_USER_NOTICE);
1090
                            }
1091
                        }
1092
                    }
1093
                }
1094
                $this->extend('onInit', $this);
1095
                //careful - this will call "onAfterWrite" again
1096
                $this->write();
1097
            }
1098
        }
1099
1100
        return $this;
1101
    }
1102
1103
    /**
1104
     * @var array
1105
     */
1106
    private static $_try_to_finalise_order_is_running = array();
1107
1108
    /**
1109
     * Goes through the order steps and tries to "apply" the next status to the order.
1110
     *
1111
     * @param bool $runAgain
1112
     * @param bool $fromOrderQueue - is it being called from the OrderProcessQueue (or similar)
1113
     **/
1114
    public function tryToFinaliseOrder($runAgain = false, $fromOrderQueue = false)
1115
    {
1116
        if (empty(self::$_try_to_finalise_order_is_running[$this->ID]) || $runAgain) {
1117
            self::$_try_to_finalise_order_is_running[$this->ID] = true;
1118
1119
            //if the order has been cancelled then we do not process it ...
1120
            if ($this->CancelledByID) {
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...
1121
                $this->Archive(true);
1122
1123
                return;
1124
            }
1125
            // if it is in the queue it has to run from the queue tasks
1126
            // if it ruins from the queue tasks then it has to be one currently processing.
1127
            $queueObjectSingleton = Injector::inst()->get('OrderProcessQueue');
1128
            if ($myQueueObject = $queueObjectSingleton->getQueueObject($this)) {
1129
                if($fromOrderQueue) {
1130
                    if ( ! $myQueueObject->InProcess) {
1131
                        return;
1132
                    }
1133
                } else {
1134
                    return;
1135
                }
1136
            }
1137
            //a little hack to make sure we do not rely on a stored value
1138
            //of "isSubmitted"
1139
            $this->_isSubmittedTempVar = -1;
1140
            //status of order is being progressed
1141
            $nextStatusID = $this->doNextStatus();
1142
            if ($nextStatusID) {
1143
                $nextStatusObject = OrderStep::get()->byID($nextStatusID);
1144
                if ($nextStatusObject) {
1145
                    $delay = $nextStatusObject->CalculatedDeferTimeInSeconds($this);
1146
                    if ($delay > 0) {
1147
                        if ($nextStatusObject->DeferFromSubmitTime) {
1148
                            $delay = $delay - $this->SecondsSinceBeingSubmitted();
1149
                            if ($delay < 0) {
1150
                                $delay = 0;
1151
                            }
1152
                        }
1153
                        $queueObjectSingleton->AddOrderToQueue(
1154
                            $this,
1155
                            $delay
1156
                        );
1157
                    } else {
1158
                        //status has been completed, so it can be released
1159
                        self::$_try_to_finalise_order_is_running[$this->ID] = false;
1160
                        $this->tryToFinaliseOrder($runAgain, $fromOrderQueue);
1161
                    }
1162
                }
1163
            }
1164
        }
1165
    }
1166
1167
    /**
1168
     * Goes through the order steps and tries to "apply" the next step
1169
     * Step is updated after the other one is completed...
1170
     *
1171
     * @return int (StatusID or false if the next status can not be "applied")
1172
     **/
1173
    public function doNextStatus()
1174
    {
1175
        if ($this->MyStep()->initStep($this)) {
1176
            if ($this->MyStep()->doStep($this)) {
1177
                if ($nextOrderStepObject = $this->MyStep()->nextStep($this)) {
1178
                    $this->StatusID = $nextOrderStepObject->ID;
1179
                    $this->write();
1180
1181
                    return $this->StatusID;
1182
                }
1183
            }
1184
        }
1185
1186
        return 0;
1187
    }
1188
1189
    /**
1190
     * cancel an order.
1191
     *
1192
     * @param Member $member - (optional) the user cancelling the order
1193
     * @param string $reason - (optional) the reason the order is cancelled
1194
     *
1195
     * @return OrderStatusLog_Cancel
1196
     */
1197
    public function Cancel($member = null, $reason = '')
1198
    {
1199
        if($member && $member instanceof Member) {
1200
            //we have a valid member
1201
        } else {
1202
            $member = EcommerceRole::get_default_shop_admin_user();
1203
        }
1204
        if($member) {
1205
            //archive and write
1206
            $this->Archive($avoidWrites = true);
1207
            if($avoidWrites) {
1208
                DB::query('Update "Order" SET CancelledByID = '.$member->ID.' WHERE ID = '.$this->ID.' LIMIT 1;');
1209
            } else {
1210
                $this->CancelledByID = $member->ID;
1211
                $this->write();
1212
            }
1213
            //create log ...
1214
            $log = OrderStatusLog_Cancel::create();
1215
            $log->AuthorID = $member->ID;
1216
            $log->OrderID = $this->ID;
1217
            $log->Note = $reason;
1218
            if ($member->IsShopAdmin()) {
1219
                $log->InternalUseOnly = true;
1220
            }
1221
            $log->write();
1222
            //remove from queue ...
1223
            $queueObjectSingleton = Injector::inst()->get('OrderProcessQueue');
1224
            $ordersinQueue = $queueObjectSingleton->removeOrderFromQueue($this);
1225
            $this->extend('doCancel', $member, $log);
1226
1227
            return $log;
1228
        }
1229
    }
1230
1231
    /**
1232
     * returns true if successful.
1233
     *
1234
     * @param bool $avoidWrites
1235
     *
1236
     * @return bool
1237
     */
1238
    public function Archive($avoidWrites = true)
1239
    {
1240
        $lastOrderStep = OrderStep::get()->Last();
1241
        if ($lastOrderStep) {
1242
            if ($avoidWrites) {
1243
                DB::query('
1244
                    UPDATE "Order"
1245
                    SET "Order"."StatusID" = '.$lastOrderStep->ID.'
1246
                    WHERE "Order"."ID" = '.$this->ID.'
1247
                    LIMIT 1
1248
                ');
1249
1250
                return true;
1251
            } else {
1252
                $this->StatusID = $lastOrderStep->ID;
1253
                $this->write();
1254
1255
                return true;
1256
            }
1257
        }
1258
1259
        return false;
1260
    }
1261
1262
/*******************************************************
1263
   * 3. STATUS RELATED FUNCTIONS / SHORTCUTS
1264
*******************************************************/
1265
1266
    /**
1267
     * Avoids caching of $this->Status().
1268
     *
1269
     * @return DataObject (current OrderStep)
1270
     */
1271
    public function MyStep()
1272
    {
1273
        $step = null;
1274
        if ($this->StatusID) {
1275
            $step = OrderStep::get()->byID($this->StatusID);
1276
        }
1277
        if (!$step) {
1278
            $step = OrderStep::get()->First(); //TODO: this could produce strange results
1279
        }
1280
        if (!$step) {
1281
            $step = OrderStep_Created::create();
1282
        }
1283
        if (!$step) {
1284
            user_error('You need an order step in your Database.');
1285
        }
1286
1287
        return $step;
1288
    }
1289
1290
    /**
1291
     * Return the OrderStatusLog that is relevant to the Order status.
1292
     *
1293
     * @return OrderStatusLog
1294
     */
1295
    public function RelevantLogEntry()
1296
    {
1297
        return $this->MyStep()->RelevantLogEntry($this);
1298
    }
1299
1300
    /**
1301
     * @return OrderStep (current OrderStep that can be seen by customer)
1302
     */
1303
    public function CurrentStepVisibleToCustomer()
1304
    {
1305
        $obj = $this->MyStep();
1306
        if ($obj->HideStepFromCustomer) {
1307
            $obj = OrderStep::get()->where('"OrderStep"."Sort" < '.$obj->Sort.' AND "HideStepFromCustomer" = 0')->Last();
1308
            if (!$obj) {
1309
                $obj = OrderStep::get()->First();
1310
            }
1311
        }
1312
1313
        return $obj;
1314
    }
1315
1316
    /**
1317
     * works out if the order is still at the first OrderStep.
1318
     *
1319
     * @return bool
1320
     */
1321
    public function IsFirstStep()
1322
    {
1323
        $firstStep = OrderStep::get()->First();
1324
        $currentStep = $this->MyStep();
1325
        if ($firstStep && $currentStep) {
1326
            if ($firstStep->ID == $currentStep->ID) {
1327
                return true;
1328
            }
1329
        }
1330
1331
        return false;
1332
    }
1333
1334
    /**
1335
     * Is the order still being "edited" by the customer?
1336
     *
1337
     * @return bool
1338
     */
1339
    public function IsInCart()
1340
    {
1341
        return (bool) $this->IsSubmitted() ? false : true;
1342
    }
1343
1344
    /**
1345
     * The order has "passed" the IsInCart phase.
1346
     *
1347
     * @return bool
1348
     */
1349
    public function IsPastCart()
1350
    {
1351
        return (bool) $this->IsInCart() ? false : true;
1352
    }
1353
1354
    /**
1355
     * Are there still steps the order needs to go through?
1356
     *
1357
     * @return bool
1358
     */
1359
    public function IsUncomplete()
1360
    {
1361
        return (bool) $this->MyStep()->ShowAsUncompletedOrder;
1362
    }
1363
1364
    /**
1365
     * Is the order in the :"processing" phaase.?
1366
     *
1367
     * @return bool
1368
     */
1369
    public function IsProcessing()
1370
    {
1371
        return (bool) $this->MyStep()->ShowAsInProcessOrder;
1372
    }
1373
1374
    /**
1375
     * Is the order completed?
1376
     *
1377
     * @return bool
1378
     */
1379
    public function IsCompleted()
1380
    {
1381
        return (bool) $this->MyStep()->ShowAsCompletedOrder;
1382
    }
1383
1384
    /**
1385
     * Has the order been paid?
1386
     * TODO: why do we check if there is a total at all?
1387
     *
1388
     * @return bool
1389
     */
1390
    public function IsPaid()
1391
    {
1392
        if ($this->IsSubmitted()) {
1393
            return (bool) (($this->Total() >= 0) && ($this->TotalOutstanding() <= 0));
1394
        }
1395
1396
        return false;
1397
    }
1398
    /**
1399
     * Has the order been paid?
1400
     * TODO: why do we check if there is a total at all?
1401
     *
1402
     * @return Boolean (object)
1403
     */
1404
    public function IsPaidNice()
1405
    {
1406
        return  DBField::create_field('Boolean', $this->IsPaid());
1407
    }
1408
1409
    /**
1410
     * Has the order been paid?
1411
     * TODO: why do we check if there is a total at all?
1412
     *
1413
     * @return bool
1414
     */
1415
    public function PaymentIsPending()
1416
    {
1417
        if ($this->IsSubmitted()) {
1418
            if ($this->IsPaid()) {
1419
                //do nothing;
1420
            } elseif (($payments = $this->Payments()) && $payments->count()) {
1421
                foreach ($payments as $payment) {
1422
                    if ('Pending' == $payment->Status) {
1423
                        return true;
1424
                    }
1425
                }
1426
            }
1427
        }
1428
1429
        return false;
1430
    }
1431
1432
    /**
1433
     * shows payments that are meaningfull
1434
     * if the order has been paid then only show successful payments.
1435
     *
1436
     * @return DataList
1437
     */
1438
    public function RelevantPayments()
1439
    {
1440
        if ($this->IsPaid()) {
1441
            return $this->Payments("\"Status\" = 'Success'");
1442
            //EcommercePayment::get()->
1443
            //	filter(array("OrderID" => $this->ID, "Status" => "Success"));
1444
        } else {
1445
            return $this->Payments();
1446
        }
1447
    }
1448
1449
    /**
1450
     * Has the order been cancelled?
1451
     *
1452
     * @return bool
1453
     */
1454
    public function IsCancelled()
1455
    {
1456
        return $this->getIsCancelled();
1457
    }
1458
    public function getIsCancelled()
1459
    {
1460
        return $this->CancelledByID ? true : false;
1461
    }
1462
1463
    /**
1464
     * Has the order been cancelled by the customer?
1465
     *
1466
     * @return bool
1467
     */
1468
    public function IsCustomerCancelled()
1469
    {
1470
        if ($this->MemberID > 0 && $this->MemberID == $this->IsCancelledID) {
1471
            return true;
1472
        }
1473
1474
        return false;
1475
    }
1476
1477
    /**
1478
     * Has the order been cancelled by the  administrator?
1479
     *
1480
     * @return bool
1481
     */
1482
    public function IsAdminCancelled()
1483
    {
1484
        if ($this->IsCancelled()) {
1485
            if (!$this->IsCustomerCancelled()) {
1486
                $admin = Member::get()->byID($this->CancelledByID);
1487
                if ($admin) {
1488
                    if ($admin->IsShopAdmin()) {
1489
                        return true;
1490
                    }
1491
                }
1492
            }
1493
        }
1494
1495
        return false;
1496
    }
1497
1498
    /**
1499
     * Is the Shop Closed for business?
1500
     *
1501
     * @return bool
1502
     */
1503
    public function ShopClosed()
1504
    {
1505
        return EcomConfig()->ShopClosed;
1506
    }
1507
1508
/*******************************************************
1509
   * 4. LINKING ORDER WITH MEMBER AND ADDRESS
1510
*******************************************************/
1511
1512
    /**
1513
     * Returns a member linked to the order.
1514
     * If a member is already linked, it will return the existing member.
1515
     * Otherwise it will return a new Member.
1516
     *
1517
     * Any new member is NOT written, because we dont want to create a new member unless we have to!
1518
     * We will not add a member to the order unless a new one is created in the checkout
1519
     * OR the member is logged in / logs in.
1520
     *
1521
     * Also note that if a new member is created, it is not automatically written
1522
     *
1523
     * @param bool $forceCreation - if set to true then the member will always be saved in the database.
1524
     *
1525
     * @return Member
1526
     **/
1527
    public function CreateOrReturnExistingMember($forceCreation = false)
1528
    {
1529
        if ($this->IsSubmitted()) {
1530
            return $this->Member();
1531
        }
1532
        if ($this->MemberID) {
1533
            $member = $this->Member();
0 ignored issues
show
The method Member() does not exist on Order. Did you maybe mean CreateOrReturnExistingMember()?

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...
1534
        } elseif ($member = Member::currentUser()) {
1535
            if (!$member->IsShopAdmin()) {
1536
                $this->MemberID = $member->ID;
1537
                $this->write();
1538
            }
1539
        }
1540
        $member = $this->Member();
1541
        if (!$member) {
1542
            $member = new Member();
1543
        }
1544
        if ($member && $forceCreation) {
1545
            $member->write();
1546
        }
1547
1548
        return $member;
1549
    }
1550
1551
    /**
1552
     * Returns either the existing one or a new Order Address...
1553
     * All Orders will have a Shipping and Billing address attached to it.
1554
     * Method used to retrieve object e.g. for $order->BillingAddress(); "BillingAddress" is the method name you can use.
1555
     * If the method name is the same as the class name then dont worry about providing one.
1556
     *
1557
     * @param string $className             - ClassName of the Address (e.g. BillingAddress or ShippingAddress)
1558
     * @param string $alternativeMethodName - method to retrieve Address
1559
     **/
1560
    public function CreateOrReturnExistingAddress($className = 'BillingAddress', $alternativeMethodName = '')
1561
    {
1562
        if ($this->exists()) {
1563
            $methodName = $className;
1564
            if ($alternativeMethodName) {
1565
                $methodName = $alternativeMethodName;
1566
            }
1567
            if ($this->IsSubmitted()) {
1568
                return $this->$methodName();
1569
            }
1570
            $variableName = $className.'ID';
1571
            $address = null;
1572
            if ($this->$variableName) {
1573
                $address = $this->$methodName();
1574
            }
1575
            if (!$address) {
1576
                $address = new $className();
1577
                if ($member = $this->CreateOrReturnExistingMember()) {
1578
                    if ($member->exists()) {
1579
                        $address->FillWithLastAddressFromMember($member, $write = false);
1580
                    }
1581
                }
1582
            }
1583
            if ($address) {
1584
                if (!$address->exists()) {
1585
                    $address->write();
1586
                }
1587
                if ($address->OrderID != $this->ID) {
1588
                    $address->OrderID = $this->ID;
1589
                    $address->write();
1590
                }
1591
                if ($this->$variableName != $address->ID) {
1592
                    if (!$this->IsSubmitted()) {
1593
                        $this->$variableName = $address->ID;
1594
                        $this->write();
1595
                    }
1596
                }
1597
1598
                return $address;
1599
            }
1600
        }
1601
1602
        return;
1603
    }
1604
1605
    /**
1606
     * Sets the country in the billing and shipping address.
1607
     *
1608
     * @param string $countryCode            - code for the country e.g. NZ
1609
     * @param bool   $includeBillingAddress
1610
     * @param bool   $includeShippingAddress
1611
     **/
1612
    public function SetCountryFields($countryCode, $includeBillingAddress = true, $includeShippingAddress = true)
1613
    {
1614
        if ($this->IsSubmitted()) {
1615
            user_error('Can not change country in submitted order', E_USER_NOTICE);
1616
        } else {
1617
            if ($includeBillingAddress) {
1618
                if ($billingAddress = $this->CreateOrReturnExistingAddress('BillingAddress')) {
1619
                    $billingAddress->SetCountryFields($countryCode);
1620
                }
1621
            }
1622
            if (EcommerceConfig::get('OrderAddress', 'use_separate_shipping_address')) {
1623
                if ($includeShippingAddress) {
1624
                    if ($shippingAddress = $this->CreateOrReturnExistingAddress('ShippingAddress')) {
1625
                        $shippingAddress->SetCountryFields($countryCode);
1626
                    }
1627
                }
1628
            }
1629
        }
1630
    }
1631
1632
    /**
1633
     * Sets the region in the billing and shipping address.
1634
     *
1635
     * @param int $regionID - ID for the region to be set
1636
     **/
1637
    public function SetRegionFields($regionID)
1638
    {
1639
        if ($this->IsSubmitted()) {
1640
            user_error('Can not change country in submitted order', E_USER_NOTICE);
1641
        } else {
1642
            if ($billingAddress = $this->CreateOrReturnExistingAddress('BillingAddress')) {
1643
                $billingAddress->SetRegionFields($regionID);
1644
            }
1645
            if ($this->CanHaveShippingAddress()) {
1646
                if ($shippingAddress = $this->CreateOrReturnExistingAddress('ShippingAddress')) {
1647
                    $shippingAddress->SetRegionFields($regionID);
1648
                }
1649
            }
1650
        }
1651
    }
1652
1653
    /**
1654
     * Stores the preferred currency of the order.
1655
     * IMPORTANTLY we store the exchange rate for future reference...
1656
     *
1657
     * @param EcommerceCurrency $currency
1658
     */
1659
    public function UpdateCurrency($newCurrency)
1660
    {
1661
        if ($this->IsSubmitted()) {
1662
            user_error('Can not set the currency after the order has been submitted', E_USER_NOTICE);
1663
        } else {
1664
            if (! is_a($newCurrency, Object::getCustomClass('EcommerceCurrency'))) {
1665
                $newCurrency = EcommerceCurrency::default_currency();
1666
            }
1667
            $this->CurrencyUsedID = $newCurrency->ID;
1668
            $this->ExchangeRate = $newCurrency->getExchangeRate();
1669
            $this->write();
1670
        }
1671
    }
1672
1673
    /**
1674
     * alias for UpdateCurrency.
1675
     *
1676
     * @param EcommerceCurrency $currency
1677
     */
1678
    public function SetCurrency($currency)
1679
    {
1680
        $this->UpdateCurrency($currency);
1681
    }
1682
1683
/*******************************************************
1684
   * 5. CUSTOMER COMMUNICATION
1685
*******************************************************/
1686
1687
    /**
1688
     * Send the invoice of the order by email.
1689
     *
1690
     * @param string $emailClassName     (optional) class used to send email
1691
     * @param string $subject            (optional) subject for the email
1692
     * @param string $message            (optional) the main message in the email
1693
     * @param bool   $resend             (optional) send the email even if it has been sent before
1694
     * @param bool   $adminOnlyOrToEmail (optional) sends the email to the ADMIN ONLY, if you provide an email, it will go to the email...
1695
     *
1696
     * @return bool TRUE on success, FALSE on failure
1697
     */
1698
    public function sendEmail(
1699
        $emailClassName = 'Order_InvoiceEmail',
1700
        $subject = '',
1701
        $message = '',
1702
        $resend = false,
1703
        $adminOnlyOrToEmail = false
1704
    ) {
1705
        return $this->prepareAndSendEmail(
1706
            $emailClassName,
1707
            $subject,
1708
            $message,
1709
            $resend,
1710
            $adminOnlyOrToEmail
1711
        );
1712
    }
1713
1714
    /**
1715
     * Sends a message to the shop admin ONLY and not to the customer
1716
     * This can be used by ordersteps and orderlogs to notify the admin of any potential problems.
1717
     *
1718
     * @param string         $emailClassName       - (optional) template to be used ...
1719
     * @param string         $subject              - (optional) subject for the email
1720
     * @param string         $message              - (optional) message to be added with the email
1721
     * @param bool           $resend               - (optional) can it be sent twice?
1722
     * @param bool | string  $adminOnlyOrToEmail   - (optional) sends the email to the ADMIN ONLY, if you provide an email, it will go to the email...
1723
     *
1724
     * @return bool TRUE for success, FALSE for failure (not tested)
1725
     */
1726
    public function sendAdminNotification(
1727
        $emailClassName = 'Order_ErrorEmail',
1728
        $subject = '',
1729
        $message = '',
1730
        $resend = false,
1731
        $adminOnlyOrToEmail = true
1732
    ) {
1733
        return $this->prepareAndSendEmail(
1734
            $emailClassName,
1735
            $subject,
1736
            $message,
1737
            $resend,
1738
            $adminOnlyOrToEmail
1739
        );
1740
    }
1741
1742
    /**
1743
     * returns the order formatted as an email.
1744
     *
1745
     * @param string $emailClassName - template to use.
1746
     * @param string $subject        - (optional) the subject (which can be used as title in email)
1747
     * @param string $message        - (optional) the additional message
1748
     *
1749
     * @return string (html)
1750
     */
1751
    public function renderOrderInEmailFormat(
1752
        $emailClassName,
1753
        $subject = '',
1754
        $message = ''
1755
    )
1756
    {
1757
        $arrayData = $this->createReplacementArrayForEmail($subject, $message);
1758
        Config::nest();
1759
        Config::inst()->update('SSViewer', 'theme_enabled', true);
1760
        $html = $arrayData->renderWith($emailClassName);
1761
        Config::unnest();
1762
1763
        return Order_Email::emogrify_html($html);
1764
    }
1765
1766
    /**
1767
     * Send a mail of the order to the client (and another to the admin).
1768
     *
1769
     * @param string         $emailClassName       - (optional) template to be used ...
1770
     * @param string         $subject              - (optional) subject for the email
1771
     * @param string         $message              - (optional) message to be added with the email
1772
     * @param bool           $resend               - (optional) can it be sent twice?
1773
     * @param bool | string  $adminOnlyOrToEmail   - (optional) sends the email to the ADMIN ONLY, if you provide an email, it will go to the email...
1774
     *
1775
     * @return bool TRUE for success, FALSE for failure (not tested)
1776
     */
1777
    protected function prepareAndSendEmail(
1778
        $emailClassName,
1779
        $subject,
1780
        $message,
1781
        $resend = false,
1782
        $adminOnlyOrToEmail = false
1783
    ) {
1784
        $arrayData = $this->createReplacementArrayForEmail($subject, $message);
1785
        $from = Order_Email::get_from_email();
1786
        //why are we using this email and NOT the member.EMAIL?
1787
        //for historical reasons????
1788
        if ($adminOnlyOrToEmail) {
1789
            if (filter_var($adminOnlyOrToEmail, FILTER_VALIDATE_EMAIL)) {
1790
                $to = $adminOnlyOrToEmail;
1791
                // invalid e-mail address
1792
            } else {
1793
                $to = Order_Email::get_from_email();
1794
            }
1795
        } else {
1796
            $to = $this->getOrderEmail();
1797
        }
1798
        if ($from && $to) {
1799
            $email = new $emailClassName();
1800
            if (!(is_a($email, Object::getCustomClass('Email')))) {
1801
                user_error('No correct email class provided.', E_USER_ERROR);
1802
            }
1803
            $email->setFrom($from);
1804
            $email->setTo($to);
1805
            //we take the subject from the Array Data, just in case it has been adjusted.
1806
            $email->setSubject($arrayData->getField('Subject'));
1807
            //we also see if a CC and a BCC have been added
1808
            ;
1809
            if ($cc = $arrayData->getField('CC')) {
1810
                $email->setCc($cc);
1811
            }
1812
            if ($bcc = $arrayData->getField('BCC')) {
1813
                $email->setBcc($bcc);
1814
            }
1815
            $email->populateTemplate($arrayData);
1816
            // This might be called from within the CMS,
1817
            // so we need to restore the theme, just in case
1818
            // templates within the theme exist
1819
            Config::nest();
1820
            Config::inst()->update('SSViewer', 'theme_enabled', true);
1821
            $email->setOrder($this);
1822
            $email->setResend($resend);
1823
            $result = $email->send(null);
1824
            Config::unnest();
1825
            if (Director::isDev()) {
1826
                return true;
1827
            } else {
1828
                return $result;
1829
            }
1830
        }
1831
1832
        return false;
1833
    }
1834
1835
    /**
1836
     * returns the Data that can be used in the body of an order Email
1837
     * we add the subject here so that the subject, for example, can be added to the <title>
1838
     * of the email template.
1839
     * we add the subject here so that the subject, for example, can be added to the <title>
1840
     * of the email template.
1841
     *
1842
     * @param string $subject  - (optional) subject for email
1843
     * @param string $message  - (optional) the additional message
1844
     *
1845
     * @return ArrayData
1846
     *                   - Subject - EmailSubject
1847
     *                   - Message - specific message for this order
1848
     *                   - Message - custom message
1849
     *                   - OrderStepMessage - generic message for step
1850
     *                   - Order
1851
     *                   - EmailLogo
1852
     *                   - ShopPhysicalAddress
1853
     *                   - CurrentDateAndTime
1854
     *                   - BaseURL
1855
     *                   - CC
1856
     *                   - BCC
1857
     */
1858
    protected function createReplacementArrayForEmail($subject = '', $message = '')
1859
    {
1860
        $step = $this->MyStep();
1861
        $config = $this->EcomConfig();
1862
        $replacementArray = array();
1863
        //set subject
1864
        if ( ! $subject) {
1865
            $subject = $step->EmailSubject;
1866
        }
1867
        if( ! $message) {
1868
            $message = $step->CustomerMessage;
1869
        }
1870
        $subject = str_replace('[OrderNumber]', $this->ID, $subject);
1871
        //set other variables
1872
        $replacementArray['Subject'] = $subject;
1873
        $replacementArray['To'] = '';
1874
        $replacementArray['CC'] = '';
1875
        $replacementArray['BCC'] = '';
1876
        $replacementArray['OrderStepMessage'] = $message;
1877
        $replacementArray['Order'] = $this;
1878
        $replacementArray['EmailLogo'] = $config->EmailLogo();
1879
        $replacementArray['ShopPhysicalAddress'] = $config->ShopPhysicalAddress;
1880
        $replacementArray['CurrentDateAndTime'] = DBField::create_field('SS_Datetime', 'Now');
1881
        $replacementArray['BaseURL'] = Director::baseURL();
1882
        $arrayData = ArrayData::create($replacementArray);
1883
        $this->extend('updateReplacementArrayForEmail', $arrayData);
1884
1885
        return $arrayData;
1886
    }
1887
1888
/*******************************************************
1889
   * 6. ITEM MANAGEMENT
1890
*******************************************************/
1891
1892
    /**
1893
     * returns a list of Order Attributes by type.
1894
     *
1895
     * @param array | String $types
1896
     *
1897
     * @return ArrayList
1898
     */
1899
    public function getOrderAttributesByType($types)
1900
    {
1901
        if (!is_array($types) && is_string($types)) {
1902
            $types = array($types);
1903
        }
1904
        if (!is_array($al)) {
1905
            user_error('wrong parameter (types) provided in Order::getOrderAttributesByTypes');
1906
        }
1907
        $al = new ArrayList();
1908
        $items = $this->Items();
1909
        foreach ($items as $item) {
1910
            if (in_array($item->OrderAttributeType(), $types)) {
1911
                $al->push($item);
1912
            }
1913
        }
1914
        $modifiers = $this->Modifiers();
1915
        foreach ($modifiers as $modifier) {
1916
            if (in_array($modifier->OrderAttributeType(), $types)) {
1917
                $al->push($modifier);
1918
            }
1919
        }
1920
1921
        return $al;
1922
    }
1923
1924
    /**
1925
     * Returns the items of the order.
1926
     * Items are the order items (products) and NOT the modifiers (discount, tax, etc...).
1927
     *
1928
     * N. B. this method returns Order Items
1929
     * also see Buaybles
1930
1931
     *
1932
     * @param string filter - where statement to exclude certain items OR ClassName (e.g. 'TaxModifier')
1933
     *
1934
     * @return DataList (OrderItems)
1935
     */
1936
    public function Items($filterOrClassName = '')
1937
    {
1938
        if (!$this->exists()) {
1939
            $this->write();
1940
        }
1941
1942
        return $this->itemsFromDatabase($filterOrClassName);
1943
    }
1944
1945
    /**
1946
     * @alias function of Items
1947
     *
1948
     * N. B. this method returns Order Items
1949
     * also see Buaybles
1950
     *
1951
     * @param string filter - where statement to exclude certain items.
1952
     * @alias for Items
1953
     * @return DataList (OrderItems)
1954
     */
1955
    public function OrderItems($filterOrClassName = '')
1956
    {
1957
        return $this->Items($filterOrClassName);
1958
    }
1959
1960
    /**
1961
     * returns the buyables asscoiated with the order items.
1962
     *
1963
     * NB. this method retursn buyables
1964
     *
1965
     * @param string filter - where statement to exclude certain items.
1966
     *
1967
     * @return ArrayList (Buyables)
1968
     */
1969
    public function Buyables($filterOrClassName = '')
1970
    {
1971
        $items = $this->Items($filterOrClassName);
1972
        $arrayList = new ArrayList();
1973
        foreach ($items as $item) {
1974
            $arrayList->push($item->Buyable());
1975
        }
1976
1977
        return $arrayList;
1978
    }
1979
1980
    /**
1981
     * Return all the {@link OrderItem} instances that are
1982
     * available as records in the database.
1983
     *
1984
     * @param string filter - where statement to exclude certain items,
1985
     *   you can also pass a classname (e.g. MyOrderItem), in which case only this class will be returned (and any class extending your given class)
1986
     *
1987
     * @return DataList (OrderItems)
1988
     */
1989
    protected function itemsFromDatabase($filterOrClassName = '')
1990
    {
1991
        $className = 'OrderItem';
1992
        $extrafilter = '';
1993
        if ($filterOrClassName) {
1994
            if (class_exists($filterOrClassName)) {
1995
                $className = $filterOrClassName;
1996
            } else {
1997
                $extrafilter = " AND $filterOrClassName";
1998
            }
1999
        }
2000
2001
        return $className::get()->filter(array('OrderID' => $this->ID))->where($extrafilter);
2002
    }
2003
2004
    /**
2005
     * @alias for Modifiers
2006
     *
2007
     * @return DataList (OrderModifiers)
2008
     */
2009
    public function OrderModifiers()
2010
    {
2011
        return $this->Modifiers();
2012
    }
2013
2014
    /**
2015
     * Returns the modifiers of the order, if it hasn't been saved yet
2016
     * it returns the modifiers from session, if it has, it returns them
2017
     * from the DB entry. ONLY USE OUTSIDE ORDER.
2018
     *
2019
     * @param string filter - where statement to exclude certain items OR ClassName (e.g. 'TaxModifier')
2020
     *
2021
     * @return DataList (OrderModifiers)
2022
     */
2023
    public function Modifiers($filterOrClassName = '')
2024
    {
2025
        return $this->modifiersFromDatabase($filterOrClassName);
2026
    }
2027
2028
    /**
2029
     * Get all {@link OrderModifier} instances that are
2030
     * available as records in the database.
2031
     * NOTE: includes REMOVED Modifiers, so that they do not get added again...
2032
     *
2033
     * @param string filter - where statement to exclude certain items OR ClassName (e.g. 'TaxModifier')
2034
     *
2035
     * @return DataList (OrderModifiers)
2036
     */
2037
    protected function modifiersFromDatabase($filterOrClassName = '')
2038
    {
2039
        $className = 'OrderModifier';
2040
        $extrafilter = '';
2041
        if ($filterOrClassName) {
2042
            if (class_exists($filterOrClassName)) {
2043
                $className = $filterOrClassName;
2044
            } else {
2045
                $extrafilter = " AND $filterOrClassName";
2046
            }
2047
        }
2048
2049
        return $className::get()->where('"OrderAttribute"."OrderID" = '.$this->ID." $extrafilter");
2050
    }
2051
2052
    /**
2053
     * Calculates and updates all the order attributes.
2054
     *
2055
     * @param bool $recalculate - run it, even if it has run already
2056
     */
2057
    public function calculateOrderAttributes($recalculate = false)
2058
    {
2059
        if ($this->IsSubmitted()) {
2060
            //submitted orders are NEVER recalculated.
2061
            //they are set in stone.
2062
        } elseif (self::get_needs_recalculating($this->ID) || $recalculate) {
2063
            if ($this->StatusID || $this->TotalItems()) {
2064
                $this->ensureCorrectExchangeRate();
2065
                $this->calculateOrderItems($recalculate);
2066
                $this->calculateModifiers($recalculate);
2067
                $this->extend('onCalculateOrder');
2068
            }
2069
        }
2070
    }
2071
2072
    /**
2073
     * Calculates and updates all the product items.
2074
     *
2075
     * @param bool $recalculate - run it, even if it has run already
2076
     */
2077
    protected function calculateOrderItems($recalculate = false)
2078
    {
2079
        //check if order has modifiers already
2080
        //check /re-add all non-removable ones
2081
        //$start = microtime();
2082
        $orderItems = $this->itemsFromDatabase();
2083
        if ($orderItems->count()) {
2084
            foreach ($orderItems as $orderItem) {
2085
                if ($orderItem) {
2086
                    $orderItem->runUpdate($recalculate);
2087
                }
2088
            }
2089
        }
2090
        $this->extend('onCalculateOrderItems', $orderItems);
2091
    }
2092
2093
    /**
2094
     * Calculates and updates all the modifiers.
2095
     *
2096
     * @param bool $recalculate - run it, even if it has run already
2097
     */
2098
    protected function calculateModifiers($recalculate = false)
2099
    {
2100
        $createdModifiers = $this->modifiersFromDatabase();
2101
        if ($createdModifiers->count()) {
2102
            foreach ($createdModifiers as $modifier) {
2103
                if ($modifier) {
2104
                    $modifier->runUpdate($recalculate);
2105
                }
2106
            }
2107
        }
2108
        $this->extend('onCalculateModifiers', $createdModifiers);
2109
    }
2110
2111
    /**
2112
     * Returns the subtotal of the modifiers for this order.
2113
     * If a modifier appears in the excludedModifiers array, it is not counted.
2114
     *
2115
     * @param string|array $excluded               - Class(es) of modifier(s) to ignore in the calculation.
2116
     * @param bool         $stopAtExcludedModifier - when this flag is TRUE, we stop adding the modifiers when we reach an excluded modifier.
2117
     *
2118
     * @return float
2119
     */
2120
    public function ModifiersSubTotal($excluded = null, $stopAtExcludedModifier = false)
2121
    {
2122
        $total = 0;
2123
        $modifiers = $this->Modifiers();
2124
        if ($modifiers->count()) {
2125
            foreach ($modifiers as $modifier) {
2126
                if (!$modifier->IsRemoved()) { //we just double-check this...
2127
                    if (is_array($excluded) && in_array($modifier->ClassName, $excluded)) {
2128
                        if ($stopAtExcludedModifier) {
2129
                            break;
2130
                        }
2131
                        //do the next modifier
2132
                        continue;
2133
                    } elseif (is_string($excluded) && ($modifier->ClassName == $excluded)) {
2134
                        if ($stopAtExcludedModifier) {
2135
                            break;
2136
                        }
2137
                        //do the next modifier
2138
                        continue;
2139
                    }
2140
                    $total += $modifier->CalculationTotal();
2141
                }
2142
            }
2143
        }
2144
2145
        return $total;
2146
    }
2147
2148
    /**
2149
     * returns a modifier that is an instanceof the classname
2150
     * it extends.
2151
     *
2152
     * @param string $className: class name for the modifier
2153
     *
2154
     * @return DataObject (OrderModifier)
2155
     **/
2156
    public function RetrieveModifier($className)
2157
    {
2158
        $modifiers = $this->Modifiers();
2159
        if ($modifiers->count()) {
2160
            foreach ($modifiers as $modifier) {
2161
                if (is_a($modifier, Object::getCustomClass($className))) {
2162
                    return $modifier;
2163
                }
2164
            }
2165
        }
2166
    }
2167
2168
/*******************************************************
2169
   * 7. CRUD METHODS (e.g. canView, canEdit, canDelete, etc...)
2170
*******************************************************/
2171
2172
    /**
2173
     * @param Member $member
2174
     *
2175
     * @return DataObject (Member)
2176
     **/
2177
     //TODO: please comment why we make use of this function
2178
    protected function getMemberForCanFunctions(Member $member = null)
2179
    {
2180
        if (!$member) {
2181
            $member = Member::currentUser();
2182
        }
2183
        if (!$member) {
2184
            $member = new Member();
2185
            $member->ID = 0;
2186
        }
2187
2188
        return $member;
2189
    }
2190
2191
    /**
2192
     * @param Member $member
0 ignored issues
show
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
2193
     *
2194
     * @return bool
2195
     **/
2196
    public function canCreate($member = null)
2197
    {
2198
        $member = $this->getMemberForCanFunctions($member);
2199
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>, but the function expects a object<Member>|integer.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2200
        if ($extended !== null) {
2201
            return $extended;
2202
        }
2203
        if ($member->exists()) {
2204
            return $member->IsShopAdmin();
2205
        }
2206
    }
2207
2208
    /**
2209
     * Standard SS method - can the current member view this order?
2210
     *
2211
     * @param Member $member
0 ignored issues
show
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
2212
     *
2213
     * @return bool
2214
     **/
2215
    public function canView($member = null)
2216
    {
2217
        if (!$this->exists()) {
2218
            return true;
2219
        }
2220
        $member = $this->getMemberForCanFunctions($member);
2221
        //check if this has been "altered" in any DataExtension
2222
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>, but the function expects a object<Member>|integer.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2322
        if ($extended !== null) {
2323
            return $extended;
2324
        }
2325
        if (Permission::checkMember($member, Config::inst()->get('EcommerceRole', 'admin_permission_code'))) {
2326
            return true;
2327
        }
2328
    }
2329
2330
    /**
2331
     * if we set canEdit to false then we
2332
     * can not see the child records
2333
     * Basically, you can edit when you can view and canEdit (even as a customer)
2334
     * Or if you are a Shop Admin you can always edit.
2335
     * Otherwise it is false...
2336
     *
2337
     * @param Member $member
0 ignored issues
show
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
2338
     *
2339
     * @return bool
2340
     **/
2341
    public function canEdit($member = null)
2342
    {
2343
        $member = $this->getMemberForCanFunctions($member);
2344
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>, but the function expects a object<Member>|integer.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2377
        if ($extended !== null) {
2378
            return $extended;
2379
        }
2380
        $submitErrors = $this->SubmitErrors();
2381
        if ($submitErrors && $submitErrors->count()) {
2382
            return false;
2383
        }
2384
2385
        return true;
2386
    }
2387
2388
    /**
2389
     * Can the order be submitted?
2390
     * this method can be used to stop an order from being submitted
2391
     * due to something not being completed or done.
2392
     *
2393
     * @see Order::SubmitErrors
2394
     *
2395
     * @param Member $member
0 ignored issues
show
Should the type for parameter $member not be null|Member?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
2396
     *
2397
     * @return bool
2398
     **/
2399
    public function canSubmit(Member $member = null)
2400
    {
2401
        $member = $this->getMemberForCanFunctions($member);
2402
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>, but the function expects a object<Member>|integer.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2403
        if ($extended !== null) {
2404
            return $extended;
2405
        }
2406
        if ($this->IsSubmitted()) {
2407
            return false;
2408
        }
2409
        $submitErrors = $this->SubmitErrors();
2410
        if ($submitErrors && $submitErrors->count()) {
2411
            return false;
2412
        }
2413
2414
        return true;
2415
    }
2416
2417
    /**
2418
     * Can a payment be made for this Order?
2419
     *
2420
     * @param Member $member
0 ignored issues
show
Should the type for parameter $member not be null|Member?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
2421
     *
2422
     * @return bool
2423
     **/
2424
    public function canPay(Member $member = null)
2425
    {
2426
        $member = $this->getMemberForCanFunctions($member);
2427
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>, but the function expects a object<Member>|integer.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2428
        if ($extended !== null) {
2429
            return $extended;
2430
        }
2431
        if ($this->IsPaid() || $this->IsCancelled() || $this->PaymentIsPending()) {
2432
            return false;
2433
        }
2434
2435
        return $this->MyStep()->CustomerCanPay;
2436
    }
2437
2438
    /**
2439
     * Can the given member cancel this order?
2440
     *
2441
     * @param Member $member
0 ignored issues
show
Should the type for parameter $member not be null|Member?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
2442
     *
2443
     * @return bool
2444
     **/
2445
    public function canCancel(Member $member = null)
2446
    {
2447
        //if it is already cancelled it can not be cancelled again
2448
        if ($this->CancelledByID) {
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...
2449
            return false;
2450
        }
2451
        $member = $this->getMemberForCanFunctions($member);
2452
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>, but the function expects a object<Member>|integer.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2453
        if ($extended !== null) {
2454
            return $extended;
2455
        }
2456
        if (EcommerceRole::current_member_can_process_orders($member)) {
2457
            return true;
2458
        }
2459
2460
        return $this->MyStep()->CustomerCanCancel && $this->canView($member);
2461
    }
2462
2463
    /**
2464
     * @param Member $member
0 ignored issues
show
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
2465
     *
2466
     * @return bool
2467
     **/
2468
    public function canDelete($member = null)
2469
    {
2470
        $member = $this->getMemberForCanFunctions($member);
2471
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>, but the function expects a object<Member>|integer.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2472
        if ($extended !== null) {
2473
            return $extended;
2474
        }
2475
        if ($this->IsSubmitted()) {
2476
            return false;
2477
        }
2478
        if (Permission::checkMember($member, Config::inst()->get('EcommerceRole', 'admin_permission_code'))) {
2479
            return true;
2480
        }
2481
2482
        return false;
2483
    }
2484
2485
    /**
2486
     * Returns all the order logs that the current member can view
2487
     * i.e. some order logs can only be viewed by the admin (e.g. suspected fraud orderlog).
2488
     *
2489
     * @return ArrayList (OrderStatusLogs)
2490
     **/
2491
    public function CanViewOrderStatusLogs()
2492
    {
2493
        $canViewOrderStatusLogs = new ArrayList();
2494
        $logs = $this->OrderStatusLogs();
2495
        foreach ($logs as $log) {
2496
            if ($log->canView()) {
2497
                $canViewOrderStatusLogs->push($log);
2498
            }
2499
        }
2500
2501
        return $canViewOrderStatusLogs;
2502
    }
2503
2504
    /**
2505
     * returns all the logs that can be viewed by the customer.
2506
     *
2507
     * @return ArrayList (OrderStausLogs)
2508
     */
2509
    public function CustomerViewableOrderStatusLogs()
2510
    {
2511
        $customerViewableOrderStatusLogs = new ArrayList();
2512
        $logs = $this->OrderStatusLogs();
2513
        if ($logs) {
2514
            foreach ($logs as $log) {
2515
                if (!$log->InternalUseOnly) {
2516
                    $customerViewableOrderStatusLogs->push($log);
2517
                }
2518
            }
2519
        }
2520
2521
        return $customerViewableOrderStatusLogs;
2522
    }
2523
2524
/*******************************************************
2525
   * 8. GET METHODS (e.g. Total, SubTotal, Title, etc...)
2526
*******************************************************/
2527
2528
    /**
2529
     * returns the email to be used for customer communication.
2530
     *
2531
     * @return string
2532
     */
2533
    public function OrderEmail()
2534
    {
2535
        return $this->getOrderEmail();
2536
    }
2537
    public function getOrderEmail()
2538
    {
2539
        $email = '';
2540
        if ($this->BillingAddressID && $this->BillingAddress()) {
2541
            $email = $this->BillingAddress()->Email;
2542
        }
2543
        if (! $email) {
2544
            if ($this->MemberID && $this->Member()) {
2545
                $email = $this->Member()->Email;
2546
            }
2547
        }
2548
        $extendedEmail = $this->extend('updateOrderEmail', $email);
2549
        if ($extendedEmail !== null && is_array($extendedEmail) && count($extendedEmail)) {
2550
            $email = implode(';', $extendedEmail);
2551
        }
2552
2553
        return $email;
2554
    }
2555
2556
    /**
2557
     * Returns true if there is a prink or email link.
2558
     *
2559
     * @return bool
2560
     */
2561
    public function HasPrintOrEmailLink()
2562
    {
2563
        return $this->EmailLink() || $this->PrintLink();
2564
    }
2565
2566
    /**
2567
     * returns the absolute link to the order that can be used in the customer communication (email).
2568
     *
2569
     * @return string
2570
     */
2571
    public function EmailLink($type = 'Order_StatusEmail')
2572
    {
2573
        return $this->getEmailLink();
2574
    }
2575
    public function getEmailLink($type = 'Order_StatusEmail')
2576
    {
2577
        if (!isset($_REQUEST['print'])) {
2578
            if ($this->IsSubmitted()) {
2579
                return Director::AbsoluteURL(OrderConfirmationPage::get_email_link($this->ID, $this->MyStep()->getEmailClassName(), $actuallySendEmail = true));
2580
            }
2581
        }
2582
    }
2583
2584
    /**
2585
     * returns the absolute link to the order for printing.
2586
     *
2587
     * @return string
0 ignored issues
show
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2588
     */
2589
    public function PrintLink()
2590
    {
2591
        return $this->getPrintLink();
2592
    }
2593
    public function getPrintLink()
2594
    {
2595
        if (!isset($_REQUEST['print'])) {
2596
            if ($this->IsSubmitted()) {
2597
                return Director::AbsoluteURL(OrderConfirmationPage::get_order_link($this->ID)).'?print=1';
2598
            }
2599
        }
2600
    }
2601
2602
    /**
2603
     * returns the absolute link to the order for printing.
2604
     *
2605
     * @return string
0 ignored issues
show
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2606
     */
2607
    public function PackingSlipLink()
2608
    {
2609
        return $this->getPackingSlipLink();
2610
    }
2611
    public function getPackingSlipLink()
2612
    {
2613
        if ($this->IsSubmitted()) {
2614
            return Director::AbsoluteURL(OrderConfirmationPage::get_order_link($this->ID)).'?packingslip=1';
2615
        }
2616
    }
2617
2618
    /**
2619
     * returns the absolute link that the customer can use to retrieve the email WITHOUT logging in.
2620
     *
2621
     * @return string
2622
     */
2623
    public function RetrieveLink()
2624
    {
2625
        return $this->getRetrieveLink();
2626
    }
2627
2628
    public function getRetrieveLink()
2629
    {
2630
        //important to recalculate!
2631
        if ($this->IsSubmitted($recalculate = true)) {
2632
            //add session ID if not added yet...
2633
            if (!$this->SessionID) {
2634
                $this->write();
2635
            }
2636
2637
            return Director::AbsoluteURL(OrderConfirmationPage::find_link()).'retrieveorder/'.$this->SessionID.'/'.$this->ID.'/';
2638
        } else {
2639
            return Director::AbsoluteURL('/shoppingcart/loadorder/'.$this->ID.'/');
2640
        }
2641
    }
2642
2643
    public function ShareLink()
2644
    {
2645
        return $this->getShareLink();
2646
    }
2647
2648
    public function getShareLink()
2649
    {
2650
        $orderItems = $this->itemsFromDatabase();
2651
        $action = 'share';
2652
        $array = array();
2653
        foreach ($orderItems as $orderItem) {
2654
            $array[] = implode(
2655
                ',',
2656
                array(
2657
                    $orderItem->BuyableClassName,
2658
                    $orderItem->BuyableID,
2659
                    $orderItem->Quantity
2660
                )
2661
            );
2662
        }
2663
2664
        return Director::AbsoluteURL(CartPage::find_link($action.'/'.implode('-', $array)));
2665
    }
2666
2667
    /**
2668
     * @alias for getFeedbackLink
2669
     * @return string
0 ignored issues
show
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2670
     */
2671
    public function FeedbackLink()
2672
    {
2673
        return $this->getFeedbackLink();
2674
    }
2675
2676
    /**
2677
     * @return string | null
2678
     */
2679
    public function getFeedbackLink()
2680
    {
2681
        $orderConfirmationPage = OrderConfirmationPage::get()->first();
2682
        if($orderConfirmationPage->IsFeedbackEnabled) {
2683
2684
            return Director::AbsoluteURL($this->getRetrieveLink()).'#OrderForm_Feedback_FeedbackForm';
2685
        }
2686
    }
2687
2688
    /**
2689
     * link to delete order.
2690
     *
2691
     * @return string
2692
     */
2693
    public function DeleteLink()
2694
    {
2695
        return $this->getDeleteLink();
2696
    }
2697
    public function getDeleteLink()
2698
    {
2699
        if ($this->canDelete()) {
2700
            return ShoppingCart_Controller::delete_order_link($this->ID);
2701
        } else {
2702
            return '';
2703
        }
2704
    }
2705
2706
    /**
2707
     * link to copy order.
2708
     *
2709
     * @return string
0 ignored issues
show
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2710
     */
2711
    public function CopyOrderLink()
2712
    {
2713
        return $this->getCopyOrderLink();
2714
    }
2715
    public function getCopyOrderLink()
2716
    {
2717
        if ($this->canView() && $this->IsSubmitted()) {
2718
            return ShoppingCart_Controller::copy_order_link($this->ID);
2719
        } else {
2720
            return '';
2721
        }
2722
    }
2723
2724
    /**
2725
     * A "Title" for the order, which summarises the main details (date, and customer) in a string.
2726
     *
2727
     * @param string $dateFormat  - e.g. "D j M Y, G:i T"
2728
     * @param bool   $includeName - e.g. by Mr Johnson
2729
     *
2730
     * @return string
2731
     **/
2732
    public function Title($dateFormat = null, $includeName = false)
2733
    {
2734
        return $this->getTitle($dateFormat, $includeName);
2735
    }
2736
    public function getTitle($dateFormat = null, $includeName = false)
2737
    {
2738
        if ($this->exists()) {
2739
            if ($dateFormat === null) {
2740
                $dateFormat = EcommerceConfig::get('Order', 'date_format_for_title');
2741
            }
2742
            if ($includeName === null) {
2743
                $includeName = EcommerceConfig::get('Order', 'include_customer_name_in_title');
2744
            }
2745
            $title = $this->i18n_singular_name()." #".number_format($this->ID);
2746
            if ($dateFormat) {
2747
                if ($submissionLog = $this->SubmissionLog()) {
2748
                    $dateObject = $submissionLog->dbObject('Created');
2749
                    $placed = _t('Order.PLACED', 'placed');
2750
                } else {
2751
                    $dateObject = $this->dbObject('Created');
2752
                    $placed = _t('Order.STARTED', 'started');
2753
                }
2754
                $title .= ' - '.$placed.' '.$dateObject->Format($dateFormat);
2755
            }
2756
            $name = '';
2757
            if ($this->CancelledByID) {
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...
2758
                $name = ' - '._t('Order.CANCELLED', 'CANCELLED');
2759
            }
2760
            if ($includeName) {
2761
                $by = _t('Order.BY', 'by');
2762
                if (!$name) {
2763
                    if ($this->BillingAddressID) {
2764
                        if ($billingAddress = $this->BillingAddress()) {
2765
                            $name = ' - '.$by.' '.$billingAddress->Prefix.' '.$billingAddress->FirstName.' '.$billingAddress->Surname;
2766
                        }
2767
                    }
2768
                }
2769
                if (!$name) {
2770
                    if ($this->MemberID) {
2771
                        if ($member = $this->Member()) {
2772
                            if ($member->exists()) {
2773
                                if ($memberName = $member->getName()) {
2774
                                    if (!trim($memberName)) {
2775
                                        $memberName = _t('Order.ANONYMOUS', 'anonymous');
2776
                                    }
2777
                                    $name = ' - '.$by.' '.$memberName;
2778
                                }
2779
                            }
2780
                        }
2781
                    }
2782
                }
2783
            }
2784
            $title .= $name;
2785
        } else {
2786
            $title = _t('Order.NEW', 'New').' '.$this->i18n_singular_name();
2787
        }
2788
        $extendedTitle = $this->extend('updateTitle', $title);
2789
        if ($extendedTitle !== null && is_array($extendedTitle) && count($extendedTitle)) {
2790
            $title = implode('; ', $extendedTitle);
2791
        }
2792
2793
        return $title;
2794
    }
2795
2796
    /**
2797
     * Returns the subtotal of the items for this order.
2798
     *
2799
     * @return float
0 ignored issues
show
Should the return type not be integer?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2800
     */
2801
    public function SubTotal()
2802
    {
2803
        return $this->getSubTotal();
2804
    }
2805
    public function getSubTotal()
2806
    {
2807
        $result = 0;
2808
        $items = $this->Items();
2809
        if ($items->count()) {
2810
            foreach ($items as $item) {
2811
                if (is_a($item, Object::getCustomClass('OrderAttribute'))) {
2812
                    $result += $item->Total();
2813
                }
2814
            }
2815
        }
2816
2817
        return $result;
2818
    }
2819
2820
    /**
2821
     * @return Currency (DB Object)
2822
     **/
2823
    public function SubTotalAsCurrencyObject()
2824
    {
2825
        return DBField::create_field('Currency', $this->SubTotal());
2826
    }
2827
2828
    /**
2829
     * @return Money
2830
     **/
2831
    public function SubTotalAsMoney()
2832
    {
2833
        return $this->getSubTotalAsMoney();
2834
    }
2835
    public function getSubTotalAsMoney()
2836
    {
2837
        return EcommerceCurrency::get_money_object_from_order_currency($this->SubTotal(), $this);
2838
    }
2839
2840
    /**
2841
     * @param string|array $excluded               - Class(es) of modifier(s) to ignore in the calculation.
0 ignored issues
show
Should the type for parameter $excluded not be string|array|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
2842
     * @param bool         $stopAtExcludedModifier - when this flag is TRUE, we stop adding the modifiers when we reach an excluded modifier.
2843
     *
2844
     * @return Currency (DB Object)
2845
     **/
2846
    public function ModifiersSubTotalAsCurrencyObject($excluded = null, $stopAtExcludedModifier = false)
2847
    {
2848
        return DBField::create_field('Currency', $this->ModifiersSubTotal($excluded, $stopAtExcludedModifier));
2849
    }
2850
2851
    /**
2852
     * @param string|array $excluded               - Class(es) of modifier(s) to ignore in the calculation.
2853
     * @param bool         $stopAtExcludedModifier - when this flag is TRUE, we stop adding the modifiers when we reach an excluded modifier.
2854
     *
2855
     * @return Money (DB Object)
2856
     **/
2857
    public function ModifiersSubTotalAsMoneyObject($excluded = null, $stopAtExcludedModifier = false)
2858
    {
2859
        return EcommerceCurrency::get_money_object_from_order_currency($this->ModifiersSubTotal($excluded, $stopAtExcludedModifier), $this);
2860
    }
2861
2862
    /**
2863
     * Returns the total cost of an order including the additional charges or deductions of its modifiers.
2864
     *
2865
     * @return float
2866
     */
2867
    public function Total()
2868
    {
2869
        return $this->getTotal();
2870
    }
2871
    public function getTotal()
2872
    {
2873
        return $this->SubTotal() + $this->ModifiersSubTotal();
2874
    }
2875
2876
    /**
2877
     * @return Currency (DB Object)
2878
     **/
2879
    public function TotalAsCurrencyObject()
2880
    {
2881
        return DBField::create_field('Currency', $this->Total());
2882
    }
2883
2884
    /**
2885
     * @return Money
2886
     **/
2887
    public function TotalAsMoney()
2888
    {
2889
        return $this->getTotalAsMoney();
2890
    }
2891
    public function getTotalAsMoney()
2892
    {
2893
        return EcommerceCurrency::get_money_object_from_order_currency($this->Total(), $this);
2894
    }
2895
2896
    /**
2897
     * Checks to see if any payments have been made on this order
2898
     * and if so, subracts the payment amount from the order.
2899
     *
2900
     * @return float
2901
     **/
2902
    public function TotalOutstanding()
2903
    {
2904
        return $this->getTotalOutstanding();
2905
    }
2906
    public function getTotalOutstanding()
2907
    {
2908
        if ($this->IsSubmitted()) {
2909
            $total = $this->Total();
2910
            $paid = $this->TotalPaid();
2911
            $outstanding = $total - $paid;
2912
            $maxDifference = EcommerceConfig::get('Order', 'maximum_ignorable_sales_payments_difference');
2913
            if (abs($outstanding) < $maxDifference) {
2914
                $outstanding = 0;
2915
            }
2916
2917
            return floatval($outstanding);
2918
        } else {
2919
            return 0;
2920
        }
2921
    }
2922
2923
    /**
2924
     * @return Currency (DB Object)
2925
     **/
2926
    public function TotalOutstandingAsCurrencyObject()
2927
    {
2928
        return DBField::create_field('Currency', $this->TotalOutstanding());
2929
    }
2930
2931
    /**
2932
     * @return Money
2933
     **/
2934
    public function TotalOutstandingAsMoney()
2935
    {
2936
        return $this->getTotalOutstandingAsMoney();
2937
    }
2938
    public function getTotalOutstandingAsMoney()
2939
    {
2940
        return EcommerceCurrency::get_money_object_from_order_currency($this->TotalOutstanding(), $this);
2941
    }
2942
2943
    /**
2944
     * @return float
2945
     */
2946
    public function TotalPaid()
2947
    {
2948
        return $this->getTotalPaid();
2949
    }
2950
    public function getTotalPaid()
2951
    {
2952
        $paid = 0;
2953
        if ($payments = $this->Payments()) {
2954
            foreach ($payments as $payment) {
2955
                if ($payment->Status == 'Success') {
2956
                    $paid += $payment->Amount->getAmount();
2957
                }
2958
            }
2959
        }
2960
        $reverseExchange = 1;
2961
        if ($this->ExchangeRate && $this->ExchangeRate != 1) {
2962
            $reverseExchange = 1 / $this->ExchangeRate;
2963
        }
2964
2965
        return $paid * $reverseExchange;
2966
    }
2967
2968
    /**
2969
     * @return Currency (DB Object)
2970
     **/
2971
    public function TotalPaidAsCurrencyObject()
2972
    {
2973
        return DBField::create_field('Currency', $this->TotalPaid());
2974
    }
2975
2976
    /**
2977
     * @return Money
2978
     **/
2979
    public function TotalPaidAsMoney()
2980
    {
2981
        return $this->getTotalPaidAsMoney();
2982
    }
2983
    public function getTotalPaidAsMoney()
2984
    {
2985
        return EcommerceCurrency::get_money_object_from_order_currency($this->TotalPaid(), $this);
2986
    }
2987
2988
    /**
2989
     * returns the total number of OrderItems (not modifiers).
2990
     * This is meant to run as fast as possible to quickly check
2991
     * if there is anything in the cart.
2992
     *
2993
     * @param bool $recalculate - do we need to recalculate (value is retained during lifetime of Object)
2994
     *
2995
     * @return int
2996
     **/
2997
    public function TotalItems($recalculate = false)
2998
    {
2999
        return $this->getTotalItems($recalculate);
3000
    }
3001
    public function getTotalItems($recalculate = false)
3002
    {
3003
        if ($this->totalItems === null || $recalculate) {
3004
            $this->totalItems = OrderItem::get()
3005
                ->where('"OrderAttribute"."OrderID" = '.$this->ID.' AND "OrderItem"."Quantity" > 0')
3006
                ->count();
3007
        }
3008
3009
        return $this->totalItems;
3010
    }
3011
3012
    /**
3013
     * Little shorthand.
3014
     *
3015
     * @param bool $recalculate
3016
     *
3017
     * @return bool
3018
     **/
3019
    public function MoreThanOneItemInCart($recalculate = false)
3020
    {
3021
        return $this->TotalItems($recalculate) > 1 ? true : false;
3022
    }
3023
3024
    /**
3025
     * returns the total number of OrderItems (not modifiers) times their respectective quantities.
3026
     *
3027
     * @param bool $recalculate - force recalculation
3028
     *
3029
     * @return float
3030
     **/
3031
    public function TotalItemsTimesQuantity($recalculate = false)
3032
    {
3033
        return $this->getTotalItemsTimesQuantity($recalculate);
3034
    }
3035
    public function getTotalItemsTimesQuantity($recalculate = false)
3036
    {
3037
        if ($this->totalItemsTimesQuantity === null || $recalculate) {
3038
            //to do, why do we check if you can edit ????
3039
            $this->totalItemsTimesQuantity = DB::query('
3040
                SELECT SUM("OrderItem"."Quantity")
3041
                FROM "OrderItem"
3042
                    INNER JOIN "OrderAttribute" ON "OrderAttribute"."ID" = "OrderItem"."ID"
3043
                WHERE
3044
                    "OrderAttribute"."OrderID" = '.$this->ID.'
3045
                    AND "OrderItem"."Quantity" > 0'
3046
            )->value();
3047
        }
3048
3049
        return $this->totalItemsTimesQuantity - 0;
3050
    }
3051
3052
    /**
3053
     *
3054
     * @return string (country code)
3055
     **/
3056
    public function Country()
3057
    {
3058
        return $this->getCountry();
3059
    }
3060
3061
    /**
3062
    * Returns the country code for the country that applies to the order.
3063
    * @alias  for getCountry
3064
    *
3065
    * @return string - country code e.g. NZ
3066
     */
3067
    public function getCountry()
3068
    {
3069
        $countryCodes = array(
3070
            'Billing' => '',
3071
            'Shipping' => '',
3072
        );
3073
        $code = null;
3074
        if ($this->BillingAddressID) {
3075
            $billingAddress = BillingAddress::get()->byID($this->BillingAddressID);
3076
            if ($billingAddress) {
3077
                if ($billingAddress->Country) {
3078
                    $countryCodes['Billing'] = $billingAddress->Country;
3079
                }
3080
            }
3081
        }
3082
        if ($this->ShippingAddressID && $this->UseShippingAddress) {
3083
            $shippingAddress = ShippingAddress::get()->byID($this->ShippingAddressID);
3084
            if ($shippingAddress) {
3085
                if ($shippingAddress->ShippingCountry) {
3086
                    $countryCodes['Shipping'] = $shippingAddress->ShippingCountry;
3087
                }
3088
            }
3089
        }
3090
        if (
3091
            (EcommerceConfig::get('OrderAddress', 'use_shipping_address_for_main_region_and_country') && $countryCodes['Shipping'])
3092
            ||
3093
            (!$countryCodes['Billing'] && $countryCodes['Shipping'])
3094
        ) {
3095
            $code = $countryCodes['Shipping'];
3096
        } elseif ($countryCodes['Billing']) {
3097
            $code = $countryCodes['Billing'];
3098
        } else {
3099
            $code = EcommerceCountry::get_country_from_ip();
3100
        }
3101
3102
        return $code;
3103
    }
3104
3105
    /**
3106
     * @alias for getFullNameCountry
3107
     *
3108
     * @return string - country name
3109
     **/
3110
    public function FullNameCountry()
3111
    {
3112
        return $this->getFullNameCountry();
3113
    }
3114
3115
    /**
3116
     * returns name of coutry.
3117
     *
3118
     * @return string - country name
3119
     **/
3120
    public function getFullNameCountry()
3121
    {
3122
        return EcommerceCountry::find_title($this->Country());
3123
    }
3124
3125
    /**
3126
     * @alis for getExpectedCountryName
3127
     * @return string - country name
3128
     **/
3129
    public function ExpectedCountryName()
3130
    {
3131
        return $this->getExpectedCountryName();
3132
    }
3133
3134
    /**
3135
     * returns name of coutry that we expect the customer to have
3136
     * This takes into consideration more than just what has been entered
3137
     * for example, it looks at GEO IP.
3138
     *
3139
     * @todo: why do we dont return a string IF there is only one item.
3140
     *
3141
     * @return string - country name
3142
     **/
3143
    public function getExpectedCountryName()
3144
    {
3145
        return EcommerceCountry::find_title(EcommerceCountry::get_country(false, $this->ID));
3146
    }
3147
3148
    /**
3149
     * return the title of the fixed country (if any).
3150
     *
3151
     * @return string | empty string
3152
     **/
3153
    public function FixedCountry()
3154
    {
3155
        return $this->getFixedCountry();
3156
    }
3157
    public function getFixedCountry()
3158
    {
3159
        $code = EcommerceCountry::get_fixed_country_code();
3160
        if ($code) {
3161
            return EcommerceCountry::find_title($code);
3162
        }
3163
3164
        return '';
3165
    }
3166
3167
    /**
3168
     * Returns the region that applies to the order.
3169
     * we check both billing and shipping, in case one of them is empty.
3170
     *
3171
     * @return DataObject | Null (EcommerceRegion)
3172
     **/
3173
    public function Region()
3174
    {
3175
        return $this->getRegion();
3176
    }
3177
    public function getRegion()
3178
    {
3179
        $regionIDs = array(
3180
            'Billing' => 0,
3181
            'Shipping' => 0,
3182
        );
3183
        if ($this->BillingAddressID) {
3184
            if ($billingAddress = $this->BillingAddress()) {
3185
                if ($billingAddress->RegionID) {
3186
                    $regionIDs['Billing'] = $billingAddress->RegionID;
3187
                }
3188
            }
3189
        }
3190
        if ($this->CanHaveShippingAddress()) {
3191
            if ($this->ShippingAddressID) {
3192
                if ($shippingAddress = $this->ShippingAddress()) {
3193
                    if ($shippingAddress->ShippingRegionID) {
3194
                        $regionIDs['Shipping'] = $shippingAddress->ShippingRegionID;
3195
                    }
3196
                }
3197
            }
3198
        }
3199
        if (count($regionIDs)) {
3200
            //note the double-check with $this->CanHaveShippingAddress() and get_use_....
3201
            if ($this->CanHaveShippingAddress() && EcommerceConfig::get('OrderAddress', 'use_shipping_address_for_main_region_and_country') && $regionIDs['Shipping']) {
3202
                return EcommerceRegion::get()->byID($regionIDs['Shipping']);
3203
            } else {
3204
                return EcommerceRegion::get()->byID($regionIDs['Billing']);
3205
            }
3206
        } else {
3207
            return EcommerceRegion::get()->byID(EcommerceRegion::get_region_from_ip());
3208
        }
3209
    }
3210
3211
    /**
3212
     * Casted variable
3213
     * Currency is not the same as the standard one?
3214
     *
3215
     * @return bool
3216
     **/
3217
    public function HasAlternativeCurrency()
3218
    {
3219
        return $this->getHasAlternativeCurrency();
3220
    }
3221
    public function getHasAlternativeCurrency()
3222
    {
3223
        if ($currency = $this->CurrencyUsed()) {
3224
            if ($currency->IsDefault()) {
3225
                return false;
3226
            } else {
3227
                return true;
3228
            }
3229
        } else {
3230
            return false;
3231
        }
3232
    }
3233
3234
    /**
3235
     * Makes sure exchange rate is updated and maintained before order is submitted
3236
     * This method is public because it could be called from a shopping Cart Object.
3237
     **/
3238
    public function EnsureCorrectExchangeRate()
3239
    {
3240
        if (!$this->IsSubmitted()) {
3241
            $oldExchangeRate = $this->ExchangeRate;
3242
            if ($currency = $this->CurrencyUsed()) {
3243
                if ($currency->IsDefault()) {
3244
                    $this->ExchangeRate = 0;
3245
                } else {
3246
                    $this->ExchangeRate = $currency->getExchangeRate();
3247
                }
3248
            } else {
3249
                $this->ExchangeRate = 0;
3250
            }
3251
            if ($this->ExchangeRate != $oldExchangeRate) {
3252
                $this->write();
3253
            }
3254
        }
3255
    }
3256
3257
    /**
3258
     * speeds up processing by storing the IsSubmitted value
3259
     * we start with -1 to know if it has been requested before.
3260
     *
3261
     * @var bool
3262
     */
3263
    protected $_isSubmittedTempVar = -1;
3264
3265
    /**
3266
     * Casted variable - has the order been submitted?
3267
     * alias
3268
     * @param bool $recalculate
3269
     *
3270
     * @return bool
3271
     **/
3272
    public function IsSubmitted($recalculate = true)
3273
    {
3274
        return $this->getIsSubmitted($recalculate);
3275
    }
3276
3277
    /**
3278
     * Casted variable - has the order been submitted?
3279
     *
3280
     * @param bool $recalculate
3281
     *
3282
     * @return bool
3283
     **/
3284
    public function getIsSubmitted($recalculate = false)
3285
    {
3286
        if ($this->_isSubmittedTempVar === -1 || $recalculate) {
3287
            if ($this->SubmissionLog()) {
3288
                $this->_isSubmittedTempVar = true;
3289
            } else {
3290
                $this->_isSubmittedTempVar = false;
3291
            }
3292
        }
3293
3294
        return $this->_isSubmittedTempVar;
3295
    }
3296
3297
    /**
3298
     *
3299
     *
3300
     * @return bool
3301
     */
3302
    public function IsArchived()
3303
    {
3304
        $lastStep = OrderStep::get()->Last();
3305
        if ($lastStep) {
3306
            if ($lastStep->ID == $this->StatusID) {
3307
                return true;
3308
            }
3309
        }
3310
        return false;
3311
    }
3312
3313
    /**
3314
     * Submission Log for this Order (if any).
3315
     *
3316
     * @return Submission Log (OrderStatusLog_Submitted) | Null
3317
     **/
3318
    public function SubmissionLog()
3319
    {
3320
        $className = EcommerceConfig::get('OrderStatusLog', 'order_status_log_class_used_for_submitting_order');
3321
3322
        return $className::get()
3323
            ->Filter(array('OrderID' => $this->ID))
3324
            ->Last();
3325
    }
3326
3327
    /**
3328
     * @return int
3329
     */
3330
    public function SecondsSinceBeingSubmitted()
3331
    {
3332
        if ($submissionLog = $this->SubmissionLog()) {
3333
            return time() - strtotime($submissionLog->Created);
3334
        } else {
3335
            return 0;
3336
        }
3337
    }
3338
3339
    /**
3340
     * if the order can not be submitted,
3341
     * then the reasons why it can not be submitted
3342
     * will be returned by this method.
3343
     *
3344
     * @see Order::canSubmit
3345
     *
3346
     * @return ArrayList | null
3347
     */
3348
    public function SubmitErrors()
3349
    {
3350
        $al = null;
3351
        $extendedSubmitErrors = $this->extend('updateSubmitErrors');
3352
        if ($extendedSubmitErrors !== null && is_array($extendedSubmitErrors) && count($extendedSubmitErrors)) {
3353
            $al = ArrayList::create();
3354
            foreach ($extendedSubmitErrors as $returnResultArray) {
3355
                foreach ($returnResultArray as $issue) {
3356
                    if ($issue) {
3357
                        $al->push(ArrayData::create(array("Title" => $issue)));
3358
                    }
3359
                }
3360
            }
3361
        }
3362
        return $al;
3363
    }
3364
3365
    /**
3366
     * Casted variable - has the order been submitted?
3367
     *
3368
     * @param bool $withDetail
3369
     *
3370
     * @return string
3371
     **/
3372
    public function CustomerStatus($withDetail = true)
3373
    {
3374
        return $this->getCustomerStatus($withDetail);
3375
    }
3376
    public function getCustomerStatus($withDetail = true)
3377
    {
3378
        $str = '';
3379
        if ($this->MyStep()->ShowAsUncompletedOrder) {
3380
            $str = _t('Order.UNCOMPLETED', 'Uncompleted');
3381
        } elseif ($this->MyStep()->ShowAsInProcessOrder) {
3382
            $str = _t('Order.IN_PROCESS', 'In Process');
3383
        } elseif ($this->MyStep()->ShowAsCompletedOrder) {
3384
            $str = _t('Order.COMPLETED', 'Completed');
3385
        }
3386
        if ($withDetail) {
3387
            if (!$this->HideStepFromCustomer) {
3388
                $str .= ' ('.$this->MyStep()->Name.')';
3389
            }
3390
        }
3391
3392
        return $str;
3393
    }
3394
3395
    /**
3396
     * Casted variable - does the order have a potential shipping address?
3397
     *
3398
     * @return bool
3399
     **/
3400
    public function CanHaveShippingAddress()
3401
    {
3402
        return $this->getCanHaveShippingAddress();
3403
    }
3404
    public function getCanHaveShippingAddress()
3405
    {
3406
        return EcommerceConfig::get('OrderAddress', 'use_separate_shipping_address');
3407
    }
3408
3409
    /**
3410
     * returns the link to view the Order
3411
     * WHY NOT CHECKOUT PAGE: first we check for cart page.
3412
     *
3413
     * @return CartPage | Null
3414
     */
3415
    public function DisplayPage()
3416
    {
3417
        if ($this->MyStep() && $this->MyStep()->AlternativeDisplayPage()) {
3418
            $page = $this->MyStep()->AlternativeDisplayPage();
3419
        } elseif ($this->IsSubmitted()) {
3420
            $page = OrderConfirmationPage::get()->First();
3421
        } else {
3422
            $page = CartPage::get()
3423
                ->Filter(array('ClassName' => 'CartPage'))
3424
                ->First();
3425
            if (!$page) {
3426
                $page = CheckoutPage::get()->First();
3427
            }
3428
        }
3429
3430
        return $page;
3431
    }
3432
3433
    /**
3434
     * returns the link to view the Order
3435
     * WHY NOT CHECKOUT PAGE: first we check for cart page.
3436
     * If a cart page has been created then we refer through to Cart Page.
3437
     * Otherwise it will default to the checkout page.
3438
     *
3439
     * @param string $action - any action that should be added to the link.
3440
     *
3441
     * @return String(URLSegment)
3442
     */
3443
    public function Link($action = null)
3444
    {
3445
        $page = $this->DisplayPage();
3446
        if ($page) {
3447
            return $page->getOrderLink($this->ID, $action);
3448
        } else {
3449
            user_error('A Cart / Checkout Page + an Order Confirmation Page needs to be setup for the e-commerce module to work.', E_USER_NOTICE);
3450
            $page = ErrorPage::get()
3451
                ->Filter(array('ErrorCode' => '404'))
3452
                ->First();
3453
            if ($page) {
3454
                return $page->Link();
3455
            }
3456
        }
3457
    }
3458
3459
    /**
3460
     * Returns to link to access the Order's API.
3461
     *
3462
     * @param string $version
3463
     * @param string $extension
3464
     *
3465
     * @return String(URL)
3466
     */
3467
    public function APILink($version = 'v1', $extension = 'xml')
3468
    {
3469
        return Director::AbsoluteURL("/api/ecommerce/$version/Order/".$this->ID."/.$extension");
3470
    }
3471
3472
    /**
3473
     * returns the link to finalise the Order.
3474
     *
3475
     * @return String(URLSegment)
3476
     */
3477
    public function CheckoutLink()
3478
    {
3479
        $page = CheckoutPage::get()->First();
3480
        if ($page) {
3481
            return $page->Link();
3482
        } else {
3483
            $page = ErrorPage::get()
3484
                ->Filter(array('ErrorCode' => '404'))
3485
                ->First();
3486
            if ($page) {
3487
                return $page->Link();
3488
            }
3489
        }
3490
    }
3491
3492
    /**
3493
     * Converts the Order into HTML, based on the Order Template.
3494
     *
3495
     * @return HTML Object
3496
     **/
3497
    public function ConvertToHTML()
3498
    {
3499
        Config::nest();
3500
        Config::inst()->update('SSViewer', 'theme_enabled', true);
3501
        $html = $this->renderWith('Order');
3502
        Config::unnest();
3503
        $html = preg_replace('/(\s)+/', ' ', $html);
3504
3505
        return DBField::create_field('HTMLText', $html);
3506
    }
3507
3508
    /**
3509
     * Converts the Order into a serialized string
3510
     * TO DO: check if this works and check if we need to use special sapphire serialization code.
3511
     *
3512
     * @return string - serialized object
3513
     **/
3514
    public function ConvertToString()
3515
    {
3516
        return serialize($this->addHasOneAndHasManyAsVariables());
3517
    }
3518
3519
    /**
3520
     * Converts the Order into a JSON object
3521
     * TO DO: check if this works and check if we need to use special sapphire JSON code.
3522
     *
3523
     * @return string -  JSON
3524
     **/
3525
    public function ConvertToJSON()
3526
    {
3527
        return json_encode($this->addHasOneAndHasManyAsVariables());
3528
    }
3529
3530
    /**
3531
     * returns itself wtih more data added as variables.
3532
     * We add has_one and has_many as variables like this: $this->MyHasOne_serialized = serialize($this->MyHasOne()).
3533
     *
3534
     * @return Order - with most important has one and has many items included as variables.
3535
     **/
3536
    protected function addHasOneAndHasManyAsVariables()
3537
    {
3538
        $object = clone $this;
3539
        $object->Member_serialized = serialize($this->Member());
3540
        $object->BillingAddress_serialized = serialize($this->BillingAddress());
3541
        $object->ShippingAddress_serialized = serialize($this->ShippingAddress());
3542
        $object->Attributes_serialized = serialize($this->Attributes());
3543
        $object->OrderStatusLogs_serialized = serialize($this->OrderStatusLogs());
3544
        $object->Payments_serialized = serialize($this->Payments());
3545
        $object->Emails_serialized = serialize($this->Emails());
3546
3547
        return $object;
3548
    }
3549
3550
/*******************************************************
3551
   * 9. TEMPLATE RELATED STUFF
3552
*******************************************************/
3553
3554
    /**
3555
     * returns the instance of EcommerceConfigAjax for use in templates.
3556
     * In templates, it is used like this:
3557
     * $EcommerceConfigAjax.TableID.
3558
     *
3559
     * @return EcommerceConfigAjax
3560
     **/
3561
    public function AJAXDefinitions()
3562
    {
3563
        return EcommerceConfigAjax::get_one($this);
3564
    }
3565
3566
    /**
3567
     * returns the instance of EcommerceDBConfig.
3568
     *
3569
     * @return EcommerceDBConfig
3570
     **/
3571
    public function EcomConfig()
3572
    {
3573
        return EcommerceDBConfig::current_ecommerce_db_config();
3574
    }
3575
3576
    /**
3577
     * Collects the JSON data for an ajax return of the cart.
3578
     *
3579
     * @param array $js
3580
     *
3581
     * @return array (for use in AJAX for JSON)
3582
     **/
3583
    public function updateForAjax(array $js)
3584
    {
3585
        $function = EcommerceConfig::get('Order', 'ajax_subtotal_format');
3586
        if (is_array($function)) {
3587
            list($function, $format) = $function;
3588
        }
3589
        $subTotal = $this->$function();
3590
        if (isset($format)) {
3591
            $subTotal = $subTotal->$format();
3592
            unset($format);
3593
        }
3594
        $function = EcommerceConfig::get('Order', 'ajax_total_format');
3595
        if (is_array($function)) {
3596
            list($function, $format) = $function;
3597
        }
3598
        $total = $this->$function();
3599
        if (isset($format)) {
3600
            $total = $total->$format();
3601
        }
3602
        $ajaxObject = $this->AJAXDefinitions();
3603
        $js[] = array(
3604
            't' => 'id',
3605
            's' => $ajaxObject->TableSubTotalID(),
3606
            'p' => 'innerHTML',
3607
            'v' => $subTotal,
3608
        );
3609
        $js[] = array(
3610
            't' => 'id',
3611
            's' => $ajaxObject->TableTotalID(),
3612
            'p' => 'innerHTML',
3613
            'v' => $total,
3614
        );
3615
        $js[] = array(
3616
            't' => 'class',
3617
            's' => $ajaxObject->TotalItemsClassName(),
3618
            'p' => 'innerHTML',
3619
            'v' => $this->TotalItems($recalculate = true),
3620
        );
3621
        $js[] = array(
3622
            't' => 'class',
3623
            's' => $ajaxObject->TotalItemsTimesQuantityClassName(),
3624
            'p' => 'innerHTML',
3625
            'v' => $this->TotalItemsTimesQuantity(),
3626
        );
3627
        $js[] = array(
3628
            't' => 'class',
3629
            's' => $ajaxObject->ExpectedCountryClassName(),
3630
            'p' => 'innerHTML',
3631
            'v' => $this->ExpectedCountryName(),
3632
        );
3633
3634
        return $js;
3635
    }
3636
3637
    /**
3638
     * @ToDO: move to more appropriate class
3639
     *
3640
     * @return float
3641
     **/
3642
    public function SubTotalCartValue()
3643
    {
3644
        return $this->SubTotal;
3645
    }
3646
3647
/*******************************************************
3648
   * 10. STANDARD SS METHODS (requireDefaultRecords, onBeforeDelete, etc...)
3649
*******************************************************/
3650
3651
    /**
3652
     *standard SS method.
3653
     **/
3654
    public function populateDefaults()
3655
    {
3656
        parent::populateDefaults();
3657
    }
3658
3659
    public function onBeforeWrite()
3660
    {
3661
        parent::onBeforeWrite();
3662
        if (! $this->getCanHaveShippingAddress()) {
3663
            $this->UseShippingAddress = false;
3664
        }
3665
        if (!$this->CurrencyUsedID) {
3666
            $this->CurrencyUsedID = EcommerceCurrency::default_currency_id();
3667
        }
3668
        if (!$this->SessionID) {
3669
            $generator = Injector::inst()->create('RandomGenerator');
3670
            $token = $generator->randomToken('sha1');
3671
            $this->SessionID = substr($token, 0, 32);
3672
        }
3673
    }
3674
3675
    /**
3676
     * standard SS method
3677
     * adds the ability to update order after writing it.
3678
     **/
3679
    public function onAfterWrite()
3680
    {
3681
        parent::onAfterWrite();
3682
        //crucial!
3683
        self::set_needs_recalculating(true, $this->ID);
3684
        // quick double-check
3685
        if ($this->IsCancelled() && ! $this->IsArchived()) {
3686
            $this->Archive($avoidWrites = true);
3687
        }
3688
        if ($this->IsSubmitted($recalculate = true)) {
3689
            //do nothing
3690
        } else {
3691
            if ($this->StatusID) {
3692
                $this->calculateOrderAttributes($recalculate = false);
3693
                if (EcommerceRole::current_member_is_shop_admin()) {
3694
                    if (isset($_REQUEST['SubmitOrderViaCMS'])) {
3695
                        $this->tryToFinaliseOrder();
3696
                        //just in case it writes again...
3697
                        unset($_REQUEST['SubmitOrderViaCMS']);
3698
                    }
3699
                }
3700
            }
3701
        }
3702
    }
3703
3704
    /**
3705
     *standard SS method.
3706
     *
3707
     * delete attributes, statuslogs, and payments
3708
     * THIS SHOULD NOT BE USED AS ORDERS SHOULD BE CANCELLED NOT DELETED
3709
     */
3710
    public function onBeforeDelete()
3711
    {
3712
        parent::onBeforeDelete();
3713
        if ($attributes = $this->Attributes()) {
3714
            foreach ($attributes as $attribute) {
3715
                $attribute->delete();
3716
                $attribute->destroy();
3717
            }
3718
        }
3719
3720
        //THE REST WAS GIVING ERRORS - POSSIBLY DUE TO THE FUNNY RELATIONSHIP (one-one, two times...)
3721
        /*
3722
        if($billingAddress = $this->BillingAddress()) {
3723
            if($billingAddress->exists()) {
3724
                $billingAddress->delete();
3725
                $billingAddress->destroy();
3726
            }
3727
        }
3728
        if($shippingAddress = $this->ShippingAddress()) {
3729
            if($shippingAddress->exists()) {
3730
                $shippingAddress->delete();
3731
                $shippingAddress->destroy();
3732
            }
3733
        }
3734
3735
        if($statuslogs = $this->OrderStatusLogs()){
3736
            foreach($statuslogs as $log){
3737
                $log->delete();
3738
                $log->destroy();
3739
            }
3740
        }
3741
        if($payments = $this->Payments()){
3742
            foreach($payments as $payment){
3743
                $payment->delete();
3744
                $payment->destroy();
3745
            }
3746
        }
3747
        if($emails = $this->Emails()) {
3748
            foreach($emails as $email){
3749
                $email->delete();
3750
                $email->destroy();
3751
            }
3752
        }
3753
        */
3754
    }
3755
3756
/*******************************************************
3757
   * 11. DEBUG
3758
*******************************************************/
3759
3760
    /**
3761
     * Debug helper method.
3762
     * Can be called from /shoppingcart/debug/.
3763
     *
3764
     * @return string
3765
     */
3766
    public function debug()
3767
    {
3768
        $this->calculateOrderAttributes(true);
3769
3770
        return EcommerceTaskDebugCart::debug_object($this);
3771
    }
3772
}
3773