Problem/Motivation

Are there any plans to implement an overview list to show the stock level for each product in every stock location? I could not find any view or backend page which shows something like this. The only way to check the stock level atm is on the product variation page, right? But there you can only see the summed up stock level, not the stock level for every single location.

Proposed resolution

I would prefer to have a big (filterable) overview list which shows the stock level for every product in every location, something like this:

Location A Location B Location C Total
Product A 0 5 9 14
Product B 10 15 3 28
Product C 1 2 3 6
Total 11 22 15 48

Comments

daveiano created an issue. See original summary.

olafkarsten’s picture

Category: Support request » Feature request

Sounds like a legitimate feature. Hope you understand, that this is not on top of the list. Before we are finetuning the ux, we have to ship a stable release. Anyway, thanks for your input and please keep posting. We really appreciate your feedback.

daveiano’s picture

Thank you for your fast response!

I fully understand that there are other priorities for the beta release. Nevertheless, I can manage to get some time for implementing this, and by looking at the database structure I don't think this would be too difficult to implement (we have all necessary information in the commerce_stock_location_level table).

Any feedback on how this should be implemented is also very appreciated! And also if this makes sense now with the possible upcoming changes in mind?

olafkarsten’s picture

Sounds great. We gladly accept patches, of course.

My Feedback:

At a first glance that looks like a easy task, but I doubt it is. This is a potentially expensive screen. We should implement some measures to prevent killing a website. Also, it doesn't seem to scale very well. You can easily have thousends of product variations. Even with 15 locations, that will end in a screen/report you can't work with, without any proper paging and filtering.

Is this really a report/screen we need? Or should we limit this for one product and its variations.

I would strictly decouple the parts where and how we get the data and how we present them. I guess the problem is, that we need variation specific data. In the stock related database records we only keep the purchasable entity id. So you need to fetch the SKU/Label or whatever you want to show from the variation.

Even if we refactor the datamodel, it should be possible to change the data fetching code without much hazzle.

Minor:
I don't see the why we need a total per row - how many product variations in that location.

daveiano’s picture

At a first glance that looks like a easy task, but I doubt it is. This is a potentially expensive screen. We should implement some measures to prevent killing a website. Also, it doesn't seem to scale very well. You can easily have thousends of product variations. Even with 15 locations, that will end in a screen/report you can't work with, without any proper paging and filtering.

Is this really a report/screen we need? Or should we limit this for one product and its variations.

This is also a valid point. Perhaps the first step should be to provide a list for the Stock level Field Widget, which shows the stock for each location, not only the summed up stock level?

I would strictly decouple the parts where and how we get the data and how we present them. I guess the problem is, that we need variation specific data. In the stock related database records we only keep the purchasable entity id. So you need to fetch the SKU/Label or whatever you want to show from the variation.

Even if we refactor the datamodel, it should be possible to change the data fetching code without much hazzle.

This whole data presenting layer should be implemented via views, shouldn't it? So we are talking about the views integration of the stock locations. After a quick search, I found some open tickets for views integration. So the first step should be to make progress with the views integration?

Minor:
I don't see the why we need a total per row - how many product variations in that location.

You are right, in most cases, the total per product should be important, not the total per location.

daveiano’s picture

So I got some time to look into these things: First thing I noticed, is that we should really implement these UX things with views to benefit from the flexibility provided by views.

First thing I did was to remove all the code from modules/local_storage/commerce_stock_local.views.inc and put it into modules/local_storage/src/Entity/StockLocationViewsData.php. The docs say:

To provide views data for an entity, instead of implementing this hook, create a class implementing \Drupal\views\EntityViewsDataInterface and reference this in the "views" annotation in the entity class. The return value of the getViewsData() method on the interface is the same as this hook, and base class in \Drupal\views\EntityViewsData will take care of adding the basic Views tables and fields for your entity. See the Entity API topic for more information about entities.

