Scenario:
In order for a flexible stock all stocks transaction should be handled by rules.
In order to minimize code changes we could implement the following hooks in uc_stock.module:
hook_uc_order_product_delete($order_product_id) that will invoke an rule event like uc_stock_product_delete
The function could look like this:

/**
 * Implements hook_uc_order_product_delete().
 */
function uc_stock_uc_order_product_delete($order_product_id) {
  // Put back the stock.
  $product = db_select('uc_order_products', 'p')
    ->conditions('order_product_id', $order_product_id)
    ->execute()
    ->fetchObject();
  rules_invoke_event('uc_stock_product_delete', $product);
}

A hook should be created on product add function uc_order_edit_products_add($form, &$form_state) and the part:

  ...
  // Decrement stock.
  if (module_exists('uc_stock')) {
    uc_stock_adjust_product_stock($product, 0, $order);
  }
  ...

should be replaced by an hook or rules_invoke_event but preferably by a module_invoke and the rules_invoke_event should be called from uc_stock_hook... In order to work properly the hook should be called with 2 args ($original_product, $modiffied_product). This way we could handle all stock operations using function uc_stock_adjust.

This way users could make rules for stocks based on all kind of condition, for example:
If an order has an "Incomplete" custom status because we found out that the product is out of stock in real warehouse or the product is deteriorated and cannot be shipped the the stock should not increment and notify other users that the product is back in stock...
But if an order has an "New" custom status and the client called to remove that product from order, product should go back in stock.

With those three hooks and with two rules_invoke_events we could handle all stock operation via rules.

The changes of other modules code is minimal. Almost all changes are made in uc_stock module and a hook is created in uc_order module.

If maintainers of uc like the ideea I can post the patches...

I'm operating a real shop and this is one of the important functions of the store especially if you display information about stocks in real time...

I'm also waiting for any other suggestions.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

longwave’s picture

Yes, I think this is a good idea. Patches welcome :)

Note you might want to use rules_invoke_all() which triggers module_invoke_all() and rules_invoke_event() for you.

SilviuChingaru’s picture

Yes, uc_order has rules as dependencies and we could use rules_invoke_all() and this way uc_order will implement the hooks and events and stock module will implement actions and default rules using those events witch is better approach I think...

I'll start working on patches latter today...

longwave’s picture

Ideally I would like tests for this as well if possible, if only to prove that stock control works the same before and after the patch (but is obviously more flexible afterwards)

SilviuChingaru’s picture

Unfortunately I didn't use tests since now but I'll try to read some documentation...

longwave’s picture

Looking at the existing tests within Ubercart should give you some pointers. The basic test we need is to set up a product with stock, add it to the cart, checkout, and ensure the stock level is decreased correctly. The test should run green both before and after your patch.

SilviuChingaru’s picture

uc_order module changelog:

In uc_order.admin.inc:

function uc_order_edit_form_submit($form, &$form_state) {
  ...
  $order->products = array();
  if (isset($form_state['values']['products']) && is_array($form_state['values']['products'])) {
    foreach ($form_state['values']['products'] as $product) {
      $product['data'] = unserialize($product['data']);
      $product = (object)$product;
      $product->order_id = $order->order_id;
      if (!isset($product['remove']) && intval($product['qty']) > 0) {
        $order->products[] = $product;
        $temp = $product->qty;
        $product->qty = $product->qty - $qtys[$product->order_product_id];
        rules_invoke_all('uc_order_product_update', $product);
        $product->qty = $temp;
        }
      }
      else {
        $product->qty = -$product->qty;
        rules_invoke_all('uc_order_product_update', $product);
        $log['remove_' . $product['nid']] = $product['title'] . ' removed from order.';
      }
    }
  }
  ...
}

in uc_order.rules.inc:

/**
 * Implements hook_rules_event_info().
 */
function uc_order_rules_event_info() {
  ...
  
  $events['uc_order_product_update'] = array(
    'label' => t('An order product is being updated'),
    'group' => t('Order'),
    'variables' => array(
      'product' => array(
        'type' => 'uc_order_product',
        'label' => t('Product'),
      ),
    ),
  );

  return $events;
}

Maybe the best approach is to pass 2 args to event/hook uc_order_product_update $product and $updated_product and do the logic increase/decrease in hook like the one of stock?! I'm waiting for suggestions...

