News:

Looking for documentation? Take a look on our wiki

Main Menu

Logging stock changes due to unforeseen stock changes

Started by Kuubs, May 23, 2024, 15:39:04 PM

Previous topic - Next topic

Kuubs

I have a very strange and weird bug. The stock gets automatically reverted back to it's old value. My client tells me it isn't that they do it by themselves, it gets done by the system. Any idea how this happens?

I checked all the plugins and components I have and none of them have anything to do with the stock. I'm very confused why this happens.

I think the best solution here is to add logging to the stock changes. I added in 2 places logging. Maybe a checkbox needs to be added so the logging only gets done when the user actually wants it, but for now I just hardcode it.

@Milbo

      public function updateStockInDB ($product, $amount, $signInStock, $signOrderedStock) {
           

        // Get the currently logged-in user
        $user = Factory::getUser();
        $userId = $user->id;
        $username = $user->username;
   
        // Log the start of the stock update process
        JLog::addLogger(
            array(
                'text_file' => 'com_virtuemart.log.php', // Name of the log file
                'text_entry_format' => '{DATETIME} {PRIORITY} {CATEGORY} {MESSAGE}' // Log entry format
            ),
            JLog::ALL, // Log all levels of messages
            array('com_virtuemart') // Categories to log
        );
   
        JLog::add(
            sprintf(
                'User %s (ID: %d) is attempting to update stock for product ID %d with amount %f, signInStock %s, signOrderedStock %s',
                $username, $userId, $product->virtuemart_product_id, $amount, $signInStock, $signOrderedStock
            ),
            Log::INFO,
            'com_virtuemart'
        );

        vmdebug('updateStockInDB start ', $signInStock, $signOrderedStock);
        $validFields = array('=', '+', '-');
        if (!in_array ($signInStock, $validFields)) {
            return FALSE;
        }
        if (!in_array ($signOrderedStock, $validFields)) {
            return FALSE;
        }

        $lproduct = $this->getProductSingle($product->virtuemart_product_id);
        if($lproduct->shared_stock){
            $productId = $lproduct->product_parent_id;
        } else {
            $productId = $product->virtuemart_product_id;
        }

        $amount = (float)$amount;
        $update = array();

        if ($signInStock != '=' or $signOrderedStock != '=') {

            if ($signInStock != '=') {
                $update[] = '`product_in_stock` = `product_in_stock` ' . $signInStock . $amount;

                if (strpos ($signInStock, '+') !== FALSE) {
                    $signInStock = '-';
                }
                else {
                    $signInStock = '+';
                }
                $update[] = '`product_sales` = `product_sales` ' . $signInStock . $amount;

            }
            if ($signOrderedStock != '=') {
                $update[] = '`product_ordered` = `product_ordered` ' . $signOrderedStock . $amount;
            }
            $q = 'UPDATE `#__virtuemart_products` SET ' . implode (", ", $update) . ' WHERE `virtuemart_product_id` = ' . (int)$productId;

            $db = JFactory::getDbo();
            $db->setQuery ($q);
            $db->execute ();
            //vmdebug('updateStockInDB executed query ', $q);
            //The low on stock notification comes now, when the people ordered.
            //You need to know that the stock is going low before you actually sent the wares, because then you ususally know it already yourself
            //note by Max Milbers
            if ($signInStock == '+' or $signOrderedStock == '+') {

                $q = 'SELECT (IFNULL(`product_in_stock`,"0")-IFNULL(`product_ordered`,"0")) < IFNULL(`low_stock_notification`,"0") '
                . 'FROM `#__virtuemart_products` '
                . 'WHERE `virtuemart_product_id` = ' . (int)$productId;
                $db->setQuery ( $q );
                //vmdebug('Check for low stock ',$q);
                if ($db->loadResult () == 1) {
                    vmdebug('Check for low stock said therre is a low stock ');
                    $this->lowStockWarningEmail( $productId) ;
                }
            }
        }

    }


And then I also added the logging to the store when a user changes the stock manually:

