Bitrix-D7  20.5.0
basketitembase.php
См. документацию.
1 <?php
2 /**
3  * Bitrix Framework
4  * @package bitrix
5  * @subpackage sale
6  * @copyright 2001-2012 Bitrix
7  */
8 namespace Bitrix\Sale;
9 
10 use Bitrix\Main;
14 
15 Localization\Loc::loadMessages(__FILE__);
16 
17 /**
18  * Class BasketItemBase
19  * @package Bitrix\Sale
20  */
22 {
23  /** @var BasketPropertiesCollectionBase $propertyCollection */
25 
26  /** @var Internals\Fields */
27  protected $calculatedFields;
28 
29  /** @var ProviderBase */
30  protected $provider;
31 
32  /** @var string */
33  protected $internalId = null;
34 
35  protected static $idBasket = 0;
36 
37  /**
38  * @param $basketCode
39  * @return BasketItemBase|null
40  * @throws Main\ArgumentNullException
41  */
42  public function findItemByBasketCode($basketCode)
43  {
44  if ((string)$this->getBasketCode() === (string)$basketCode)
45  return $this;
46 
47  return null;
48  }
49 
50  /**
51  * @return string|void
52  */
53  public static function getRegistryEntity()
54  {
56  }
57 
58  /**
59  * @param $id
60  * @return BasketItemBase|null
61  * @throws Main\ArgumentNullException
62  */
63  public function findItemById($id)
64  {
65  if ($id <= 0)
66  return null;
67 
68  if ((int)$this->getId() === (int)$id)
69  return $this;
70 
71  return null;
72  }
73 
74  /**
75  * @return int|null|string
76  * @throws Main\ArgumentNullException
77  */
78  public function getBasketCode()
79  {
80  if ($this->internalId == null)
81  {
82  if ($this->getId() > 0)
83  {
84  $this->internalId = $this->getId();
85  }
86  else
87  {
88  static::$idBasket++;
89  $this->internalId = 'n'.static::$idBasket;
90  }
91  }
92 
93  return $this->internalId;
94  }
95 
96  /**
97  * @param BasketItemCollection $basketItemCollection
98  * @param $moduleId
99  * @param $productId
100  * @param null $basketCode
101  * @return BasketItemBase
102  * @throws Main\ArgumentException
103  * @throws Main\NotImplementedException
104  * @throws Main\ObjectException
105  */
106  public static function create(BasketItemCollection $basketItemCollection, $moduleId, $productId, $basketCode = null)
107  {
108  $fields = [
109  "MODULE" => $moduleId,
110  "BASE_PRICE" => 0,
111  "CAN_BUY" => 'Y',
112  "CUSTOM_PRICE" => 'N',
113  "PRODUCT_ID" => $productId,
114  'XML_ID' => static::generateXmlId(),
115  ];
116 
117  $basket = $basketItemCollection->getBasket();
118  if ($basket instanceof BasketBase)
119  {
120  $fields['LID'] = $basket->getSiteId();
121  }
122 
123  $basketItem = static::createBasketItemObject($fields);
124 
125  if ($basketCode !== null)
126  {
127  $basketItem->internalId = $basketCode;
128  if (mb_strpos($basketCode, 'n') === 0)
129  {
130  $internalId = intval(mb_substr($basketCode, 1));
131  if ($internalId > static::$idBasket)
132  {
133  static::$idBasket = $internalId;
134  }
135  }
136  }
137 
138  $basketItem->setCollection($basketItemCollection);
139 
140  return $basketItem;
141  }
142 
143  /**
144  * @return string
145  */
146  protected static function generateXmlId()
147  {
148  return uniqid('bx_');
149  }
150 
151  /**
152  * @param array $fields
153  * @return mixed
154  * @throws Main\ArgumentException
155  * @throws Main\NotImplementedException
156  */
157  private static function createBasketItemObject(array $fields = [])
158  {
159  $registry = Registry::getInstance(static::getRegistryType());
160  $basketItemClassName = $registry->getBasketItemClassName();
161 
162  return new $basketItemClassName($fields);
163  }
164 
165  /**
166  * @return array
167  */
168  public static function getSettableFields()
169  {
170  $result = [
171  "NAME", "LID", "SORT", "PRODUCT_ID", "PRODUCT_PRICE_ID", "PRICE_TYPE_ID",
172  "CATALOG_XML_ID", "PRODUCT_XML_ID", "DETAIL_PAGE_URL",
173  "BASE_PRICE", "PRICE", "DISCOUNT_PRICE", "CURRENCY", "CUSTOM_PRICE",
174  "QUANTITY", "WEIGHT", "DIMENSIONS", "MEASURE_CODE", "MEASURE_NAME",
175  "DELAY", "CAN_BUY", "NOTES", "VAT_RATE", "VAT_INCLUDED", "BARCODE_MULTI",
176  "SUBSCRIBE", "PRODUCT_PROVIDER_CLASS", "CALLBACK_FUNC", "ORDER_CALLBACK_FUNC",
177  "CANCEL_CALLBACK_FUNC", "PAY_CALLBACK_FUNC", "TYPE", "SET_PARENT_ID",
178  "DISCOUNT_NAME", "DISCOUNT_VALUE", "DISCOUNT_COUPON", "RECOMMENDATION", "XML_ID",
179  "MARKING_CODE_GROUP"
180  ];
181 
182  return array_merge($result, static::getCalculatedFields());
183  }
184 
185  /**
186  * @return array|null
187  */
188  public static function getSettableFieldsMap()
189  {
190  static $fieldsMap = null;
191 
192  if ($fieldsMap === null)
193  {
194  $fieldsMap = array_fill_keys(static::getSettableFields(), true);
195  }
196 
197  return $fieldsMap;
198  }
199 
200  /**
201  * @return array
202  */
203  public static function getCalculatedFields()
204  {
205  return [
206  'DISCOUNT_PRICE_PERCENT',
207  'IGNORE_CALLBACK_FUNC',
208  'DEFAULT_PRICE',
209  'DISCOUNT_LIST'
210  ];
211  }
212 
213  /**
214  * @return array
215  */
216  public static function getAvailableFields()
217  {
218  return static::getSettableFields();
219  }
220 
221  /**
222  * @return array
223  */
224  public static function getCustomizableFields() : array
225  {
226  return ['PRICE' => 'PRICE'];
227  }
228 
229  /**
230  * @return array
231  */
232  protected static function getMeaningfulFields()
233  {
234  return ['QUANTITY', 'PRICE', 'CUSTOM_PRICE'];
235  }
236 
237  /**
238  * BasketItemBase constructor.
239  * @param array $fields
240  * @throws Main\ArgumentException
241  * @throws Main\ArgumentNullException
242  * @throws Main\ObjectPropertyException
243  * @throws Main\SystemException
244  */
245  protected function __construct(array $fields = [])
246  {
247  $priceFields = ['BASE_PRICE', 'PRICE', 'DISCOUNT_PRICE'];
248 
249  foreach ($priceFields as $code)
250  {
251  if (isset($fields[$code]))
252  {
254  }
255  }
256 
258 
259  $this->calculatedFields = new Internals\Fields();
260  }
261 
262  /**
263  * @return Result
264  */
265  protected function checkBeforeDelete()
266  {
267  return new Result();
268  }
269 
270  /**
271  * @return Result
272  * @throws Main\ArgumentOutOfRangeException
273  * @throws Main\ObjectNotFoundException
274  * @throws \Exception
275  */
276  public function delete()
277  {
278  $result = new Result();
279 
280  $checkResult = $this->checkBeforeDelete();
281  if (!$checkResult->isSuccess())
282  {
283  $result->addErrors($checkResult->getErrors());
284  return $result;
285  }
286 
287  /** @var array $oldEntityValues */
288  $oldEntityValues = $this->fields->getOriginalValues();
289 
290  /** @var Main\Event $event */
291  $event = new Main\Event('sale', "OnBeforeSaleBasketItemEntityDeleted", [
292  'ENTITY' => $this,
293  'VALUES' => $oldEntityValues,
294  ]);
295  $event->send();
296 
297  if ($event->getResults())
298  {
299  /** @var Main\EventResult $eventResult */
300  foreach($event->getResults() as $eventResult)
301  {
302  if($eventResult->getType() == Main\EventResult::ERROR)
303  {
304  $eventResultData = $eventResult->getParameters();
305  if ($eventResultData instanceof ResultError)
306  {
307  $error = $eventResultData;
308  }
309  else
310  {
311  $error = new ResultError(
312  Localization\Loc::getMessage('SALE_EVENT_ON_BEFORE_SALEBASKETITEM_ENTITY_DELETED_ERROR'),
313  'SALE_EVENT_ON_BEFORE_SALEBASKETITEM_ENTITY_DELETED_ERROR'
314  );
315  }
316 
317  $result->addError($error);
318  }
319  }
320 
321  if (!$result->isSuccess())
322  {
323  return $result;
324  }
325  }
326 
327  $this->setFieldNoDemand("QUANTITY", 0);
328 
329  /** @var Result $r */
330  $r = parent::delete();
331  if (!$r->isSuccess())
332  {
333  $result->addErrors($r->getErrors());
334  return $result;
335  }
336 
337  /** @var array $oldEntityValues */
338  $oldEntityValues = $this->fields->getOriginalValues();
339 
340  /** @var Main\Event $event */
341  $event = new Main\Event('sale', "OnSaleBasketItemEntityDeleted", [
342  'ENTITY' => $this,
343  'VALUES' => $oldEntityValues,
344  ]);
345  $event->send();
346 
347  if ($event->getResults())
348  {
349  /** @var Main\EventResult $eventResult */
350  foreach($event->getResults() as $eventResult)
351  {
352  if($eventResult->getType() == Main\EventResult::ERROR)
353  {
354  $eventResultData = $eventResult->getParameters();
355  if ($eventResultData instanceof ResultError)
356  {
357  $error = $eventResultData;
358  }
359  else
360  {
361  $error = new ResultError(
362  Localization\Loc::getMessage('SALE_EVENT_ON_SALEBASKETITEM_ENTITY_DELETED_ERROR'),
363  'SALE_EVENT_ON_SALEBASKETITEM_ENTITY_DELETED_ERROR'
364  );
365  }
366 
367  $result->addError($error);
368  }
369  }
370  }
371 
372  return $result;
373  }
374 
375  /**
376  * @param string $name Field name.
377  * @param string|int|float $value Field value.
378  * @return Result
379  * @throws Main\ArgumentOutOfRangeException
380  * @throws \Exception
381  */
382  public function setField($name, $value)
383  {
384  $priceFields = [
385  'BASE_PRICE' => 'BASE_PRICE',
386  'PRICE' => 'PRICE',
387  'DISCOUNT_PRICE' => 'DISCOUNT_PRICE',
388  ];
389  if (isset($priceFields[$name]))
390  {
391  $value = PriceMaths::roundPrecision($value);
392  }
393 
394  if ($this->isCalculatedField($name))
395  {
396  $this->calculatedFields->set($name, $value);
397  return new Result();
398  }
399 
400  if ($name === 'CUSTOM_PRICE')
401  {
402  if ($value == 'Y')
403  {
404  $this->markFieldCustom('PRICE');
405  }
406  else
407  {
408  $this->unmarkFieldCustom('PRICE');
409  }
410  }
411 
412  return parent::setField($name, $value);
413  }
414 
415  /**
416  * @internal
417  *
418  * @param $name
419  * @param $value
420  * @throws Main\ArgumentNullException
421  * @throws Main\ArgumentOutOfRangeException
422  */
423  public function setFieldNoDemand($name, $value)
424  {
425  $priceFields = [
426  'BASE_PRICE' => 'BASE_PRICE',
427  'PRICE' => 'PRICE',
428  'DISCOUNT_PRICE' => 'DISCOUNT_PRICE',
429  ];
430  if (isset($priceFields[$name]))
431  {
432  $value = PriceMaths::roundPrecision($value);
433  }
434 
435  if ($this->isCalculatedField($name))
436  {
437  $this->calculatedFields->set($name, $value);
438  return;
439  }
440 
441  if ($name === 'CUSTOM_PRICE')
442  {
443  if ($value === 'Y')
444  {
445  $this->markFieldCustom('PRICE');
446  }
447  else
448  {
449  $this->unmarkFieldCustom('PRICE');
450  }
451  }
452 
453  parent::setFieldNoDemand($name, $value);
454  }
455 
456  /**
457  * @param $name
458  * @return float|string|null
459  * @throws Main\ArgumentNullException
460  * @throws Main\ArgumentOutOfRangeException
461  */
462  public function getField($name)
463  {
464  static $calculatedFields = null;
465 
466  if ($calculatedFields === null)
467  {
468  $calculatedFields = array_fill_keys(static::getCalculatedFields(), true);
469  }
470 
471  if (isset($calculatedFields[$name]))
472  {
473  if (
474  isset($this->calculatedFields[$name])
475  || (is_array($this->calculatedFields) && array_key_exists($name, $this->calculatedFields))
476  )
477  {
478  return $this->calculatedFields->get($name);
479  }
480 
481  return null;
482  }
483 
484  return parent::getField($name);
485  }
486 
487  /**
488  * @param array $values
489  * @return array
490  */
491  protected function onBeforeSetFields(array $values)
492  {
493  $priorityFields = [
494  'CURRENCY', 'CUSTOM_PRICE', 'VAT_RATE', 'VAT_INCLUDED',
495  'PRODUCT_PROVIDER_CLASS', 'SUBSCRIBE', 'TYPE', 'LID', 'FUSER_ID'
496  ];
497 
498  $priorityValues = [];
499  foreach ($priorityFields as $field)
500  {
501  if (isset($values[$field]))
502  {
503  $priorityValues[$field] = $values[$field];
504  }
505  }
506 
507  return $priorityValues + $values;
508  }
509 
510  /**
511  * @param array $fields
512  * @return Result
513  * @throws Main\ArgumentOutOfRangeException
514  * @throws Main\NotSupportedException
515  * @throws \Exception
516  */
517  public function setFields(array $fields)
518  {
519  foreach ($fields as $name => $value)
520  {
521  if ($this->isCalculatedField($name))
522  {
523  $this->calculatedFields[$name] = $value;
524  unset($fields[$name]);
525  }
526  }
527 
528  return parent::setFields($fields);
529  }
530 
531  /**
532  * @return ProviderBase|null
533  * @throws Main\ArgumentNullException
534  */
535  public function getProviderName()
536  {
537  return $this->getProvider();
538  }
539 
540  /**
541  * @return null|string
542  * @throws Main\ArgumentNullException
543  */
544  public function getCallbackFunction()
545  {
546  $callbackFunction = trim($this->getField('CALLBACK_FUNC'));
547  if (!isset($callbackFunction) || (strval(trim($callbackFunction)) == ""))
548  {
549  return null;
550  }
551 
552  if (!function_exists($callbackFunction))
553  {
554  return null;
555  }
556 
557  return $callbackFunction;
558  }
559 
560  /**
561  * @return bool|mixed|null
562  * @throws Main\ArgumentNullException
563  * @throws Main\LoaderException
564  */
565  public function getProviderEntity()
566  {
567  $module = $this->getField('MODULE');
568  $productProviderName = $this->getField('PRODUCT_PROVIDER_CLASS');
569  if (
570  !isset($module)
571  || !isset($productProviderName)
572  || (strval($productProviderName) == "")
573  )
574  {
575  return false;
576  }
577 
578  if (!empty($module) && Main\Loader::includeModule($module))
579  {
580  return Internals\Catalog\Provider::getProviderEntity($productProviderName);
581  }
582 
583  return null;
584  }
585 
586  /**
587  * @return ProviderBase|null|string
588  * @throws Main\ArgumentNullException
589  * @throws Main\LoaderException
590  */
591  public function getProvider()
592  {
593  if ($this->provider !== null)
594  return $this->provider;
595 
596  $module = $this->getField('MODULE');
597  $productProviderName = $this->getField('PRODUCT_PROVIDER_CLASS');
598  if (
599  !isset($module)
600  || !isset($productProviderName)
601  || (strval($productProviderName) == "")
602  )
603  {
604  return null;
605  }
606 
607  $providerName = Internals\Catalog\Provider::getProviderName($module, $productProviderName);
608  if ($providerName !== null)
609  {
610  $this->provider = $providerName;
611  }
612 
613  return $providerName;
614  }
615 
616  /**
617  * @param string $name
618  * @param mixed $oldValue
619  * @param mixed $value
620  * @return Result
621  * @throws Main\ArgumentException
622  * @throws Main\ArgumentNullException
623  * @throws Main\ArgumentOutOfRangeException
624  * @throws Main\LoaderException
625  * @throws Main\ObjectNotFoundException
626  * @throws Main\SystemException
627  * @throws \Exception
628  */
629  protected function onFieldModify($name, $oldValue, $value)
630  {
631  $result = new Result();
632 
633  if ($name == "QUANTITY")
634  {
635  if ($value == 0)
636  {
637  $result->addError(new Main\Error(
638  Localization\Loc::getMessage(
639  'SALE_BASKET_ITEM_ERR_QUANTITY_ZERO',
640  ['#PRODUCT_NAME#' => $this->getField('NAME')]
641  )
642  ));
643  return $result;
644  }
645 
646  $value = (float)$value;
647  $oldValue = (float)$oldValue;
648  $deltaQuantity = $value - $oldValue;
649 
650  /** @var Basket $basket */
651  $basket = $this->getCollection();
652  $context = $basket->getContext();
653 
654  /** @var Result $r */
656  if (!$r->isSuccess())
657  {
658  $result->addErrors($r->getErrors());
659  $result->setData($r->getData());
660 
661  return $result;
662  }
663 
664  $providerData = $r->getData();
665 
666  if ($this->getField('SUBSCRIBE') !== 'Y')
667  {
668  if (array_key_exists('AVAILABLE_QUANTITY', $providerData) && $providerData['AVAILABLE_QUANTITY'] > 0)
669  {
670  $availableQuantity = $providerData['AVAILABLE_QUANTITY'];
671  }
672  else
673  {
674  $result->addError(
675  new ResultError(
676  Localization\Loc::getMessage(
677  'SALE_BASKET_ITEM_WRONG_AVAILABLE_QUANTITY',
678  ['#PRODUCT_NAME#' => $this->getField('NAME')]
679  ),
680  'SALE_BASKET_ITEM_WRONG_AVAILABLE_QUANTITY'
681  )
682  );
683 
684  return $result;
685  }
686  }
687  else
688  {
689  $availableQuantity = $value;
690  }
691 
692  if (!empty($providerData['PRICE_DATA']))
693  {
694  if (isset($providerData['PRICE_DATA']['CUSTOM_PRICE']))
695  {
696  $this->markFieldCustom('PRICE');
697  }
698  }
699 
700  if ($value != 0
701  && (($deltaQuantity > 0) && (roundEx($availableQuantity, SALE_VALUE_PRECISION) < roundEx($value, SALE_VALUE_PRECISION)) // plus
702  || ($deltaQuantity < 0) && (roundEx($availableQuantity, SALE_VALUE_PRECISION) > roundEx($value, SALE_VALUE_PRECISION)))
703  ) // minus
704  {
705  if ($deltaQuantity > 0)
706  {
707  $mess = Localization\Loc::getMessage(
708  'SALE_BASKET_AVAILABLE_FOR_PURCHASE_QUANTITY',
709  [
710  '#PRODUCT_NAME#' => $this->getField('NAME'),
711  '#AVAILABLE_QUANTITY#' => $availableQuantity
712  ]
713  );
714  }
715  else
716  {
717  $mess = Localization\Loc::getMessage(
718  'SALE_BASKET_AVAILABLE_FOR_DECREASE_QUANTITY',
719  [
720  '#PRODUCT_NAME#' => $this->getField('NAME'),
721  '#AVAILABLE_QUANTITY#' => $availableQuantity
722  ]
723  );
724  }
725 
726  $result->addError(new ResultError($mess, "SALE_BASKET_AVAILABLE_QUANTITY"));
727  $result->setData([
728  "AVAILABLE_QUANTITY" => $availableQuantity,
729  "REQUIRED_QUANTITY" => $deltaQuantity
730  ]);
731 
732  return $result;
733  }
734 
735  /** @var BasketItemCollection $collection */
736  $collection = $this->getCollection();
737 
738  /** @var BasketBase $basket */
739  $basket = $collection->getBasket();
740 
741  if ((!$basket->getOrder() || $basket->getOrderId() == 0) && !($collection instanceof BundleCollection))
742  {
743  if (!$this->isMarkedFieldCustom('PRICE') && $value > 0)
744  {
745  $r = $basket->refresh(RefreshFactory::createSingle($this->getBasketCode()));
746  if (!$r->isSuccess())
747  {
748  $result->addErrors($r->getErrors());
749  return $result;
750  }
751  }
752  }
753 
754  if (!$this->isMarkedFieldCustom('PRICE'))
755  {
756  $providerName = $this->getProviderName();
757  if (strval($providerName) == '')
758  {
759  $providerName = $this->getCallbackFunction();
760  }
761 
762 
763  if (!empty($providerData['PRICE_DATA']))
764  {
765  if (isset($providerData['PRICE_DATA']['PRICE']))
766  {
767  $this->setField('PRICE', $providerData['PRICE_DATA']['PRICE']);
768  }
769 
770  if (isset($providerData['PRICE_DATA']['BASE_PRICE']))
771  {
772  $this->setField('BASE_PRICE', $providerData['PRICE_DATA']['BASE_PRICE']);
773  }
774 
775  if (isset($providerData['PRICE_DATA']['DISCOUNT_PRICE']))
776  {
777  $this->setField('DISCOUNT_PRICE', $providerData['PRICE_DATA']['DISCOUNT_PRICE']);
778  }
779  }
780  elseif ($providerName && !$this->isCustom())
781  {
782  $result->addError(
783  new ResultError(
784  Localization\Loc::getMessage(
785  'SALE_BASKET_ITEM_WRONG_PRICE',
786  ['#PRODUCT_NAME#' => $this->getField('NAME')]
787  ),
788  'SALE_BASKET_ITEM_WRONG_PRICE'
789  )
790  );
791 
792  return $result;
793  }
794  }
795  }
796 
797  $r = parent::onFieldModify($name, $oldValue, $value);
798  if ($r->isSuccess())
799  {
800  if ($r->hasWarnings())
801  {
802  $result->addWarnings($r->getWarnings());
803  }
804 
805  if (($name === 'BASE_PRICE') || ($name === 'DISCOUNT_PRICE'))
806  {
807  if (!$this->isCustomPrice())
808  {
809  $price = $this->getField('BASE_PRICE') - $this->getField('DISCOUNT_PRICE');
810  $r = $this->setField('PRICE', $price);
811  if (!$r->isSuccess())
812  $result->addErrors($r->getErrors());
813  }
814  }
815  }
816  else
817  {
818  $result->addErrors($r->getErrors());
819  }
820 
821  return $result;
822  }
823 
824  /**
825  * @return bool
826  * @throws Main\ArgumentNullException
827  */
828  public function isVatInPrice()
829  {
830  return $this->getField('VAT_INCLUDED') === 'Y';
831  }
832 
833  /**
834  * @return float|int
835  * @throws Main\ArgumentNullException
836  */
837  public function getVat()
838  {
839  $vatRate = $this->getVatRate();
840  if ($vatRate == 0)
841  return 0;
842 
843  if ($this->isVatInPrice())
844  $vat = PriceMaths::roundPrecision(($this->getPrice() * $this->getQuantity() * $vatRate / ($vatRate + 1)));
845  else
846  $vat = PriceMaths::roundPrecision(($this->getPrice() * $this->getQuantity() * $vatRate));
847 
848  return $vat;
849  }
850 
851  /**
852  * @return float|int
853  * @throws Main\ArgumentNullException
854  */
855  public function getInitialPrice()
856  {
857  $price = PriceMaths::roundPrecision($this->getPrice() * $this->getQuantity());
858 
859  if ($this->isVatInPrice())
860  $price -= $this->getVat();
861 
862  return $price;
863  }
864 
865  /**
866  * @return float|int
867  * @throws Main\ArgumentNullException
868  */
869  public function getBasePriceWithVat()
870  {
871  $price = $this->getBasePrice();
872 
873  if (!$this->isVatInPrice())
874  {
875  $vatRate = $this->getVatRate();
876  $price += $this->getBasePrice() * $vatRate;
877  }
878 
879  return PriceMaths::roundPrecision($price);
880  }
881 
882  /**
883  * @return float|int
884  * @throws Main\ArgumentNullException
885  */
886  public function getPriceWithVat()
887  {
888  $price = $this->getPrice();
889 
890  if (!$this->isVatInPrice())
891  {
892  $vatRate = $this->getVatRate();
893  $price += $this->getPrice() * $vatRate;
894  }
895 
896  return PriceMaths::roundPrecision($price);
897  }
898 
899  /**
900  * @return float|int
901  * @throws Main\ArgumentNullException
902  */
903  public function getFinalPrice()
904  {
905  $price = PriceMaths::roundPrecision($this->getPrice() * $this->getQuantity());
906 
907  if (!$this->isVatInPrice())
908  $price += $this->getVat();
909 
910  return $price;
911  }
912 
913  /**
914  * @param string $field Field name.
915  * @return bool
916  */
917  protected function isCalculatedField($field)
918  {
919  static $calculateFields = null;
920 
921  if ($calculateFields === null)
922  {
923  $calculateFields = array_fill_keys(static::getCalculatedFields(), true);
924  }
925 
926  return isset($calculateFields[$field]);
927  }
928 
929  /**
930  * @return float|null|string
931  * @throws Main\ArgumentNullException
932  */
933  public function getProductId()
934  {
935  return $this->getField('PRODUCT_ID');
936  }
937 
938  /**
939  * @return float
940  * @throws Main\ArgumentNullException
941  */
942  public function getPrice()
943  {
944  return (float)$this->getField('PRICE');
945  }
946 
947  /**
948  * @return float
949  * @throws Main\ArgumentNullException
950  */
951  public function getBasePrice()
952  {
953  return (float)$this->getField('BASE_PRICE');
954  }
955 
956  /**
957  * @return float
958  * @throws Main\ArgumentNullException
959  */
960  public function getDiscountPrice()
961  {
962  return (float)$this->getField('DISCOUNT_PRICE');
963  }
964 
965  /**
966  * @return bool
967  * @throws Main\ArgumentOutOfRangeException
968  */
969  public function isCustomPrice()
970  {
971  return $this->isMarkedFieldCustom('PRICE');
972  }
973 
974  /**
975  * @return string
976  * @throws Main\ArgumentNullException
977  * @throws Main\ArgumentOutOfRangeException
978  */
979  public function getCurrency()
980  {
981  return $this->getField('CURRENCY');
982  }
983 
984  /**
985  * @return float
986  * @throws Main\ArgumentNullException
987  */
988  public function getQuantity()
989  {
990  return (float)$this->getField('QUANTITY');
991  }
992 
993  /**
994  * @return float|null|string
995  * @throws Main\ArgumentNullException
996  */
997  public function getWeight()
998  {
999  return $this->getField('WEIGHT');
1000  }
1001 
1002  /**
1003  * @return float|null|string
1004  * @throws Main\ArgumentNullException
1005  */
1006  public function getVatRate()
1007  {
1008  return $this->getField('VAT_RATE');
1009  }
1010 
1011  /**
1012  * @return float|null|string
1013  * @throws Main\ArgumentNullException
1014  */
1015  public function getFUserId()
1016  {
1017  return $this->getField('FUSER_ID');
1018  }
1019 
1020  /**
1021  * @param $id
1022  * @throws Main\ArgumentOutOfRangeException
1023  * @throws \Exception
1024  */
1025  public function setOrderId($id)
1026  {
1027  $this->setField('ORDER_ID', (int)$id);
1028  }
1029 
1030  /**
1031  * @return bool
1032  * @throws Main\ArgumentNullException
1033  */
1034  public function isBarcodeMulti()
1035  {
1036  return $this->getField('BARCODE_MULTI') === "Y";
1037  }
1038 
1039  /**
1040  * @return bool
1041  * @throws Main\ArgumentNullException
1042  * @throws Main\ArgumentOutOfRangeException
1043  */
1044  public function canBuy() : bool
1045  {
1046  return $this->getField('CAN_BUY') === 'Y';
1047  }
1048 
1049  /**
1050  * @return bool
1051  * @throws Main\ArgumentNullException
1052  * @throws Main\ArgumentOutOfRangeException
1053  */
1054  public function isDelay() : bool
1055  {
1056  return $this->getField('DELAY') === 'Y';
1057  }
1058 
1059  /**
1060  * @return bool
1061  * @throws Main\ArgumentNullException
1062  * @throws Main\ArgumentOutOfRangeException
1063  */
1064  public function isSupportedMarkingCode() : bool
1065  {
1066  return $this->getMarkingCodeGroup() !== '';
1067  }
1068 
1069  /**
1070  * @return string
1071  * @throws Main\ArgumentNullException
1072  * @throws Main\ArgumentOutOfRangeException
1073  */
1074  public function getMarkingCodeGroup() : string
1075  {
1076  return (string)$this->getField('MARKING_CODE_GROUP');
1077  }
1078 
1079  /**
1080  * @return BasketPropertiesCollectionBase|null
1081  * @throws Main\ArgumentException
1082  * @throws Main\ArgumentNullException
1083  * @throws Main\NotImplementedException
1084  * @throws Main\ObjectNotFoundException
1085  */
1086  public function getPropertyCollection()
1087  {
1088  if ($this->propertyCollection === null)
1089  {
1090  $registry = Registry::getInstance(static::getRegistryType());
1091 
1092  /** @var BasketPropertiesCollectionBase $basketPropertyCollectionClassName */
1093  $basketPropertyCollectionClassName = $registry->getBasketPropertiesCollectionClassName();
1094 
1095  if ($this->getId() > 0)
1096  {
1097  /** @var BasketItemCollection $collection */
1098  $collection = $this->getCollection();
1099  $basketPropertyCollectionClassName::loadByCollection($collection);
1100  }
1101 
1102  if ($this->propertyCollection === null)
1103  {
1104  $this->propertyCollection = $basketPropertyCollectionClassName::load($this);
1105  }
1106  }
1107 
1109  }
1110 
1111  /**
1112  * @return bool
1113  */
1114  public function isExistPropertyCollection()
1115  {
1116  return $this->propertyCollection !== null;
1117  }
1118 
1119  /**
1120  * @internal
1121  * @param BasketPropertiesCollectionBase $propertyCollection
1122  */
1124  {
1125  $this->propertyCollection = $propertyCollection;
1126  }
1127 
1128  /**
1129  * @param $value
1130  * @param bool $custom
1131  * @return Result
1132  * @throws Main\ArgumentOutOfRangeException
1133  * @throws \Exception
1134  */
1135  public function setPrice($value, $custom = false)
1136  {
1137  $result = new Result();
1138 
1139  if ($custom)
1140  {
1141  $this->markFieldCustom('PRICE');
1142  }
1143  else
1144  {
1145  $this->unmarkFieldCustom('PRICE');
1146  }
1147 
1148  $r = $this->setField('PRICE', $value);
1149  if (!$r->isSuccess())
1150  {
1151  $result->addErrors($r->getErrors());
1152  }
1153 
1154  return $result;
1155  }
1156 
1157  /**
1158  * @internal
1159  * @return array
1160  */
1161  public static function getRoundFields()
1162  {
1163  return [
1164  'BASE_PRICE',
1165  'DISCOUNT_PRICE',
1166  'DISCOUNT_PRICE_PERCENT',
1167  ];
1168  }
1169 
1170  /**
1171  * @param array $values
1172  * @throws Main\ArgumentOutOfRangeException
1173  */
1174  public function initFields(array $values)
1175  {
1176  if (!isset($values['BASE_PRICE']) || doubleval($values['BASE_PRICE']) == 0)
1177  $values['BASE_PRICE'] = $values['PRICE'] + $values['DISCOUNT_PRICE'];
1178 
1179  parent::initFields($values);
1180  }
1181 
1182  /**
1183  * @internal
1184  *
1185  * @return Result
1186  * @throws Main\ArgumentException
1187  * @throws Main\ArgumentNullException
1188  * @throws \Exception
1189  */
1190  public function save()
1191  {
1192  $this->checkCallingContext();
1193 
1194  $result = new Result();
1195 
1196  $id = (int)$this->getId();
1197  $isNew = $id === 0;
1198 
1199  $this->onBeforeSave();
1200 
1201  $r = $this->callEventSaleBasketItemBeforeSaved($isNew);
1202  if (!$r->isSuccess())
1203  {
1204  return $r;
1205  }
1206 
1207  if (!$this->isChanged())
1208  {
1209  return $result;
1210  }
1211 
1212  if ($id > 0)
1213  {
1214  $r = $this->update();
1215  }
1216  else
1217  {
1218  $r = $this->add();
1219  if ($r->getId() > 0)
1220  {
1221  $id = $r->getId();
1222  }
1223  }
1224 
1225  if (!$r->isSuccess())
1226  {
1227  return $r;
1228  }
1229 
1230  if ($id > 0)
1231  {
1232  $result->setId($id);
1233 
1235  $controller->save($this);
1236  }
1237 
1238  $r = $this->callEventSaleBasketItemSaved($isNew);
1239  if (!$r->isSuccess())
1240  {
1241  return $r;
1242  }
1243 
1244  $r = $this->saveProperties();
1245  if (!$r->isSuccess())
1246  {
1247  $result->addErrors($r->getErrors());
1248  }
1249 
1251 
1252  return $result;
1253  }
1254 
1255  /**
1256  * @return BasketBase
1257  */
1258  public function getBasket()
1259  {
1260  /** @var BasketItemCollection $collection */
1261  $collection = $this->getCollection();
1262 
1263  return $collection->getBasket();
1264  }
1265 
1266  /**
1267  * @return void
1268  */
1269  private function checkCallingContext()
1270  {
1271  $basket = $this->getBasket();
1272 
1273  $order = $basket->getOrder();
1274 
1275  if ($order)
1276  {
1277  if (!$order->isSaveRunning())
1278  {
1279  trigger_error("Incorrect call to the save process. Use method save() on \Bitrix\Sale\Order entity", E_USER_WARNING);
1280  }
1281  }
1282  else
1283  {
1284  if (!$basket->isSaveRunning())
1285  {
1286  trigger_error("Incorrect call to the save process. Use method save() on \Bitrix\Sale\Basket entity", E_USER_WARNING);
1287  }
1288  }
1289  }
1290 
1291  /**
1292  * @return Result
1293  * @throws Main\ArgumentException
1294  * @throws Main\NotImplementedException
1295  */
1296  private function saveProperties()
1297  {
1298  /** @var BasketPropertiesCollection $propertyCollection */
1300  return $propertyCollection->save();
1301  }
1302 
1303  /**
1304  * @throws Main\ArgumentNullException
1305  * @throws Main\ArgumentOutOfRangeException
1306  * @throws Main\ObjectNotFoundException
1307  * @return void
1308  */
1309  protected function onBeforeSave()
1310  {
1311  /** @var BasketItemCollection $collection */
1312  $collection = $this->getCollection();
1313 
1314  /** @var BasketBase $basket */
1315  if (!$basket = $collection->getBasket())
1316  {
1317  throw new Main\ObjectNotFoundException('Entity "Basket" not found');
1318  }
1319 
1320  /** @var BasketBase $basket */
1321  $basket = $collection->getBasket();
1322 
1323  if ($this->getField('ORDER_ID') <= 0)
1324  {
1325  $orderId = (int)$collection->getOrderId();
1326  if ($orderId > 0)
1327  {
1328  $this->setFieldNoDemand('ORDER_ID', $orderId);
1329  }
1330  }
1331 
1332  if ($this->getId() <= 0)
1333  {
1334  if ($this->getField('FUSER_ID') <= 0)
1335  {
1336  $fUserId = (int)$basket->getFUserId(true);
1337  if ($fUserId <= 0)
1338  throw new Main\ArgumentNullException('FUSER_ID');
1339 
1340  $this->setFieldNoDemand('FUSER_ID', $fUserId);
1341  }
1342  }
1343  }
1344 
1345  /**
1346  * @return Result
1347  * @throws Main\ArgumentNullException
1348  * @throws Main\ArgumentOutOfRangeException
1349  */
1350  protected function add()
1351  {
1352  $result = new Result();
1353 
1354  $dateInsert = new Main\Type\DateTime();
1355 
1356  $this->setFieldNoDemand('DATE_INSERT', $dateInsert);
1357  $this->setFieldNoDemand('DATE_UPDATE', $dateInsert);
1358 
1359  $fields = $this->fields->getValues();
1360 
1361  $r = $this->addInternal($fields);
1362  if (!$r->isSuccess())
1363  {
1364  $result->addErrors($r->getErrors());
1365  return $result;
1366  }
1367 
1368  if ($resultData = $r->getData())
1369  {
1370  $result->setData($resultData);
1371  }
1372 
1373  $id = $r->getId();
1374  $this->setFieldNoDemand('ID', $id);
1375  $result->setId($id);
1376 
1377  return $result;
1378  }
1379 
1380  /**
1381  * @return Result
1382  * @throws Main\ArgumentNullException
1383  * @throws Main\ArgumentOutOfRangeException
1384  * @throws Main\ObjectException
1385  */
1386  protected function update()
1387  {
1388  $result = new Result();
1389 
1390  $this->setFieldNoDemand('DATE_UPDATE', new Main\Type\DateTime());
1391 
1392  $fields = $this->fields->getChangedValues();
1393 
1394  if (!empty($fields))
1395  {
1396  $r = $this->updateInternal($this->getId(), $fields);
1397  if (!$r->isSuccess())
1398  {
1399  $result->addErrors($r->getErrors());
1400  return $result;
1401  }
1402 
1403  if ($resultData = $r->getData())
1404  {
1405  $result->setData($resultData);
1406  }
1407  }
1408 
1409  return $result;
1410  }
1411 
1412  /**
1413  * @return void
1414  */
1416  {
1417  /** @var array $oldEntityValues */
1418  $oldEntityValues = $this->fields->getOriginalValues();
1419 
1420  if (!empty($oldEntityValues))
1421  {
1422  /** @var Main\Event $event */
1423  $event = new Main\Event(
1424  'sale',
1425  'OnSaleBasketItemEntitySaved',
1426  [
1427  'ENTITY' => $this,
1428  'VALUES' => $oldEntityValues,
1429  ]
1430  );
1431 
1432  $event->send();
1433  }
1434  }
1435 
1436  /**
1437  * @param $isNewEntity
1438  * @return Result
1439  */
1440  protected function callEventSaleBasketItemBeforeSaved($isNewEntity)
1441  {
1442  $result = new Result();
1443 
1444  /** @var array $oldEntityValues */
1445  $oldEntityValues = $this->fields->getOriginalValues();
1446 
1447  /** @var Main\Event $event */
1449  'ENTITY' => $this,
1450  'IS_NEW' => $isNewEntity,
1451  'VALUES' => $oldEntityValues,
1452  ]);
1453  $event->send();
1454 
1455  if ($event->getResults())
1456  {
1457  /** @var Main\EventResult $eventResult */
1458  foreach($event->getResults() as $eventResult)
1459  {
1460  if($eventResult->getType() == Main\EventResult::ERROR)
1461  {
1462  $errorMsg = new ResultError(
1463  Localization\Loc::getMessage('SALE_EVENT_ON_BEFORE_BASKET_ITEM_SAVED'),
1464  'SALE_EVENT_ON_BEFORE_BASKET_ITEM_SAVED'
1465  );
1466 
1467  $eventResultData = $eventResult->getParameters();
1468  if ($eventResultData instanceof ResultError)
1469  $errorMsg = $eventResultData;
1470 
1471  $result->addError($errorMsg);
1472  }
1473  }
1474  }
1475 
1476  return $result;
1477  }
1478 
1479  /**
1480  * @param $isNewEntity
1481  * @return Result
1482  */
1483  protected function callEventSaleBasketItemSaved($isNewEntity)
1484  {
1485  $result = new Result();
1486 
1487  /** @var array $oldEntityValues */
1488  $oldEntityValues = $this->fields->getOriginalValues();
1489 
1490  /** @var Main\Event $event */
1492  'ENTITY' => $this,
1493  'IS_NEW' => $isNewEntity,
1494  'VALUES' => $oldEntityValues,
1495  ]);
1496  $event->send();
1497 
1498  if ($event->getResults())
1499  {
1500  /** @var Main\EventResult $eventResult */
1501  foreach($event->getResults() as $eventResult)
1502  {
1503  if($eventResult->getType() == Main\EventResult::ERROR)
1504  {
1505  $errorMsg = new ResultError(
1506  Localization\Loc::getMessage('SALE_EVENT_ON_BASKET_ITEM_SAVED_ERROR'),
1507  'SALE_EVENT_ON_BASKET_ITEM_SAVED_ERROR'
1508  );
1509  $eventResultData = $eventResult->getParameters();
1510 
1511  if ($eventResultData instanceof ResultError)
1512  $errorMsg = $eventResultData;
1513 
1514  $result->addError($errorMsg);
1515  }
1516  }
1517  }
1518 
1519  return $result;
1520  }
1521 
1522  /**
1523  * @param array $fields
1524  * @return Main\Entity\AddResult
1525  */
1526  abstract protected function addInternal(array $fields);
1527 
1528  /**
1529  * @param $primary
1530  * @param array $fields
1531  * @return Main\Entity\UpdateResult
1532  */
1533  abstract protected function updateInternal($primary, array $fields);
1534 
1535  /**
1536  * @return bool
1537  * @throws Main\ArgumentException
1538  * @throws Main\NotImplementedException
1539  */
1540  public function isChanged()
1541  {
1542  $isChanged = parent::isChanged();
1543  if ($isChanged === false)
1544  {
1546  $isChanged = $propertyCollection->isChanged();
1547  }
1548 
1549  return $isChanged;
1550  }
1551 
1552  /**
1553  * @param BasketItemCollection $basketItemCollection
1554  * @param $data
1555  * @return mixed
1556  * @throws Main\ArgumentException
1557  * @throws Main\NotImplementedException
1558  */
1559  public static function load(BasketItemCollection $basketItemCollection, $data)
1560  {
1561  $basketItem = static::createBasketItemObject($data);
1562  $basketItem->setCollection($basketItemCollection);
1563 
1564  return $basketItem;
1565  }
1566 
1567  /**
1568  * @return Result
1569  * @throws Main\ArgumentException
1570  * @throws Main\NotImplementedException
1571  * @throws Main\ObjectNotFoundException
1572  */
1573  public function verify()
1574  {
1575  $result = new Result();
1576 
1577  if ((float)$this->getField('QUANTITY') <= 0)
1578  {
1579  $result->addError(new Main\Error(
1580  Localization\Loc::getMessage(
1581  'SALE_BASKET_ITEM_ERR_QUANTITY_ZERO',
1582  ['#PRODUCT_NAME#' => $this->getField('NAME')]
1583  )
1584  ));
1585  }
1586 
1587  if (!$this->getField('CURRENCY'))
1588  {
1589  $result->addError(new Main\Error(
1590  Localization\Loc::getMessage('SALE_BASKET_ITEM_ERR_CURRENCY_EMPTY')
1591  ));
1592  }
1593 
1594  if ($basketPropertyCollection = $this->getPropertyCollection())
1595  {
1596  $r = $basketPropertyCollection->verify();
1597  if (!$r->isSuccess())
1598  {
1599  $result->addErrors($r->getErrors());
1600  }
1601  }
1602 
1603  return $result;
1604  }
1605 
1606  /**
1607  * @return float
1608  */
1609  abstract public function getReservedQuantity();
1610 
1611  /**
1612  * @return bool
1613  * @throws Main\ArgumentNullException
1614  * @throws Main\ArgumentOutOfRangeException
1615  */
1616  public function isCustom()
1617  {
1618  $moduleId = trim($this->getField('MODULE'));
1619  $providerClassName = trim($this->getField('PRODUCT_PROVIDER_CLASS'));
1620  $callback = trim($this->getField('CALLBACK_FUNC'));
1621 
1622  return (empty($moduleId) && empty($providerClassName) && empty($callback));
1623  }
1624 
1625  /**
1626  * @return null|string
1627  * @internal
1628  *
1629  */
1630  public static function getEntityEventName()
1631  {
1632  return 'SaleBasketItem';
1633  }
1634 
1635  /**
1636  * @return array
1637  * @throws Main\ArgumentException
1638  * @throws Main\SystemException
1639  */
1640  public function toArray() : array
1641  {
1642  $result = parent::toArray();
1643 
1644  $result['PROPERTIES'] = $this->getPropertyCollection()->toArray();
1645 
1646  return $result;
1647  }
1648 
1649  /**
1650  * @deprecated
1651  *
1652  * @return float
1653  * @throws Main\ArgumentNullException
1654  * @throws Main\ArgumentOutOfRangeException
1655  */
1656  public function getDefaultPrice()
1657  {
1658  return (float)$this->getField('DEFAULT_PRICE');
1659  }
1660 }
Exception is thrown when "empty" value is passed to a function that does not accept it as a valid arg...
static includeModule($moduleName)
Includes a module by its name.
static getMessage($code, $replace=null, $language=null)
Returns translation by message code.
Definition: loc.php:29
Exception is thrown when an object is not present.
static createSingle($basketItemCode)
onFieldModify($name, $oldValue, $value)
callEventSaleBasketItemBeforeSaved($isNewEntity)
setPropertyCollection(BasketPropertiesCollectionBase $propertyCollection)
__construct(array $fields=[])
BasketItemBase constructor.
setPrice($value, $custom=false)
updateInternal($primary, array $fields)
addInternal(array $fields)
onBeforeSetFields(array $values)
callEventSaleBasketItemSaved($isNewEntity)
static create(BasketItemCollection $basketItemCollection, $moduleId, $productId, $basketCode=null)
static load(BasketItemCollection $basketItemCollection, $data)
static getAvailableQuantityAndPriceByBasketItem(Sale\BasketItemBase $basketItem, array $context=array())
static roundPrecision($value)
Definition: pricemaths.php:17
__construct(Base $connector)
Constructor.
Definition: resultview.php:40