Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stock control #81

Closed
michaelbromley opened this issue Mar 25, 2019 · 8 comments
Closed

Stock control #81

michaelbromley opened this issue Mar 25, 2019 · 8 comments

Comments

@michaelbromley
Copy link
Member

We need a way to express stock levels of ProductVariants.

Typically, there are 2 workflows we should support: managed and unmanaged.

  • Managed is where Vendure takes care of updating stock levels. If we have 100 Widgets and then a customer buys 3 Widgets, Vendure will ensure that the stock level of Widgets gets updated to equal 97.
  • Unmanaged is where the stock levels are updated externally by some other system (or even manually via the admin ui). This is used by businesses which e.g. have a number of channels (website, physical shop) and a central stock control database which is separate to Vendure. In this case, they could set up a task to automatically update the stock levels in Vendure via the admin API periodically.
@michaelbromley
Copy link
Member Author

michaelbromley commented May 1, 2019

MVP Approach

There are levels of sophistication when dealing with stock:

  • The simplest is to have a field on the ProductVariant entity with an integer representing the stock level.
  • More complex is to track "on hand" and "allocated", incrementing the "allocated" for OrderItems where the order moves to a certain state e.g. "PaymentSettled".
  • More complex still is to have discrete "stock movement" entities created each time stock comes in or out, which forms an auditable trail of all stock movement in and out of the system.

For now we will take the "minimum viable product" approach and implement something along the lines of the first item above.

Implementation

class ProductVariant {
  // ...
  stockOnHand: number;
  trackInventory: boolean;
}

The stockOnHand field contains the quantity of that variant which is available to buy.

The trackInventory field determines whether Vendure will automatically adjust the stockOnHand level when an Order is completed.

Additionally we need to think about how we deal with attempts to order more items than we have on hand. Some businesses will allow this and have a back-order system in place. Others will not. This should probably be configurable at the GlobalSettings level.

@michaelbromley
Copy link
Member Author

michaelbromley commented May 2, 2019

StockMovement

Thinking about this further, I think it makes sense to implement a StockMovement entity which represents a change in the stockOnHand of a ProductVariant. This should make the system more transparent by allowing insight into historical stock movements.

abstract class StockMovement {
  id: ID;
  createdAt: Date;
  productVariantId: ID;
  value: number;
}

class Adjustment extends StockMovement {
  type: 'adjustment';
}

class Sale extends StockMovement {
  type: 'sale';
  orderLineId: ID;
}

class Cancellation extends StockMovement {
  type: 'cancellation';
  orderLineId: ID;
}

class Return extends StockMovement {
  type: 'return';
  orderItemId: ID;
}

StockMovements will be created by Vendure when needed - there is no CRUD operations exposed via the API. Here are the scenarios:

  1. On updating a ProductVariant, if the stockOnHand is set and is different from the current value, a Adjustment will be generated with the value being the delta in stockOnHand.
  2. On an Order transitioning to the 'PaymentAuthorized' or 'PaymentSettled' state, a Sale is created for each OrderLine with the value being the negative of the quantity for that OrderLine.
  3. On an Order being cancelled (yet to be implemented), a Cancellation will be created for each OrderLine with the value being equal to the quantity for that OrderLine.
  4. On an OrderItem of a completed Order being returned, a Return will be created for that OrderItem with the value being 1 (since there is an OrderItem generated for each individual ProductVariant in an OrderLine)

These StockMovements will always be created, even if the ProductVariant is set to trackInventory: false.

@michaelbromley
Copy link
Member Author

Future features:

A quick note on future stock control features to implement (out of scope of this initial impl):

  • Inventory policy. A ProductVariant can specify whether a greater quantity than we have on hand may be added to the Order. Also would need a global default.
  • Handling of digital goods.

@joe-thong
Copy link

I wonder if there's any way to add a concept of reserved stock since these items are not fulfilled yet, but have been already ordered by one of the customers

@michaelbromley
Copy link
Member Author

@joe-thong This would be possible. Currently the Sale stock movement is created as soon as the payment is authorized/settled and the ProductVariant stock level is decremented (if stock levels are being tracked). It would be possible to create a Fulfillment stock movement when the order is fulfilled. This would allow you to differentiate between "reserved" and actually gone out of the warehouse.

Feel free to create a new issue with this feature request, we'll be then able to gauge interest.

@Zwergal
Copy link

Zwergal commented Apr 16, 2020

Additionally we need to think about how we deal with attempts to order more items than we have on hand. Some businesses will allow this and have a back-order system in place. Others will not. This should probably be configurable at the GlobalSettings level.

How is the current implementation about this issue? This would be a must have for most of our customers. Would be nice to have a global config but also a config on every ProductVariant if one has some exceptions. Like no back-orders in general but for this one product I will allow it.

@michaelbromley
Copy link
Member Author

@Zwergal there's been no further work on this since closing the issue, so back-orders are not yet implemented. I think the system you outlined makes sense. Please feel free to open a new feature request issue describing in detail how you think this should work and then we can put it on the roadmap for an upcoming release.

@kartikjethani-solidity
Copy link

I wrote a small plugin to get productVariant stock availability in the search query. Posting it here just in case anyone else finds the use case for it.

/** src/plugins/product-variant-in-stock/api/api-extension.ts */

import gql from "graphql-tag";

export const shopApiExtensions = gql`
  extend type SearchResult {
    productVariantInStock: Boolean!
  }
`; 




/** src/plugins/product-variant-in-stock/product-variant-in-stock.plugin.ts */

import { PluginCommonModule, VendurePlugin } from "@vendure/core";
import { shopApiExtensions } from "./api/api-extension";
import { SearchResultEntityResolver } from "./search-result-entity.resolver";

@VendurePlugin({
  imports: [PluginCommonModule],
  shopApiExtensions: {
    schema: shopApiExtensions,
    resolvers: [SearchResultEntityResolver],
  },
})
export class ProductVariantInStockPlugin {}




/** src/plugins/product-variant-in-stock/search-result-entity.resolver.ts */

import { Parent, ResolveField, Resolver } from "@nestjs/graphql";
import { Ctx, RequestContext, StockLevelService } from "@vendure/core";

@Resolver("SearchResult")
export class SearchResultEntityResolver {
  constructor(private stockLevelService: StockLevelService) {}

  @ResolveField()
  async productVariantInStock(@Ctx() ctx: RequestContext, @Parent() item: any) {
    const { stockOnHand, stockAllocated } =
      await this.stockLevelService.getAvailableStock(
        ctx,
        item.productVariantId
      );
    return stockOnHand > stockAllocated;
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants