1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25:
26:
27:
28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72:
73: class Mage_CatalogInventory_Model_Stock_Item extends Mage_Core_Model_Abstract
74: {
75: const XML_PATH_GLOBAL = 'cataloginventory/options/';
76: const XML_PATH_CAN_SUBTRACT = 'cataloginventory/options/can_subtract';
77: const XML_PATH_CAN_BACK_IN_STOCK = 'cataloginventory/options/can_back_in_stock';
78:
79: const XML_PATH_ITEM = 'cataloginventory/item_options/';
80: const XML_PATH_MIN_QTY = 'cataloginventory/item_options/min_qty';
81: const XML_PATH_MIN_SALE_QTY = 'cataloginventory/item_options/min_sale_qty';
82: const XML_PATH_MAX_SALE_QTY = 'cataloginventory/item_options/max_sale_qty';
83: const XML_PATH_BACKORDERS = 'cataloginventory/item_options/backorders';
84: const XML_PATH_NOTIFY_STOCK_QTY = 'cataloginventory/item_options/notify_stock_qty';
85: const XML_PATH_MANAGE_STOCK = 'cataloginventory/item_options/manage_stock';
86: const XML_PATH_ENABLE_QTY_INCREMENTS = 'cataloginventory/item_options/enable_qty_increments';
87: const XML_PATH_QTY_INCREMENTS = 'cataloginventory/item_options/qty_increments';
88:
89: const ENTITY = 'cataloginventory_stock_item';
90:
91: 92: 93:
94: private $_minSaleQtyCache = array();
95:
96: 97: 98:
99: protected $_qtyIncrements;
100:
101: 102: 103: 104: 105:
106: protected $_eventPrefix = 'cataloginventory_stock_item';
107:
108: 109: 110: 111: 112: 113: 114:
115: protected $_eventObject = 'item';
116:
117: 118: 119: 120: 121:
122: protected $_productInstance = null;
123:
124: 125: 126: 127: 128:
129: protected $_customerGroupId;
130:
131: 132: 133: 134: 135:
136: protected $_processIndexEvents = true;
137:
138: 139: 140: 141:
142: protected function _construct()
143: {
144: $this->_init('cataloginventory/stock_item');
145: }
146:
147: 148: 149: 150: 151: 152:
153: protected function _initOldFieldsMap()
154: {
155: $this->_oldFieldsMap = array(
156: 'stock_status_changed_automatically' => 'stock_status_changed_auto',
157: 'use_config_enable_qty_increments' => 'use_config_enable_qty_inc'
158: );
159: }
160:
161: 162: 163: 164: 165: 166:
167: public function getStockId()
168: {
169: return 1;
170: }
171:
172: 173: 174: 175: 176:
177: public function getProductId()
178: {
179: return $this->_getData('product_id');
180: }
181:
182: 183: 184: 185: 186: 187:
188: public function loadByProduct($product)
189: {
190: if ($product instanceof Mage_Catalog_Model_Product) {
191: $product = $product->getId();
192: }
193: $this->_getResource()->loadByProductId($this, $product);
194: $this->setOrigData();
195: return $this;
196: }
197:
198: 199: 200: 201: 202: 203:
204: public function subtractQty($qty)
205: {
206: if ($this->canSubtractQty()) {
207: $this->setQty($this->getQty()-$qty);
208: }
209: return $this;
210: }
211:
212: 213: 214: 215: 216:
217: public function canSubtractQty()
218: {
219: return $this->getManageStock() && Mage::getStoreConfigFlag(self::XML_PATH_CAN_SUBTRACT);
220: }
221:
222: 223: 224: 225: 226: 227:
228: public function addQty($qty)
229: {
230: if (!$this->getManageStock()) {
231: return $this;
232: }
233: $config = Mage::getStoreConfigFlag(self::XML_PATH_CAN_SUBTRACT);
234: if (!$config) {
235: return $this;
236: }
237:
238: $this->setQty($this->getQty()+$qty);
239: return $this;
240: }
241:
242: 243: 244: 245: 246:
247: public function getStoreId()
248: {
249: $storeId = $this->getData('store_id');
250: if (is_null($storeId)) {
251: $storeId = Mage::app()->getStore()->getId();
252: $this->setData('store_id', $storeId);
253: }
254: return $storeId;
255: }
256:
257: 258: 259: 260: 261: 262:
263: public function assignProduct(Mage_Catalog_Model_Product $product)
264: {
265: if (!$this->getId() || !$this->getProductId()) {
266: $this->_getResource()->loadByProductId($this, $product->getId());
267: $this->setOrigData();
268: }
269:
270: $this->setProduct($product);
271: $product->setStockItem($this);
272:
273: $product->setIsInStock($this->getIsInStock());
274: Mage::getSingleton('cataloginventory/stock_status')
275: ->assignProduct($product, $this->getStockId(), $this->getStockStatus());
276: return $this;
277: }
278:
279: 280: 281: 282: 283:
284: public function getMinQty()
285: {
286: return (float)($this->getUseConfigMinQty() ? Mage::getStoreConfig(self::XML_PATH_MIN_QTY)
287: : $this->getData('min_qty'));
288: }
289:
290: 291: 292: 293: 294:
295: public function getCustomerGroupId()
296: {
297: return $this->_customerGroupId;
298: }
299:
300: 301: 302: 303: 304: 305:
306: public function setCustomerGroupId($value)
307: {
308: $this->_customerGroupId = $value;
309: return $this;
310: }
311:
312: 313: 314: 315: 316:
317: public function getMinSaleQty()
318: {
319: $customerGroupId = $this->getCustomerGroupId();
320: if (!$customerGroupId) {
321: $customerGroupId = Mage::app()->getStore()->isAdmin()
322: ? Mage_Customer_Model_Group::CUST_GROUP_ALL
323: : Mage::getSingleton('customer/session')->getCustomerGroupId();
324: }
325:
326: if (!isset($this->_minSaleQtyCache[$customerGroupId])) {
327: $minSaleQty = $this->getUseConfigMinSaleQty()
328: ? Mage::helper('cataloginventory/minsaleqty')->getConfigValue($customerGroupId)
329: : $this->getData('min_sale_qty');
330:
331: $this->_minSaleQtyCache[$customerGroupId] = empty($minSaleQty) ? 0 : (float)$minSaleQty;
332: }
333:
334: return $this->_minSaleQtyCache[$customerGroupId] ? $this->_minSaleQtyCache[$customerGroupId] : null;
335: }
336:
337: 338: 339: 340: 341:
342: public function getMaxSaleQty()
343: {
344: return (float)($this->getUseConfigMaxSaleQty() ? Mage::getStoreConfig(self::XML_PATH_MAX_SALE_QTY)
345: : $this->getData('max_sale_qty'));
346: }
347:
348: 349: 350: 351: 352:
353: public function getNotifyStockQty()
354: {
355: if ($this->getUseConfigNotifyStockQty()) {
356: return (float) Mage::getStoreConfig(self::XML_PATH_NOTIFY_STOCK_QTY);
357: }
358: return (float) $this->getData('notify_stock_qty');
359: }
360:
361: 362: 363: 364: 365:
366: public function getEnableQtyIncrements()
367: {
368: return $this->getUseConfigEnableQtyIncrements()
369: ? Mage::getStoreConfigFlag(self::XML_PATH_ENABLE_QTY_INCREMENTS)
370: : (bool)$this->getData('enable_qty_increments');
371: }
372:
373: 374: 375: 376: 377:
378: public function getQtyIncrements()
379: {
380: if ($this->_qtyIncrements === null) {
381: if ($this->getEnableQtyIncrements()) {
382: $this->_qtyIncrements = (float)($this->getUseConfigQtyIncrements()
383: ? Mage::getStoreConfig(self::XML_PATH_QTY_INCREMENTS)
384: : $this->getData('qty_increments'));
385: if ($this->_qtyIncrements <= 0) {
386: $this->_qtyIncrements = false;
387: }
388: } else {
389: $this->_qtyIncrements = false;
390: }
391: }
392: return $this->_qtyIncrements;
393: }
394:
395: 396: 397: 398: 399: 400:
401: public function getDefaultQtyIncrements()
402: {
403: return Mage::getStoreConfigFlag(self::XML_PATH_ENABLE_QTY_INCREMENTS)
404: ? (int)Mage::getStoreConfig(self::XML_PATH_QTY_INCREMENTS)
405: : false;
406: }
407:
408: 409: 410: 411: 412:
413: public function getBackorders()
414: {
415: if ($this->getUseConfigBackorders()) {
416: return (int) Mage::getStoreConfig(self::XML_PATH_BACKORDERS);
417: }
418: return $this->getData('backorders');
419: }
420:
421: 422: 423: 424: 425:
426: public function getManageStock()
427: {
428: if ($this->getUseConfigManageStock()) {
429: return (int) Mage::getStoreConfigFlag(self::XML_PATH_MANAGE_STOCK);
430: }
431: return $this->getData('manage_stock');
432: }
433:
434: 435: 436: 437: 438:
439: public function getCanBackInStock()
440: {
441: return Mage::getStoreConfigFlag(self::XML_PATH_CAN_BACK_IN_STOCK);
442: }
443:
444: 445: 446: 447: 448: 449: 450:
451: public function checkQty($qty)
452: {
453: if (!$this->getManageStock() || Mage::app()->getStore()->isAdmin()) {
454: return true;
455: }
456:
457: if ($this->getQty() - $this->getMinQty() - $qty < 0) {
458: switch ($this->getBackorders()) {
459: case Mage_CatalogInventory_Model_Stock::BACKORDERS_YES_NONOTIFY:
460: case Mage_CatalogInventory_Model_Stock::BACKORDERS_YES_NOTIFY:
461: break;
462: default:
463: return false;
464: break;
465: }
466: }
467: return true;
468: }
469:
470: 471: 472: 473: 474: 475: 476:
477: public function suggestQty($qty)
478: {
479:
480: if ($qty <= 0 || !$this->getManageStock()) {
481: return $qty;
482: }
483:
484: $qtyIncrements = (int)$this->getQtyIncrements();
485: if ($qtyIncrements < 2) {
486: return $qty;
487: }
488:
489: $minQty = max($this->getMinSaleQty(), $qtyIncrements);
490: $divisibleMin = ceil($minQty / $qtyIncrements) * $qtyIncrements;
491:
492: $maxQty = min($this->getQty() - $this->getMinQty(), $this->getMaxSaleQty());
493: $divisibleMax = floor($maxQty / $qtyIncrements) * $qtyIncrements;
494:
495: if ($qty < $minQty || $qty > $maxQty || $divisibleMin > $divisibleMax) {
496:
497: return $qty;
498: }
499:
500:
501: $closestDivisibleLeft = floor($qty / $qtyIncrements) * $qtyIncrements;
502: $closestDivisibleRight = $closestDivisibleLeft + $qtyIncrements;
503: $acceptableLeft = min(max($divisibleMin, $closestDivisibleLeft), $divisibleMax);
504: $acceptableRight = max(min($divisibleMax, $closestDivisibleRight), $divisibleMin);
505: return abs($acceptableLeft - $qty) < abs($acceptableRight - $qty) ? $acceptableLeft : $acceptableRight;
506: }
507:
508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518:
519: public function checkQuoteItemQty($qty, $summaryQty, $origQty = 0)
520: {
521: $result = new Varien_Object();
522: $result->setHasError(false);
523:
524: if (!is_numeric($qty)) {
525: $qty = Mage::app()->getLocale()->getNumber($qty);
526: }
527:
528: 529: 530:
531: $result->setItemIsQtyDecimal($this->getIsQtyDecimal());
532:
533: if (!$this->getIsQtyDecimal()) {
534: $result->setHasQtyOptionUpdate(true);
535: $qty = intval($qty);
536:
537: 538: 539:
540: $result->setItemQty($qty);
541:
542: if (!is_numeric($qty)) {
543: $qty = Mage::app()->getLocale()->getNumber($qty);
544: }
545: $origQty = intval($origQty);
546: $result->setOrigQty($origQty);
547: }
548:
549: if ($this->getMinSaleQty() && $qty < $this->getMinSaleQty()) {
550: $result->setHasError(true)
551: ->setMessage(
552: Mage::helper('cataloginventory')->__('The minimum quantity allowed for purchase is %s.', $this->getMinSaleQty() * 1)
553: )
554: ->setErrorCode('qty_min')
555: ->setQuoteMessage(Mage::helper('cataloginventory')->__('Some of the products cannot be ordered in requested quantity.'))
556: ->setQuoteMessageIndex('qty');
557: return $result;
558: }
559:
560: if ($this->getMaxSaleQty() && $qty > $this->getMaxSaleQty()) {
561: $result->setHasError(true)
562: ->setMessage(
563: Mage::helper('cataloginventory')->__('The maximum quantity allowed for purchase is %s.', $this->getMaxSaleQty() * 1)
564: )
565: ->setErrorCode('qty_max')
566: ->setQuoteMessage(Mage::helper('cataloginventory')->__('Some of the products cannot be ordered in requested quantity.'))
567: ->setQuoteMessageIndex('qty');
568: return $result;
569: }
570:
571: $result->addData($this->checkQtyIncrements($qty)->getData());
572: if ($result->getHasError()) {
573: return $result;
574: }
575:
576: if (!$this->getManageStock()) {
577: return $result;
578: }
579:
580: if (!$this->getIsInStock()) {
581: $result->setHasError(true)
582: ->setMessage(Mage::helper('cataloginventory')->__('This product is currently out of stock.'))
583: ->setQuoteMessage(Mage::helper('cataloginventory')->__('Some of the products are currently out of stock'))
584: ->setQuoteMessageIndex('stock');
585: $result->setItemUseOldQty(true);
586: return $result;
587: }
588:
589: if (!$this->checkQty($summaryQty) || !$this->checkQty($qty)) {
590: $message = Mage::helper('cataloginventory')->__('The requested quantity for "%s" is not available.', $this->getProductName());
591: $result->setHasError(true)
592: ->setMessage($message)
593: ->setQuoteMessage($message)
594: ->setQuoteMessageIndex('qty');
595: return $result;
596: } else {
597: if (($this->getQty() - $summaryQty) < 0) {
598: if ($this->getProductName()) {
599: if ($this->getIsChildItem()) {
600: $backorderQty = ($this->getQty() > 0) ? ($summaryQty - $this->getQty()) * 1 : $qty * 1;
601: if ($backorderQty > $qty) {
602: $backorderQty = $qty;
603: }
604:
605: $result->setItemBackorders($backorderQty);
606: } else {
607: $orderedItems = $this->getOrderedItems();
608: $itemsLeft = ($this->getQty() > $orderedItems) ? ($this->getQty() - $orderedItems) * 1 : 0;
609: $backorderQty = ($itemsLeft > 0) ? ($qty - $itemsLeft) * 1 : $qty * 1;
610:
611: if ($backorderQty > 0) {
612: $result->setItemBackorders($backorderQty);
613: }
614: $this->setOrderedItems($orderedItems + $qty);
615: }
616:
617: if ($this->getBackorders() == Mage_CatalogInventory_Model_Stock::BACKORDERS_YES_NOTIFY) {
618: if (!$this->getIsChildItem()) {
619: $result->setMessage(
620: Mage::helper('cataloginventory')->__('This product is not available in the requested quantity. %s of the items will be backordered.', ($backorderQty * 1))
621: );
622: } else {
623: $result->setMessage(
624: Mage::helper('cataloginventory')->__('"%s" is not available in the requested quantity. %s of the items will be backordered.', $this->getProductName(), ($backorderQty * 1))
625: );
626: }
627: } elseif (Mage::app()->getStore()->isAdmin()) {
628: $result->setMessage(
629: Mage::helper('cataloginventory')->__('The requested quantity for "%s" is not available.', $this->getProductName())
630: );
631: }
632: }
633: } else {
634: if (!$this->getIsChildItem()) {
635: $this->setOrderedItems($qty + (int)$this->getOrderedItems());
636: }
637: }
638: }
639:
640: return $result;
641: }
642:
643: 644: 645: 646: 647: 648:
649: public function checkQtyIncrements($qty)
650: {
651: $result = new Varien_Object();
652: if ($this->getSuppressCheckQtyIncrements()) {
653: return $result;
654: }
655:
656: $qtyIncrements = $this->getQtyIncrements();
657: if ($qtyIncrements && (Mage::helper('core')->getExactDivision($qty, $qtyIncrements) != 0)) {
658: $result->setHasError(true)
659: ->setQuoteMessage(
660: Mage::helper('cataloginventory')->__('Some of the products cannot be ordered in the requested quantity.')
661: )
662: ->setErrorCode('qty_increments')
663: ->setQuoteMessageIndex('qty');
664: if ($this->getIsChildItem()) {
665: $result->setMessage(
666: Mage::helper('cataloginventory')->__('%s is available for purchase in increments of %s only.',$this->getProductName(), $qtyIncrements * 1)
667: );
668: } else {
669: $result->setMessage(
670: Mage::helper('cataloginventory')->__('This product is available for purchase in increments of %s only.', $qtyIncrements * 1)
671: );
672: }
673: }
674:
675: return $result;
676: }
677:
678: 679: 680: 681: 682: 683:
684: public function addCatalogInventoryToProductCollection($productCollection)
685: {
686: $this->_getResource()->addCatalogInventoryToProductCollection($productCollection);
687: return $this;
688: }
689:
690: 691: 692: 693: 694: 695: 696: 697: 698:
699: protected function _addQuoteItemError(Mage_Sales_Model_Quote_Item $item, $itemError,
700: $quoteError, $errorIndex='error'
701: ) {
702: $item->setHasError(true);
703: $item->setMessage($itemError);
704: $item->setQuoteMessage($quoteError);
705: $item->setQuoteMessageIndex($errorIndex);
706: return $this;
707: }
708:
709: 710: 711: 712: 713:
714: protected function _beforeSave()
715: {
716:
717: $typeId = $this->getTypeId();
718: if ($productTypeId = $this->getProductTypeId()) {
719: $typeId = $productTypeId;
720: }
721:
722: $isQty = Mage::helper('catalogInventory')->isQty($typeId);
723:
724: if ($isQty) {
725: if (!$this->verifyStock()) {
726: $this->setIsInStock(false)
727: ->setStockStatusChangedAutomaticallyFlag(true);
728: }
729:
730:
731: $this->setLowStockDate(null);
732: if ($this->verifyNotification()) {
733: $this->setLowStockDate(Mage::app()->getLocale()->date(null, null, null, false)
734: ->toString(Varien_Date::DATETIME_INTERNAL_FORMAT)
735: );
736: }
737:
738: $this->setStockStatusChangedAutomatically(0);
739: if ($this->hasStockStatusChangedAutomaticallyFlag()) {
740: $this->setStockStatusChangedAutomatically((int)$this->getStockStatusChangedAutomaticallyFlag());
741: }
742: } else {
743: $this->setQty(0);
744: }
745:
746: return $this;
747: }
748:
749: 750: 751: 752: 753: 754:
755: public function verifyStock($qty = null)
756: {
757: if ($qty === null) {
758: $qty = $this->getQty();
759: }
760: if ($this->getBackorders() == Mage_CatalogInventory_Model_Stock::BACKORDERS_NO && $qty <= $this->getMinQty()) {
761: return false;
762: }
763: return true;
764: }
765:
766: 767: 768: 769: 770: 771:
772: public function verifyNotification($qty = null)
773: {
774: if ($qty === null) {
775: $qty = $this->getQty();
776: }
777: return (float)$qty < $this->getNotifyStockQty();
778: }
779:
780: 781: 782: 783: 784:
785: protected function _afterSave()
786: {
787: parent::_afterSave();
788:
789:
790: $indexer = Mage::getSingleton('index/indexer');
791: if ($this->_processIndexEvents) {
792: $indexer->processEntityAction($this, self::ENTITY, Mage_Index_Model_Event::TYPE_SAVE);
793: } else {
794: $indexer->logEvent($this, self::ENTITY, Mage_Index_Model_Event::TYPE_SAVE);
795: }
796: return $this;
797: }
798:
799:
800: 801: 802: 803: 804:
805: public function getIsInStock()
806: {
807: if (!$this->getManageStock()) {
808: return true;
809: }
810: return $this->_getData('is_in_stock');
811: }
812:
813: 814: 815: 816: 817: 818:
819: public function setProduct($product)
820: {
821: $this->setProductId($product->getId())
822: ->setProductName($product->getName())
823: ->setStoreId($product->getStoreId())
824: ->setProductName($product->getName())
825: ->setProductTypeId($product->getTypeId())
826: ->setProductStatusChanged($product->dataHasChangedFor('status'))
827: ->setProductChangedWebsites($product->getIsChangedWebsites());
828:
829: $this->_productInstance = $product;
830:
831: return $this;
832: }
833:
834: 835: 836: 837: 838:
839: public function getProduct()
840: {
841: return $this->_productInstance ? $this->_productInstance : $this->_getData('product');
842: }
843:
844: 845: 846: 847: 848:
849: public function getStockQty()
850: {
851: if (!$this->hasStockQty()) {
852: $this->setStockQty(0);
853: $product = $this->_productInstance;
854: if (!$product || !$product->isComposite()) {
855: $stockQty = $this->getQty();
856: } else {
857: $stockQty = null;
858: $productsByGroups = $product->getTypeInstance(true)->getProductsToPurchaseByReqGroups($product);
859: foreach ($productsByGroups as $productsInGroup) {
860: $qty = 0;
861: foreach ($productsInGroup as $childProduct) {
862: if ($childProduct->hasStockItem()) {
863: $qty += $childProduct->getStockItem()->getStockQty();
864: }
865: }
866: if (is_null($stockQty) || $qty < $stockQty) {
867: $stockQty = $qty;
868: }
869: }
870: }
871: $stockQty = (float) $stockQty;
872: if ($stockQty < 0 || !$this->getManageStock()
873: || !$this->getIsInStock() || ($product && !$product->isSaleable())
874: ) {
875: $stockQty = 0;
876: }
877: $this->setStockQty($stockQty);
878: }
879: return $this->getData('stock_qty');
880: }
881:
882: 883: 884: 885:
886: public function reset()
887: {
888: if ($this->_productInstance) {
889: $this->_productInstance = null;
890: }
891: return $this;
892: }
893:
894: 895: 896: 897: 898: 899:
900: public function setProcessIndexEvents($process = true)
901: {
902: $this->_processIndexEvents = $process;
903: return $this;
904: }
905: }
906: