Passed
Push — 1.3 ( 8d68c8...47b397 )
by Morven
04:30
created

Estimate::isLocked()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 4
c 2
b 0
f 0
nc 3
nop 0
dl 0
loc 9
rs 10
1
<?php
2
3
namespace SilverCommerce\OrdersAdmin\Model;
4
5
use DateTime;
6
use SilverStripe\i18n\i18n;
7
use SilverStripe\ORM\ArrayList;
8
use SilverStripe\ORM\DataObject;
9
use SilverStripe\View\ArrayData;
10
use SilverStripe\Forms\DateField;
11
use SilverStripe\Forms\FieldList;
12
use SilverStripe\Forms\TextField;
13
use SilverStripe\Security\Member;
14
use SilverStripe\Forms\HeaderField;
15
use SilverStripe\Control\Controller;
16
use SilverStripe\Core\Config\Config;
17
use SilverStripe\Forms\LiteralField;
18
use SilverStripe\Forms\DropdownField;
19
use SilverStripe\Forms\ReadonlyField;
20
use SilverStripe\Security\Permission;
21
use SilverStripe\Versioned\Versioned;
22
use SilverStripe\SiteConfig\SiteConfig;
23
use SilverStripe\ORM\FieldType\DBCurrency;
24
use SilverStripe\Security\PermissionProvider;
25
use SilverCommerce\ContactAdmin\Model\Contact;
26
use SilverCommerce\TaxAdmin\Helpers\MathsHelper;
27
use SilverStripe\Forms\GridField\GridFieldConfig;
28
use SilverStripe\Forms\GridField\GridFieldButtonRow;
29
use SilverStripe\Forms\GridField\GridFieldDetailForm;
30
use SilverStripe\Forms\GridField\GridFieldEditButton;
31
use SilverCommerce\ContactAdmin\Model\ContactLocation;
32
use Symbiote\GridFieldExtensions\GridFieldTitleHeader;
33
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
34
use SilverCommerce\OrdersAdmin\Control\DisplayController;
35
use Symbiote\GridFieldExtensions\GridFieldEditableColumns;
36
use SilverCommerce\OrdersAdmin\Forms\GridField\AddLineItem;
37
use SilverCommerce\OrdersAdmin\Forms\GridField\ReadOnlyGridField;
38
use SilverCommerce\VersionHistoryField\Forms\VersionHistoryField;
39
use SilverStripe\Forms\CompositeField;
40
use SilverCommerce\OrdersAdmin\Compat\NumberMigrationTask;
0 ignored issues
show
Bug introduced by
The type SilverCommerce\OrdersAdm...pat\NumberMigrationTask was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
41
use SilverCommerce\OrdersAdmin\Tasks\OrdersMigrationTask;
42
use SilverStripe\Core\Injector\Injector;
43
use SilverStripe\Control\HTTPRequest;
44
use SilverShop\HasOneField\HasOneButtonField;
45
46
/**
47
 * Represents an estimate (an unofficial quotation that has not yet been paid for)
48
 *
49
 */