(https://api.drupal.org/api/drupal/core%21modules%21views%21views.api.php...)

I then removed some parts of the old hook_views_data definition because of the Implementation of EntityViewsData, Drupal automatically catches the entity fields definitions. Then I added and corrected some relationships (just like in https://www.drupal.org/project/commerce_stock/issues/3027543).

With these changes, the duplicates of "Stock location" and "Commerce Stock location" disappeared from the "Create new view" form. I also created a relationship to the commerce_stock_location_level table to access the stock level per location.

I created two views (configs are attached):

  • a "history" view which shows the transactions with qty, product, and location, which is filterable by product variation
  • a stock level per location view which is also filterable by product variation

I can imagine that we could add a configuration in the "stock_level" field widget to show these views in the form or display mode to give more information and flexibility?

The second question I got is: should we use the data from the commerce_stock_location_level table to provide the level or should we calculate these values based on the transactions? I did not have enough time to look into the logic, but I assume it should be transaction-based, and the commerce_stock_location_level table is just a cache table?

daveiano’s picture

daveiano’s picture

Issue summary: View changes
StatusFileSize
new33.43 KB

I extended the patch and changed the following in the StockLocationViewsData class:

From

$data['commerce_stock_location_level']['qty'] = [
      'title' => $this->t('Qty'),
      'label' => $this->t('Qty'),
      'help' => $this->t('The stock level quantity.'),
      'field' => [
        'id' => 'numeric',
      ],
      'filter' => [
        'id' => 'numeric',
      ],
      'argument' => [
        'id' => 'numeric',
      ],
      'sort' => [
        'id' => 'standard',
      ],
    ];

to:

$data['commerce_stock_location_level']['qty'] = [
      'title' => $this->t('Qty'),
      'label' => $this->t('Qty'),
      'help' => $this->t('The stock level quantity.'),
      'field' => [
        'id' => 'stock_location_level',
      ],
      'filter' => [
        'id' => 'numeric',
      ],
      'argument' => [
        'id' => 'numeric',
      ],
      'sort' => [
        'id' => 'standard',
      ],
    ];

I added a new stock_location_level field formatter in the field submodule, which shows the stock from a specific location (you can configure which location, I also updated the schema). The new formatter also calculates the stock level transaction-based, it does not simply get the data from the database.

This way it is now possible to create a view like shown in the issue description:

Location A Location B Location C Total
Product A 0 5 9 14
Product B 10 15 3 28
Product C 1 2 3 6
daveiano’s picture

Issue summary: View changes

Accidently changed issue description.

gge’s picture

Hello,

I tried #8 but unfortunately there's a problem with stock_location_level.
If I try to use it in views, only the "Simple stock level formatter" works. When Stock level formatter by location is chosen, the following error is logged:
AssertionError: Cannot load a NULL ID. în assert() (rândul 249 din /home/public_html/web/core/lib/Drupal/Core/Entity/EntityStorageBase.php) #0 /home/public_html/web/core/lib/Drupal/Core/Entity/EntityStorageBase.php(249): assert(false, 'Cannot load a N...') #1 /home/public_html/web/modules/contrib/commerce_stock/modules/field/src/Plugin/Field/FieldFormatter/StockLevelFormatterByLocation.php(96): Drupal\Core\Entity\EntityStorageBase->load(NULL) #2 /home/public_html/web/core/modules/views/src/Plugin/views/field/EntityField.php(519): Drupal\commerce_stock_field\Plugin\Field\FieldFormatter\StockLevelFormatterByLocation->settingsForm(Array, Object(Drupal\Core\Form\FormState)) #3 /home/public_html/web/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php(167): Drupal\views\Plugin\views\field\EntityField->buildOptionsForm(Array, Object(Drupal\Core\Form\FormState)) #4 [internal function]: Drupal\views_ui\Form\Ajax\ConfigHandler->buildForm(Array, Object(Drupal\Core\Form\FormState)) #5 /home/public_html/web/core/lib/Drupal/Core/Form/FormBuilder.php(519): call_user_func_array(Array, Array) #6 /home/public_html/web/core/lib/Drupal/Core/Form/FormBuilder.php(276): Drupal\Core\Form\FormBuilder->retrieveForm('views_ui_config...', Object(Drupal\Core\Form\FormState)) #7 /home/public_html/web/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php(214): Drupal\Core\Form\FormBuilder->buildForm('Drupal\\views_ui...', Object(Drupal\Core\Form\FormState)) #8 /home/public_html/web/core/lib/Drupal/Core/Render/Renderer.php(582): Drupal\views_ui\Form\Ajax\ViewsFormBase->Drupal\views_ui\Form\Ajax\{closure}() #9 /home/public_html/web/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php(216): Drupal\Core\Render\Renderer->executeInRenderContext(Object(Drupal\Core\Render\RenderContext), Object(Closure)) #10 /home/public_html/web/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php(125): Drupal\views_ui\Form\Ajax\ViewsFormBase->ajaxFormWrapper('Drupal\\views_ui...', Object(Drupal\Core\Form\FormState)) #11 /home/public_html/web/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php(39): Drupal\views_ui\Form\Ajax\ViewsFormBase->getForm(Object(Drupal\views_ui\ViewUI), 'page_1', 'ajax') #12 [internal function]: Drupal\views_ui\Form\Ajax\ConfigHandler->getForm(Object(Drupal\views_ui\ViewUI), 'page_1', 'ajax', 'field', 'field_stock_lev...') #13 /home/public_html/web/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(123): call_user_func_array(Array, Array) #14 /home/public_html/web/core/lib/Drupal/Core/Render/Renderer.php(582): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() #15 /home/public_html/web/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(124): Drupal\Core\Render\Renderer->executeInRenderContext(Object(Drupal\Core\Render\RenderContext), Object(Closure)) #16 /home/public_html/web/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(97): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array) #17 /home/public_html/vendor/symfony/http-kernel/HttpKernel.php(151): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() #18 /home/public_html/vendor/symfony/http-kernel/HttpKernel.php(68): Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object(Symfony\Component\HttpFoundation\Request), 1) #19 /home/public_html/web/core/lib/Drupal/Core/StackMiddleware/Session.php(57): Symfony\Component\HttpKernel\HttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #20 /home/public_html/web/core/lib/Drupal/Core/StackMiddleware/KernelPreHandle.php(47): Drupal\Core\StackMiddleware\Session->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #21 /home/public_html/vendor/asm89/stack-cors/src/Asm89/Stack/Cors.php(49): Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #22 /home/public_html/web/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php(47): Asm89\Stack\Cors->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #23 /home/public_html/web/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php(52): Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #24 /home/public_html/vendor/stack/builder/src/Stack/StackedHttpKernel.php(23): Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #25 /home/public_html/web/core/lib/Drupal/Core/DrupalKernel.php(693): Stack\StackedHttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #26 /home/public_html/web/index.php(19): Drupal\Core\DrupalKernel->handle(Object(Symfony\Component\HttpFoundation\Request)) #27 {main}.

Right now, this seems to be the only way to see the stock level per location but it doesn't work...

Thank you!

daveiano’s picture

StatusFileSize
new51.22 KB
new137.98 KB

Ok, you can try to add a view by doing the following:

  1. Add a new view which shows 'Product Variation'
  2. Make it a table view and add some basic fields like Product Title
  3. Add your stock locations by adding Stock level fields. Choose "Stock level formatter by location" for the formatter and choose your location. See Screenshot:

views stock level field config

This is my whole view:

stock level view config

Your error message looks like you forgot to configure your stock location in the field settings ("Location" autocomplete field in the first screenshot).

Let me know if this helps!

olafkarsten’s picture

Status: Active » Needs review
olafkarsten’s picture

Status: Needs review » Needs work

The patch is to big and tries to do to much. We should have split the issues. One for the views integration, on for field formatter stuff.

First thing I did was to remove all the code from modules/local_storage/commerce_stock_local.views.inc and put it into modules/local_storage/src/Entity/StockLocationViewsData.php

I think this is right for the stock location data. But I think we should keep the transaction data definition in the views.inc file. These are not entity related data. I'm not sure for the commerce_stock_location_level data. I'm okay with putting them in the StockLocationViewsData class i guess.

The second question I got is: should we use the data from the commerce_stock_location_level table to provide the level or should we calculate these values based on the transactions? I did not have enough time to look into the logic, but I assume it should be transaction-based, and the commerce_stock_location_level table is just a cache table?

We calculate location stock levels per queueworker, so this data could be total valid or outdated, it depends on how often cron runs and how fast you are selling stuff. It's the responsibility of the user to adjust his system. So we can and should use the commerce_stock_location_level.

We need tests for all this, too.

- So lets split up the issues first. Remove the stock level field stuff from here and put it in own issue.
- Address the stuff above.
- Add tests.

guy_schneerson’s picture

Haven't properly reviewed this yet but just wanted to second olafkarsten. Currently the only way to get the stock level is using the API. The commerce_stock_location_level should not be used directly.

lukasss’s picture

+1 for this

lukasss’s picture

I think this has long been in need of processing.

It is currently impossible to create a normal views with commerce_stock_location.

I see https://www.drupal.org/node/2433153

Maybe we need to remove the 'data_table' key from the annotation altogether?
Without it, I can create a view and all the entity fields are available to me.

What do you think?

daveiano’s picture

@olafkarsten @guy_schneerson Thanks for the feedback.

What I noticed in the first place: Arent these completely different arguments?

We calculate location stock levels per queueworker, so this data could be total valid or outdated, it depends on how often cron runs and how fast you are selling stuff. It's the responsibility of the user to adjust his system. So we can and should use the commerce_stock_location_level.

and

Currently the only way to get the stock level is using the API. The commerce_stock_location_level should not be used directly.

So what to do?

@olafkarsten Ok, I also think the patch is too big. I will split this issue into several sub-issues and add them as a reference to this ticket, you should be notified about that!

daveiano’s picture

daveiano’s picture

StatusFileSize
new694 bytes

Ok I reviewed my old code. The remaining patch (see comment #8) is about exposing the commerce_stock_location_level database table to views. The patch includes the views data defined in commerce_stock_local_views_data() for the table, and a viewfield to render stock level by location.

But the question is if this data should even be exposed, in my understanding there are different opinions (see comment #17).

The only thing missing in the views data in my opinion is the relationship between the commerce_stock_transaction and the commerce_product_variation:

$data['commerce_stock_transaction']['entity_id'] = [
    'title' => t('Purchasable entity ID'),
    'help' => t('The purchasable entity ID.'),
    'field' => [
      'id' => 'numeric',
    ],
    'filter' => [
      'id' => 'numeric',
    ],
    'argument' => [
      'id' => 'numeric',
    ],
    'sort' => [
      'id' => 'standard',
    ],
    'relationship' => [
      'base' => 'commerce_product_variation_field_data',
      'base field' => 'variation_id',
      'id' => 'standard',
      'label' => t('Product Variation'),
    ],
  ];

With this change, you can access the product variation data in a stock transaction view. See attached patch for the change.