Index: branches/5.2.x/units/pricing/pricing_event_handler.php =================================================================== --- branches/5.2.x/units/pricing/pricing_event_handler.php (revision 16774) +++ branches/5.2.x/units/pricing/pricing_event_handler.php (revision 16775) @@ -1,524 +1,523 @@ Array ('subitem' => 'add|edit'), 'OnInfinity' => Array ('subitem' => 'add|edit'), 'OnArrange' => Array ('subitem' => 'add|edit'), 'OnDeleteBrackets' => Array ('subitem' => 'add|edit'), ); $this->permMapping = array_merge($this->permMapping, $permissions); } /** * Define alternative event processing method names * * @return void * @see kEventHandler::$eventMethods * @access protected */ protected function mapEvents() { parent::mapEvents(); // ensure auto-adding of approve/decline and so on events $brackets_events = Array( 'OnMoreBrackets' => 'PricingBracketsAction', 'OnArrange' => 'PricingBracketsAction', 'OnInfinity' => 'PricingBracketsAction', 'OnDeleteBrackets' => 'PricingBracketsAction', ); $this->eventMethods = array_merge($this->eventMethods, $brackets_events); } function PricingBracketsAction($event) { $event->redirect=false; $temp = $this->Application->GetVar($event->getPrefixSpecial(true)); // $object = $event->getObject(); // $formatter = $this->Application->recallObject('kFormatter'); // $temp = $formatter->TypeCastArray($temp, $object); //uasort($temp, 'pr_bracket_comp'); $bracket = $this->Application->recallObject($event->getPrefixSpecial()); foreach($temp as $id => $record) { if( $record['MaxQty'] == '∞' || $record['MaxQty'] == '∞') { $temp[$id]['MaxQty'] = -1; } } $group_id = $this->Application->getVar('group_id'); if($group_id>0){ $where_group=' GroupId = '.$group_id.' '; } else { $where_group= ' TRUE '; } switch ($event->Name) { case 'OnMoreBrackets': $new_id = (int)$this->Conn->GetOne('SELECT MIN('.$bracket->IDField.') FROM '.$bracket->TableName); if($new_id > 0) $new_id = 0; do { $new_id--; } while ($this->check_array($this->Application->GetVar($event->getPrefixSpecial(true)), 'PriceId', $new_id)); $last_max_qty = $this->Conn->GetOne('SELECT MAX(MaxQty) FROM '.$bracket->TableName.' WHERE '.$where_group); $min_qty = $this->Conn->GetOne('SELECT MIN(MaxQty) FROM '.$bracket->TableName.' WHERE '.$where_group); if ($min_qty==-1) $last_max_qty = -1; if (!$last_max_qty) $last_max_qty=1; for($i = $new_id; $i > $new_id - 5; $i--) { $temp[$i]['PriceId'] = $i; $temp[$i]['MinQty'] = ($i == $new_id-4 && $last_max_qty != -1) ? $last_max_qty : ''; $temp[$i]['MaxQty'] = ($i == $new_id-4 && $last_max_qty != -1) ? -1 : ''; $temp[$i]['Price'] = ''; $temp[$i]['Cost'] = ''; $temp[$i]['Points'] = ''; $temp[$i]['Negotiated'] = '0'; $temp[$i]['IsPrimary'] = '0'; $temp[$i]['GroupId'] = $group_id; } $this->Application->SetVar($event->getPrefixSpecial(true), $temp); $event->CallSubEvent('OnPreSaveBrackets'); break; case 'OnArrange': $temp=$this->OnArrangeBrackets($event, $temp, $bracket); $this->Application->SetVar($event->getPrefixSpecial(true), $temp); $event->CallSubEvent('OnPreSaveBrackets'); break; case 'OnInfinity': $temp=$this->OnArrangeBrackets($event, $temp, $bracket); $this->Application->SetVar($event->getPrefixSpecial(true), $temp); $event->CallSubEvent('OnPreSaveBrackets'); $infinite_exists = $this->Conn->GetOne('SELECT count(*) FROM '.$bracket->TableName.' WHERE MaxQty=-1 '.' AND '.$where_group); if($infinite_exists==0){ reset($temp); $last_bracket=end($temp); $new_id = (int)$this->Conn->GetOne('SELECT MIN('.$bracket->IDField.') FROM '.$bracket->TableName); $brackets_exist = (int)$this->Conn->GetOne('SELECT COUNT(*) FROM '.$bracket->TableName.' WHERE '.$where_group); if($new_id > 0) $new_id = 0; do { $new_id--; } while ($this->check_array($this->Application->GetVar($event->getPrefixSpecial(true)), 'PriceId', $new_id)); $infinite_bracket['PriceId'] = $new_id; $infinite_bracket['MinQty'] = ($brackets_exist>0)?$last_bracket['MaxQty']:1; $infinite_bracket['MaxQty'] = '-1'; $infinite_bracket['Price'] = ''; $infinite_bracket['Cost'] = ''; $infinite_bracket['Points'] = ''; $infinite_bracket['Negotiated'] = '0'; $infinite_bracket['IsPrimary'] = '0'; $infinite_bracket['GroupId'] = $group_id; $temp[$new_id]=$infinite_bracket; reset($temp); } $this->Application->SetVar($event->getPrefixSpecial(true), $temp); $event->CallSubEvent('OnPreSaveBrackets'); break; case 'OnDeleteBrackets': if ($group_id) { $temp = ''; // delete all pricings from "pr_tang" var $sql = 'DELETE FROM ' . $bracket->TableName . ' WHERE ProductId = ' . $this->Application->GetVar('p_id') . ' AND GroupId = ' . $group_id; $this->Conn->Query($sql); } break; default: } $this->Application->SetVar($event->getPrefixSpecial(true), $temp); // store pr_tang var } function OnPreSaveBrackets(kEvent $event) { if( $this->Application->GetVar('pr_tang') ) { /** @var kDBItem $object */ $object = $event->getObject(); $product_id = $this->Application->GetVar('p_id'); $group_id = $this->Application->getVar('group_id'); $sql = 'SELECT PriceId FROM ' . $object->TableName . ' WHERE ProductId = ' . $product_id . ' ' . ($group_id? 'AND GroupId = ' . $group_id : ''); $stored_ids = $this->Conn->GetCol($sql); $items_info = $this->Application->GetVar( $event->getPrefixSpecial(true) ); // get pr_tang var uasort($items_info, 'pr_bracket_comp'); foreach ($items_info as $item_id => $field_values) { if (in_array($item_id, $stored_ids)) { //if it's already exist $object->Load($item_id); $object->SetFieldsFromHash($field_values); $event->setEventParam('form_data', $field_values); if (!$object->Validate()) { unset($stored_ids[array_search($item_id, $stored_ids)]); $event->redirect = false; continue; } if( $object->Update($item_id) ) { $event->status=kEvent::erSUCCESS; } else { $event->status=kEvent::erFAIL; $event->redirect=false; break; } unset($stored_ids[array_search($item_id, $stored_ids)]); } else { $object->Clear(0); $object->SetFieldsFromHash($field_values); $event->setEventParam('form_data', $field_values); $object->SetDBField('ProductId', $product_id); if( $object->Create() ) { $event->status=kEvent::erSUCCESS; } } } // delete foreach ($stored_ids as $stored_id) { $this->Conn->Query('DELETE FROM ' . $object->TableName . ' WHERE PriceId = ' . $stored_id); } } } /** * Apply custom processing to item * * @param kEvent $event * @param string $type * @return void * @access protected */ protected function customProcessing(kEvent $event, $type) { /** @var kDBItem $bracket */ $bracket = $event->getObject(); switch ($type) { case 'before': $bracket->SetDBField('ProductId', $this->Application->GetVar('p_id')); if ( $bracket->GetDBField('MaxQty') == '∞' || $bracket->GetDBField('MaxQty') == '∞' ) { $bracket->SetDBField('MaxQty', -1); } break; } } function OnArrangeBrackets($event, &$temp, &$bracket) { $temp_orig = $temp; reset($temp); if (is_array($temp)) { // array to store max values (2nd column) $end_values = Array(); // get minimal value of Min $first_elem=current($temp); $start = $first_elem['MinQty']; if (!$start){ $start = 1; } foreach($temp as $id => $record) { /* This 3-ifs logic fixes collision with invalid input values having 1 pricing record. The logic is: 1) If we got Max less than Min, we set Min to 1 that gives us integrity. 2) If we got equal values for Min and Max, we set range 1..Max like in previous. But if Min was 1 and Max was 1 we set full range 1..infinity 3) If we got Max = 0 we just set it tom infinity because we can't guess what user meant */ if (sizeof($temp) == 1 && $record['MinQty'] > ($record['MaxQty'] == -1 ? $record['MinQty']+1 : $record['MaxQty']) ){ $record['MinQty'] = 1; $temp[$id]['MinQty'] = 1; $start = 1; } if (sizeof($temp) == 1 && $record['MinQty'] == $record['MaxQty']){ if ($record['MaxQty'] == 1){ $record['MaxQty'] = -1; $temp[$id]['MaxQty'] = -1; } else { $record['MinQty'] = 1; $temp[$id]['MinQty'] = 1; } } if (sizeof($temp) == 1 && $record['MaxQty'] == 0){ $record['MaxQty'] = -1; $temp[$id]['MaxQty'] = -1; } if( // MAX is less than start ($record['MaxQty'] <= $start && $record['MaxQty'] != -1) || // Max is empty !$record['MaxQty'] || // Max already defined in $end_values (array_search($record['MaxQty'], $end_values) !== false) ) { // then delete from brackets list unset($temp[$id]); } else { // this is when ok - add to end_values list $end_values[] = $record['MaxQty']; } } // sort brackets by 2nd column (Max values) uasort($temp, 'pr_bracket_comp'); reset($temp); $first_item_key = key($temp); $group_id = $this->Application->getVar('group_id'); $default_group = $this->Application->ConfigValue('User_LoggedInGroup'); if($group_id>0){ $where_group=' AND GroupId = '.$group_id.' '; } $ids = $this->Conn->GetCol('SELECT PriceId FROM '.$bracket->TableName.' WHERE ProductId='.$this->Application->GetVar('p_id').' '.$where_group); if(is_array($ids)) { usort($ids, 'pr_bracket_id_sort'); } $min_id = min( min($ids) - 1, -1 ); foreach($temp as $key => $record) { $temp[$key]['MinQty']=$start; $temp[$key]['IsPrimary']=0; $temp[$key]['GroupId']=$group_id; $start=$temp[$key]['MaxQty']; } if ($temp[$first_item_key]['GroupId'] == $default_group) { $temp[$first_item_key]['IsPrimary']=1; } } return $temp; } /** * Set's price as primary for product * * @param kEvent $event */ function OnSetPrimary($event) { $object = $event->getObject( Array('skip_autoload' => true) ); $this->StoreSelectedIDs($event); $ids=$this->getSelectedIDs($event); if($ids) { $id = array_shift($ids); $table_info = $object->getLinkedInfo(); $this->Conn->Query('UPDATE '.$object->TableName.' SET IsPrimary = 0 WHERE '.$table_info['ForeignKey'].' = '.$table_info['ParentId']); $this->Conn->Query('UPDATE '.$object->TableName.' SET IsPrimary = 1 WHERE ('.$table_info['ForeignKey'].' = '.$table_info['ParentId'].') AND (PriceId = '.$id.')'); } $event->SetRedirectParam('opener', 's'); } /** * Resets primary mark for other prices of given product, when current pricing is primary * * @param kEvent $event * @return void * @access protected */ protected function OnBeforeItemUpdate(kEvent $event) { parent::OnBeforeItemUpdate($event); /** @var kDBItem $object */ $object = $event->getObject(); if ( $object->GetDBField('IsPrimary') == 1 ) { // make all prices non primary, when this one is $sql = 'UPDATE ' . $object->TableName . ' SET IsPrimary = 0 WHERE (ProductId = ' . $object->GetDBField('ProductId') . ') AND (' . $object->IDField . ' <> ' . $object->GetID() . ')'; $this->Conn->Query($sql); } } /** * Occurs before creating item * * @param kEvent $event * @return void * @access protected */ protected function OnBeforeItemCreate(kEvent $event) { parent::OnBeforeItemCreate($event); /** @var kDBItem $object */ $object = $event->getObject(); $table_info = $object->getLinkedInfo($event->Special); $table_info['ParentId'] = ($table_info['ParentId'] ? $table_info['ParentId'] : 0); if ( $object->GetDBField('IsPrimary') == 1 ) { $sql = 'UPDATE ' . $object->TableName . ' SET IsPrimary = 0 WHERE ' . $table_info['ForeignKey'] . ' = ' . $table_info['ParentId']; $this->Conn->Query($sql); } else { $sql = 'SELECT COUNT(*) FROM ' . $object->TableName . ' WHERE ' . $table_info['ForeignKey'] . ' = ' . $table_info['ParentId']; $prices_qty = $this->Conn->GetOne($sql); if ( $prices_qty == 0 ) { $object->SetDBField('IsPrimary', 1); } } } /** * Apply any custom changes to list's sql query * * @param kEvent $event * @return void * @access protected * @see kDBEventHandler::OnListBuild() */ protected function SetCustomQuery(kEvent $event) { /** @var kDBList $object */ $object = $event->getObject(); if ( $this->Application->isAdminUser ) { return; } if ( $this->Application->ConfigValue('Comm_PriceBracketCalculation') == 1 ) { - $sql = 'SELECT PrimaryGroupId - FROM ' . TABLE_PREFIX . 'Users - WHERE PortalUserId = ' . $this->Application->GetVar('u_id'); - $pricing_group = $this->Conn->GetOne($sql); + /** @var UserHelper $user_helper */ + $user_helper = $this->Application->recallObject('UserHelper'); + $pricing_group = $user_helper->getPrimaryGroup($this->Application->RecallVar('user_id')); - if ( $pricing_group ) { + if ( $this->Application->LoggedIn() ) { $sql = 'SELECT COUNT(*) FROM ' . TABLE_PREFIX . 'ProductsPricing WHERE ProductId = ' . $this->Application->GetVar('p_id') . ' AND GroupId = ' . $pricing_group . ' AND Price IS NOT NULL'; $pricing_for_group_exists = $this->Conn->GetOne($sql); - } - if ( !$pricing_group || !$pricing_for_group_exists ) { - $pricing_group = $this->Application->ConfigValue('User_LoggedInGroup'); + if ( !$pricing_for_group_exists ) { + $pricing_group = $this->Application->ConfigValue('User_LoggedInGroup'); + } } } else { $user_groups = $this->Application->RecallVar('UserGroups'); //$cheapest_group = $this->Conn->GetOne('SELECT GroupId FROM '.$object->TableName.' WHERE ProductId='.$this->Application->GetVar('p_id').' AND Price IS NOT NULL AND GroupId IN ('.$user_groups.') AND MinQty = 1 GROUP BY GroupId ORDER BY Price ASC'); $sql = 'SELECT PriceId, Price, GroupId FROM ' . $object->TableName . ' WHERE ProductId = ' . $this->Application->GetVar('p_id') . ' AND Price IS NOT NULL AND GroupId IN (' . $user_groups . ') ORDER BY GroupId ASC, MinQty ASC'; $effective_brackets = $this->Conn->Query($sql, 'PriceId'); $group_prices = array (); $min_price = -1; $cheapest_group = 0; foreach ($effective_brackets as $bracket) { if ( !isset($group_prices[$bracket['GroupId']]) ) { $group_prices[$bracket['GroupId']] = $bracket['Price']; if ( $bracket['Price'] < $min_price || $min_price == -1 ) { $min_price = $bracket['Price']; $cheapest_group = $bracket['GroupId']; } } } if ( !$cheapest_group ) { $cheapest_group = $this->Application->ConfigValue('User_LoggedInGroup'); } $pricing_group = $cheapest_group; } $object->addFilter('price_user_group', $object->TableName . '.GroupId=' . $pricing_group); } } Index: branches/5.2.x/units/orders/order_calculator.php =================================================================== --- branches/5.2.x/units/orders/order_calculator.php (revision 16774) +++ branches/5.2.x/units/orders/order_calculator.php (revision 16775) @@ -1,859 +1,844 @@ reset(); } /** * Sets order manager instance to calculator * * @param OrderManager $manager */ public function setManager(&$manager) { $this->manager =& $manager; } public function reset() { $this->items = Array (); } /** * Returns order object used in order manager * * @return OrdersItem */ protected function &getOrder() { $order =& $this->manager->getOrder(); return $order; } /** * Sets checkout error * * @param int $error_type = {product,coupon,gc} * @param int $error_code * @param int $product_id - {ProductId}:{OptionsSalt}:{BackOrderFlag}:{FieldName} * @return void * @access protected */ protected function setError($error_type, $error_code, $product_id = null) { $this->manager->setError($error_type, $error_code, $product_id); } /** * Perform order calculations and prepares operations for order manager * */ public function calculate() { $this->queryItems(); $this->groupItems(); $this->generateOperations(); $this->applyWholeOrderFlatDiscount(); } /** * Groups order items, when requested * * @return Array */ protected function groupItems() { $skipped_items = Array (); foreach ($this->items as $item_id => $item_data) { if ( in_array($item_id, $skipped_items) ) { continue; } $group_items = $this->getItemsToGroupWith($item_id); if (!$group_items) { continue; } foreach ($group_items as $group_item_id) { $this->items[$item_id]['Quantity'] += $this->items[$group_item_id]['Quantity']; $this->items[$group_item_id]['Quantity'] = 0; } $skipped_items = array_merge($skipped_items, $group_items); } } /** * Returns order item ids, that can be grouped with given order item id * * @param int $target_item_id * @return Array * @see OrderCalculator::canBeGrouped */ protected function getItemsToGroupWith($target_item_id) { $ret = Array (); foreach ($this->items as $item_id => $item_data) { if ( $this->canBeGrouped($this->items[$item_id], $this->items[$target_item_id]) ) { $ret[] = $item_id; } } return array_diff($ret, Array ($target_item_id)); } /** * Checks if 2 given order items can be grouped together * * @param Array $src_item * @param Array $dst_item * @return bool */ public function canBeGrouped($src_item, $dst_item) { if ($dst_item['Type'] != PRODUCT_TYPE_TANGIBLE) { return false; } return ($src_item['ProductId'] == $dst_item['ProductId']) && ($src_item['OptionsSalt'] == $dst_item['OptionsSalt']); } /** * Retrieves order contents from database * */ protected function queryItems() { $poc_table = $this->Application->getUnitOption('poc', 'TableName'); $query = ' SELECT oi.ProductId, oi.OptionsSalt, oi.ItemData, oi.Quantity, IF(p.InventoryStatus = ' . ProductInventory::BY_OPTIONS . ', poc.QtyInStock, p.QtyInStock) AS QtyInStock, p.QtyInStockMin, p.BackOrder, p.InventoryStatus, p.Type, oi.OrderItemId FROM ' . $this->getTable('orditems') . ' AS oi LEFT JOIN ' . TABLE_PREFIX . 'Products AS p ON oi.ProductId = p.ProductId LEFT JOIN ' . $poc_table . ' poc ON (poc.CombinationCRC = oi.OptionsSalt) AND (oi.ProductId = poc.ProductId) WHERE oi.OrderId = ' . $this->getOrder()->GetID(); $this->items = $this->Conn->Query($query, 'OrderItemId'); } /** * Generates operations and returns true, when something was changed * * @return bool */ protected function generateOperations() { $this->manager->resetOperationTotals(); foreach ($this->items as $item) { $this->ensureMinQty($item); $to_order = $back_order = 0; $available = $this->getAvailableQty($item); if ( $this->allowBackordering($item) ) { // split order into order & backorder if ($item['BackOrder'] == ProductBackorder::ALWAYS) { $to_order = $available = 0; $back_order = $item['Quantity']; } elseif ($item['BackOrder'] == ProductBackorder::AUTO) { $to_order = $available; $back_order = $item['Quantity'] - $available; } $qty = $to_order + $back_order; $price = $this->getPlainProductPrice($item, $qty); $cost = $this->getProductCost($item, $qty); $discount_info = $this->getDiscountInfo( $item['ProductId'], $price, $qty ); $this->manager->addOperation($item, 0, $to_order, $price, $cost, $discount_info); $this->manager->addOperation($item, 1, $back_order, $price, $cost, $discount_info); } else { // store as normal order (and remove backorder) // we could get here with backorder=never then we should order only what's available $to_order = min($item['Quantity'], $available); $price = $this->getPlainProductPrice($item, $to_order); $cost = $this->getProductCost($item, $to_order); $discount_info = $this->getDiscountInfo( $item['ProductId'], $price, $to_order ); $this->manager->addOperation($item, 0, $to_order, $price, $cost, $discount_info, $item['OrderItemId']); $this->manager->addOperation($item, 1, 0, $price, $cost, $discount_info); // remove backorder record if ($to_order < $item['Quantity']) { // ordered less, then requested -> inform user if ( $to_order > 0 ) { $this->setError(OrderCheckoutErrorType::PRODUCT, OrderCheckoutError::QTY_UNAVAILABLE, $item['ProductId'] . ':' . $item['OptionsSalt'] . ':0:Quantity'); } else { $this->setError(OrderCheckoutErrorType::PRODUCT, OrderCheckoutError::QTY_OUT_OF_STOCK, $item['ProductId'] . ':' . $item['OptionsSalt'] . ':0:Quantity'); } } } } } /** * Adds product to order (not to db) * * @param Array $item * @param kCatDBItem $product * @param int $qty */ public function addProduct($item, &$product, $qty) { $this->updateItemDataFromProduct($item, $product); $price = $this->getPlainProductPrice($item, $qty); $cost = $this->getProductCost($item, $qty); $discount_info = $this->getDiscountInfo( $item['ProductId'], $price, $qty ); $this->manager->addOperation( $item, 0, $qty, $price, $cost, $discount_info, $item['OrderItemId'] ); } /** * Apply whole order flat discount after sub-total been calculated * */ protected function applyWholeOrderFlatDiscount() { $sub_total_flat = $this->manager->getOperationTotal('SubTotalFlat'); $flat_discount = min( $sub_total_flat, $this->getWholeOrderPlainDiscount($global_discount_id) ); $coupon_flat_discount = min( $sub_total_flat, $this->getWholeOrderCouponDiscount() ); if ($coupon_flat_discount && $coupon_flat_discount > $flat_discount) { $global_discount_type = 'coupon'; $flat_discount = $coupon_flat_discount; $global_discount_id = $coupon_id; } else { $global_discount_type = 'discount'; } $sub_total = $this->manager->getOperationTotal('SubTotal'); if ($sub_total_flat - $sub_total < $flat_discount) { // individual item discounts together are smaller when order flat discount $this->manager->setOperationTotal('CouponDiscount', $flat_discount == $coupon_flat_discount ? $flat_discount : 0); $this->manager->setOperationTotal('SubTotal', $sub_total_flat - $flat_discount); // replace discount for each operation foreach ($this->operations as $index => $operation) { $discounted_price = ($operation['Price'] / $sub_total_flat) * $sub_total; $this->operations[$index]['DiscountInfo'] = Array ($global_discount_id, $global_discount_type, $discounted_price, 0); } } } /** * Returns discount information for given product price and qty * * @param int $product_id * @param float $price * @param int $qty * @return Array */ protected function getDiscountInfo($product_id, $price, $qty) { $discounted_price = $this->getDiscountedProductPrice($product_id, $price, $discount_id); $couponed_price = $this->getCouponDiscountedPrice($product_id, $price); if ($couponed_price < $discounted_price) { $discount_type = 'coupon'; $discount_id = $coupon_id; $discounted_price = $couponed_price; $coupon_discount = ($price - $couponed_price) * $qty; } else { $coupon_discount = 0; $discount_type = 'discount'; } return Array ($discount_id, $discount_type, $discounted_price, $coupon_discount); } /** * Returns product qty, available for ordering * * @param Array $item * @return int */ protected function getAvailableQty($item) { if ( $item['InventoryStatus'] == ProductInventory::DISABLED ) { // always available return $item['Quantity'] * 2; } return max(0, $item['QtyInStock'] - $item['QtyInStockMin']); } /** * Checks, that product in given order item can be backordered * * @param Array $item * @return bool */ protected function allowBackordering($item) { if ($item['BackOrder'] == ProductBackorder::ALWAYS) { return true; } $available = $this->getAvailableQty($item); $backordering = $this->Application->ConfigValue('Comm_Enable_Backordering'); return $backordering && ($item['Quantity'] > $available) && ($item['BackOrder'] == ProductBackorder::AUTO); } /** * Make sure, that user can't order less, then minimal required qty of product * * @param Array $item */ protected function ensureMinQty(&$item) { $sql = 'SELECT MIN(MinQty) FROM ' . TABLE_PREFIX . 'ProductsPricing WHERE ProductId = ' . $item['ProductId']; $min_qty = max(1, $this->Conn->GetOne($sql)); $qty = $item['Quantity']; if ($qty > 0 && $qty < $min_qty) { // qty in cart increased to meat minimal qry requirements of given product $this->setError(OrderCheckoutErrorType::PRODUCT, OrderCheckoutError::QTY_CHANGED_TO_MINIMAL, $item['ProductId'] . ':' . $item['OptionsSalt'] . ':0:Quantity'); $item['Quantity'] = $min_qty; } } /** * Return product price for given qty, taking no discounts into account * * @param Array $item * @param int $qty * @return float */ public function getPlainProductPrice($item, $qty) { $item_data = $this->getItemData($item); if ( isset($item_data['ForcePrice']) ) { return $item_data['ForcePrice']; } $pricing_id = $this->getPriceBracketByQty($item, $qty); $sql = 'SELECT Price FROM ' . TABLE_PREFIX . 'ProductsPricing WHERE PriceId = ' . $pricing_id; $price = (float)$this->Conn->GetOne($sql); if ( isset($item_data['Options']) ) { $price += $this->getOptionPriceAddition($price, $item_data); $price = $this->getCombinationPriceOverride($price, $item_data); } return max($price, 0); } /** * Return product cost for given qty, taking no discounts into account * * @param Array $item * @param int $qty * @return float */ public function getProductCost($item, $qty) { $pricing_id = $this->getPriceBracketByQty($item, $qty); $sql = 'SELECT Cost FROM ' . TABLE_PREFIX . 'ProductsPricing WHERE PriceId = ' . $pricing_id; return (float)$this->Conn->GetOne($sql); } /** * Return product price for given qty, taking no discounts into account * * @param Array $item * @param int $qty * @return float */ protected function getPriceBracketByQty($item, $qty) { $orderby_clause = ''; $where_clause = Array (); $product_id = $item['ProductId']; if ( $this->usePriceBrackets($item) ) { $user_id = $this->getOrder()->GetDBField('PortalUserId'); $where_clause = Array ( 'GroupId IN (' . $this->Application->getUserGroups($user_id) . ')', 'pp.ProductId = ' . $product_id, 'pp.MinQty <= ' . $qty, $qty . ' < pp.MaxQty OR pp.MaxQty = -1', ); $orderby_clause = $this->getPriceBracketOrderClause($user_id); } else { $item_data = $this->getItemData($item); $where_clause = Array( 'pp.ProductId = ' . $product_id, 'pp.PriceId = ' . $this->getPriceBracketFromRequest($product_id, $item_data), ); } $sql = 'SELECT pp.PriceId FROM ' . TABLE_PREFIX . 'ProductsPricing AS pp LEFT JOIN ' . TABLE_PREFIX . 'Products AS p ON p.ProductId = pp.ProductId WHERE (' . implode(') AND (', $where_clause) . ')'; if ($orderby_clause) { $sql .= ' ORDER BY ' . $orderby_clause; } return (float)$this->Conn->GetOne($sql); } /** * Checks if price brackets should be used in price calculations * * @param Array $item * @return bool */ protected function usePriceBrackets($item) { return $item['Type'] == PRODUCT_TYPE_TANGIBLE; } /** * Return product pricing id for given product. * If not passed - return primary pricing ID * * @param int $product_id * @return int */ public function getPriceBracketFromRequest($product_id, $item_data) { if ( !is_array($item_data) ) { $item_data = unserialize($item_data); } // remembered pricing during checkout if ( isset($item_data['PricingId']) && $item_data['PricingId'] ) { return $item_data['PricingId']; } // selected pricing from product detail page $price_id = $this->Application->GetVar('pr_id'); if ($price_id) { return $price_id; } $sql = 'SELECT PriceId FROM ' . TABLE_PREFIX . 'ProductsPricing WHERE ProductId = ' . $product_id . ' AND IsPrimary = 1'; return $this->Conn->GetOne($sql); } /** * Returns order clause for price bracket selection based on configration * * @param int $user_id * @return string */ protected function getPriceBracketOrderClause($user_id) { if ($this->Application->ConfigValue('Comm_PriceBracketCalculation') == 1) { // if we have to stick to primary group, then its pricing will go first, // but if there is no pricing for primary group, then next optimal will be taken - $primary_group = $this->getUserPrimaryGroup($user_id); + + /** @var UserHelper $user_helper */ + $user_helper = $this->Application->recallObject('UserHelper'); + $primary_group = $user_helper->getPrimaryGroup($user_id); return '( IF(GroupId = ' . $primary_group . ', 1, 2) ) ASC, pp.Price ASC'; } return 'pp.Price ASC'; } /** * Returns addition to product price based on used product option * * @param float $price * @param Array $item_data * @return float */ protected function getOptionPriceAddition($price, $item_data) { $addition = 0; /** @var kProductOptionsHelper $opt_helper */ $opt_helper = $this->Application->recallObject('kProductOptionsHelper'); foreach ($item_data['Options'] as $opt => $val) { $sql = 'SELECT * FROM ' . TABLE_PREFIX . 'ProductOptions WHERE ProductOptionId = ' . $opt; $data = $this->Conn->GetRow($sql); $parsed = $opt_helper->ExplodeOptionValues($data); if ( !$parsed ) { continue; } if ( is_array($val) ) { foreach ($val as $a_val) { $addition += $this->formatPrice($a_val, $price, $parsed); } } else { $addition += $this->formatPrice($val, $price, $parsed); } } return $addition; } protected function formatPrice($a_val, $price, $parsed) { $a_val = kUtil::unescape($a_val, kUtil::ESCAPE_HTML); // TODO: Not sure why we're unescaping. $addition = 0; $conv_prices = $parsed['Prices']; $conv_price_types = $parsed['PriceTypes']; if ( isset($conv_prices[$a_val]) && $conv_prices[$a_val] ) { if ($conv_price_types[$a_val] == '$') { $addition += $conv_prices[$a_val]; } elseif ($conv_price_types[$a_val] == '%') { $addition += $price * $conv_prices[$a_val] / 100; } } return $addition; } /** * Returns product price after applying combination price override * * @param float $price * @param Array $item_data * @return float */ protected function getCombinationPriceOverride($price, $item_data) { $combination_salt = $this->generateOptionsSalt( $item_data['Options'] ); if (!$combination_salt) { return $price; } $sql = 'SELECT * FROM ' . TABLE_PREFIX . 'ProductOptionCombinations WHERE CombinationCRC = ' . $combination_salt; $combination = $this->Conn->GetRow($sql); if (!$combination) { return $price; } switch ( $combination['PriceType'] ) { case OptionCombinationPriceType::EQUALS: return $combination['Price']; break; case OptionCombinationPriceType::FLAT: return $price + $combination['Price']; break; case OptionCombinationPriceType::PECENT: return $price * (1 + $combination['Price'] / 100); break; } return $price; } /** * Generates salt for given option set * * @param Array $options * @return int */ public function generateOptionsSalt($options) { /** @var kProductOptionsHelper $opt_helper */ $opt_helper = $this->Application->recallObject('kProductOptionsHelper'); return $opt_helper->OptionsSalt($options, true); } /** * Return product price for given qty, taking possible discounts into account * * @param int $product_id * @param int $price * @param int $discount_id * @return float */ public function getDiscountedProductPrice($product_id, $price, &$discount_id) { $discount_id = 0; $user_id = $this->getOrder()->GetDBField('PortalUserId'); $join_clause = Array ( 'd.DiscountId = di.DiscountId', 'di.ItemType = ' . DiscountItemType::PRODUCT . ' OR (di.ItemType = ' . DiscountItemType::WHOLE_ORDER . ' AND d.Type = ' . DiscountType::PERCENT . ')', 'd.Status = ' . STATUS_ACTIVE, 'd.GroupId IN (' . $this->Application->getUserGroups($user_id) . ')', 'd.Start IS NULL OR d.Start < ' . $this->getOrder()->GetDBField('OrderDate'), 'd.End IS NULL OR d.End > ' . $this->getOrder()->GetDBField('OrderDate'), ); $sql = 'SELECT ROUND(CASE d.Type WHEN ' . DiscountType::FLAT . ' THEN ' . $price . ' - d.Amount WHEN ' . DiscountType::PERCENT . ' THEN ' . $price . ' * (1 - d.Amount / 100) ELSE ' . $price . ' END, 4), d.DiscountId FROM ' . TABLE_PREFIX . 'Products AS p LEFT JOIN ' . TABLE_PREFIX . 'ProductsDiscountItems AS di ON (di.ItemResourceId = p.ResourceId) OR (di.ItemType = ' . DiscountItemType::WHOLE_ORDER . ') LEFT JOIN ' . TABLE_PREFIX . 'ProductsDiscounts AS d ON (' . implode(') AND (', $join_clause) . ') WHERE (p.ProductId = ' . $product_id . ') AND (d.DiscountId IS NOT NULL)'; $pricing = $this->Conn->GetCol($sql, 'DiscountId'); if (!$pricing) { return $price; } // get minimal price + discount $discounted_price = min($pricing); $pricing = array_flip($pricing); $discount_id = $pricing[$discounted_price]; // optimal discount, but prevent negative price return max( min($discounted_price, $price), 0 ); } public function getWholeOrderPlainDiscount(&$discount_id) { $discount_id = 0; $user_id = $this->getOrder()->GetDBField('PortalUserId'); $join_clause = Array ( 'd.DiscountId = di.DiscountId', 'di.ItemType = ' . DiscountItemType::WHOLE_ORDER . ' AND d.Type = ' . DiscountType::FLAT, 'd.Status = ' . STATUS_ACTIVE, 'd.GroupId IN (' . $this->Application->getUserGroups($user_id) . ')', 'd.Start IS NULL OR d.Start < ' . $this->getOrder()->GetDBField('OrderDate'), 'd.End IS NULL OR d.End > ' . $this->getOrder()->GetDBField('OrderDate'), ); $sql = 'SELECT d.Amount AS Discount, d.DiscountId FROM ' . TABLE_PREFIX . 'ProductsDiscountItems AS di LEFT JOIN ' . TABLE_PREFIX . 'ProductsDiscounts AS d ON (' . implode(') AND (', $join_clause) . ') WHERE d.DiscountId IS NOT NULL'; $pricing = $this->Conn->GetCol($sql, 'DiscountId'); if (!$pricing) { return 0; } $discounted_price = max($pricing); $pricing = array_flip($pricing); $discount_id = $pricing[$discounted_price]; return max($discounted_price, 0); } public function getCouponDiscountedPrice($product_id, $price) { if ( !$this->getCoupon() ) { return $price; } $join_clause = Array ( 'c.CouponId = ci.CouponId', 'ci.ItemType = ' . CouponItemType::PRODUCT . ' OR (ci.ItemType = ' . CouponItemType::WHOLE_ORDER . ' AND c.Type = ' . CouponType::PERCENT . ')', ); $sql = 'SELECT MIN( ROUND(CASE c.Type WHEN ' . CouponType::FLAT . ' THEN ' . $price . ' - c.Amount WHEN ' . CouponType::PERCENT . ' THEN ' . $price . ' * (1 - c.Amount / 100) ELSE ' . $price . ' END, 4) ) FROM ' . TABLE_PREFIX . 'Products AS p LEFT JOIN ' . TABLE_PREFIX . 'ProductsCouponItems AS ci ON (ci.ItemResourceId = p.ResourceId) OR (ci.ItemType = ' . CouponItemType::WHOLE_ORDER . ') LEFT JOIN ' . TABLE_PREFIX . 'ProductsCoupons AS c ON (' . implode(') AND (', $join_clause) . ') WHERE p.ProductId = ' . $product_id . ' AND ci.CouponId = ' . $this->getCoupon() . ' GROUP BY p.ProductId'; $coupon_price = $this->Conn->GetOne($sql); if ($coupon_price === false) { return $price; } return max( min($price, $coupon_price), 0 ); } public function getWholeOrderCouponDiscount() { if ( !$this->getCoupon() ) { return 0; } $where_clause = Array ( 'ci.CouponId = ' . $this->getCoupon(), 'ci.ItemType = ' . CouponItemType::WHOLE_ORDER, 'c.Type = ' . CouponType::FLAT, ); $sql = 'SELECT Amount FROM ' . TABLE_PREFIX . 'ProductsCouponItems AS ci LEFT JOIN ' . TABLE_PREFIX . 'ProductsCoupons AS c ON c.CouponId = ci.CouponId WHERE (' . implode(') AND (', $where_clause) . ')'; return $this->Conn->GetOne($sql); } protected function getCoupon() { return $this->getOrder()->GetDBField('CouponId'); } /** - * Returns primary group of given user - * - * @param int $user_id - * @return int - */ - protected function getUserPrimaryGroup($user_id) - { - if ($user_id > 0) { - $sql = 'SELECT PrimaryGroupId - FROM ' . TABLE_PREFIX . 'Users - WHERE PortalUserId = ' . $user_id; - return $this->Conn->GetOne($sql); - } - - return $this->Application->ConfigValue('User_LoggedInGroup'); - } - - /** * Returns ItemData associated with given order item * * @param Array $item * @return Array */ protected function getItemData($item) { $item_data = $item['ItemData']; if ( is_array($item_data) ) { return $item_data; } return $item_data ? unserialize($item_data) : Array (); } /** * Sets ItemData according to product * * @param Array $item * @param kCatDBItem $product */ protected function updateItemDataFromProduct(&$item, &$product) { $item_data = $this->getItemData($item); $item_data['IsRecurringBilling'] = $product->GetDBField('IsRecurringBilling'); // it item is processed in order using new style, then put such mark in orderitem record $processing_data = $product->GetDBField('ProcessingData'); if ($processing_data) { $processing_data = unserialize($processing_data); if ( isset($processing_data['HasNewProcessing']) ) { $item_data['HasNewProcessing'] = 1; } } $item['ItemData'] = serialize($item_data); } /** * Returns table name according to order temp mode * * @param string $prefix * @return string */ protected function getTable($prefix) { return $this->manager->getTable($prefix); } } Index: branches/5.2.x/units/products/products_event_handler.php =================================================================== --- branches/5.2.x/units/products/products_event_handler.php (revision 16774) +++ branches/5.2.x/units/products/products_event_handler.php (revision 16775) @@ -1,1600 +1,1597 @@ Array('self' => true), 'OnRateProduct' => Array('self' => true), 'OnClearRecent' => Array('self' => true), 'OnRecommendProduct' => Array('self' => true), 'OnAddToCompare' => Array('self' => true), 'OnRemoveFromCompare' => Array('self' => true), 'OnCancelCompare' => Array('self' => true), // admin 'OnQtyAdd' => Array('self' => 'add|edit'), 'OnQtyRemove' => Array('self' => 'add|edit'), 'OnQtyOrder' => Array('self' => 'add|edit'), 'OnQtyReceiveOrder' => Array('self' => 'add|edit'), 'OnQtyCancelOrder' => Array('self' => 'add|edit'), ); $this->permMapping = array_merge($this->permMapping, $permissions); } /** * Define alternative event processing method names * * @return void * @see kEventHandler::$eventMethods * @access protected */ protected function mapEvents() { parent::mapEvents(); // ensure auto-adding of approve/decine and so on events $product_events = Array ( 'OnQtyAdd'=>'InventoryAction', 'OnQtyRemove'=>'InventoryAction', 'OnQtyOrder'=>'InventoryAction', 'OnQtyReceiveOrder'=>'InventoryAction', 'OnQtyCancelOrder'=>'InventoryAction', ); $this->eventMethods = array_merge($this->eventMethods, $product_events); } /** * Sets default processing data for subscriptions * * @param kEvent $event * @return void * @access protected */ protected function OnBeforeItemCreate(kEvent $event) { parent::OnBeforeItemCreate($event); /** @var kDBItem $object */ $object = $event->getObject(); $product_approve_events = Array ( 2 => 'p:OnSubscriptionApprove', 4 => 'p:OnDownloadableApprove', 5 => 'p:OnPackageApprove' ); $product_type = $object->GetDBField('Type'); $type_found = in_array($product_type, array_keys($product_approve_events)); if ( $type_found && !$object->GetDBField('ProcessingData') ) { $processing_data = Array ('ApproveEvent' => $product_approve_events[$product_type]); $object->SetDBField('ProcessingData', serialize($processing_data)); } } /** * Process product count manipulations * * @param kEvent $event * @access private */ function InventoryAction($event) { /** @var kDBItem $object */ $object = $event->getObject(); $field_values = $this->getSubmittedFields($event); $object->SetFieldsFromHash($field_values); $event->setEventParam('form_data', $field_values); if ($object->GetDBField('InventoryStatus') == 2) { // inventory by options (use first selected combination in grid) $combinations = $this->Application->GetVar('poc_grid'); $combination_id = key($combinations); } else { // inventory by product $combination_id = 0; } // save id of selected option combination & preselect it in grid $this->Application->SetVar('combination_id', $combination_id); $this->ScheduleInventoryAction($event->Name, $object->GetId(), $object->GetDBField('Qty'), $combination_id); $object->Validate(); if ( !$object->GetErrorPseudo('Qty') ){ // only update, when no error on that field $this->modifyInventory($event->Name, $object, $object->GetDBField('Qty'), $combination_id); } $object->SetDBField('Qty', null); $event->redirect = false; } /** * Perform inventory action on supplied object * * @param string $action event name which is actually called by user * @param ProductsItem $product * @param int $qty * @param int $combination_id */ function modifyInventory($action, &$product, $qty, $combination_id) { if ($product->GetDBField('InventoryStatus') == 2) { // save inventory changes to option combination instead of product $object = $this->Application->recallObject('poc.-item', null, Array('skip_autoload' => true)); $object->Load($combination_id); } elseif ($combination_id > 0) { // combination id present, but not inventory by combinations => skip return false; } elseif ($product->GetDBField('InventoryStatus') == 1) { // save inventory changes to product $object =& $product; } else { // product has inventory actions, but don't use inventory => skip return false; } if (!$object->isLoaded()) { // product/combination in action doesn't exist in database by now return false; } switch ($action) { case 'OnQtyAdd': $object->SetDBField('QtyInStock', $object->GetDBField('QtyInStock') + $qty); break; case 'OnQtyRemove': if ($object->GetDBField('QtyInStock') < $qty) { $qty = $object->GetDBField('QtyInStock'); } $object->SetDBField('QtyInStock', $object->GetDBField('QtyInStock') - $qty); break; case 'OnQtyOrder': $object->SetDBField('QtyOnOrder', $object->GetDBField('QtyOnOrder') + $qty); break; case 'OnQtyReceiveOrder': $object->SetDBField('QtyOnOrder', $object->GetDBField('QtyOnOrder') - $qty); $object->SetDBField('QtyInStock', $object->GetDBField('QtyInStock') + $qty); break; case 'OnQtyCancelOrder': $object->SetDBField('QtyOnOrder', $object->GetDBField('QtyOnOrder') - $qty); break; } return $object->Update(); } function ScheduleInventoryAction($action, $prod_id, $qty, $combination_id = 0) { $inv_actions = $this->Application->RecallVar('inventory_actions'); if (!$inv_actions) { $inv_actions = Array(); } else { $inv_actions = unserialize($inv_actions); } array_push($inv_actions, Array('action' => $action, 'product_id' => $prod_id, 'combination_id' => $combination_id, 'qty' => $qty)); $this->Application->StoreVar('inventory_actions', serialize($inv_actions)); } function RealInventoryAction($action, $prod_id, $qty, $combination_id) { $product = $this->Application->recallObject('p.liveitem', null, Array('skip_autoload' => true)); $product->SwitchToLive(); $product->Load($prod_id); $this->modifyInventory($action, $product, $qty, $combination_id); } function RunScheduledInventoryActions($event) { $inv_actions = $this->Application->GetVar('inventory_actions'); if (!$inv_actions) { return; } $inv_actions = unserialize($inv_actions); $products = array(); foreach($inv_actions as $an_action) { $this->RealInventoryAction($an_action['action'], $an_action['product_id'], $an_action['qty'], $an_action['combination_id']); array_push($products, $an_action['product_id'].'_'.$an_action['combination_id']); } $products = array_unique($products); if ($products) { $product_obj = $this->Application->recallObject('p.liveitem', null, Array('skip_autoload' => true)); $product_obj->SwitchToLive(); foreach ($products as $product_key) { list($prod_id, $combination_id) = explode('_', $product_key); $product_obj->Load($prod_id); $this->FullfillBackOrders($product_obj, $combination_id); } } } /** * In case if products arrived into inventory and they are required by old (non processed) orders, then use them (products) in that orders * * @param ProductsItem $product * @param int $combination_id */ function FullfillBackOrders(&$product, $combination_id) { if ( !$this->Application->ConfigValue('Comm_Process_Backorders_Auto') ) return; if ($combination_id && ($product->GetDBField('InventoryStatus') == 2)) { // if combination id present and inventory by combinations $poc_idfield = $this->Application->getUnitOption('poc', 'IDField'); $poc_tablename = $this->Application->getUnitOption('poc', 'TableName'); $sql = 'SELECT QtyInStock FROM '.$poc_tablename.' WHERE '.$poc_idfield.' = '.$combination_id; $stock_qty = $this->Conn->GetOne($sql); } else { // inventory by product $stock_qty = $product->GetDBField('QtyInStock'); } $qty = (int) $stock_qty - $product->GetDBField('QtyInStockMin'); $prod_id = $product->GetID(); if ($prod_id <= 0 || !$prod_id || $qty <= 0) return; //selecting up to $qty backorders with $prod_id where full qty is not reserved $query = 'SELECT '.TABLE_PREFIX.'Orders.OrderId FROM '.TABLE_PREFIX.'OrderItems LEFT JOIN '.TABLE_PREFIX.'Orders ON '.TABLE_PREFIX.'Orders.OrderId = '.TABLE_PREFIX.'OrderItems.OrderId WHERE (ProductId = '.$prod_id.') AND (Quantity > QuantityReserved) AND (Status = '.ORDER_STATUS_BACKORDERS.') GROUP BY '.TABLE_PREFIX.'Orders.OrderId ORDER BY OrderDate ASC LIMIT 0,'.$qty; //assuming 1 item per order - minimum possible $orders = $this->Conn->GetCol($query); if ( !$orders ) { return; } /** @var OrdersItem $order */ $order = $this->Application->recallObject('ord.-inv', null, array('skip_autoload' => true)); foreach ($orders as $ord_id) { $order->Load($ord_id); $this->Application->emailAdmin('BACKORDER.FULLFILL'); // Reserve what's possible in any case. $reserve_event = new kEvent('ord:OnReserveItems'); $this->Application->HandleEvent($reserve_event); // In case the order is ready to process - process it. if ( $reserve_event->status == kEvent::erSUCCESS ) { $this->Application->HandleEvent(new kEvent('ord:OnOrderProcess')); } } } /** * Occurs before an item is deleted from live table when copying from temp * (temp handler deleted all items from live and then copy over all items from temp) * Id of item being deleted is passed as event' 'id' param * * @param kEvent $event * @return void * @access protected */ protected function OnBeforeDeleteFromLive(kEvent $event) { parent::OnBeforeDeleteFromLive($event); /** @var kCatDBItem $product */ $product = $this->Application->recallObject($event->Prefix . '.itemlive', null, Array ('skip_autoload' => true)); $product->SwitchToLive(); $id = $event->getEventParam('id'); if ( !$product->Load($id) ) { // this will make sure New product will not be overwritten with empty data return ; } /** @var kCatDBItem $temp */ $temp = $this->Application->recallObject($event->Prefix . '.itemtemp', null, Array ('skip_autoload' => true)); $temp->SwitchToTemp(); $temp->Load($id); $temp->SetDBFieldsFromHash($product->GetFieldValues(), Array ('QtyInStock', 'QtyReserved', 'QtyBackOrdered', 'QtyOnOrder')); $temp->Update(); } /** * Removes any information about current/selected ids * from Application variables and Session * * @param kEvent $event * @return void * @access protected */ protected function clearSelectedIDs(kEvent $event) { parent::clearSelectedIDs($event); $this->Application->SetVar('inventory_actions', $this->Application->RecallVar('inventory_actions')); $this->Application->RemoveVar('inventory_actions'); } /** * Saves content of temp table into live and * redirects to event' default redirect (normally grid template) * * @param kEvent $event * @return void * @access protected */ protected function OnSave(kEvent $event) { parent::OnSave($event); if ( $event->status == kEvent::erSUCCESS ) { $this->RunScheduledInventoryActions($event); } } /** * Prepare temp tables for creating new item * but does not create it. Actual create is * done in OnPreSaveCreated * * @param kEvent $event * @return void * @access protected */ protected function OnPreCreate(kEvent $event) { parent::onPreCreate($event); /** @var kDBItem $object */ $object = $event->getObject(); $object->SetDBField('Type', $this->Application->GetVar($event->getPrefixSpecial(true) . '_new_type')); } /** * Saves edited item in temp table and loads * item with passed id in current template * Used in Prev/Next buttons * * @param kEvent $event * @return void * @access protected */ protected function OnPreSaveAndGo(kEvent $event) { $event->CallSubEvent('OnPreSave'); $this->LoadItem($event); /** @var kDBItem $object */ $object = $event->getObject(); $from_type = $object->GetDBField('Type'); if ( $event->status == kEvent::erSUCCESS ) { $this->Application->SetVar($event->getPrefixSpecial() . '_id', $this->Application->GetVar($event->getPrefixSpecial(true) . '_GoId')); $this->LoadItem($event); $to_type = $object->GetDBField('Type'); if ( $from_type != $to_type ) { $from_tabs = $this->GetTabs($from_type); $from_tab_i = array_search($this->Application->GetVar('t'), $from_tabs); $to_tabs = $this->GetTabs($to_type); $to_tab = $this->Application->GetVar('t'); $found = false; while (!isset($to_tabs[$from_tab_i]) && $from_tab_i < count($to_tabs)) { $from_tab_i++; } if ( !isset($to_tabs[$from_tab_i]) ) { $from_tab_i = 0; } $to_tab = $to_tabs[$from_tab_i]; $event->redirect = $to_tab; } } } function GetTabs($type) { switch($type) { case 1: return Array( 0 => 'in-commerce/products/products_edit', 1 => 'in-commerce/products/products_inventory', 2 => 'in-commerce/products/products_pricing', 3 => 'in-commerce/products/products_categories', 4 => 'in-commerce/products/products_images', 5 => 'in-commerce/products/products_reviews', 6 => 'in-commerce/products/products_custom', ); case 2: return Array( 0 => 'in-commerce/products/products_edit', 1 => 'in-commerce/products/products_access', /*2 => 'in-commerce/products/products_access_pricing',*/ 3 => 'in-commerce/products/products_categories', 4 => 'in-commerce/products/products_images', 5 => 'in-commerce/products/products_reviews', 6 => 'in-commerce/products/products_custom', ); case 3: return Array( 0 => 'in-commerce/products/products_edit', 2 => 'in-commerce/products/products_access_pricing', 3 => 'in-commerce/products/products_categories', 4 => 'in-commerce/products/products_images', 5 => 'in-commerce/products/products_reviews', 6 => 'in-commerce/products/products_custom', ); case 4: return Array( 0 => 'in-commerce/products/products_edit', 2 => 'in-commerce/products/products_files', 3 => 'in-commerce/products/products_categories', 4 => 'in-commerce/products/products_images', 5 => 'in-commerce/products/products_reviews', 6 => 'in-commerce/products/products_custom', ); } } /** * Return type clauses for list bulding on front * * @param kEvent $event * @return Array */ function getTypeClauses($event) { $types = $event->getEventParam('types'); $types = $types ? explode(',', $types) : Array (); $except_types = $event->getEventParam('except'); $except_types = $except_types ? explode(',', $except_types) : Array (); /** @var kDBList $object */ $object = $event->getObject(); $type_clauses = parent::getTypeClauses($event); $type_clauses['featured']['include'] = '%1$s.Featured = 1 AND ' . TABLE_PREFIX . 'CategoryItems.PrimaryCat = 1'; $type_clauses['featured']['except'] = '%1$s.Featured != 1 AND ' . TABLE_PREFIX . 'CategoryItems.PrimaryCat = 1'; $type_clauses['featured']['having_filter'] = false; $type_clauses['onsale']['include'] = '%1$s.OnSale = 1 AND ' . TABLE_PREFIX . 'CategoryItems.PrimaryCat = 1'; $type_clauses['onsale']['except'] = '%1$s.OnSale != 1 AND ' . TABLE_PREFIX . 'CategoryItems.PrimaryCat = 1'; $type_clauses['onsale']['having_filter'] = false; // products from selected manufacturer: begin $manufacturer = $event->getEventParam('manufacturer'); if ( !$manufacturer ) { $manufacturer = $this->Application->GetVar('manuf_id'); } if ( $manufacturer ) { $type_clauses['manufacturer']['include'] = '%1$s.ManufacturerId = ' . $manufacturer . ' AND PrimaryCat = 1'; $type_clauses['manufacturer']['except'] = '%1$s.ManufacturerId != ' . $manufacturer . ' AND PrimaryCat = 1'; $type_clauses['manufacturer']['having_filter'] = false; } // products from selected manufacturer: end // recent products: begin $recent = $this->Application->RecallVar('recent_products'); if ( $recent ) { $recent = unserialize($recent); $type_clauses['recent']['include'] = '%1$s.ProductId IN (' . implode(',', $recent) . ') AND PrimaryCat = 1'; $type_clauses['recent']['except'] = '%1$s.ProductId NOT IN (' . implode(',', $recent) . ') AND PrimaryCat = 1'; } else { $type_clauses['recent']['include'] = '0'; $type_clauses['recent']['except'] = '1'; } $type_clauses['recent']['having_filter'] = false; // recent products: end // compare products: begin if ( in_array('compare', $types) || in_array('compare', $except_types) ) { $compare_products = $this->getCompareProducts(); if ( $compare_products ) { $compare_products = $this->Conn->qstrArray($compare_products); $type_clauses['compare']['include'] = '%1$s.ProductId IN (' . implode(',', $compare_products) . ') AND PrimaryCat = 1'; $type_clauses['compare']['except'] = '%1$s.ProductId NOT IN (' . implode(',', $compare_products) . ') AND PrimaryCat = 1'; } else { $type_clauses['compare']['include'] = '0'; $type_clauses['compare']['except'] = '1'; } $type_clauses['compare']['having_filter'] = false; if ( $event->getEventParam('per_page') === false ) { $event->setEventParam('per_page', $this->Application->ConfigValue('MaxCompareProducts')); } } // compare products: end // products already in shopping cart: begin if ( in_array('in_cart', $types) || in_array('in_cart', $except_types) ) { $order_id = $this->Application->RecallVar('ord_id'); if ( $order_id ) { $sql = 'SELECT ProductId FROM ' . TABLE_PREFIX . 'OrderItems WHERE OrderId = ' . $order_id; $in_cart = $this->Conn->GetCol($sql); if ( $in_cart ) { $type_clauses['in_cart']['include'] = '%1$s.ProductId IN (' . implode(',', $in_cart) . ') AND PrimaryCat = 1'; $type_clauses['in_cart']['except'] = '%1$s.ProductId NOT IN (' . implode(',', $in_cart) . ') AND PrimaryCat = 1'; } else { $type_clauses['in_cart']['include'] = '0'; $type_clauses['in_cart']['except'] = '1'; } } else { $type_clauses['in_cart']['include'] = '0'; $type_clauses['in_cart']['except'] = '1'; } $type_clauses['in_cart']['having_filter'] = false; } // products already in shopping cart: end // my downloadable products: begin if ( in_array('my_downloads', $types) || in_array('my_downloads', $except_types) ) { $user_id = $this->Application->RecallVar('user_id'); $sql = 'SELECT ProductId FROM ' . TABLE_PREFIX . 'UserFileAccess WHERE PortalUserId = ' . $user_id; $my_downloads = $user_id > 0 ? $this->Conn->GetCol($sql) : false; if ( $my_downloads ) { $type_clauses['my_downloads']['include'] = '%1$s.ProductId IN (' . implode(',', $my_downloads) . ') AND PrimaryCat = 1'; $type_clauses['my_downloads']['except'] = '%1$s.ProductId NOT IN (' . implode(',', $my_downloads) . ') AND PrimaryCat = 1'; } else { $type_clauses['my_downloads']['include'] = '0'; $type_clauses['my_downloads']['except'] = '1'; } $type_clauses['my_downloads']['having_filter'] = false; } // my downloadable products: end // my favorite products: begin if ( in_array('wish_list', $types) || in_array('wish_list', $except_types) ) { $sql = 'SELECT ResourceId FROM ' . $this->Application->getUnitOption('fav', 'TableName') . ' WHERE PortalUserId = ' . (int)$this->Application->RecallVar('user_id'); $wishlist_ids = $this->Conn->GetCol($sql); if ( $wishlist_ids ) { $type_clauses['wish_list']['include'] = '%1$s.ResourceId IN (' . implode(',', $wishlist_ids) . ') AND PrimaryCat = 1'; $type_clauses['wish_list']['except'] = '%1$s.ResourceId NOT IN (' . implode(',', $wishlist_ids) . ') AND PrimaryCat = 1'; } else { $type_clauses['wish_list']['include'] = '0'; $type_clauses['wish_list']['except'] = '1'; } $type_clauses['wish_list']['having_filter'] = false; } // my favorite products: end // products from package: begin if ( in_array('content', $types) || in_array('content', $except_types) ) { $object->removeFilter('category_filter'); $object->AddGroupByField('%1$s.ProductId'); /** @var ProductsItem $object_product */ $object_product = $this->Application->recallObject($event->Prefix); $content_ids_array = $object_product->GetPackageContentIds(); if ( sizeof($content_ids_array) == 0 ) { $content_ids_array = array ('-1'); } if ( sizeof($content_ids_array) > 0 ) { $type_clauses['content']['include'] = '%1$s.ProductId IN (' . implode(',', $content_ids_array) . ')'; } else { $type_clauses['content']['include'] = '0'; } $type_clauses['related']['having_filter'] = false; } // products from package: end $object->addFilter('not_virtual', '%1$s.Virtual = 0'); if ( !$this->Application->isAdminUser ) { $object->addFilter('expire_filter', '%1$s.Expire IS NULL OR %1$s.Expire > ' . adodb_mktime()); } return $type_clauses; } function OnClearRecent($event) { $this->Application->RemoveVar('recent_products'); } /** * Occurs, when user rates a product * * @param kEvent $event */ function OnRateProduct($event) { $event->SetRedirectParam('pass', 'all,p'); $event->redirect = $this->Application->GetVar('success_template'); /** @var kDBItem $object */ $object = $event->getObject(); $user_id = $this->Application->RecallVar('user_id'); $sql = ' SELECT * FROM ' . TABLE_PREFIX . 'SpamControl WHERE ItemResourceId=' . $object->GetDBField('ResourceId') . ' AND IPaddress="' . $this->Application->getClientIp() . '" AND PortalUserId=' . $user_id . ' AND DataType="Rating"'; $res = $this->Conn->GetRow($sql); if ( $res && $res['Expire'] < adodb_mktime() ) { $sql = ' DELETE FROM ' . TABLE_PREFIX . 'SpamControl WHERE ItemResourceId=' . $object->GetDBField('ResourceId') . ' AND IPaddress="' . $this->Application->getClientIp() . '" AND PortalUserId=' . $user_id . ' AND DataType="Rating"'; $this->Conn->Query($sql); unset($res); } $new_rating = $this->Application->GetVar('rating'); if ( $new_rating !== false && !$res ) { $rating = $object->GetDBField('CachedRating'); $votes = $object->GetDBField('CachedVotesQty'); $new_votes = $votes + 1; $rating = (($rating * $votes) + $new_rating) / $new_votes; $object->SetDBField('CachedRating', $rating); $object->SetDBField('CachedVotesQty', $new_votes); $object->Update(); $expire = adodb_mktime() + $this->Application->ConfigValue('product_ReviewDelay_Value') * $this->Application->ConfigValue('product_ReviewDelay_Interval'); $sql = ' INSERT INTO ' . TABLE_PREFIX . 'SpamControl (ItemResourceId, IPaddress, PortalUserId, DataType, Expire) VALUES (' . $object->GetDBField('ResourceId') . ', "' . $this->Application->getClientIp() . '", ' . $user_id . ', "Rating", ' . $expire . ')'; $this->Conn->Query($sql); } else { $event->status == kEvent::erFAIL; $event->redirect = false; $object->SetError('CachedRating', 'too_frequent', 'lu_ferror_rate_duplicate'); } } /** * Enter description here... * * @param kEvent $event */ function OnCancelAction($event) { $event->SetRedirectParam('pass', 'all,p'); $event->redirect = $this->Application->GetVar('cancel_template'); } /** * Enter description here... * * @param kEvent $event */ function OnRecommendProduct($event) { // used for error reporting only -> rewrite code + theme (by Alex) $object = $this->Application->recallObject('u', null, Array('skip_autoload' => true)); // TODO: change theme too /** @var kDBItem $object */ $friend_email = $this->Application->GetVar('friend_email'); $friend_name = $this->Application->GetVar('friend_name'); $my_email = $this->Application->GetVar('your_email'); $my_name = $this->Application->GetVar('your_name'); $my_message = $this->Application->GetVar('your_message'); $send_params = array(); $send_params['to_email']=$friend_email; $send_params['to_name']=$friend_name; $send_params['from_email']=$my_email; $send_params['from_name']=$my_name; $send_params['message']=$my_message; if ( preg_match('/' . REGEX_EMAIL_USER . '@' . REGEX_EMAIL_DOMAIN . '/', $friend_email) ) { $user_id = $this->Application->RecallVar('user_id'); $email_sent = $this->Application->emailUser('PRODUCT.SUGGEST', $user_id, $send_params); $this->Application->emailAdmin('PRODUCT.SUGGEST'); if ( $email_sent ) { $event->setRedirectParams(Array ('opener' => 's', 'pass' => 'all')); $event->redirect = $this->Application->GetVar('template_success'); } else { // $event->setRedirectParams(Array('opener' => 's', 'pass' => 'all')); // $event->redirect = $this->Application->GetVar('template_fail'); $object->SetError('Email', 'send_error', 'lu_email_send_error'); $event->status = kEvent::erFAIL; } } else { $object->SetError('Email', 'invalid_email', 'lu_InvalidEmail'); $event->status = kEvent::erFAIL; } } /** * Creates/updates virtual product based on listing type data * * @param kEvent $event */ function OnSaveVirtualProduct($event) { $object = $event->getObject( Array('skip_autoload' => true) ); $listing_type = $this->Application->recallObject('lst', null, Array('skip_autoload' => true)); $listing_type->Load($event->MasterEvent->getEventParam('id')); $product_id = $listing_type->GetDBField('VirtualProductId'); if ($product_id) { $object->Load($product_id); } if (!$listing_type->GetDBField('EnableBuying')) { if ($product_id) { // delete virtual product here $temp_handler = $this->Application->recallObject($event->getPrefixSpecial().'_TempHandler', 'kTempTablesHandler'); $temp_handler->DeleteItems($event->Prefix, $event->Special, Array($product_id)); $listing_type->SetDBField('VirtualProductId', 0); $listing_type->Update(); } return true; } $ml_formatter = $this->Application->recallObject('kMultiLanguage'); $object->SetDBField($ml_formatter->LangFieldName('Name'), $listing_type->GetDBField('ShopCartName') ); $object->SetDBField($ml_formatter->LangFieldName('Description'), $listing_type->GetDBField('Description')); $object->SetDBField('SKU', 'ENHANCE_LINK_'.abs( crc32( $listing_type->GetDBField('Name') ) ) ); if ($product_id) { $object->Update(); } else { $object->SetDBField('Type', 2); $object->SetDBField('Status', 1); $object->SetDBField('HotItem', 0); $object->SetDBField('PopItem', 0); $object->SetDBField('NewItem', 0); $object->SetDBField('Virtual', 1); // $processing_data = Array('ApproveEvent' => 'ls:EnhanceLinkAfterOrderApprove', 'ExpireEvent' => 'ls:ExpireLink'); $processing_data = Array( 'ApproveEvent' => 'ls:EnhanceLinkAfterOrderApprove', 'DenyEvent' => 'ls:EnhanceLinkAfterOrderDeny', 'CompleteOrderEvent' => 'ls:EnhancedLinkOnCompleteOrder', 'ExpireEvent' => 'ls:ExpireLink', 'HasNewProcessing' => 1); $object->SetDBField('ProcessingData', serialize($processing_data)); $object->Create(); $listing_type->SetDBField('VirtualProductId', $object->GetID()); $listing_type->Update(); } $additiona_fields = Array( 'AccessDuration' => $listing_type->GetDBField('Duration'), 'AccessUnit' => $listing_type->GetDBField('DurationType'), ); $this->setPrimaryPrice($object->GetID(), (double)$listing_type->GetDBField('Price'), $additiona_fields); } /** * [HOOK] Deletes virtual product when listing type is deleted * * @param kEvent $event */ function OnDeleteListingType($event) { /** @var kDBItem $listing_type */ $listing_type = $event->MasterEvent->getObject(); $product_id = $listing_type->GetDBField('VirtualProductId'); if ( $product_id ) { $temp_handler = $this->Application->recallObject($event->getPrefixSpecial() . '_TempHandler', 'kTempTablesHandler'); $temp_handler->DeleteItems($event->Prefix, $event->Special, Array ($product_id)); } } /** * Extends user membership in group when his order is approved * * @param kEvent $event */ function OnSubscriptionApprove($event) { $field_values = $event->getEventParam('field_values'); $item_data = unserialize($field_values['ItemData']); if ( !getArrayValue($item_data,'PortalGroupId') ) { // is subscription product, but no group defined in it's properties trigger_error('Invalid product '.$field_values['ProductName'].' (id: '.$field_values['ProductId'].')', E_USER_WARNING); return false; } $sql = 'SELECT PortalUserId FROM ' . $this->Application->getUnitOption('ord', 'TableName') . ' WHERE ' . $this->Application->getUnitOption('ord', 'IDField') . ' = ' . $field_values['OrderId']; $user_id = $this->Conn->GetOne($sql); $group_id = $item_data['PortalGroupId']; $duration = $item_data['Duration']; $sql = 'SELECT * FROM ' . TABLE_PREFIX . 'UserGroupRelations WHERE PortalUserId = ' . $user_id; $user_groups = $this->Conn->Query($sql, 'GroupId'); if ( !isset($user_groups[$group_id]) ) { $expire = adodb_mktime() + $duration; } else { $expire = $user_groups[$group_id]['MembershipExpires']; $expire = $expire < adodb_mktime() ? adodb_mktime() + $duration : $expire + $duration; } /*// Customization healtheconomics.org if ($item_data['DurationType'] == 2) { $expire = $item_data['AccessExpiration']; } // Customization healtheconomics.org --*/ $fields_hash = Array ( 'PortalUserId' => $user_id, 'GroupId' => $group_id, 'MembershipExpires' => $expire, ); $this->Conn->doInsert($fields_hash, TABLE_PREFIX . 'UserGroupRelations', 'REPLACE'); $sub_order = $this->Application->recallObject('ord.-sub'.$event->getEventParam('next_sub_number'), 'ord'); $sub_order->SetDBField('IsRecurringBilling', getArrayValue($item_data, 'IsRecurringBilling') ? 1 : 0); $sub_order->SetDBField('GroupId', $group_id); $sub_order->SetDBField('NextCharge_date', $expire); $sub_order->SetDBField('NextCharge_time', $expire); } function OnDownloadableApprove($event) { $field_values = $event->getEventParam('field_values'); $product_id = $field_values['ProductId']; $sql = 'SELECT PortalUserId FROM '.$this->Application->getUnitOption('ord', 'TableName').' WHERE OrderId = '.$field_values['OrderId']; $user_id = $this->Conn->GetOne($sql); $sql = 'INSERT INTO '.TABLE_PREFIX.'UserFileAccess VALUES("", '.$product_id.', '.$user_id.')'; $this->Conn->Query($sql); } protected function OnPackageApprove(kEvent $event) { $field_values = $event->getEventParam('field_values'); $item_data = unserialize($field_values['ItemData']); $package_content_ids = $item_data['PackageContent']; /** @var ProductsItem $object_item */ $object_item = $this->Application->recallObject('p.packageitem', null, array ('skip_autoload' => true)); foreach ($package_content_ids as $package_item_id) { $object_field_values = array (); // query processing data from product and run approve event $sql = 'SELECT ProcessingData FROM ' . TABLE_PREFIX . 'Products WHERE ProductId = ' . $package_item_id; $processing_data = $this->Conn->GetOne($sql); if ( $processing_data ) { $processing_data = unserialize($processing_data); $approve_event = new kEvent($processing_data['ApproveEvent']); //$order_item_fields = $this->Conn->GetRow('SELECT * FROM '.TABLE_PREFIX.'OrderItems WHERE OrderItemId = '.$grouping_data[1]); $object_item->Load($package_item_id); $object_field_values['OrderId'] = $field_values['OrderId']; $object_field_values['ProductId'] = $package_item_id; $object_field_values['ItemData'] = serialize($item_data['PackageItemsItemData'][$package_item_id]); $approve_event->setEventParam('field_values', $object_field_values); $this->Application->HandleEvent($approve_event); } } } /** * Saves edited item into temp table * If there is no id, new item is created in temp table * * @param kEvent $event * @return void * @access protected */ protected function OnPreSave(kEvent $event) { $this->CheckRequiredOptions($event); parent::OnPreSave($event); } /** * Set new price to ProductsPricing * * @param kEvent $event * @return void * @access protected */ protected function OnAfterItemCreate(kEvent $event) { parent::OnAfterItemCreate($event); $this->_updateProductPrice($event); } /** * Set new price to ProductsPricing * * @param kEvent $event * @return void * @access protected */ protected function OnAfterItemUpdate(kEvent $event) { parent::OnAfterItemUpdate($event); $this->_updateProductPrice($event); } /** * Updates product's primary price based on Price virtual field value * * @param kEvent $event */ function _updateProductPrice($event) { /** @var kDBItem $object */ $object = $event->getObject(); $price = $object->GetDBField('Price'); // always create primary pricing, to show on Pricing tab (in admin) for tangible products $force_create = ($object->GetDBField('Type') == PRODUCT_TYPE_TANGIBLE) && is_null($price); if ($force_create || ($price != $object->GetOriginalField('Price'))) { // new product OR price was changed in virtual field $this->setPrimaryPrice($object->GetID(), (float)$price); } } function CheckRequiredOptions($event) { $object = $event->getObject(); if ($object->GetDBField('ProductId') == '') return ; // if product does not have ID - it's not yet created $opt_object = $this->Application->recallObject('po', null, Array('skip_autoload' => true) ); $has_required = $this->Conn->GetOne('SELECT COUNT(*) FROM '.$opt_object->TableName.' WHERE Required = 1 AND ProductId = '.$object->GetDBField('ProductId')); //we need to imitate data sumbit, as parent' PreSave sets object values from $items_info $items_info = $this->Application->GetVar( $event->getPrefixSpecial(true) ); $items_info[$object->GetDBField('ProductId')]['HasRequiredOptions'] = $has_required ? '1' : '0'; $this->Application->SetVar($event->getPrefixSpecial(true), $items_info); $object->SetDBField('HasRequiredOptions', $has_required ? 1 : 0); } /** * Sets required price in primary price backed, if it's missing, then create it * * @param int $product_id * @param double $price * @param Array $additional_fields * @return bool */ function setPrimaryPrice($product_id, $price, $additional_fields = Array()) { /** @var kDBItem $pr_object */ $pr_object = $this->Application->recallObject('pr.-item', null, Array('skip_autoload' => true) ); $pr_object->Load( Array('ProductId' => $product_id, 'IsPrimary' => 1) ); $sql = 'SELECT COUNT(*) FROM '.$pr_object->TableName.' WHERE ProductId = '.$product_id; $has_pricings = $this->Conn->GetOne($sql); if ($additional_fields) { $pr_object->SetDBFieldsFromHash($additional_fields); } if( ($price === false) && $has_pricings ) return false; if( $pr_object->isLoaded() ) { $pr_object->SetField('Price', $price); return $pr_object->Update(); } else { $group_id = $this->Application->ConfigValue('User_LoggedInGroup'); $field_values = Array('ProductId' => $product_id, 'IsPrimary' => 1, 'MinQty' => 1, 'MaxQty' => -1, 'GroupId'=>$group_id); $pr_object->SetDBFieldsFromHash($field_values); $pr_object->SetField('Price', $price); return $pr_object->Create(); } } /** * Occurs after deleting item, id of deleted item * is stored as 'id' param of event * * @param kEvent $event * @return void * @access protected */ protected function OnAfterItemDelete(kEvent $event) { parent::OnAfterItemDelete($event); $product_id = $event->getEventParam('id'); if ( !$product_id ) { return; } $sql = 'DELETE FROM ' . TABLE_PREFIX . 'UserFileAccess WHERE ProductId = ' . $product_id; $this->Conn->Query($sql); } /** * Load price from temp table if product mode is temp table * * @param kEvent $event */ /** * Load price from temp table if product mode is temp table * * @param kEvent $event * @return void * @access protected */ protected function OnAfterItemLoad(kEvent $event) { parent::OnAfterItemLoad($event); /** @var ProductsItem $object */ $object = $event->getObject(); $a_pricing = $object->getPrimaryPricing(); if ( !$a_pricing ) { // pricing doesn't exist for new products $price = $cost = null; } else { $price = (float)$a_pricing['Price']; $cost = (float)$a_pricing['Cost']; } // set original fields to use them in OnAfterItemCreate/OnAfterItemUpdate later $object->SetDBField('Price', $price); $object->SetOriginalField('Price', $price); $object->SetDBField('Cost', $cost); $object->SetOriginalField('Cost', $cost); } /** * Allows to add products to package besides all that parent method does * * @param kEvent $event */ function OnProcessSelected($event) { $dst_field = $this->Application->RecallVar('dst_field'); if ($dst_field == 'PackageContent') { $this->OnAddToPackage($event); } elseif ($dst_field == 'AssignedCoupon') { $coupon_id = $this->Application->GetVar('selected_ids'); $object = $event->getObject(); $object->SetDBField('AssignedCoupon', $coupon_id); $this->RemoveRequiredFields($object); $object->Update(); } else { parent::OnProcessSelected($event); } $this->finalizePopup($event); } /** * Called when some products are selected in products selector for this prefix * * @param kEvent $event */ function OnAddToPackage($event) { $selected_ids = $this->Application->GetVar('selected_ids'); // update current package content with selected products /** @var ProductsItem $object */ $object = $event->getObject(); $product_ids = $selected_ids['p'] ? explode(',', $selected_ids['p']) : Array(); if ($product_ids) { $current_ids = $object->GetPackageContentIds(); $current_ids = array_unique(array_merge($current_ids, $product_ids)); // remove package product from selected list $this_product = array_search($object->GetID(), $current_ids); if ($this_product !== false) { unset($current_ids[$this_product]); } $dst_field = $this->Application->RecallVar('dst_field'); $object->SetDBField($dst_field, '|'.implode('|', $current_ids).'|'); $object->Update(); $this->ProcessPackageItems($event); } $this->finalizePopup($event); } function ProcessPackageItems(kEvent $event) { //$this->Application->SetVar('p_mode', 't'); /** @var ProductsItem $object */ $object = $event->getObject(); $content_ids = $object->GetPackageContentIds(); if (sizeof($content_ids) > 0) { $total_weight = $this->Conn->GetOne('SELECT SUM(Weight) FROM '.TABLE_PREFIX.'Products WHERE ProductId IN ('.implode(', ', $content_ids).') AND Type=1'); if (!$total_weight) $total_weight = 0; $this->Conn->Query('UPDATE '.$object->TableName.' SET Weight='.$total_weight.' WHERE ProductId='.$object->GetID()); } /* $this->Application->SetVar('p_mode', false); $list = $this->Application->recallObject('p.content', 'p_List', array('types'=>'content')); $this->Application->SetVar('p_mode', 't'); $list->Query(); $total_weight_a = 0; $total_weight_b = 0; $list->GoFirst(); while (!$list->EOL()) { if ($list->GetDBField('Type')==1){ $total_weight_a += $list->GetField('Weight_a'); $total_weight_b += $list->GetField('Weight_b'); } $list->GoNext(); } $object->SetField('Weight_a', $total_weight_a); $object->SetField('Weight_b', $total_weight_b); */ //$object->Update(); } /** * Enter description here... * * @param kEvent $event */ function OnSaveItems($event) { //$event->CallSubEvent('OnUpdate'); $event->redirect = false; //$event->setRedirectParams(Array ('opener' => 's', 'pass' => 'all,p')); } /** * Removes product from package * * @param kEvent $event */ function OnRemovePackageItem($event) { $this->Application->SetVar('p_mode', 't'); $object = $event->getObject(); $items_info = $this->Application->GetVar('p_content'); if($items_info) { $product_ids = array_keys($items_info); $current_ids = $object->GetPackageContentIds(); $current_ids_flip = array_flip($current_ids); foreach($product_ids as $key=>$val){ unset($current_ids_flip[$val]); } $current_ids = array_keys($current_ids_flip); $current_ids_str = '|'.implode('|', array_unique($current_ids)).'|'; $object->SetDBField('PackageContent', $current_ids_str); } $object->Update(); $this->ProcessPackageItems($event); } /** * Occurs before deleting item, id of item being * deleted is stored as 'id' event param * * @param kEvent $event * @return void * @access protected */ protected function OnBeforeItemDelete(kEvent $event) { parent::OnBeforeItemDelete($event); /** @var kDBItem $object */ $object = $event->getObject(); $sql = 'SELECT COUNT(*) FROM ' . TABLE_PREFIX . 'Products WHERE PackageContent LIKE "%|' . $object->GetID() . '%"'; $product_includes_in = $this->Conn->GetOne($sql); if ( $product_includes_in > 0 ) { $event->status = kEvent::erFAIL; } } /** * Returns specific to each item type columns only * * @param kEvent $event * @return Array * @access protected */ public function getCustomExportColumns(kEvent $event) { $columns = parent::getCustomExportColumns($event); $new_columns = Array ( '__VIRTUAL__Price' => 'Price', '__VIRTUAL__Cost' => 'Cost', ); return array_merge($columns, $new_columns); } /** * Sets non standart virtual fields (e.g. to other tables) * * @param kEvent $event */ function setCustomExportColumns($event) { parent::setCustomExportColumns($event); /** @var kDBItem $object */ $object = $event->getObject(); $this->setPrimaryPrice($object->GetID(), (double)$object->GetDBField('Price'), Array ('Cost' => (double)$object->GetDBField('Cost'))); } function OnPreSaveAndOpenPopup($event) { /** @var kDBItem $object */ $object = $event->getObject(); $this->RemoveRequiredFields($object); $event->CallSubEvent('OnPreSave'); $event->redirect = $this->Application->GetVar('t'); // pass ID too, in case if product is created by OnPreSave call to ensure proper editing $event->SetRedirectParam('pass', 'all'); $event->SetRedirectParam($event->getPrefixSpecial(true) . '_id', $object->GetID()); } /** * Returns ID of current item to be edited * by checking ID passed in get/post as prefix_id * or by looking at first from selected ids, stored. * Returned id is also stored in Session in case * it was explicitly passed as get/post * * @param kEvent $event * @return int * @access public */ public function getPassedID(kEvent $event) { if ( $this->Application->isAdminUser ) { $event->setEventParam('raise_warnings', 0); } $passed = parent::getPassedID($event); if ( $passed ) { return $passed; } if ( $this->Application->isAdminUser ) { // we may get product id out of OrderItem, if it exists /** @var OrdersItem $ord_item */ $ord_item = $this->Application->recallObject('orditems', null, Array ('raise_warnings' => 0)); if ( $ord_item->GetDBField('ProductId') ) { $passed = $ord_item->GetDBField('ProductId'); } } return $passed; } /** * Occurs, when config was parsed, allows to change config data dynamically * * @param kEvent $event * @return void * @access protected */ protected function OnAfterConfigRead(kEvent $event) { parent::OnAfterConfigRead($event); if (!$this->Application->LoggedIn()) { return ; } - $user_id = $this->Application->RecallVar('user_id'); - - $sql = 'SELECT PrimaryGroupId - FROM ' . TABLE_PREFIX . 'Users - WHERE PortalUserId = ' . $user_id; - $primary_group_id = $this->Conn->GetOne($sql); + /** @var UserHelper $user_helper */ + $user_helper = $this->Application->recallObject('UserHelper'); + $primary_group_id = $user_helper->getPrimaryGroup($this->Application->RecallVar('user_id')); if (!$primary_group_id) { return; } $sub_select = ' SELECT pp.Price FROM ' . TABLE_PREFIX . 'ProductsPricing AS pp WHERE pp.ProductId = %1$s.ProductId AND GroupId = ' . $primary_group_id . ' ORDER BY MinQty LIMIT 0,1'; $calculated_fields = $this->Application->getUnitOption($event->Prefix, 'CalculatedFields'); $calculated_fields['']['Price'] = 'IFNULL((' . $sub_select . '), ' . $calculated_fields['']['Price'] . ')'; $this->Application->setUnitOption($event->Prefix, 'CalculatedFields', $calculated_fields); } /** * Starts product editing, remove any pending inventory actions * * @param kEvent $event * @return void * @access protected */ protected function OnEdit(kEvent $event) { $this->Application->RemoveVar('inventory_actions'); parent::OnEdit($event); } /** * Adds "Shop Cart" tab on paid listing type editing tab * * @param kEvent $event */ function OnModifyPaidListingConfig($event) { $edit_tab_presets = $this->Application->getUnitOption($event->MasterEvent->Prefix, 'EditTabPresets'); $edit_tab_presets['Default']['shopping_cart'] = Array ('title' => 'la_tab_ShopCartEntry', 't' => 'in-commerce/paid_listings/paid_listing_type_shopcart', 'priority' => 2); $this->Application->setUnitOption($event->MasterEvent->Prefix, 'EditTabPresets', $edit_tab_presets); } /** * [HOOK] Allows to add cloned subitem to given prefix * * @param kEvent $event * @return void * @access protected */ protected function OnCloneSubItem(kEvent $event) { parent::OnCloneSubItem($event); if ( $event->MasterEvent->Prefix == 'rev' ) { $clones = $this->Application->getUnitOption($event->MasterEvent->Prefix, 'Clones'); $subitem_prefix = $event->Prefix . '-' . $event->MasterEvent->Prefix; $clones[$subitem_prefix]['ConfigMapping'] = Array ( 'PerPage' => 'Comm_Perpage_Reviews', 'ReviewDelayInterval' => 'product_ReviewDelay_Value', 'ReviewDelayValue' => 'product_ReviewDelay_Interval', ); $this->Application->setUnitOption($event->MasterEvent->Prefix, 'Clones', $clones); } } /** * Adds product to comparison list * * @param kEvent $event * @return void * @access protected */ protected function OnAddToCompare(kEvent $event) { $products = $this->getCompareProducts(); $product_id = (int)$this->Application->GetVar($event->Prefix . '_id'); if ( $product_id ) { $max_products = $this->Application->ConfigValue('MaxCompareProducts'); if ( count($products) < $max_products ) { $products[] = $product_id; $this->Application->Session->SetCookie('compare_products', implode('|', array_unique($products))); $event->SetRedirectParam('result', 'added'); } else { $event->SetRedirectParam('result', 'error'); } } $event->SetRedirectParam('pass', 'm,p'); } /** * Adds product to comparison list * * @param kEvent $event * @return void * @access protected */ protected function OnRemoveFromCompare(kEvent $event) { $products = $this->getCompareProducts(); $product_id = (int)$this->Application->GetVar($event->Prefix . '_id'); if ( $product_id && in_array($product_id, $products) ) { $products = array_diff($products, Array ($product_id)); $this->Application->Session->SetCookie('compare_products', implode('|', array_unique($products))); $event->SetRedirectParam('result', 'removed'); } $event->SetRedirectParam('pass', 'm,p'); } /** * Cancels product compare * * @param kEvent $event * @return void * @access protected */ protected function OnCancelCompare(kEvent $event) { $this->Application->Session->SetCookie('compare_products', '', -1); $event->SetRedirectParam('result', 'all_removed'); } /** * Returns products, that needs to be compared with each other * * @return Array * @access protected */ protected function getCompareProducts() { $products = $this->Application->GetVarDirect('compare_products', 'Cookie'); $products = $products ? explode('|', $products) : Array (); return $products; } } Index: branches/5.2.x/units/products/products_item.php =================================================================== --- branches/5.2.x/units/products/products_item.php (revision 16774) +++ branches/5.2.x/units/products/products_item.php (revision 16775) @@ -1,85 +1,87 @@ GetDBField('PackageContent'), '|'); if ($ids_string) { $ids_array = explode('|', $ids_string); return $ids_array; } else { return array(); } } /** * Returns field values from primary pricing for product * * @return array */ function getPrimaryPricing() { // product + pricing based $cache_key = 'product_primary_pricing[%PIDSerial:' . $this->GetID() . '%][%PrIDSerial:ProductId:' . $this->GetID() . '%]'; if (!$this->Application->isAdmin && $this->Application->LoggedIn()) { // also group based - $primary_group = (int)$this->Application->Session->GetField('GroupId'); + /** @var UserHelper $user_helper */ + $user_helper = $this->Application->recallObject('UserHelper'); + $primary_group = $user_helper->getPrimaryGroup($this->Application->RecallVar('user_id')); $cache_key .= ':group=' . $primary_group; } // don't cache, while in temp table $price_info = $this->IsTempTable() ? false : $this->Application->getCache($cache_key); if ($price_info === false) { if (!$this->Application->isAdmin && $this->Application->LoggedIn()) { // logged in user on front-end $this->Conn->nextQueryCachable = true; $sql = 'SELECT Price, Cost FROM ' . TABLE_PREFIX . 'ProductsPricing WHERE (ProductId = ' . $this->GetID() . ') AND (GroupId = ' . $primary_group . ') ORDER BY MinQty'; $price_info = $this->Conn->GetRow($sql); if ($price_info !== false) { $this->Application->setCache($cache_key, $price_info); return $price_info; } } // not logged-in user on front-end or in administrative console $pr_table = $this->Application->getUnitOption('pr', 'TableName'); if ($this->IsTempTable()) { $pr_table = $this->Application->GetTempName($pr_table, 'prefix:' . $this->Prefix); } $this->Conn->nextQueryCachable = true; $sql = 'SELECT Price, Cost FROM ' . $pr_table . ' WHERE (' . $this->IDField . ' = ' . $this->GetID() . ') AND (IsPrimary = 1)'; $price_info = $this->Conn->GetRow($sql); $this->Application->setCache($cache_key, $price_info); } return $price_info; } - } \ No newline at end of file + }