Passed
Push — master ( a1b694...022b7d )
by Nic
11:51 queued 08:19
created

Variation::getGeneratedValue()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 6
c 1
b 0
f 0
nc 4
nop 0
dl 0
loc 9
rs 10
1
<?php
2
3
namespace Dynamic\Foxy\Model;
4
5
use Bummzack\SortableFile\Forms\SortableUploadField;
6
use Dynamic\Foxy\Page\Product;
7
use SilverStripe\Assets\File;
8
use SilverStripe\Dev\Deprecation;
9
use SilverStripe\Forms\CheckboxField;
10
use SilverStripe\Forms\CurrencyField;
11
use SilverStripe\Forms\DropdownField;
12
use SilverStripe\Forms\FieldGroup;
13
use SilverStripe\Forms\FieldList;
14
use SilverStripe\Forms\NumericField;
15
use SilverStripe\Forms\ReadonlyField;
16
use SilverStripe\Forms\TextField;
17
use SilverStripe\ORM\DataObject;
18
use SilverStripe\ORM\ManyManyList;
19
use SilverStripe\ORM\ValidationResult;
20
use SilverStripe\Security\Permission;
21
use SilverStripe\Security\Security;
22
23
/**
24
 * Class Variation
25
 * @package Dynamic\Foxy\Model
26
 *
27
 * @property string $Title
28
 * @property bool $UseProductContent
29
 * @property string Content
30
 * @property float $WeightModifier
31
 * @property string $CodeModifier
32
 * @property float $PriceModifier
33
 * @property string $WeightModifierAction
34
 * @property string $CodeModifierAction
35
 * @property string $PriceModifierAction
36
 * @property bool $Available
37
 * @property int $Type
38
 * @property string $OptionModifierKey
39
 * @property int $SortOrder
40
 * @property float $FinalPrice
41
 * @property float $FinalWeight
42
 * @property string $FinalCode
43
 * @property bool $IsDefault
44
 * @property int $VariationTypeID
45
 * @property int $ProductID
46
 *
47
 * @method VariationType VariationType()
48
 * @method Product Product
49
 *
50
 * @method ManyManyList Images()
51
 */