50
class Estimate extends DataObject implements PermissionProvider
51
{
52
    private static $table_name = 'Estimate';
53
54
    /**
55
     * The amount of days that by default that this estimate
56
     * will end (cease to be valid).
57
     *
58
     * @var integer
59
     */
60
    private static $default_end = 30;
61
62
    /**
63
     * Standard DB columns
64
     *
65
     * @var array
66
     * @config
67
     */
68
    private static $db = [
69
        'Ref'               => 'Int',
70
        'Prefix'            => 'Varchar',
71
        'Number'            => 'Varchar',
72
        'StartDate'         => 'Date',
73
        'EndDate'           => 'Date',
74
75
        // Personal Details
76
        'Company'           => 'Varchar',
77
        'FirstName'         => 'Varchar',
78
        'Surname'           => 'Varchar',
79
        'Email'             => 'Varchar',
80
        'PhoneNumber'       => 'Varchar',
81
        
82
        // Billing Address
83
        'Address1'          => 'Varchar',
84
        'Address2'          => 'Varchar',
85
        'City'              => 'Varchar',
86
        'County'            => 'Varchar',
87
        'PostCode'          => 'Varchar',
88
        'Country'           => 'Varchar',
89
        
90
        // Delivery Details
91
        'DeliveryCompany'   => 'Varchar',
92
        'DeliveryFirstName' => 'Varchar',
93
        'DeliverySurname'   => 'Varchar',
94
        'DeliveryAddress1'  => 'Varchar',
95
        'DeliveryAddress2'  => 'Varchar',
96
        'DeliveryCity'      => 'Varchar',
97
        'DeliveryCounty'    => 'Varchar',
98
        'DeliveryPostCode'  => 'Varchar',
99
        'DeliveryCountry'   => 'Varchar',
100
101
        // Access key (for viewing via non logged in users)
102
        'AccessKey'         => "Varchar(40)"
103
    ];
104
105
    /**
106
     * Foreign key associations
107
     *
108
     * @var array
109
     * @config
110
     */
111
    private static $has_one = [
112
        'Customer'  => Contact::class
113
    ];
114
115
    /**
116
     * One to many assotiations
117
     *
118
     * @var array
119
     * @config
120
     */
121
    private static $has_many = [
122
        'Items'     => LineItem::class
123
    ];
124
125
    /**
126
     * Cast methods for templates
127
     *
128
     * @var array
129
     * @config
130
     */
131
    private static $casting = [
132
        'FullRef'           => 'Varchar(255)',
133
        "PersonalDetails"   => "Text",
134
        'BillingAddress'    => 'Text',
135
        'CountryFull'       => 'Varchar',
136
        'CountryUC'         => 'Varchar',
137
        'DeliveryAddress'   => 'Text',
138
        'DeliveryCountryFull'=> 'Varchar',
139
        'DeliveryCountryUC' => 'Varchar',
140
        'SubTotal'          => 'Currency(9,4)',
141
        'TaxTotal'          => 'Currency(9,4)',
142
        'Total'             => 'Currency(9,4)',
143
        'TotalItems'        => 'Int',
144
        'TotalWeight'       => 'Decimal',
145
        'ItemSummary'       => 'Text',
146
        'ItemSummaryHTML'   => 'HTMLText',
147
        'TranslatedStatus'  => 'Varchar'
148
    ];
149
150
    /**
151
     * Fields to show in summary views
152
     *
153
     * @var array
154
     * @config
155
     */
156
    private static $summary_fields = [
157
        'FullRef',
158
        'StartDate',
159
        'EndDate',
160
        'Company',
161
        'FirstName',
162
        'Surname',
163
        'Email',
164
        'PostCode',
165
        'Total',
166
        'LastEdited'
167
    ];
168
169
    /**
170
     * Fields to search
171
     *
172
     * @var array
173
     * @config
174
     */
175
    private static $searchable_fields = [
176
        'StartDate',
177
        'EndDate',
178
        'Company',
179
        'FirstName',
180
        'Surname',
181
        'Email',
182
        'PostCode',
183
        'LastEdited'
184
    ];
185
186
    /**
187
     * Human readable labels for fields
188
     *
189
     * @var array
190
     */
191
    private static $field_labels = [
192
        'FullRef'       => 'Ref',
193
        'StartDate'     => 'Date',
194
        'EndDate'       => 'Expires'
195
    ];
196
197
    /**
198
     * Fields to show in summary views
199
     *
200
     * @var array
201
     * @config
202
     */
203
    private static $export_fields = [
204
        "ID",
205
        "Prefix",
206
        "Ref",
207
        "Created",
208
        "StartDate",
209
        "EndDate",
210
        "ItemSummary",
211
        "SubTotal",
212
        "TaxTotal",
213
        "Total",
214
        "Company",
215
        "FirstName",
216
        "Surname",
217
        "Email",
218
        "PhoneNumber",
219
        "Address1",
220
        "Address2",
221
        "City",
222
        "PostCode",
223
        "Country",
224
        "County",
225
        "DeliveryCompany",
226
        "DeliveryFirstName",
227
        "DeliverySurname",
228
        "DeliveryAddress1",
229
        "DeliveryAddress2",
230
        "DeliveryCity",
231
        "DeliveryCountry",
232
        "DeliveryCounty",
233
        "DeliveryPostCode",
234
    ];
235
236
    /**
237
     * Add extension classes
238
     *
239
     * @var array
240
     * @config
241
     */
242
    private static $extensions = [
243
        Versioned::class . '.versioned',
244
    ];
245
246
    /**
247
     * Declare version history
248
     *
249
     * @var array
250
     * @config
251
     */
252
    private static $versioning = [
253
        "History"
254
    ];
255
256
    /**
257
     * Default sort order for ORM
258
     *
259
     * @var array
260
     * @config
261
     */
262
    private static $default_sort = [
263
        "Ref"       => "DESC",
264
        "StartDate" => "DESC"
265
    ];
266
267
    /**
268
     * Generate a link to view the associated front end
269
     * display for this order
270
     *
271
     * @return string
272
     */
273
    public function DisplayLink()
274
    {
275
        return Controller::join_links(
276
            DisplayController::create()->AbsoluteLink("estimate"),
277
            $this->ID,
278
            $this->AccessKey
279
        );
280
    }
281
282
    /**
283
     * Generate a link to view the associated front end
284
     * display for this order
285
     *
286
     * @return string
287
     */
288
    public function PDFLink()
289
    {
290
        return Controller::join_links(
291
            DisplayController::create()->AbsoluteLink("estimatepdf"),
292
            $this->ID,
293
            $this->AccessKey
294
        );
295
    }
296
297
    /**
298
     * Get the default export fields for this object
299
     *
300
     * @return array
301
     */
302
    public function getExportFields()
303
    {
304
        $rawFields = $this->config()->get('export_fields');
305
306
        // Merge associative / numeric keys
307
        $fields = [];
308
        foreach ($rawFields as $key => $value) {
309
            if (is_int($key)) {
310
                $key = $value;
311
            }
312
            $fields[$key] = $value;
313
        }
314
315
        $this->extend("updateExportFields", $fields);
316
317
        // Final fail-over, just list ID field
318
        if (!$fields) {
319
            $fields['ID'] = 'ID';
320
        }
321
322
        return $fields;
323
    }
324
325
    /**
326
     * Get the full reference number for this estimate/invoice.
327
     *
328
     * This is the stored prefix and ref
329
     *
330
     * @return string
331
     */
332
    public function getFullRef()
333
    {
334
        $config = SiteConfig::current_site_config();
335
        $length = $config->OrderNumberLength;
336
        $prefix = ($this->Prefix) ? $this->Prefix : "";
337
        $return = str_pad($this->Ref, $length, "0", STR_PAD_LEFT);
338
339
        // Work out if an order prefix string has been set
340
        if ($prefix) {
341
            $return = $prefix . '-' . $return;
342
        }
343
344
        return $return;
345
    }
346
347
    /**
348
     * Generate a string of the customer's personal details
349
     *
350
     * @return string
351
     */
352
    public function getPersonalDetails()
353
    {
354
        $return = [];
355
356
        if ($this->Company) {
0 ignored issues
show
Bug Best Practice introduced by
The property Company does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
357
            $return[] = $this->Company;
358
        }
359
360
        if ($this->FirstName) {
0 ignored issues
show
Bug Best Practice introduced by
The property FirstName does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
361
            $return[] = $this->FirstName;
362
        }
363
364
        if ($this->Surname) {
0 ignored issues
show
Bug Best Practice introduced by
The property Surname does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
365
            $return[] = $this->Surname;
366
        }
367
368
        if ($this->Email) {
0 ignored issues
show
Bug Best Practice introduced by
The property Email does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
369
            $return[] = $this->Email;
370
        }
371
372
        if ($this->PhoneNumber) {
0 ignored issues
show
Bug Best Practice introduced by
The property PhoneNumber does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
373
            $return[] = $this->PhoneNumber;
374
        }
375
376
        return implode(",\n", $return);
377
    }
378
379
    /**
380
     * Get the complete billing address for this order
381
     *
382
     * @return string
383
     */
384
    public function getBillingAddress()
385
    {
386
        $address = ($this->Address1) ? $this->Address1 . ",\n" : '';
0 ignored issues
show
Bug Best Practice introduced by
The property Address1 does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
387
        $address .= ($this->Address2) ? $this->Address2 . ",\n" : '';
0 ignored issues
show
Bug Best Practice introduced by
The property Address2 does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
388
        $address .= ($this->City) ? $this->City . ",\n" : '';
0 ignored issues
show
Bug Best Practice introduced by
The property City does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
389
        $address .= ($this->PostCode) ? $this->PostCode . ",\n" : '';
0 ignored issues
show
Bug Best Practice introduced by
The property PostCode does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
390
        $address .= ($this->Country) ? $this->Country : '';
0 ignored issues
show
Bug Best Practice introduced by
The property Country does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
391
392
        return $address;
393
    }
394
395
    /**
396
     * Get the rendered name of the billing country, based on the local
397
     *
398
     * @return string
399
     */
400
    public function getCountryFull()
401
    {
402
        $list = i18n::getData()->getCountries();
403
        $country = strtolower($this->Country);
0 ignored issues
show
Bug Best Practice introduced by
The property Country does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
404
        return (array_key_exists($country, $list)) ? $list[$country] : $country;
405
    }
406
407
    /**
408
     * Get the uppercase name of this country
409
     *
410
     * @return string
411
     */
412
    public function getCountryUC()
413
    {
414
        return strtoupper($this->Country);
0 ignored issues
show
Bug Best Practice introduced by
The property Country does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
415
    }
416
417
    /**
418
     * Get the complete delivery address for this order
419
     *
420
     * @return string
421
     */
422
    public function getDeliveryAddress()
423
    {
424
        $address = ($this->DeliveryAddress1) ? $this->DeliveryAddress1 . ",\n" : '';
425
        $address .= ($this->DeliveryAddress2) ? $this->DeliveryAddress2 . ",\n" : '';
426
        $address .= ($this->DeliveryCity) ? $this->DeliveryCity . ",\n" : '';
427
        $address .= ($this->DeliveryPostCode) ? $this->DeliveryPostCode . ",\n" : '';
428
        $address .= ($this->DeliveryCountry) ? $this->DeliveryCountry : '';
429
430
        return $address;
431
    }
432
433
    /**
434
     * Get the rendered name of the delivery country, based on the local
435
     *
436
     * @return string
437
     */
438
    public function getDeliveryCountryFull()
439
    {
440
        $list = i18n::getData()->getCountries();
441
        $country = strtolower($this->DeliveryCountry);
442
        return (array_key_exists($country, $list)) ? $list[$country] : $country;
443
    }
444
445
    /**
446
     * Get the uppercase name of this country
447
     *
448
     * @return string
449
     */
450
    public function getDeliveryCountryUC()
451
    {
452
        return strtoupper($this->DeliveryCountry);
453
    }
454
455
    /**
456
     * Find the total quantity of items in the shopping cart
457
     *
458
     * @return int
459
     */
460
    public function getTotalItems()
461
    {
462
        $total = 0;
463
464
        foreach ($this->Items() as $item) {
0 ignored issues
show
Bug introduced by
The method Items() does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

464
        foreach ($this->/** @scrutinizer ignore-call */ Items() as $item) {
Loading history...
465
            $total += ($item->Quantity) ? $item->Quantity : 1;
466
        }
467
468
        $this->extend("updateTotalItems", $total);
469
470
        return $total;
471
    }
472
473
    /**
474
    * Find the total weight of all items in the shopping cart
475
    *
476
    * @return float
477
    */
478
    public function getTotalWeight()
479
    {
480
        $total = 0;
481
482
        foreach ($this->Items() as $item) {
483
            $product = $item->findStockItem();
484
            $weight = null;
485
486
            if (!empty($product) && isset($product->Weight)) {
487
                $weight = $product->Weight;
488
            }
489
490
            if ($weight && $item->Quantity) {
491
                $total = $total + ($weight * $item->Quantity);
492
            }
493
        }
494
495
        $this->extend("updateTotalWeight", $total);
496
        
497
        return $total;
498
    }
499
500
    /**
501
     * Total values of items in this order (without any tax)
502
     *
503
     * @return float
504
     */
505
    public function getSubTotal()
506
    {
507
        $total = 0;
508
509
        // Calculate total from items in the list
510
        foreach ($this->Items() as $item) {
511
            $total += $item->SubTotal;
512
        }
513
        
514
        $this->extend("updateSubTotal", $total);
515
516
        return $total;
517
    }
518
519
    /**
520
     * Total values of items in this order
521
     *
522
     * @return float
523
     */
524
    public function getTaxTotal()
525
    {
526
        $total = 0;
527
        $items = $this->Items();
528
        
529
        // Calculate total from items in the list
530
        // We round here
531
        foreach ($items as $item) {
532
            $tax = $item->UnitTax;
533
            $total += $tax * $item->Quantity;
534
        }
535
536
        $this->extend("updateTaxTotal", $total);
537
538
        return $total;
539
    }
540
541
    /**
542
     * Get a list of all taxes used and an associated value
543
     *
544
     * @return ArrayList
545
     */
546
    public function getTaxList()
547
    {
548
        $taxes = ArrayList::create();
549
550
        foreach ($this->Items() as $item) {
551
            $existing = null;
552
            $rate = $item->TaxRate();
553
554
            if ($rate->exists()) {
555
                $existing = $taxes->find("ID", $rate->ID);
556
            }
557
558
            if (!$existing) {
559
                $currency = DBCurrency::create();
560
                $currency->setValue($item->getTaxTotal());
561
                $taxes->push(ArrayData::create([
562
                    "ID" => $rate->ID,
563
                    "Rate" => $rate,
564
                    "Total" => $currency
565
                ]));
566
            } elseif ($rate && $existing) {
567
                $existing->Total->setValue(
568
                    $existing->Total->getValue() + $item->getTaxTotal()
569
                );
570
            }
571
        }
572
573
        return $taxes;
574
    }
575
576
    /**
577
     * Total value of order
578
     *
579
     * @return float
580
     */
581
    public function getTotal()
582
    {
583
        $total = $this->SubTotal + $this->TaxTotal;
0 ignored issues
show
Bug Best Practice introduced by
The property TaxTotal does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property SubTotal does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
584
585
        $this->extend("updateTotal", $total);
586
        
587
        return $total;
588
    }
589
590
    /**
591
     * Factory method to convert this estimate to an
592
     * order.
593
     *
594
     * This method writes and reloads the object so
595
     * we are now working with the new object type
596
     *
597
     * @return Invoice
598
     */
599
    public function convertToInvoice()
600
    {
601
        $id = $this->ID;
602
        $this->ClassName = Invoice::class;
603
        $this->write();
604
605
        // Get our new Invoice
606
        $record = Invoice::get()->byID($id);
607
        $record->Ref = null;
608
        $record->Prefix = null;
609
        $record->StartDate = null;
610
        $record->EndDate = null;
611
        $record->write();
612
613
        return $record;
614
    }
615
616
    /**
617
     * Return a list string summarising each item in this order
618
     *
619
     * @return string
620
     */
621
    public function getItemSummary()
622
    {
623
        $return = [];
624
625
        foreach ($this->Items() as $item) {
626
            $return[] = "{$item->Quantity} x {$item->Title}";
627
        }
628
629
        $this->extend("updateItemSummary", $return);
630
631
        return implode("\n", $return);
632
    }
633
634
    /**
635
     * Return a list string summarising each item in this order
636
     *
637
     * @return string
638
     */
639
    public function getItemSummaryHTML()
640
    {
641
        $html = nl2br($this->ItemSummary);
0 ignored issues
show
Bug Best Practice introduced by
The property ItemSummary does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
642
        
643
        $this->extend("updateItemSummaryHTML", $html);
644
645
        return $html;
646
    }
647
648
    /**
649
     * Determine if the current estimate contains delivereable
650
     * items.
651
     *
652
     * @return boolean
653
     */
654
    public function isDeliverable()
655
    {
656
        foreach ($this->Items() as $item) {
657
            if ($item->Deliverable) {
658
                return true;
659
            }
660
        }
661
        
662
        return false;
663
    }
664
665
    /**
666
     * Determine if the current estimate contains only locked items.
667
     *
668
     * @return boolean
669
     */
670
    public function isLocked()
671
    {
672
        foreach ($this->getItems() as $item) {
0 ignored issues
show
Bug introduced by
The method getItems() does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

672
        foreach ($this->/** @scrutinizer ignore-call */ getItems() as $item) {
Loading history...
673
            if (!$item->Locked) {
674
                return false;
675
            }
676
        }
677
        
678
        return true;
679
    }
680
681
    /**
682
     * Scaffold CMS form fields
683
     *
684
     * @return FieldList
685
     */
686
    public function getCMSFields()
687
    {
688
        $self = $this;
689
   
690
        $this->beforeUpdateCMSFields(function ($fields) use ($self) {
691
            $fields->removeByName("StartDate");
692
            $fields->removeByName("CustomerID");
693
            $fields->removeByName("EndDate");
694
            $fields->removeByName("Number");
695
            $fields->removeByName("Ref");
696
            $fields->removeByName("AccessKey");
697
            $fields->removeByName("Items");
698
            $fields->removeByName("Prefix");
699
            
700
            $fields->addFieldsToTab(
701
                "Root.Main",
702
                [
703
                    // Items field
704
                    ReadOnlyGridField::create(
705
                        "Items",
706
                        "",
707
                        $this->Items(),
708
                        $config = GridFieldConfig::create()
709
                            ->addComponents(
710
                                new GridFieldButtonRow('before'),
711
                                new GridFieldTitleHeader(),
712
                                new GridFieldEditableColumns(),
713
                                new GridFieldEditButton(),
714
                                new GridFieldDetailForm(),
715
                                new GridFieldDeleteAction(),
716
                                new AddLineItem()
717
                            )
718
                    ),
719
720
                    LiteralField::create(
721
                        "ItemsDivider",
722
                        '<div class="field form-group"></div>'
723
                    ),
724
                    
725
                    // Totals and settings
726
                    CompositeField::create(
727
                        CompositeField::create(
728
                            DateField::create("StartDate", _t("OrdersAdmin.Date", "Date")),
729
                            DateField::create("EndDate", _t("OrdersAdmin.Expires", "Expires")),
730
                            ReadonlyField::create("FullRef", "#"),
731
                            TextField::create("Ref", $this->fieldLabel("Ref"))
732
                        )->setName("OrdersDetailsInfo")
733
                        ->addExtraClass("col"),
734
                        CompositeField::create([])
735
                            ->setName("OrdersDetailsMisc")
736
                        ->addExtraClass("col"),
737
                        CompositeField::create(
738
                            ReadonlyField::create("SubTotalValue", _t("OrdersAdmin.SubTotal", "Sub Total"))
739
                                ->setValue($this->obj("SubTotal")->Nice()),
740
                            ReadonlyField::create("TaxValue", _t("OrdersAdmin.Tax", "Tax"))
741
                                ->setValue($this->obj("TaxTotal")->Nice()),
742
                            ReadonlyField::create("TotalValue", _t("OrdersAdmin.Total", "Total"))
743
                                ->setValue($this->obj("Total")->Nice())
744
                        )->setName("OrdersDetailsTotals")
745
                        ->addExtraClass("col")
746
                    )->setName("OrdersDetails")
747
                    ->addExtraClass("orders-details-field")
748
                    ->setColumnCount(2)
749
                    ->setFieldHolderTemplate("SilverCommerce\\OrdersAdmin\\Forms\\OrderDetailsField_holder")
750
                ]
751
            );
752
753
            $fields->addFieldsToTab(
754
                "Root.Customer",
755
                [
756
                    HasOneButtonField::create(
757
                        $this,
758
                        'Customer'
759
                    ),
760
                    TextField::create("Company"),
761
                    TextField::create("FirstName"),
762
                    TextField::create("Surname"),
763
                    TextField::create("Address1"),
764
                    TextField::create("Address2"),
765
                    TextField::create("City"),
766
                    TextField::create("County"),
767
                    TextField::create("PostCode"),
768
                    DropdownField::create(
769
                        'Country',
770
                        _t('OrdersAdmin.Country', 'Country'),
771
                        i18n::getData()->getCountries()
772
                    )->setEmptyString(""),
773
                    TextField::create("Email"),
774
                    TextField::create("PhoneNumber")
775
                ]
776
            );
777
778
            $fields->addFieldsToTab(
779
                "Root.Delivery",
780
                [
781
                    HeaderField::create(
782
                        "DeliveryDetailsHeader",
783
                        _t("Orders.DeliveryDetails", "Delivery Details")
784
                    ),
785
                    TextField::create("DeliveryCompany"),
786
                    TextField::create("DeliveryFirstName"),
787
                    TextField::create("DeliverySurname"),
788
                    TextField::create("DeliveryAddress1"),
789
                    TextField::create("DeliveryAddress2"),
790
                    TextField::create("DeliveryCity"),
791
                    TextField::create("DeliveryCounty"),
792
                    TextField::create("DeliveryPostCode"),
793
                    DropdownField::create(
794
                        'DeliveryCountry',
795
                        _t('OrdersAdmin.Country', 'Country'),
796
                        i18n::getData()->getCountries()
797
                    )->setEmptyString("")
798
                ]
799
            );
800
801
            $fields->addFieldToTab(
802
                "Root.History",
803
                VersionHistoryField::create(
804
                    "History",
805
                    _t("SilverCommerce\VersionHistoryField.History", "History"),
806
                    $self
807
                )->addExtraClass("stacked")
808
            );
809
            
810
            $root = $fields->findOrMakeTab("Root");
811
812
            if ($root) {
813
                $root->addextraClass('orders-root');
814
            }
815
        });
816
        
817
        return parent::getCMSFields();
818
    }
819
820
    public function requireDefaultRecords()
821
    {
822
        parent::requireDefaultRecords();
823
824
        $run_migration = OrdersMigrationTask::config()->run_during_dev_build;
825
826
        if ($run_migration) {
827
            $request = Injector::inst()->get(HTTPRequest::class);
828
            OrdersMigrationTask::create()->run($request);
829
        }
830
    }
831
832
    /**
833
     * Retrieve an order prefix from siteconfig
834
     * for an Estimate
835
     *
836
     * @return string
837
     */
838
    protected function get_prefix()
839
    {
840
        $config = SiteConfig::current_site_config();
841
        return $config->EstimateNumberPrefix;
842
    }
843
844
    /**
845
     * Get a base ID for the last Estimate in the DataBase
846
     *
847
     * @return int
848
     */
849
    protected function getBaseNumber()
850
    {
851
        $base = 0;
852
        $prefix = $this->get_prefix();
853
        $classname = $this->ClassName;
854
855
        // Get the last instance of the current class
856
        $last = $classname::get()
857
            ->filter("ClassName", $classname)
858
            ->sort("Ref", "DESC")
859
            ->first();
860
861
        // If we have a last estimate/invoice, get the ID of the last invoice
862
        // so we can increment
863
        if (isset($last)) {
864
            $base = str_replace($prefix, "", $last->Ref);
865
            $base = (int)str_replace("-", "", $base);
866
        }
867
868
        // Increment base
869
        $base++;
870
871
        return $base;
872
    }
873
874
    /**
875
     * legacy method name - soon to be depreciated
876
     *
877
     */
878
    protected function generate_order_number()
879
    {
880
        return $this->generateOrderNumber();
881
    }
882
883
    /**
884
     * Generate an incremental estimate / invoice number.
885
     *
886
     * We then add an order prefix (if one is set).
887
     *
888
     * @return string
889
     */
890
    protected function generateOrderNumber()
891
    {
892
        $base_number = $this->getBaseNumber();
893
894
        while (!$this->validOrderNumber($base_number)) {
895
            $base_number++;
896
        }
897
898
        return $base_number;
899
    }
900
901
    protected function generate_random_string($length = 20)
902
    {
903
        $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
904
        $randomString = '';
905
        for ($i = 0; $i < $length; $i++) {
906
            $randomString .= $characters[rand(0, strlen($characters) - 1)];
907
        }
908
        return $randomString;
909
    }
910
911
    /**
912
     * Check if the currently generated order number
913
     * is valid (not duplicated)
914
     *
915
     * @return boolean
916
     */
917
    protected function validOrderNumber($number = null)
918
    {
919
        $number = (isset($number)) ? $number : $this->Ref;
920
921
        $existing = Estimate::get()
922
            ->filter(
923
                [
924
                    "ClassName" => self::class,
925
                    "Ref" => $number
926
                ]
927
            )->first();
928
929
        return !($existing);
930
    }
931
932
    /**
933
     * Check if the access key generated for this estimate is
934
     * valid (exists on another object)
935
     *
936
     * @return boolean
937
     */
938
    protected function validAccessKey()
939
    {
940
        $existing = Estimate::get()
941
            ->filter("AccessKey", $this->AccessKey)
942
            ->first();
943
        
944
        return !($existing);
945
    }
946
947
    /**
948
     * Create a duplicate of this order/estimate as well as duplicating
949
     * associated items
950
     *
951
     * @param bool $doWrite Perform a write() operation before returning the object.
952
     * @param array|null|false $relations List of relations to duplicate.
953
     * @return DataObject A duplicate of this node. The exact type will be the type of this node.
954
     */
955
    public function duplicate($doWrite = true, $relations = null)
956
    {
957
        $clone = parent::duplicate($doWrite, $relations);
958
        
959
        // Set up items
960
        if ($doWrite) {
961
            $clone->Ref = "";
962
            $clone->Prefix = "";
963
            $clone->write();
964
965
            foreach ($this->Items() as $item) {
966
                $item_class = $item->ClassName;
967
                $clone_item = new $item_class($item->toMap(), false, $this->model);
0 ignored issues
show
Bug Best Practice introduced by
The property model does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
968
                $clone_item->ID = 0;
969
                $clone_item->ParentID = $clone->ID;
970
                $clone_item->write();
971
            }
972
        }
973
        
974
        $clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite);
975
        
976
        return $clone;
977
    }
978
979
    public function onBeforeWrite()
980
    {
981
        parent::onBeforeWrite();
982
983
        // Ensure that this object has a non-conflicting Access Key
984
        if (!$this->AccessKey) {
985
            $this->AccessKey = $this->generate_random_string(40);
0 ignored issues
show
Bug Best Practice introduced by
The property AccessKey does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
986
            
987
            while (!$this->validAccessKey()) {
988
                $this->AccessKey = $this->generate_random_string(40);
989
            }
990
        }
991
992
993
        // Set a prefix if required
994
        if (!$this->Prefix) {
995
            $this->Prefix = $this->get_prefix();
0 ignored issues
show
Bug Best Practice introduced by
The property Prefix does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
996
        }
997
998
        $contact = $this->Customer();
0 ignored issues
show
Bug introduced by
The method Customer() does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

998
        /** @scrutinizer ignore-call */ 
999
        $contact = $this->Customer();
Loading history...
999
1000
        // If a contact is assigned and no customer details set
1001
        // then use contact details
1002
        if (!$this->PersonalDetails && $contact->exists()) {
0 ignored issues
show
Bug Best Practice introduced by
The property PersonalDetails does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
1003
            foreach (Config::inst()->get(Contact::class, "db") as $param => $value) {
1004
                $this->$param = $contact->$param;
1005
            }
1006
        }
1007
1008
        // if Billing Address is not set, use customer's default
1009
        // location
1010
        if (!$this->BillingAddress && $contact->exists() && $contact->DefaultLocation()) {
0 ignored issues
show
Bug Best Practice introduced by
The property BillingAddress does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
1011
            $location = $contact->DefaultLocation();
1012
            foreach (Config::inst()->get(ContactLocation::class, "db") as $param => $value) {
1013
                $this->$param = $location->$param;
1014
            }
1015
        }
1016
1017
1018
        // Is delivery address set, if not, set it here
1019
        if (!$this->DeliveryAddress && $this->BillingAddress) {
0 ignored issues
show
Bug Best Practice introduced by
The property DeliveryAddress does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
1020
            $this->DeliveryCompany = $this->Company;
0 ignored issues
show
Bug Best Practice introduced by
The property DeliveryCompany does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
Bug Best Practice introduced by
The property Company does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
1021
            $this->DeliveryFirstName = $this->FirstName;
0 ignored issues
show
Bug Best Practice introduced by
The property FirstName does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property DeliveryFirstName does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1022
            $this->DeliverySurname = $this->Surname;
0 ignored issues
show
Bug Best Practice introduced by
The property Surname does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property DeliverySurname does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1023
            $this->DeliveryAddress1 = $this->Address1;
0 ignored issues
show
Bug Best Practice introduced by
The property DeliveryAddress1 does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
Bug Best Practice introduced by
The property Address1 does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
1024
            $this->DeliveryAddress2 = $this->Address2;
0 ignored issues
show
Bug Best Practice introduced by
The property DeliveryAddress2 does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
Bug Best Practice introduced by
The property Address2 does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
1025
            $this->DeliveryCity = $this->City;
0 ignored issues
show
Bug Best Practice introduced by
The property DeliveryCity does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
Bug Best Practice introduced by
The property City does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
1026
            $this->DeliveryPostCode = $this->PostCode;
0 ignored issues
show
Bug Best Practice introduced by
The property DeliveryPostCode does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
Bug Best Practice introduced by
The property PostCode does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
1027
            $this->DeliveryCountry = $this->Country;
0 ignored issues
show
Bug Best Practice introduced by
The property DeliveryCountry does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
Bug Best Practice introduced by
The property Country does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
1028
        }
1029
1030
        // If date not set, make this equal the created date
1031
        if (!$this->StartDate) {
1032
            $this->StartDate = $this->LastEdited;
0 ignored issues
show
Bug Best Practice introduced by
The property StartDate does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1033
        }
1034
1035
        if (!$this->EndDate && $this->StartDate) {
1036
            $start = new DateTime($this->StartDate);
1037
            $start->modify("+ {$this->config()->default_end} days");
1038
            $this->EndDate = $start->format("Y-m-d");
0 ignored issues
show
Bug Best Practice introduced by
The property EndDate does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1039
        }
1040
    }
1041
1042
    /**
1043
     * API Callback after this object is written to the DB
1044
     *
1045
     */
1046
    public function onAfterWrite()
1047
    {
1048
        parent::onAfterWrite();
1049
1050
        // Check if an order number has been generated, if not, add it and save again
1051
        if (!$this->Ref) {
1052
            $this->Ref = $this->generateOrderNumber();
0 ignored issues
show
Bug Best Practice introduced by
The property Ref does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1053
            $this->write();
1054
        }
1055
    }
1056
1057
    /**
1058
     * API Callback before this object is removed from to the DB
1059
     *
1060
     */
1061
    public function onBeforeDelete()
1062
    {
1063
        parent::onBeforeDelete();
1064
        
1065
        // Delete all items attached to this order
1066
        foreach ($this->Items() as $item) {
1067
            $item->delete();
1068
        }
1069
    }
1070
1071
    public function providePermissions()
1072
    {
1073
        return [
1074
            "ORDERS_VIEW_ESTIMATES" => [
1075
                'name' => 'View any estimate',
1076
                'help' => 'Allow user to view any estimate',
1077
                'category' => 'Orders',
1078
                'sort' => 89
1079
            ],
1080
            "ORDERS_CREATE_ESTIMATES" => [
1081
                'name' => 'Create estimates',
1082
                'help' => 'Allow user to create new estimates',
1083
                'category' => 'Orders',
1084
                'sort' => 88
1085
            ],
1086
            "ORDERS_EDIT_ESTIMATES" => [
1087
                'name' => 'Edit any estimate',
1088
                'help' => 'Allow user to edit any estimate',
1089
                'category' => 'Orders',
1090
                'sort' => 87
1091
            ],
1092
            "ORDERS_DELETE_ESTIMATES" => [
1093
                'name' => 'Delete any estimate',
1094
                'help' => 'Allow user to delete any estimate',
1095
                'category' => 'Orders',
1096
                'sort' => 86
1097
            ]
1098
        ];
1099
    }
1100
1101
    /**
1102
     * Only order creators or users with VIEW admin rights can view
1103
     *
1104
     * @return boolean
1105
     */
1106
    public function canView($member = null)
1107
    {
1108
        $extended = $this->extendedCan(__FUNCTION__, $member);
1109
        
1110
        if ($extended !== null) {
1111
            return $extended;
1112
        }
1113
1114
        if (!$member) {
1115
            $member = Member::currentUser();
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Security\Member::currentUser() has been deprecated: 5.0.0 use Security::getCurrentUser() ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1115
            $member = /** @scrutinizer ignore-deprecated */ Member::currentUser();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1116
        }
1117
1118
        if ($member && Permission::checkMember($member->ID, ["ADMIN", "ORDERS_VIEW_ESTIMATES"])) {
1119
            return true;
1120
        }
1121
1122
        return false;
1123
    }
1124
1125
    /**
1126
     * Anyone can create orders, even guest users
1127
     *
1128
     * @return boolean
1129
     */
1130
    public function canCreate($member = null, $context = [])
1131
    {
1132
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
1133
        
1134
        if ($extended !== null) {
1135
            return $extended;
1136
        }
1137
1138
        if (!$member) {
1139
            $member = Member::currentUser();
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Security\Member::currentUser() has been deprecated: 5.0.0 use Security::getCurrentUser() ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1139
            $member = /** @scrutinizer ignore-deprecated */ Member::currentUser();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1140
        }
1141
        
1142
        if ($member && Permission::checkMember($member->ID, ["ADMIN", "ORDERS_CREATE_ESTIMATES"])) {
1143
            return true;
1144
        }
1145
1146
        return false;
1147
    }
1148
1149
    /**
1150
     * Only users with EDIT admin rights can view an order
1151
     *
1152
     * @return boolean
1153
     */
1154
    public function canEdit($member = null)
1155
    {
1156
        $extended = $this->extendedCan(__FUNCTION__, $member);
1157
        
1158
        if ($extended !== null) {
1159
            return $extended;
1160
        }
1161
1162
        if (!$member) {
1163
            $member = Member::currentUser();
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Security\Member::currentUser() has been deprecated: 5.0.0 use Security::getCurrentUser() ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1163
            $member = /** @scrutinizer ignore-deprecated */ Member::currentUser();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1164
        }
1165
1166
        if ($member && Permission::checkMember($member->ID, ["ADMIN", "ORDERS_EDIT_ESTIMATES"])) {
1167
            return true;
1168
        }
1169
1170
        return false;
1171
    }
1172
1173
    /**
1174
     * No one should be able to delete an order once it has been created
1175
     *
1176
     * @return boolean
1177
     */
1178
    public function canDelete($member = null)
1179
    {
1180
        $extended = $this->extendedCan(__FUNCTION__, $member);
1181
        
1182
        if ($extended !== null) {
1183
            return $extended;
1184
        }
1185
1186
        if (!$member) {
1187
            $member = Member::currentUser();
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Security\Member::currentUser() has been deprecated: 5.0.0 use Security::getCurrentUser() ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1187
            $member = /** @scrutinizer ignore-deprecated */ Member::currentUser();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1188
        }
1189
1190
        if ($member && Permission::checkMember($member->ID, ["ADMIN", "ORDERS_DELETE_ESTIMATES"])) {
1191
            return true;
1192
        }
1193
1194
        return false;
1195
    }
1196
}
1197