public function store (&$data) {

        vRequest::vmCheckToken();

        if(!vmAccess::manager('product.edit')){
            vmError('You are not a vendor or administrator, storing of product cancelled');
            return FALSE;
        }

        if ($data and is_object($data)) {
            $data = get_object_vars($data);
        }

        $isChild = FALSE;
        if(!empty($data['isChild'])) $isChild = $data['isChild'];

        if (isset($data['intnotes'])) {
            $data['intnotes'] = trim ($data['intnotes']);
        }

        // Setup some place holders
        $product_data = $this->getTable ('products');

        $data['new'] = '1';
        if(!empty($data['virtuemart_product_id'])){
            $product_data -> load($data['virtuemart_product_id']);
            $data['new'] = '0';
        }
        if( (empty($data['virtuemart_product_id']) or empty($product_data->virtuemart_product_id)) and !vmAccess::manager('product.create')){
            vmWarn('Insufficient permission to create product');
            return false;
        }

        $vendorId = vmAccess::isSuperVendor();
        $vM = VmModel::getModel('vendor');
        $ven = $vM->getVendor($vendorId);

        if(VmConfig::get('multix','none')!='none' and !vmAccess::manager('core')){

            if($ven->max_products!=-1){
                $this->setGetCount (true);
                //$this->setDebugSql(true);
                parent::exeSortSearchListQuery(2,'virtuemart_product_id',' FROM #__virtuemart_products',' WHERE ( `virtuemart_vendor_id` = "'.$vendorId.'" AND `published`="1") ');
                $this->setGetCount (false);
                if($ven->max_products<($this->_total+1)){
                    vmWarn('You are not allowed to create more than '.$ven->max_products.' products');
                    return false;
                }
            }
        }

        if(!vmAccess::manager('product.edit.state')){
            if( (empty($data['virtuemart_product_id']) or empty($product_data->virtuemart_product_id))){
                $data['published'] = 0;
            } else {
                $data['published'] = $product_data->published;
            }
        }

        //Set the decimals like product packaging
        foreach(self::$decimals as $decimal){
            if (array_key_exists ($decimal, $data)) {
                if(!empty($data[$decimal])){
                    $data[$decimal] = str_replace(',','.',$data[$decimal]);
                    //vmdebug('Store product '.$data['virtuemart_product_id'].', set $decimal '.$decimal.' = '.$data[$decimal]);
                } else {
                    $data[$decimal] = null;
                    $product_data->{$decimal} = null;
                    //vmdebug('Store product '.$data['virtuemart_product_id'].', set $decimal '.$decimal.' = null');
                }
            }
        }

        if($ven->force_product_pattern>0 and empty($data['product_parent_id']) and $ven->force_product_pattern!=$data['virtuemart_product_id']){
            $data['product_parent_id'] = $ven->force_product_pattern;
        }

        //We prevent with this line, that someone is storing a product as its own parent
        if(!empty($data['product_parent_id']) and !empty($data['virtuemart_product_id']) and $data['product_parent_id'] == $data['virtuemart_product_id']){
            $data['product_parent_id'] = 0;
        }

        $product_data->has_prices = (isset($data['mprices']['product_price']) and count($data['mprices']['product_price']) > 0)? 1:0;

        if (!$isChild) {
            $product_data->has_shoppergroups = empty($data['virtuemart_shoppergroup_id'])? 0:1;
            $product_data->has_manufacturers = empty($data['virtuemart_manufacturer_id'])? 0:1;
            //$product_data->has_medias = !empty($data['virtuemart_media_id']) or !empty($data['media']['virtuemart_media_id'])? 1:0;
            $product_data->has_categories = empty($data['categories'])? 0:1;
            if(!empty($data['virtuemart_media_id']) or !empty($data['media']['virtuemart_media_id']) or !empty($data['media']['media_action'])){
                $product_data->has_medias = 1;
            } else {
                $product_data->has_medias = 0;
            }
        }


        vDispatcher::importVMPlugins('vmcustom');
        vDispatcher::trigger('plgVmBeforeStoreProduct',array(&$data, &$product_data));

        $stored = $product_data->bindChecknStore($data, false);

       

        if(!$stored ){
            vmError('You are not an administrator or the correct vendor, storing of product cancelled');
            vmdebug('You are not an administrator or the correct vendor, storing of product cancelled', $data, $product_data->loadFieldValues());
            return FALSE;
        }

        $this->_id = $data['virtuemart_product_id'] = (int)$product_data->virtuemart_product_id;

        if (empty($this->_id)) {
            vmError('Product not stored, no id');
            return FALSE;
        }

        //We may need to change this, the reason it is not in the other list of commands for parents
        if (!$isChild) {
            $modelCustomfields = VmModel::getModel ('Customfields');
            $modelCustomfields->storeProductCustomfields ('product', $data, $product_data->virtuemart_product_id);
        }

        // Get old IDS
        $old_price_ids = $this->loadProductPrices($this->_id,array(0),false);

        if (isset($data['mprices']['product_price']) and count($data['mprices']['product_price']) > 0){

            foreach($data['mprices']['product_price'] as $k => $product_price){

                $pricesToStore = array();
                $pricesToStore['virtuemart_product_id'] = $this->_id;
                $pricesToStore['virtuemart_product_price_id'] = (int)$data['mprices']['virtuemart_product_price_id'][$k];

                if (!$isChild){
                    //$pricesToStore['basePrice'] = $data['mprices']['basePrice'][$k];
                    $pricesToStore['product_override_price'] = $data['mprices']['product_override_price'][$k];
                    $pricesToStore['override'] = isset($data['mprices']['override'][$k])?(int)$data['mprices']['override'][$k]:0;
                    $pricesToStore['virtuemart_shoppergroup_id'] = (int)$data['mprices']['virtuemart_shoppergroup_id'][$k];
                    $pricesToStore['product_tax_id'] = !empty($data['mprices']['product_tax_id'][$k])? (int)$data['mprices']['product_tax_id'][$k]:0;
                    $pricesToStore['product_discount_id'] = !empty($data['mprices']['product_discount_id'][$k])? (int)$data['mprices']['product_discount_id'][$k]:0;
                    $pricesToStore['product_currency'] = !empty($data['mprices']['product_currency'][$k])? (int)$data['mprices']['product_currency'][$k] : $ven->vendor_currency;
                    $pricesToStore['product_price_publish_up'] = !empty($data['mprices']['product_price_publish_up'][$k])? $data['mprices']['product_price_publish_up'][$k]:0;
                    $pricesToStore['product_price_publish_down'] = !empty($data['mprices']['product_price_publish_down'][$k])? $data['mprices']['product_price_publish_down'][$k]:0;
                    $pricesToStore['price_quantity_start'] = !empty($data['mprices']['price_quantity_start'][$k])? (int)$data['mprices']['price_quantity_start'][$k]:0;
                    $pricesToStore['price_quantity_end'] = !empty($data['mprices']['price_quantity_end'][$k])? (int)$data['mprices']['price_quantity_end'][$k]:0;
                }

                if (!$isChild and isset($data['mprices']['use_desired_price'][$k]) and $data['mprices']['use_desired_price'][$k] == "1") {

                    $calculator = calculationHelper::getInstance ();
                    if(isset($data['mprices']['salesPrice'][$k])){
                        $data['mprices']['salesPrice'][$k] = str_replace(array(',',' '),array('.',''),$data['mprices']['salesPrice'][$k]);
                    }
                    $pricesToStore['salesPrice'] = $data['mprices']['salesPrice'][$k];
                    $pricesToStore['product_price'] = $data['mprices']['product_price'][$k] = $calculator->calculateCostprice ($this->_id, $pricesToStore);
                    unset($data['mprices']['use_desired_price'][$k]);
                } else {
                    if(isset($data['mprices']['product_price'][$k]) ){
                        $pricesToStore['product_price'] = $data['mprices']['product_price'][$k];
                    }

                }

                if ($isChild) $childPrices = $this->loadProductPrices($this->_id,array(0),false);

                if ((isset($pricesToStore['product_price']) and $pricesToStore['product_price']!='' and $pricesToStore['product_price']!=='0') || (isset($childPrices) and is_array($childPrices) and count($childPrices)>1)) {

                    if ($isChild) {

                        if(is_array($old_price_ids) and count($old_price_ids)>1){

                            //We do not touch multiple child prices. Because in the parent list, we see no price, the gui is
                            //missing to reflect the information properly.
                            $pricesToStore = false;
                            $old_price_ids = array();
                        } else {
                            unset($data['mprices']['product_override_price'][$k]);
                            unset($pricesToStore['product_override_price']);
                            unset($data['mprices']['override'][$k]);
                            unset($pricesToStore['override']);
                        }

                    }

                    if($pricesToStore){
                        $toUnset = array();
                        if (!empty($old_price_ids) and count($old_price_ids) ) {
                            foreach($old_price_ids as $key => $oldprice){
                                if($pricesToStore['virtuemart_product_price_id'] == $oldprice['virtuemart_product_price_id'] ){
                                    $pricesToStore = array_merge($oldprice,$pricesToStore);
                                    $toUnset[] = $key;
                                }
                            }
                        }
                        $this->updateXrefAndChildTables ($pricesToStore, 'product_prices',$isChild);

                        foreach($toUnset as $key){
                            unset( $old_price_ids[ $key ] );
                        }
                    }
                }
            }
        }

        if (!empty($old_price_ids) and count($old_price_ids) ) {
            $oldPriceIdsSql = array();
            foreach($old_price_ids as $oldPride){
                $oldPriceIdsSql[] = $oldPride['virtuemart_product_price_id'];
            }
            $db = JFactory::getDbo();
            // delete old unused Prices
            $db->setQuery( 'DELETE FROM `#__virtuemart_product_prices` WHERE `virtuemart_product_price_id` in ("'.implode('","', $oldPriceIdsSql ).'") ');
            $err = '';
            try {
                $db->execute();
            } catch(Exception $e) {
                $err = $e->getMessage();
            }
           
            if(!empty($err)){
                vmWarn('In store prodcut, deleting old price error',$err);
            }
        }

        if (!empty($data['childs'])) {
            foreach ($data['childs'] as $productId => $child) {
                if(empty($productId)) continue;
                if($productId!=$data['virtuemart_product_id']){

                    if(empty($child['product_parent_id'])) $child['product_parent_id'] = $data['virtuemart_product_id'];
                    $child['virtuemart_product_id'] = $productId;

                    if(!empty($child['product_parent_id']) and $child['product_parent_id'] == $child['virtuemart_product_id']){
                        $child['product_parent_id'] = 0;
                    }

                    $child['isChild'] = $this->_id;
                    $this->store ($child);
                }
            }
        }

        if (!$isChild) {

            $data = $this->updateXrefAndChildTables ($data, 'product_shoppergroups');

            $data = $this->updateXrefAndChildTables ($data, 'product_manufacturers');

            $storeCats = false;
            if (empty($data['categories']) or (!empty($data['categories'][0]) and $data['categories'][0]!="-2")){
                $storeCats = true;
            }

            if($storeCats){
                if (!empty($data['categories']) && count ($data['categories']) > 0) {
                    if(VmConfig::get('multix','none')!='none' and !vmAccess::manager('managevendors')){

                        if($ven->max_cats_per_product>=0){
                            while($ven->max_cats_per_product<count($data['categories'])){
                                array_pop($data['categories']);
                            }
                        }

                    }
                    $data['virtuemart_category_id'] = $data['categories'];
                } else {
                    $data['virtuemart_category_id'] = array();
                }
                $data = $this->updateXrefAndChildTables ($data, 'product_categories');
            }

            // Update waiting list
            if (!empty($data['notify_users'])) {
                if ($data['product_in_stock'] > 0 && $data['notify_users'] == '1') {
                    $waitinglist = VmModel::getModel ('Waitinglist');
                    $waitinglist->notifyList ($data['virtuemart_product_id']);
                }
            }

            // Process the images
            $mediaModel = VmModel::getModel ('Media');
            $mediaModel->storeMedia ($data, 'product');

        }

        $oldStock = $product_data->product_in_stock;

        // Attempt to store product data
       

        // Get the currently logged-in user
        $user = Factory::getUser();
        $userId = $user->id;
        $username = $user->username;

        // Initialize the logger
        Log::addLogger(
            array(
                'text_file' => 'com_virtuemart.log.php', // Name of the log file
                'text_entry_format' => '{DATETIME} {PRIORITY} {CATEGORY} {MESSAGE}' // Log entry format
            ),
            Log::ALL, // Log all levels of messages
            array('com_virtuemart') // Categories to log
        );

        // Check if the stock has changed and log the changes
        if ($oldStock != $data['product_in_stock']) {
            Log::add(
                sprintf(
                    'User %s (ID: %d) changed stock for product ID %d from %d to %d',
                    $username, $userId, $product_data->virtuemart_product_id, $oldStock, $data['product_in_stock']
                ),
                Log::INFO,
                'com_virtuemart'
            );
        }

        $cache = VmConfig::getCache('com_virtuemart_orderby_manus','callback');
        $cache->clean();

        vDispatcher::trigger('plgVmAfterStoreProduct',array(&$data, &$product_data));

        return $product_data->virtuemart_product_id;
    }

Milbo

If you store a parent, which has the extra tab for children, than it takes this values for the children.

that works most time correctly, but if someone buys something meanwhile, it wont work 100%.

But that is a common problem for our stock feature.
Should I fix your bug, please support the VirtueMart project and become a member
______________________________________
Extensions approved by the core team: http://extensions.virtuemart.net/

Kuubs

Quote from: Milbo on May 23, 2024, 21:35:45 PMIf you store a parent, which has the extra tab for children, than it takes this values for the children.

that works most time correctly, but if someone buys something meanwhile, it wont work 100%.

But that is a common problem for our stock feature.

Thanks Max, for now I added the logging. But there is one thing that bothers me in this logging that is that when you log it it will show that the child products get set to 0 and then after it somewhere else sets the child products back to it's current stock values again. But I'd like to log that as well, do you know where I can find it?