52
class Variation extends DataObject
53
{
54
    /**
55
     * @var array
56
     */
57
    protected $default_read_only_fields = [
58
        'WeightModifierAction',
59
        'CodeModifierAction',
60
        'PriceModifierAction',
61
        'CodeModifier',
62
    ];
63
64
    /**
65
     * @var string
66
     */
67
    private static $table_name = 'Variation';
68
69
    /**
70
     * @var string
71
     */
72
    private static $singular_name = 'Variation';
73
74
    /**
75
     * @var string
76
     */
77
    private static $plural_name = 'Variations';
78
79
    /**
80
     * @var bool
81
     */
82
    private static $code_trim_right_space = true;
83
84
    /**
85
     * @var bool
86
     */
87
    private static $code_enforce_single_spaces = true;
88
89
    /**
90
     * @var bool
91
     */
92
    private static $code_trim_left_spaces = false;
93
94
    /**
95
     * Mapping from a Product's field to the Variation's field for a default variation
96
     *
97
     * @var string[]
98
     */
99
    private static $default_variation_mapping = [
100
        'Title' => 'Title',
101
        'Available' => 'Available',
102
    ];
103
104
    /**
105
     * @var string[]
106
     */
107
    private static $db = [
108
        'Title' => 'Varchar(255)',
109
        'UseProductContent' => 'Boolean',
110
        'Content' => 'HTMLText',
111
        'WeightModifier' => 'Decimal(9,3)',
112
        'CodeModifier' => 'Text',
113
        'PriceModifier' => 'Currency',
114
        'WeightModifierAction' => "Enum('Add,Subtract,Set', null)",
115
        'CodeModifierAction' => "Enum('Add,Subtract,Set', null)",
116
        'PriceModifierAction' => "Enum('Add,Subtract,Set', null)",
117
        'Available' => 'Boolean',
118
        'Type' => 'Int',
119
        'OptionModifierKey' => 'Varchar(255)',
120
        'SortOrder' => 'Int',
121
        'FinalPrice' => 'Currency',
122
        'FinalWeight' => 'Decimal(9,3)',
123
        'FinalCode' => 'Varchar(255)',
124
        'IsDefault' => 'Boolean',
125
        'ReceiptTitle' => 'HTMLVarchar(255)',
126
        'QuantityMax' => 'Int',
127
        'InventoryLevel' => 'Int',
128
    ];
129
130
    /**
131
     * @var string[]
132
     */
133
    private static $indexes = [
134
        'FinalPrice' => true,
135
        'FinalWeight' => true,
136
        'FinalCode' => true,
137
    ];
138
139
    /**
140
     * @var string[]
141
     */
142
    private static $has_one = [
143
        'VariationType' => VariationType::class,
144
        'Product' => Product::class,
145
    ];
146
147
    /**
148
     * @var array
149
     */
150
    private static $many_many = [
151
        'Images' => File::class,
152
    ];
153
154
    /**
155
     * @var \string[][]
156
     */
157
    private static $many_many_extraFields = [
158
        'Images' => [
159
            'SortOrder' => 'Int',
160
        ],
161
    ];
162
163
    /**
164
     * @var string[]
165
     */
166
    private static $owns = [
167
        'Images',
168
    ];
169
170
    /**
171
     * @var string[]
172
     */
173
    private static $default_sort = 'SortOrder';
174
175
    /**
176
     * @var bool
177
     */
178
    private static $force_price_variation_display = false;
179
180
    /**
181
     * @var bool
182
     */
183
    private static $variant_price_with_plus_minus = true;
184
185
    /**
186
     * The relation name was established before requests for videos.
187
     * The relation has subsequently been updated from Image::class to File::class
188
     * to allow for additional file types such as mp4
189
     *
190
     * @var array
191
     */
192
    private static $allowed_images_extensions = [
193
        'gif',
194
        'jpeg',
195
        'jpg',
196
        'png',
197
        'bmp',
198
        'ico',
199
        'mp4',
200
    ];
201
202
    /**
203
     * @return FieldList
204
     */
205
    public function getCMSFields()
206
    {
207
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
208
            $fields->removeByName([
209
                'Images',
210
                'WeightModifier',
211
                'CodeModifier',
212
                'PriceModifier',
213
                'WeightModifierAction',
214
                'CodeModifierAction',
215
                'PriceModifierAction',
216
                'Available',
217
                'Type',
218
                'OptionModifierKey',
219
                'SortOrder',
220
                'ProductID',
221
                'FinalPrice',
222
                'FinalWeight',
223
                'FinalCode',
224
                'IsDefault',
225
            ]);
226
227
            $fields->insertBefore(
228
                'Content',
229
                CheckboxField::create('Available')
230
                    ->setTitle('Available for purchase')
231
            );
232
233
            $fields->insertBefore(
234
                'Content',
235
                $fields->dataFieldByName('VariationTypeID')
236
            );
237
238
            $fields->insertBefore(
239
                'Content',
240
                $fields->dataFieldByName('UseProductContent')
241
            );
242
243
            $content = $fields->dataFieldByName('Content');
244
245
            $content->hideUnless('UseProductContent')->isNotChecked()->end();
246
247
            if ($this->exists()) {
248
                $fields->addFieldToTab(
249
                    'Root.ProductModifications',
250
                    ReadonlyField::create('OptionModifierKey')
251
                        ->setTitle(_t('Variation.ModifierKey', 'Modifier Key'))
252
                );
253
            }
254
255
            if ($this->Product()->hasDatabaseField('Weight')) {
256
                $fields->addFieldtoTab(
257
                    'Root.ProductModifications',
258
                    FieldGroup::create(
259
                        DropdownField::create(
260
                            'WeightModifierAction',
261
                            _t('Variation.WeightModifierAction', 'Weight Modification Type'),
262
                            [
263
                                'Add' => _t(
264
                                    'Variation.WeightAdd',
265
                                    'Add to Base Weight',
266
                                    'Add to weight'
267
                                ),
268
                                'Subtract' => _t(
269
                                    'Variation.WeightSubtract',
270
                                    'Subtract from Base Weight',
271
                                    'Subtract from weight'
272
                                ),
273
                                'Set' => _t('Variation.WeightSet', 'Set as a new Weight'),
274
                            ]
275
                        )
276
                            ->setEmptyString('')
277
                            ->setDescription(_t(
278
                                'Variation.WeightDescription',
279
                                'Does weight modify or replace base weight?'
280
                            )),
281
                        NumericField::create("WeightModifier")
282
                            ->setTitle(_t('Variation.WeightModifier', 'Weight'))
283
                            ->setScale(3)
284
                            ->setDescription(_t(
285
                                'Variation.WeightDescription',
286
                                'Only supports up to 3 decimal places'
287
                            ))->displayIf('WeightModifierAction')->isNotEmpty()->end(),
288
                        NumericField::create('FinalWeight')
289
                            ->setTitle('Final Modified Weight')
290
                            ->setDescription("Product's weight is {$this->Product()->Weight}")
0 ignored issues
show
Bug Best Practice introduced by
The property Weight does not exist on Dynamic\Foxy\Page\Product. Since you implemented __get, consider adding a @property annotation.
Loading history...
291
                            ->performDisabledTransformation()
292
                    )->setTitle('Weight Modification')
293
                );
294
            }
295
296
            $fields->addFieldsToTab(
297
                'Root.ProductModifications',
298
                [
299
                    // Price Modifier Fields
300
                    //HeaderField::create('PriceHD', _t('Variation.PriceHD', 'Modify Price'), 4),
301
                    FieldGroup::create(
302
                        DropdownField::create(
303
                            'PriceModifierAction',
304
                            _t('Variation.PriceModifierAction', 'Price Modification Type'),
305
                            [
306
                                'Add' => _t(
307
                                    'Variation.PriceAdd',
308
                                    'Add to Base Price',
309
                                    'Add to price'
310
                                ),
311
                                'Subtract' => _t(
312
                                    'Variation.PriceSubtract',
313
                                    'Subtract from Base Price',
314
                                    'Subtract from price'
315
                                ),
316
                                'Set' => _t('Variation.PriceSet', 'Set as a new Price'),
317
                            ]
318
                        )
319
                            ->setEmptyString('')
320
                            ->setDescription(_t('Variation.PriceDescription', 'Does price modify or replace base price?')),
321
                        CurrencyField::create('PriceModifier')
322
                            ->setTitle(_t('Variation.PriceModifier', 'Price'))
323
                            ->displayIf('PriceModifierAction')->isNotEmpty()->end(),
324
                        CurrencyField::create('FinalPrice')
325
                            ->setTitle('Final Modified Price')
326
                            ->setDescription("Product's price is {$this->Product()->Price}")
327
                            ->performDisabledTransformation()
328
                    )->setTitle('Price Modifications'),
329
330
                    // Code Modifier Fields
331
                    //HeaderField::create('CodeHD', _t('Variation.CodeHD', 'Modify Code'), 4),
332
                    FieldGroup::create(
333
                        DropdownField::create(
334
                            'CodeModifierAction',
335
                            _t('Variation.CodeModifierAction', 'Code Modification Type'),
336
                            [
337
                                'Add' => _t(
338
                                    'Variation.CodeAdd',
339
                                    'Add to Base Code',
340
                                    'Add to code'
341
                                ),
342
                                'Subtract' => _t(
343
                                    'Variation.CodeSubtract',
344
                                    'Subtract from Base Code',
345
                                    'Subtract from code'
346
                                ),
347
                                'Set' => _t('Variation.CodeSet', 'Set as a new Code'),
348
                            ]
349
                        )
350
                            ->setEmptyString('')
351
                            ->setDescription(_t('Variation.CodeDescription', 'Does code modify or replace base code?')),
352
                        TextField::create('CodeModifier')
353
                            ->setTitle(_t('Variation.CodeModifier', 'Code'))
354
                            ->displayIf('CodeModifierAction')->isNotEmpty()->end(),
355
                        TextField::create('FinalCode')
356
                            ->setTitle('Final Modified Code')
357
                            ->setDescription("Product's code is {$this->Product()->Code}")
358
                            ->performDisabledTransformation()
359
                    )->setTitle('Code Modification'),
360
                ]
361
            );
362
363
            // Images tab
364
            $images = SortableUploadField::create('Images')
365
                ->setSortColumn('SortOrder')
366
                ->setIsMultiUpload(true)
367
                ->setAllowedExtensions($this->config()->get('allowed_images_extensions'))
368
                ->setFolderName('Uploads/Products/Images');
369
370
            $fields->addFieldsToTab('Root.Images', [
371
                $images,
372
            ]);
373
        });