longwave’s picture

Please post .patch files if you can, it's much easier to read and understand your changes that way.

SilviuChingaru’s picture

Changes for uc_order module...

SilviuChingaru’s picture

Status: Active » Needs review
SilviuChingaru’s picture

I think we should pass to this new events also the $order object as 3rd param in order to let user manipulate stock action based on order status/state, total, etc....

SilviuChingaru’s picture

Patch with $order object as event param.

TR’s picture

Status: Needs review » Needs work

Whoa, hold on there. I'm totally opposed to this approach unless you can demonstrate that it's able to properly deal with concurrency. And I doubt that a Rules-based system can - far too much code would have to be wrapped in transactions, including access to the Rules tables and including payments, making deadlocks extremely likely. And we would have no way of enforcing how stock operations are performed outside Ubercart core, again making deadlocks or wrong behavior likely. If you can't show that this approach will yield the correct results for multiple customers simultaneously trying to purchase the same unique item, then the only thing it could achieve is to swap out one broken system for another that's more broken.

SilviuChingaru’s picture

I'm totally opposed to this approach unless you can demonstrate that it's able to properly deal with concurrency.

What you mean by this? The stock operation I think should be handled by Rules because user will have flexibility on how the system react based on any conditions...
I don't see where the problem is with concurency??? This approach, at least by now is only for orders edited by administrators and the checkout remain the same old way?!

Practical example:
For instance an operator could have some products that are not in stock anymore and when he finds out, basically when he tries to fulfill the order, he puts the order in a custom status like "Incomplete". Then he call the customer and tell him that the product is not available anymore, the customer wants other products from his order so the operator delete the out of stock product and change the order status to "Ready for shipping". The out of stock product is displayed on site that is out of stock untill operator delete it from order, then product is back in stock but in fact it is not. Should operator manually edit the stock of product?! I think not. What if you have an module that notifies customer by email of back in stock products on the out of stock product? The client will receive notification that the product has been restocked but in fact it is not...

TR’s picture

I don't see where the problem is with concurency

That's exactly my point - it seems like you're jumping into this without awareness of the need for concurrent programming when dealing with stock, and perhaps you're not familiar with the concept of concurrency in computer science either. There's a four year history of issues on ubercart.org and here which talk about all the problems that arise because concurrency has not been properly taken into account by the current uc_stock. Replacing or augmenting uc_stock with Rules like you suggest will make it much more likely that sites encounter these problems. Stock operations need to be transactional and journaled in order to meet even the most basic accounting standards. So yes, in your example if there's a discrepancy between your physical stock count with your electronic stock count there needs to be the equivalent of a ledger entry reconciling the two, because this reconciliation has tax implications among other things.

Fixing stock handling is a big job that requires architectural changes to Ubercart, and I don't see that it makes any sense whatsoever to spend effort heading down a path like the above without some evidence that it's going the right direction. It's easy to do more harm than good here; I'm one of the people who will have to support this and I don't want more problems dumped on me.

SilviuChingaru’s picture

This is the final unpolished approach to prove the concepts... It's working on every product add/edit/delete from admin. Take a look and tell me what you think?

SilviuChingaru’s picture

The concurrency problem should be fixed at rules_invoke_all() call level so the uc_order will have to deal with when the event is called or not or at uc_stock_adjust() level for better results so I don't see why rules can't be used for conditions?!?!

SilviuChingaru’s picture

Status: Needs work » Needs review
SilviuChingaru’s picture

Using rules we can solve the following issues:
#1034044: Add checkboxes to enable/disable stock level changes when editing orders (the admin can put the order in a status like "Returned Stockable" and "Returned Nonstockable" for example and select if stock adjust action is applied to order by order status, order date, order role, order shipping method, peyment method and so on. More flexible than checkbox where the user input is required)
#588746: After deleting/cancelling an order, stock is not incremented again (The user can put uc_stock_action_adjust on Order delete event)
#751994: Possible to decrement stock on add to cart instead of checkout? (Stock level can decrement on add item to cart event and increment when order status is "Abandoned")
#1234446: Publish a rules event when stock level changes for a product (Like longwave said in #3)

SilviuChingaru’s picture

