1 <?php
2
3 4 5 6 7 8
9 class Product extends MediawebPage implements ImportInterface{
10
11
12 static $can_be_root = false;
13 static $allowed_children = 'none';
14 static $default_parent = 'Catalog';
15
16 static $db = array(
17 'ImportID' => 'Varchar',
18
19 'Description' => 'Text',
20 'Vendor' => 'Varchar',
21
22 'Price' => 'CatalogPrice',
23 'BasePrice' => 'CatalogPrice',
24 'CostPrice' => 'CatalogPrice',
25
26 'Available' => 'Boolean',
27 'AllowPurchase' => 'Boolean',
28 );
29
30 static $defaults = array(
31 'ShowInMenus' => 0,
32 'ShowOnlyInTab' => 1,
33 'Available' => 1,
34 'AllowPurchase' => 1,
35 'BasePrice' => 0,
36 'CostPrice' => 0,
37 );
38
39 static $has_one = array(
40 'Photo' => 'Image',
41 );
42
43 static $indexes = array(
44 'ImportID' => true,
45 'Price' => true,
46 'Vendor' => true,
47 );
48
49 static $summary_fields = array('ImportID', 'Title', 'BasePrice', 'CostPrice', 'Vendor', 'Available', 'AllowPurchase');
50
51 static $fulltext_fields = array('Title', 'Content', 'Description', 'BasePrice', 'CostPrice', 'Vendor');
52
53 static $searchable_fields = array(
54 'Title' => 'PartialMatchFilter',
55 'ImportID' => 'ExactMatchFilter',
56 'Available' => array('filter'=>'ExactMatchFilter'),
57 'AllowPurchase' => array('filter'=>'ExactMatchFilter'),
58 'Price' => array('field'=>'RangeField', 'filter'=>'WithinRangeFilter'),
59 'Vendor' => array('field'=>'CheckboxSetField', 'filter'=>'ExactMatchMultiFilter'),
60 );
61
62 static $filter_allowed = true;
63
64 static function disable_filter($val = true) {
65 self::$filter_allowed = ! $val;
66 }
67
68 static function filter_allowed() {
69 return self::$filter_allowed;
70 }
71
72 73 74 75 76 77
78 static function set_searchable_fields(array $data) {
79 static::$searchable_fields = $data;
80 }
81
82 83 84 85 86
87 static function get_searchable_fields() {
88 return static::$searchable_fields;
89 }
90
91 92 93 94
95 private static $sort_options = array('title' => 'Title ASC', 'pricea' => 'Price ASC', 'priced' => 'Price DESC');
96
97 98 99 100 101 102
103 static function set_sort_options(Array $data) {
104 self::$sort_options = $data;
105 }
106
107 108 109 110 111
112 static function get_sort_options() {
113 return self::$sort_options;
114 }
115
116 117 118 119 120
121 static function sort_options_orderby($sort) {
122 $options = self::get_sort_options();
123 return (array_key_exists($sort, $options)) ? $options[$sort] : '';
124 }
125
126 127 128 129 130 131 132
133 static function sort_options_dropdown_map($addDefault = false) {
134 $options = self::get_sort_options();
135 if (!$options) return array();
136
137 $map = array();
138 if ($addDefault) {
139 $map[''] = _t('Product.SortOption_default', 'Default');
140 }
141 foreach ($options as $key => $val) {
142 $map[$key] = _t('Product.SortOption_'.$key, ucfirst($key));
143 }
144 return $map;
145 }
146
147 148 149 150 151
152 static function filter_fields_list() {
153 if (!self::filter_allowed()) return array();
154 $filterFields = array();
155 $product = singleton(get_called_class());
156 foreach (array_keys($product->searchableFields()) as $field) {
157 $filterFields[$field] = $product->fieldLabel($field);
158 }
159
160 return $filterFields;
161 }
162
163 164 165 166 167
168 static $order_empty_price = false;
169
170 static function allow_order_empty_price($val = true) {
171 self::$order_empty_price = $val;
172 }
173
174 175 176
177 static $order_button_class = 'SimpleOrderButton';
178
179 static function set_order_button_class($value) {
180 self::$order_button_class = $value;
181 }
182
183 184 185
186 static $empty_order_button_class = 'SimpleOrderButton';
187
188 static function set_empty_order_button_class($value) {
189 self::$empty_order_button_class = $value;
190 }
191
192 public function defaultSearchColumns() {
193 return implode(',', $this->stat('fulltext_fields'));
194 }
195
196 public function __construct($record = null, $isSingleton = false) {
197 if (!isset($record['Price']) && isset($record['ID'])) {
198 $p = DataObject::get_by_id('Product', $record['ID']);
199 $record = $p->record;
200 }
201 parent::__construct($record, $isSingleton);
202 }
203
204 205 206 207 208 209
210
211 function MainPhoto() {
212 if ($this->PhotoID && $this->Photo()->ID) {
213 return $this->Photo();
214 }
215 if ($this->Photos()->Count()) {
216 return $this->Photos()->First()->Photo();
217 }
218 return false;
219 }
220
221 222 223 224 225 226
227
228 function AllPhotos() {
229 $photos = $this->Photos();
230 if ($this->PhotoID && $this->Photo()->ID) {
231 $mainPhoto = new MediawebPage_Photo();
232 $mainPhoto->Caption = $this->Title;
233 $mainPhoto->MediawebPageID = $this->ID;
234 $mainPhoto->PhotoID = $this->Photo()->ID;
235 $photos->unshift($mainPhoto);
236 }
237 return $photos;
238 }
239
240
241 static function import_find($importID) {
242 return DataObject::get_one('Product', "ImportID = '" . Convert::raw2sql($importID) . "'");
243 }
244
245 246 247
248 static $possibleFields = array(
249 'Title', 'Available', 'AllowPurchase', 'BasePrice', 'CostPrice', 'Vendor', 'SKU', 'Description', 'Content', 'URLSegment', 'MenuTitle', 'MetaTitle', 'MetaDescription', 'MetaKeywords', 'Sort'
250 );
251
252 253 254 255 256
257 static function addPossibleFields($fields) {
258 if ($fields)
259 foreach($fields as $field)
260 self::$possibleFields[] = $field;
261 }
262
263 264 265 266 267
268 static function import_parse_bool_value($value) {
269 if (in_array($value, array('true', '1'))) {
270 return 1;
271 }
272 if (in_array($value, array('false', '0'))) {
273 return 0;
274 }
275 return null;
276 }
277
278 function getProductAdminLink(){
279 return "<a target='_blank' href='/admin/show/{$this->ID}'>{$this->Title}</a>";
280 }
281
282 283 284 285 286 287
288 function importUpdate($importLog, $data) {
289 if (!$this->importValidate($importLog, $data)) {
290 return false;
291 }
292
293 $rs = $this->extend('onBeforeImport', $importLog, $data);
294 if ($rs && max($rs) == false) return false;
295
296 $this->ImportID = $data['id'];
297 foreach(self::$possibleFields as $field) {
298 if (isset($data[$field]))
299 $this->{$field} = Convert::raw2sql($data[$field]);
300 }
301
302
303 if (isset($data['Photo'])) {
304 $this->PhotoID = ($data['Photo']) ? $data['Photo']->ID : 0;
305 }
306
307
308 if (isset($data['params'])) {
309 foreach($data['params'] as $param) {
310
311 $name = Convert::raw2sql($param->attributes()->name);
312
313 if ($this->db($name)) {
314 $value = Convert::raw2sql($param);
315 $this->{$name} = $value;
316 } else {
317 $importLog->addLog("У товара не определено поле {$name}!", 'error');
318 }
319 }
320 }
321
322 if (!$this->ID)
323 $this->ParentID = ($data['ParentID'] !== false) ? $data['ParentID'] : 0;
324
325 if (isset($data['ParentID']) && $data['ParentID'] === false) {
326 $this->write();
327 } else {
328 $this->doPublish();
329 }
330
331
332
333 if ($data['Photos']) {
334
335 $oldPhotos = $this->Photos();
336 $oldPhotosIDs = $oldPhotos->getIdList();
337 $this->Photos()->removeAll();
338
339 foreach($data['Photos'] as $importedImage) {
340 if ($mwPhoto = $oldPhotos->find('PhotoID', $importedImage->ID)) {
341 unset($oldPhotosIDs[$mwPhoto->ID]);
342 } else {
343 $mwPhoto = new MediawebPage_Photo();
344 $mwPhoto->PhotoID = $importedImage->ID;
345 }
346 $mwPhoto->MediawebPageID = $this->ID;
347 $mwPhoto->Caption = $importedImage->Title;
348 $mwPhoto->write();
349 }
350
351 if (count($oldPhotosIDs) > 0) {
352 foreach($oldPhotosIDs as $oldPhotosID) {
353 if ($oldPhoto = $oldPhotos->find('ID', $oldPhotosID)) {
354 $oldPhoto->delete();
355 }
356 }
357 }
358 }
359
360
361 if ($data['Files']) {
362
363 $oldFiles = $this->Files();
364 $oldFilesIDs = $oldFiles->getIdList();
365 $this->Files()->removeAll();
366
367 foreach($data['Files'] as $importedFile) {
368 if ($mwFile = $oldFiles->find('AttachID', $importedFile->ID)) {
369 unset($oldFilesIDs[$mwFile->ID]);
370 } else {
371 $mwFile = new MediawebPage_File();
372 $mwFile->AttachID = $importedFile->ID;
373 }
374 $mwFile->Caption = $importedFile->Title;
375 $mwFile->MediawebPageID = $this->ID;
376 $mwFile->write();
377 }
378
379 if (count($oldFilesIDs) > 0) {
380 foreach($oldFilesIDs as $oldFilesID) {
381 if ($oldFile = $oldFiles->find('ID', $oldFilesID)) {
382 $oldFile->delete();
383 }
384 }
385 }
386 }
387
388
389 if ($data['SpecialCatalogs']) {
390 $productSpecialCatalogs = array();
391
392 $allSpecialCatalogs = DataObject::get('SpecialCatalog');
393 if ($allSpecialCatalogs) {
394 foreach($allSpecialCatalogs as $specialCatalog) {
395 $specialCatalogProducts = $specialCatalog->LinkedProducts()->getIdList();
396 if (isset($specialCatalogProducts[$this->ID])) {
397 $productSpecialCatalogs[$specialCatalog->ID] = $specialCatalog->ID;
398 }
399 }
400 }
401
402
403 foreach($data['SpecialCatalogs'] as $specialCatalog) {
404 if (isset($productSpecialCatalogs[$specialCatalog->ID])) {
405 unset($productSpecialCatalogs[$specialCatalog->ID]);
406 } else {
407 $specialCatalog->LinkedProducts()->add($this);
408 }
409 }
410
411 if (count($productSpecialCatalogs)) {
412 foreach($productSpecialCatalogs as $productSpecialCatalog) {
413 if ($oldSpecialCatalog = $allSpecialCatalogs->find('ID', $productSpecialCatalog)) {
414 $oldSpecialCatalog->LinkedProducts()->remove($this);
415 }
416 }
417 }
418 }
419 $this->extend('onAfterImport');
420 return true;
421 }
422
423 424 425 426 427
428 function importValidate($importLog, & $data) {
429
430 if ((!$this->Title) && (!isset($data['Title']) || trim($data['Title']) == '')) {
431 $importLog->addLog("Товар с id='{$data['id']}' не имеет названия!", 'error');
432 return false;
433 }
434 if (isset($data['Title']) && trim($data['Title']) == '') {
435 $importLog->addLog("Товар с id='{$data['id']}' не имеет названия!", 'error');
436 return false;
437 }
438 if (isset($data['Available'])) {
439 $rs = self::import_parse_bool_value($data['Available']);
440 if ($rs === null) {
441 $importLog->addLog(sprintf(_t('Product.ImportBadBoolValue'), "Наличие товара на складе", $data['Title']), 'warning');
442 } else {
443 $data['Available'] = $rs;
444 }
445 }
446 if (isset($data['AllowPurchase'])) {
447 $rs = self::import_parse_bool_value($data['AllowPurchase']);
448 if ($rs === null) {
449 $importLog->addLog(sprintf(_t('Product.ImportBadBoolValue'), "Разрешено покупать", $data['Title']), 'warning');
450 } else {
451 $data['AllowPurchase'] = $rs;
452 }
453 }
454 if (isset($data['BasePrice']) && !is_numeric($data['BasePrice'])) {
455 $importLog->addLog("Параметр 'Цена' товара {$data['Title']} должен быть числовым!", 'error');
456 return false;
457 }
458 if (isset($data['CostPrice']) && !is_numeric($data['CostPrice'])) {
459 $importLog->addLog("Параметр 'Цена по акции' товара {$data['Title']} должен быть числовым!", 'error');
460 $data['CostPrice'] = 0;
461 }
462 if (isset($data['Sort']) && $data['Sort'] != (int)$data['Sort']) {
463 $importLog->addLog("Параметр Sort товара {$data['Title']} не является целым числом!", 'warning');
464 $data['Sort'] = 0;
465 }
466
467 $rs = $this->extend('importValidate', $importLog, $data);
468 if ($rs && max($rs) == false) return false;
469
470 return true;
471 }
472
473 474 475 476
477 function importClearAll($importLog) {
478 $oldMode = Versioned::get_reading_mode();
479
480 Versioned::reading_stage('Stage');
481 $products = DataObject::get('Product');
482 if ($products)
483 foreach($products as $product) {
484 $product->doUnpublish();
485 $product->delete();
486 }
487
488 Versioned::reading_stage('Live');
489 $products = DataObject::get('Product');
490 if ($products)
491 foreach($products as $product) {
492 $product->doUnpublish();
493 $product->delete();
494 }
495
496 Versioned::set_reading_mode($oldMode);
497 }
498
499
500
501
502 public function getCMSFields() {
503 SiteTree::disableCMSFieldsExtensions();
504 $fields = parent::getCMSFields();
505 SiteTree::enableCMSFieldsExtensions();
506
507 $fields->replaceField('ParentType', new HiddenField('ParentType', '', 'subpage'));
508
509 $fields->replaceField('Title', new TextField('Title', _t('Product.db_Title', 'Наименование')));
510
511
512 $cat = $this->Parent();
513 while ($cat->Parent() && $cat->Parent()->ClassName == self::$default_parent) {
514 $cat = $cat->Parent();
515 }
516 $parentIDField = new TreeDropdownField('ParentID', $this->fieldLabel('ParentID'), 'SiteTree');
517 $parentIDField->setTreeBaseID($cat->ParentID);
518 $parentIDField->setFilterFunction(create_function('$node', 'return ($node->ClassName == "'.self::$default_parent.'");'));
519
520 $fields->replaceField('ParentID', $parentIDField);
521
522 $fields->addFieldToTab('Root.Content.Main', new NumericField('BasePrice', $this->fieldLabel('BasePrice')));
523 $fields->addFieldToTab('Root.Content.Main', new NumericField('CostPrice', $this->fieldLabel('CostPrice')));
524
525 $fields->addFieldToTab('Root.Content.Main', new CheckboxField('Available', $this->fieldLabel('Available')));
526 $fields->addFieldToTab('Root.Content.Main', new CheckboxField('AllowPurchase', $this->fieldLabel('AllowPurchase')));
527
528 $fields->addFieldToTab('Root.Content.Main', $imgField = new ImageField('Photo', $this->fieldLabel('Photo')));
529 if ($folder = $this->getAssociatedFolder()) {
530 $folderPath = substr_replace(str_replace(ASSETS_DIR.'/', '', $folder->Filename), "", -1);
531 $imgField->setFolderName($folderPath);
532 }
533
534 $fields->addFieldToTab('Root.Content.Main', new TextField('Vendor', $this->fieldLabel('Vendor')));
535 $fields->addFieldToTab('Root.Content.Main', new TextField('ImportID', $this->fieldLabel('ImportID')));
536 $fields->addFieldToTab('Root.Content.Main', new TextareaField('Description', $this->fieldLabel('Description')));
537
538 if (HtmlEditorConfig::get_active() != 'cms') {
539 HtmlEditorConfig::set_active('cms');
540 }
541
542 $fields->addFieldToTab('Root.Content', new Tab("FullContent", $this->fieldLabel('Content')), 'Metadata');
543 $fields->addFieldToTab('Root.Content.FullContent', $fields->dataFieldByName('Content'));
544
545
546
547 $this->extend('updateCMSFields', $fields);
548
549 $this->extend('hideCMSFields', $fields);
550 return $fields;
551 }
552
553 public function getCMSActions() {
554 Requirements::javascript('webylon/javascript/SubpageAction.js');
555 $fields = parent::getCMSActions();
556 $fields->push(new HiddenField('backLink', '', $this->Parent()->ID));
557 $fields->insertFirst(new FormAction('goBack', _t('Product.BACKBUTTON', 'Back')));
558 return $fields;
559 }
560
561 public function populateDefaults() {
562 parent::populateDefaults();
563
564 $sc = SiteConfig::current_site_config();
565 $this->ShowInMenus = ($sc->ProductShowInTree) ? 1 : 0;
566 $this->ShowOnlyInTab = ($sc->ProductShowInTree) ? 0 : 1;
567 }
568
569 function onBeforeWrite() {
570 parent::onBeforeWrite();
571 if ($this->isChanged('BasePrice') || $this->isChanged('CostPrice')) {
572 $this->Price = ($this->CostPrice > 0) ? $this->CostPrice : $this->BasePrice;
573 }
574 $sc = SiteConfig::current_site_config();
575 if ($sc->ProductShowInTree) {
576 $this->ShowOnlyInTab = 0;
577 }
578 else {
579 $this->ShowInMenus = 0;
580 }
581 }
582
583 584 585 586 587
588 function VirtualPages() {
589 if (!$this->ID)
590 return null;
591 if (class_exists('Subsite')) {
592 return Subsite::get_from_all_subsites('VirtualProduct', "\"CopyContentFromID\" = " . (int) $this->ID);
593 } else {
594 return DataObject::get('VirtualProduct', "\"CopyContentFromID\" = " . (int) $this->ID);
595 }
596 }
597
598 public function canCreate($member = null) {
599 return (DataObject::get_one('Catalog')) ? parent::canCreate($member) : false;
600 }
601
602 function fieldLabels($includerelations = true) {
603 $labels = parent::fieldLabels($includerelations);
604 $labels['Title'] = _t('Product.db_Title', 'Title');
605 return $labels;
606 }
607
608 609 610 611 612
613 function оrderButtonClass() {
614 if (!$this->AllowPurchase)
615 return false;
616
617 $class = false;
618 if ($this->Price == 0) {
619 if (!self::$order_empty_price)
620 return false;
621 $class = self::$empty_order_button_class;
622 }
623 $class = self::$order_button_class;
624 $this->extend('updateOrderButtonClass', $class);
625 return $class;
626 }
627
628 629 630 631 632
633 function OrderButton() {
634 $buttonClass = $this->оrderButtonClass();
635 if (!$buttonClass)
636 return false;
637 return new $buttonClass($this->ID);
638 }
639
640 641 642 643 644
645 function OrderButtonOne() {
646 if ($button = $this->OrderButton())
647 return $button->One();
648 return false;
649 }
650
651 652 653 654 655
656 function OrderButtonWithNum() {
657 if ($button = $this->OrderButton())
658 return $button->WithNum();
659 return false;
660 }
661
662 663 664 665 666
667
668 function getRealPrice() {
669 $price = $this->Price;
670 $this->extend('updatePrice', $price);
671 return $price;
672 }
673
674 675 676 677 678 679 680
681 function Price($onlyreal=false) {
682
683 $real = $this->getRealPrice();
684 $client = $real;
685 $delta = 0;
686 $percent = 0;
687 $decimal = 2;
688
689 if ($this->CostPrice == 0) {
690
691 $member = Member::currentUser();
692 if ($member){
693 if ($member->hasMethod('getPersonalDiscount') && $percent < $member->getPersonalDiscount()) {
694 $percent = $member->getPersonalDiscount();
695 }
696
697 if ($groups = $member->Groups()) {
698 foreach ($groups as $g) {
699 if ($g->hasMethod('getDiscount') && $percent < $g->getDiscount()) {
700 $percent = $g->getDiscount();
701 }
702 }
703 }
704 }
705
706 $this->extend('updateDiscount', $percent);
707 }
708
709 $sc = SiteConfig::current_site_config();
710 $ceil = $sc->CatalogDiscountCeil;
711
712 if ($ceil) {
713 $real = ceil($real);
714 $client = ceil($client);
715 $decimal = 0;
716 }
717
718 if ($percent) {
719 $delta = ($ceil) ? ceil(($real / 100) * $percent) : round(($real / 100) * $percent, 2);
720 $client = $real - $delta;
721 }
722
723 if ($onlyreal !== false) {
724 return $client;
725 }
726
727 return new ArrayData(array(
728 'Real' => DBField::create('CatalogPrice', $real),
729 'Client' => DBField::create('CatalogPrice', $client),
730 'Delta' => DBField::create('CatalogPrice', $delta),
731 'Percent' => $percent,
732 'Discount' => ($percent > 0) ? 1 : 0,
733 'Currency' => $sc->CatalogCurrency,
734 ));
735 }
736
737 function HasDiscount() {
738 return ($this->CostPrice > 0);
739 }
740
741 function Currency() {
742 return SiteConfig::current_site_config()->CatalogCurrency;
743 }
744
745 function getOrderItem() {
746
747 }
748 }
749
750 class Product_Controller extends MediawebPage_Controller {
751 private $siblings = null;
752 private $next = null;
753 private $prev = null;
754
755 756 757 758 759
760 function Siblings() {
761 if (!isset($this->siblings)) {
762 $catalog = new Catalog_Controller($this->Parent());
763 $catalog->init();
764 $this->siblings = $this->Parent()->filteredProducts($catalog->CurrentSort, $catalog->FilterData);
765 }
766 return $this->siblings;
767 }
768
769 770 771 772 773
774 function NextProduct() {
775 if (!isset($this->next)) {
776 $list = $this->Siblings()->toArray();
777 while ($row = array_shift($list)) {
778 if ($row->ID == $this->dataRecord->ID) break;
779 }
780 $this->next = array_shift($list);
781 }
782 return $this->next;
783 }
784
785 786 787 788 789
790 function PrevProduct() {
791 if (!isset($this->prev)) {
792 $this->prev = false;
793 $list = $this->Siblings()->toArray();
794 $prev = array_shift($list);
795 while ($row = array_shift($list)) {
796 if ($row->ID == $this->dataRecord->ID) {
797 $this->prev = $prev;
798 break;
799 }
800 $prev = $row;
801 }
802 }
803 return $this->prev;
804 }
805 }
806
807
[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the
Silverstripe Forums to ask development related questions.
-