374
375
        $fields = parent::getCMSFields();
376
377
        $this->applyReadOnlyTransformations($fields);
378
379
        return $fields;
380
    }
381
382
    /**
383
     * @param $fields
384
     * @return void
385
     */
386
    protected function applyReadonlyTransformations(&$fields)
387
    {
388
        if ($this->IsDefault) {
389
            foreach ($this->getDefaultReadOnlyFields() as $fieldName) {
390
                if ($field = $fields->dataFieldByName($fieldName)) {
391
                    if ($field instanceof DropdownField) {
392
                        $field->setDisabled(true);
393
                    } else {
394
                        $field->setReadOnly(true);
395
                    }
396
                }
397
            }
398
        }
399
    }
400
401
    /**
402
     * @return array
403
     */
404
    protected function getDefaultReadOnlyFields()
405
    {
406
        $fields = $this->default_read_only_fields;
407
408
        $this->extend('updateDefaultReadOnlyFields', $fields);
409
410
        return $fields;
411
    }
412
413
    /**
414
     *
415
     */
416
    public function onBeforeWrite()
417
    {
418
        parent::onBeforeWrite();
419
420
        $modifierKeyField = 'OptionModifierKey';
421
        $this->{$modifierKeyField} = $this->getGeneratedValue();
422
423
        $codeModifierField = 'CodeModifier';
424
        $codeModifier = $this->{$codeModifierField};
425
426
        if ($this->IsDefault) {
427
            $this->PriceModifierAction = 'Set';
428
            $this->PriceModifier = $this->Product()->Price;
429
        }
430
431
        switch ($this->CodeModifierAction) {
432
            case 'Subtract':
433
            case 'Add':
434
                if (static::config()->get('code_trim_left_spaces')) {
435
                    //remove duplicate leading spaces
436
                    if (strpos($codeModifier, ' ') == 0) {
437
                        $codeModifier = ltrim($codeModifier);
438
                    }
439
                }
440
                if (static::config()->get('code_trim_right_space')) {
441
                    $codeModifier = rtrim($codeModifier);
442
                }
443
                break;
444
            case 'Set':
445
                //We must trim for Foxy
446
                $codeModifier = trim($codeModifier);
447
                break;
448
        }
449
450
        if (static::config()->get('code_enforce_single_spaces')) {
451
            //replace duplicate spaces
452
            $codeModifier = preg_replace('/\s+/', ' ', $codeModifier);
453
        }
454
455
        /**
456
         * This method supersedes configs code_trim_right_space and code_enforce_single_spaces
457
         */
458
        if (static::config()->get('code_remove_spaces')) {
459
            // replace duplicate spaces
460
            $codeModifier = preg_replace('/\s+/', '', $codeModifier);
461
        }
462
463
        //can be used for backwards compatibility (do your own logic)
464
        $this->extend('updateCodeModifier', $this, $codeModifier);
465
466
        $this->{$codeModifierField} = $codeModifier;
467
468
        $this->FinalPrice = $this->calculateFinalPrice();
469
        $this->FinalCode = $this->calculateFinalCode();
470
471
        if ($this->Product()->hasDatabaseField('Weight')) {
472
            $this->FinalWeight = $this->calculateFinalWeight();
473
        }
474
    }