That's exactly my point - it seems like you're jumping into this without awareness of the need for concurrent programming when dealing with stock, and perhaps you're not familiar with the concept of concurrency in computer science either. There's a four year history of issues on ubercart.org and here which talk about all the problems that arise because concurrency has not been properly taken into account by the current uc_stock. Replacing or augmenting uc_stock with Rules like you suggest will make it much more likely that sites encounter these problems. Stock operations need to be transactional and journaled in order to meet even the most basic accounting standards. So yes, in your example if there's a discrepancy between your physical stock count with your electronic stock count there needs to be the equivalent of a ledger entry reconciling the two, because this reconciliation has tax implications among other things.

Fist of all Ubercart is not an accountancy software.
I have the following scenario at the store I manage:
I have one supplier (but this could be extended to unlimited level of suppliers) and our stock (wich is composed of products that client don't want anymore + returned products + products that we stock for faster shipping).
The supplier sends us his stock once per day in xls format. When we receive the supplier's stock, we upload it to the site as document custom node type with taxonomy term "Supplier stock" wich is child of "Stocks" term. On form submit, if tid has parrent "Stocks", a batch runs that on first operation read the stock file and save it to db in a table with the following format:
[TID][SKU] [PRODUCT NAME] [QTY] [PRICE - supplier list price]
A second operation sum the stock level using group by SKU and sum(QTY) and we get the real stock of the shop. If qty in uc_stock != summed qty then the uc_stock is set to sum(QTY) so the site display our stocks and our supplier(s) stock cumulative.
Our stock is exported from accountancy softer in xls format and uploaded to the site in same manner. The same batch is ran and the stock is updated on every stock document update.

Because of these scenario and because we don't have access to supplier's live stock there might be some cases when a product of the supplier's stock is out of stock without letting us know about this.

Some of our clients buy one of these products and other products that can be delivered. So we order to supplier all the products in orders in custom status, like "Confirmed", without knowing that one of those products are out of stock. When we receive the goods we find out that we didn't recieve one or more products that were ordered so we cannot fulfill orders with those products, so we put those orders in a custom state like "Incomplete" and we'll call customer latter to see if they want only products that we received without the ones out of stock.
If customer wants the order without the out of stock product(s) we manually remove them from orders, recalculate shipping and change the order status to "Processing" in order to be delivered. Obvious products from orders that are "Incomplete" should not be putted back in stock.

I don't see the imperative need of uc_stock to be journaled. Uc stock is a basic stock module, if users have hooks and the stock is not hardcoded they can make their own modules or their own set of rules. Almost any store has an accountancy software from which admins could export the real stock of the store and, using a custom module like ours, update the shop stock.

If you want to build an accountancy software the hole ubercart must be rewritten with accountancy standards in mind. Remember that accountancy laws in every country are different so no universal software could work worldwide.

By providing a rule based uc_stock you let user implement his own module that could accomplish accountancy standards or even using rules interface to calculate his stock, taxes or even journal his transactions, because uc provides the hooks and events for rules. Accountants could make their own rules.

Maybe a journaled stock module can be written as an optional module but not all users need it.

By the way my store is at: http://www.magazinulcuscule.ro

longwave’s picture

I agree with the above. There are longstanding issues with stock handling in Ubercart, mostly down to the node-SKU relationship, but this issue is not trying to solve that problem. To solve all stock problems in Ubercart I believe we need both a transaction log/journal and a decision on whether SKUs can be shared by different nodes or not (or possibly, support both cases), but that is for another issue such as #384996: Better stock support

Separately to that, we can add conditions, actions and events to uc_stock which will allow store owners to more flexibly control inventory levels.

longwave’s picture

Title: Stock transaction should be handled entirely by rules » Stock transaction should be more flexible through Rules

Better title, as we can't solve everything with Rules :)

DanZ’s picture

See the discussion at #1884512-20: Make ordered products entity fieldable.

Of note: hook_uc_order_product_delete() is invoked only from the admin order edit form, when an admin removes a product. This is a bad name which conflicts with what Entity API wants to call its own hook, and might be changed. The standard entity hook, on the other hand, is invoked every single time an order product entity is deleted.

SilviuChingaru’s picture

Status: Needs review » Needs work