475
476
    /**
477
     * @return ValidationResult
478
     */
479
    public function validate()
480
    {
481
        $validate = parent::validate();
482
        $product = $this->Product();
483
484
        if (!$this->Title) {
485
            $validate->addFieldError('Title', 'A title is required');
486
        }
487
488
        /*if (!$this->VariationTypeID) {
489
            $validate->addFieldError('VariationTypeID', 'A variation type is required');
490
        }//*/
491
492
        if ($this->PriceModifierAction == 'Subtract' && $this->PriceModifier > $product->Price) {
493
            $validate->addFieldError('PriceModifier', "You can't subtract more than the price of the product ({$product->Price})");
494
        }
495
496
        if ($this->WeightModifierAction == 'Subtract' && $this->WeightModifier > $product->Weight) {
0 ignored issues
show
Bug Best Practice introduced by
The property Weight does not exist on Dynamic\Foxy\Page\Product. Since you implemented __get, consider adding a @property annotation.
Loading history...
497
            $validate->addFieldError('WeightModifier', "You can't subtract more than the weight of the product ({$product->Weight})");
498
        }
499
500
        return $validate;
501
    }
502
503
    /**
504
     * @return string
505
     */
506
    public function getGeneratedValue()
507
    {
508
        $modPrice = ($this->PriceModifier) ? (string)$this->PriceModifier : '0';
509
        $modPriceWithSymbol = self::getOptionModifierActionSymbol($this->PriceModifierAction) . $modPrice;
510
        $modWeight = ($this->WeightModifier) ? (string)$this->WeightModifier : '0';
511
        $modWeight = self::getOptionModifierActionSymbol($this->WeightModifierAction) . $modWeight;
512
        $modCode = self::getOptionModifierActionSymbol($this->CodeModifierAction) . $this->CodeModifier;
513
514
        return $this->Title . '{p' . $modPriceWithSymbol . '|w' . $modWeight . '|c' . $modCode . '}';
515
    }
516
517
    /**
518
     * @param $oma
519
     * @param bool $returnWithOnlyPlusMinus
520
     *
521
     * @return string
522
     */
523
    public static function getOptionModifierActionSymbol($oma, $returnWithOnlyPlusMinus = false)
524
    {
525
        switch ($oma) {
526
            case 'Subtract':
527
                $symbol = '-';
528
                break;
529
            case 'Set':
530
                $symbol = ($returnWithOnlyPlusMinus) ? '' : ':';
531
                break;
532
            default:
533
                $symbol = '+';
534
        }
535
536
        return $symbol;
537
    }
538
539
    /**
540
     * @return string
541
     */
542
    protected function getWeightModifierWithSymbol()
543
    {
544
        return $this->getOptionModifierActionSymbol($this->WeightModifierAction) . $this->WeightModifier;
545
    }
546
547
    /**
548
     * @return string
549
     */
550
    protected function getPriceModifierWithSymbol()
551
    {
552
        return $this->getOptionModifierActionSymbol($this->PriceModifierAction) . $this->PriceModifier;
553
    }
554
555
    /**
556
     * @return string
557
     */
558
    protected function getCodeModifierWithSymbol()
559
    {
560
        return $this->getOptionModifierActionSymbol($this->CodeModifierAction) . $this->CodeModifier;
561
    }
562
563
    /**
564
     * @return float
565
     */
566
    protected function calculateFinalPrice()
567
    {
568
        $product = $this->Product();// this relation is set by a developer's data extension
569
570
        if ($this->PriceModifierAction == 'Add') {
571
            return $this->PriceModifier + $product->Price;
572
        } elseif ($this->PriceModifierAction == 'Subtract') {
573
            return $product->Price - $this->PriceModifier;
574
        } elseif ($this->PriceModifierAction == 'Set') {
575
            return $this->PriceModifier;
576
        }
577
578
        return $product->Price;
579
    }
580
581
    /**
582
     * @return float
583
     */
584
    protected function calculateFinalWeight()
585
    {
586
        $product = $this->Product();// this relation is set by a developer's data extension
587
588
        if ($this->WeightModifierAction == 'Add') {
589
            return $this->WeightModifier + $product->Weight;
0 ignored issues
show
Bug Best Practice introduced by
The property Weight does not exist on Dynamic\Foxy\Page\Product. Since you implemented __get, consider adding a @property annotation.
Loading history...
590
        } elseif ($this->WeightModifierAction == 'Subtract') {
591
            return $product->Weight - $this->WeightModifier;
592
        } elseif ($this->WeightModifierAction == 'Set') {
593
            return $this->WeightModifier;
594
        }
595
596
        return $product->Weight;
597
    }
598
599
    /**
600
     * @return string
601
     */
602
    protected function calculateFinalCode()
603
    {
604
        $product = $this->Product();// this relation is set by a developer's data extension
605
606
        if ($this->CodeModifierAction == 'Add') {
607
            return "{$product->Code}{$this->CodeModifier}";
608
        } elseif ($this->CodeModifierAction == 'Subtract') {
609
            return rtrim($product->Code, $this->CodeModifier);
610
        } elseif ($this->CodeModifierAction == 'Set') {
611
            return $this->CodeModifier;
612
        }
613
614
        return $product->Code;
615
    }
616
617
    /**
618
     * @return mixed|string
619
     */
620
    public function getGeneratedTitle()
621
    {
622
        $modPrice = ($this->PriceModifier) ? (string)$this->PriceModifier : '0';
623
        $title = $this->Title;
624
625
        if ($this->PriceModifier != 0 || $this->config()->get('force_price_variation_display')) {
626
            if ($modPrice == '0') {
627
                $modPrice = $this->Product()->Price;
628
            }
629
630
            $title .= ': (' . self::getOptionModifierActionSymbol(
631
                    $this->PriceModifierAction,
632
                    $this->config()->get('variant_price_with_plus_minus')
633
                ) . '$' . $modPrice . ')';
634
        }
635
636
        return $title;
637
    }
638
639
    /**
640
     * @return bool
641
     */
642
    public function getIsAvailable()
643
    {
644
        $available = true;
645
646
        if (!$this->Available) {
647
            $available = false;
648
        }
649
650
        $this->extend('updateGetIsAvailable', $available);
651
652
        return $available;
653
    }
654
655
    /**
656
     * @return bool
657
     */
658
    public function getAvailability()
659
    {
660
        Deprecation::notice('1.4', 'Use getIsAvailable() instead');
661
        return $this->getIsAvailable();
662
    }
663
664
    /**
665
     * @param $member
666
     * @return bool|int|void
667
     */
668
    public function canCreate($member = null, $context = [])
669
    {
670
        if (!$member) {
671
            $member = Security::getCurrentUser();
672
        }
673
674
        return Permission::checkMember($member, 'MANAGE_FOXY_PRODUCTS');
675
    }
676
677
    /**
678
     * @param $member
679
     * @return bool|int|void|null
680
     */
681
    public function canEdit($member = null, $context = [])
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed. ( Ignorable by Annotation )

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

681
    public function canEdit($member = null, /** @scrutinizer ignore-unused */ $context = [])

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
682
    {
683
        if (!$member) {
684
            $member = Security::getCurrentUser();
685
        }
686
687
        return Permission::checkMember($member, 'MANAGE_FOXY_PRODUCTS');
688
    }
689
690
    /**
691
     * @param $member
692
     * @return bool|int|void
693
     */
694
    public function canDelete($member = null, $context = [])
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed. ( Ignorable by Annotation )

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

694
    public function canDelete($member = null, /** @scrutinizer ignore-unused */ $context = [])

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
695
    {
696
        if (!$member) {
697
            $member = Security::getCurrentUser();
698
        }
699
700
        if ($this->IsDefault) {
701
            return false;
702
        }
703
704
        return Permission::checkMember($member, 'MANAGE_FOXY_PRODUCTS');
705
    }
706
}
707