Prechádzať zdrojové kódy

feat(core): Track stock allocations

Relates to #319.

BREAKING CHANGE: The internal handling of stock movements has been refined,
which required changes to the DB schema. This will require a migration.
Michael Bromley 5 rokov pred
rodič
commit
75e3f9c56d
29 zmenil súbory, kde vykonal 821 pridanie a 381 odobranie
  1. 29 2
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  2. 154 209
      packages/admin-ui/src/lib/core/src/common/introspection-result.ts
  3. 30 2
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  4. 28 2
      packages/common/src/generated-shop-types.ts
  5. 29 2
      packages/common/src/generated-types.ts
  6. 10 10
      packages/core/e2e/__snapshots__/import.e2e-spec.ts.snap
  7. 1 0
      packages/core/e2e/graphql/fragments.ts
  8. 74 42
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  9. 31 3
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  10. 22 0
      packages/core/e2e/graphql/shared-definitions.ts
  11. 3 0
      packages/core/e2e/graphql/shop-definitions.ts
  12. 32 52
      packages/core/e2e/order.e2e-spec.ts
  13. 145 11
      packages/core/e2e/stock-control.e2e-spec.ts
  14. 4 0
      packages/core/src/api/config/configure-graphql-module.ts
  15. 1 0
      packages/core/src/api/schema/admin-api/product.api.graphql
  16. 24 2
      packages/core/src/api/schema/type/stock-movement.type.graphql
  17. 27 27
      packages/core/src/data-import/providers/import-parser/__snapshots__/import-parser.spec.ts.snap
  18. 4 0
      packages/core/src/entity/entities.ts
  19. 3 0
      packages/core/src/entity/product-variant/product-variant.entity.ts
  20. 19 0
      packages/core/src/entity/stock-movement/allocation.entity.ts
  21. 2 3
      packages/core/src/entity/stock-movement/cancellation.entity.ts
  22. 19 0
      packages/core/src/entity/stock-movement/release.entity.ts
  23. 2 2
      packages/core/src/entity/stock-movement/sale.entity.ts
  24. 1 1
      packages/core/src/service/helpers/order-state-machine/order-state-machine.ts
  25. 12 3
      packages/core/src/service/services/order.service.ts
  26. 85 6
      packages/core/src/service/services/stock-movement.service.ts
  27. 30 2
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  28. 0 0
      schema-admin.json
  29. 0 0
      schema-shop.json

+ 29 - 2
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -1636,6 +1636,7 @@ export type ProductVariant = Node & {
   __typename?: 'ProductVariant';
   enabled: Scalars['Boolean'];
   stockOnHand: Scalars['Int'];
+  stockAllocated: Scalars['Int'];
   trackInventory: GlobalFlag;
   stockMovements: StockMovementList;
   id: Scalars['ID'];
@@ -3521,6 +3522,8 @@ export type ShippingMethodList = PaginatedList & {
 
 export enum StockMovementType {
   ADJUSTMENT = 'ADJUSTMENT',
+  ALLOCATION = 'ALLOCATION',
+  RELEASE = 'RELEASE',
   SALE = 'SALE',
   CANCELLATION = 'CANCELLATION',
   RETURN = 'RETURN'
@@ -3545,6 +3548,17 @@ export type StockAdjustment = Node & StockMovement & {
   quantity: Scalars['Int'];
 };
 
+export type Allocation = Node & StockMovement & {
+  __typename?: 'Allocation';
+  id: Scalars['ID'];
+  createdAt: Scalars['DateTime'];
+  updatedAt: Scalars['DateTime'];
+  productVariant: ProductVariant;
+  type: StockMovementType;
+  quantity: Scalars['Int'];
+  orderLine: OrderLine;
+};
+
 export type Sale = Node & StockMovement & {
   __typename?: 'Sale';
   id: Scalars['ID'];
@@ -3553,7 +3567,7 @@ export type Sale = Node & StockMovement & {
   productVariant: ProductVariant;
   type: StockMovementType;
   quantity: Scalars['Int'];
-  orderLine: OrderLine;
+  orderItem: OrderItem;
 };
 
 export type Cancellation = Node & StockMovement & {
@@ -3578,7 +3592,18 @@ export type Return = Node & StockMovement & {
   orderItem: OrderItem;
 };
 
-export type StockMovementItem = StockAdjustment | Sale | Cancellation | Return;
+export type Release = Node & StockMovement & {
+  __typename?: 'Release';
+  id: Scalars['ID'];
+  createdAt: Scalars['DateTime'];
+  updatedAt: Scalars['DateTime'];
+  productVariant: ProductVariant;
+  type: StockMovementType;
+  quantity: Scalars['Int'];
+  orderItem: OrderItem;
+};
+
+export type StockMovementItem = StockAdjustment | Allocation | Sale | Cancellation | Return | Release;
 
 export type StockMovementList = {
   __typename?: 'StockMovementList';
@@ -4050,6 +4075,7 @@ export type TaxRateSortParameter = {
 export type ProductVariantFilterParameter = {
   enabled?: Maybe<BooleanOperators>;
   stockOnHand?: Maybe<NumberOperators>;
+  stockAllocated?: Maybe<NumberOperators>;
   trackInventory?: Maybe<StringOperators>;
   createdAt?: Maybe<DateOperators>;
   updatedAt?: Maybe<DateOperators>;
@@ -4064,6 +4090,7 @@ export type ProductVariantFilterParameter = {
 
 export type ProductVariantSortParameter = {
   stockOnHand?: Maybe<SortOrder>;
+  stockAllocated?: Maybe<SortOrder>;
   id?: Maybe<SortOrder>;
   productId?: Maybe<SortOrder>;
   createdAt?: Maybe<SortOrder>;

+ 154 - 209
packages/admin-ui/src/lib/core/src/common/introspection-result.ts

@@ -1,212 +1,157 @@
 // tslint:disable
 
-      export interface PossibleTypesResultData {
-        possibleTypes: {
-          [key: string]: string[]
-        }
-      }
-      const result: PossibleTypesResultData = {
-  "possibleTypes": {
-    "CreateAssetResult": [
-      "Asset",
-      "MimeTypeError"
-    ],
-    "NativeAuthenticationResult": [
-      "CurrentUser",
-      "InvalidCredentialsError",
-      "NativeAuthStrategyError"
-    ],
-    "AuthenticationResult": [
-      "CurrentUser",
-      "InvalidCredentialsError"
-    ],
-    "CreateChannelResult": [
-      "Channel",
-      "LanguageNotAvailableError"
-    ],
-    "UpdateChannelResult": [
-      "Channel",
-      "LanguageNotAvailableError"
-    ],
-    "CreateCustomerResult": [
-      "Customer",
-      "EmailAddressConflictError"
-    ],
-    "UpdateCustomerResult": [
-      "Customer",
-      "EmailAddressConflictError"
-    ],
-    "UpdateGlobalSettingsResult": [
-      "GlobalSettings",
-      "ChannelDefaultLanguageError"
-    ],
-    "TransitionOrderToStateResult": [
-      "Order",
-      "OrderStateTransitionError"
-    ],
-    "SettlePaymentResult": [
-      "Payment",
-      "SettlePaymentError",
-      "PaymentStateTransitionError",
-      "OrderStateTransitionError"
-    ],
-    "AddFulfillmentToOrderResult": [
-      "Fulfillment",
-      "EmptyOrderLineSelectionError",
-      "ItemsAlreadyFulfilledError"
-    ],
-    "CancelOrderResult": [
-      "Order",
-      "EmptyOrderLineSelectionError",
-      "QuantityTooGreatError",
-      "MultipleOrderError",
-      "CancelActiveOrderError",
-      "OrderStateTransitionError"
-    ],
-    "RefundOrderResult": [
-      "Refund",
-      "QuantityTooGreatError",
-      "NothingToRefundError",
-      "OrderStateTransitionError",
-      "MultipleOrderError",
-      "PaymentOrderMismatchError",
-      "RefundOrderStateError",
-      "AlreadyRefundedError",
-      "RefundStateTransitionError"
-    ],
-    "SettleRefundResult": [
-      "Refund",
-      "RefundStateTransitionError"
-    ],
-    "TransitionFulfillmentToStateResult": [
-      "Fulfillment",
-      "FulfillmentStateTransitionError"
-    ],
-    "RemoveOptionGroupFromProductResult": [
-      "Product",
-      "ProductOptionInUseError"
-    ],
-    "CreatePromotionResult": [
-      "Promotion",
-      "MissingConditionsError"
-    ],
-    "UpdatePromotionResult": [
-      "Promotion",
-      "MissingConditionsError"
-    ],
-    "PaginatedList": [
-      "CustomerGroupList",
-      "JobList",
-      "PaymentMethodList",
-      "AdministratorList",
-      "AssetList",
-      "CollectionList",
-      "ProductVariantList",
-      "CountryList",
-      "CustomerList",
-      "FacetList",
-      "HistoryEntryList",
-      "OrderList",
-      "ProductList",
-      "PromotionList",
-      "RoleList",
-      "ShippingMethodList",
-      "TaxRateList"
-    ],
-    "Node": [
-      "Collection",
-      "Customer",
-      "Facet",
-      "Fulfillment",
-      "Job",
-      "Order",
-      "Product",
-      "ProductVariant",
-      "Address",
-      "Administrator",
-      "Asset",
-      "Channel",
-      "Country",
-      "CustomerGroup",
-      "FacetValue",
-      "HistoryEntry",
-      "OrderItem",
-      "OrderLine",
-      "Payment",
-      "Refund",
-      "PaymentMethod",
-      "ProductOptionGroup",
-      "ProductOption",
-      "Promotion",
-      "Role",
-      "ShippingMethod",
-      "StockAdjustment",
-      "Sale",
-      "Cancellation",
-      "Return",
-      "TaxCategory",
-      "TaxRate",
-      "User",
-      "AuthenticationMethod",
-      "Zone"
-    ],
-    "ErrorResult": [
-      "MimeTypeError",
-      "LanguageNotAvailableError",
-      "ChannelDefaultLanguageError",
-      "SettlePaymentError",
-      "EmptyOrderLineSelectionError",
-      "ItemsAlreadyFulfilledError",
-      "MultipleOrderError",
-      "CancelActiveOrderError",
-      "PaymentOrderMismatchError",
-      "RefundOrderStateError",
-      "NothingToRefundError",
-      "AlreadyRefundedError",
-      "QuantityTooGreatError",
-      "RefundStateTransitionError",
-      "PaymentStateTransitionError",
-      "FulfillmentStateTransitionError",
-      "ProductOptionInUseError",
-      "MissingConditionsError",
-      "NativeAuthStrategyError",
-      "InvalidCredentialsError",
-      "OrderStateTransitionError",
-      "EmailAddressConflictError"
-    ],
-    "CustomField": [
-      "StringCustomFieldConfig",
-      "LocaleStringCustomFieldConfig",
-      "IntCustomFieldConfig",
-      "FloatCustomFieldConfig",
-      "BooleanCustomFieldConfig",
-      "DateTimeCustomFieldConfig"
-    ],
-    "CustomFieldConfig": [
-      "StringCustomFieldConfig",
-      "LocaleStringCustomFieldConfig",
-      "IntCustomFieldConfig",
-      "FloatCustomFieldConfig",
-      "BooleanCustomFieldConfig",
-      "DateTimeCustomFieldConfig"
-    ],
-    "SearchResultPrice": [
-      "PriceRange",
-      "SinglePrice"
-    ],
-    "StockMovement": [
-      "StockAdjustment",
-      "Sale",
-      "Cancellation",
-      "Return"
-    ],
-    "StockMovementItem": [
-      "StockAdjustment",
-      "Sale",
-      "Cancellation",
-      "Return"
-    ]
-  }
+export interface PossibleTypesResultData {
+    possibleTypes: {
+        [key: string]: string[];
+    };
+}
+const result: PossibleTypesResultData = {
+    possibleTypes: {
+        CreateAssetResult: ['Asset', 'MimeTypeError'],
+        NativeAuthenticationResult: ['CurrentUser', 'InvalidCredentialsError', 'NativeAuthStrategyError'],
+        AuthenticationResult: ['CurrentUser', 'InvalidCredentialsError'],
+        CreateChannelResult: ['Channel', 'LanguageNotAvailableError'],
+        UpdateChannelResult: ['Channel', 'LanguageNotAvailableError'],
+        CreateCustomerResult: ['Customer', 'EmailAddressConflictError'],
+        UpdateCustomerResult: ['Customer', 'EmailAddressConflictError'],
+        UpdateGlobalSettingsResult: ['GlobalSettings', 'ChannelDefaultLanguageError'],
+        TransitionOrderToStateResult: ['Order', 'OrderStateTransitionError'],
+        SettlePaymentResult: [
+            'Payment',
+            'SettlePaymentError',
+            'PaymentStateTransitionError',
+            'OrderStateTransitionError',
+        ],
+        AddFulfillmentToOrderResult: [
+            'Fulfillment',
+            'EmptyOrderLineSelectionError',
+            'ItemsAlreadyFulfilledError',
+        ],
+        CancelOrderResult: [
+            'Order',
+            'EmptyOrderLineSelectionError',
+            'QuantityTooGreatError',
+            'MultipleOrderError',
+            'CancelActiveOrderError',
+            'OrderStateTransitionError',
+        ],
+        RefundOrderResult: [
+            'Refund',
+            'QuantityTooGreatError',
+            'NothingToRefundError',
+            'OrderStateTransitionError',
+            'MultipleOrderError',
+            'PaymentOrderMismatchError',
+            'RefundOrderStateError',
+            'AlreadyRefundedError',
+            'RefundStateTransitionError',
+        ],
+        SettleRefundResult: ['Refund', 'RefundStateTransitionError'],
+        TransitionFulfillmentToStateResult: ['Fulfillment', 'FulfillmentStateTransitionError'],
+        RemoveOptionGroupFromProductResult: ['Product', 'ProductOptionInUseError'],
+        CreatePromotionResult: ['Promotion', 'MissingConditionsError'],
+        UpdatePromotionResult: ['Promotion', 'MissingConditionsError'],
+        PaginatedList: [
+            'CustomerGroupList',
+            'JobList',
+            'PaymentMethodList',
+            'AdministratorList',
+            'AssetList',
+            'CollectionList',
+            'ProductVariantList',
+            'CountryList',
+            'CustomerList',
+            'FacetList',
+            'HistoryEntryList',
+            'OrderList',
+            'ProductList',
+            'PromotionList',
+            'RoleList',
+            'ShippingMethodList',
+            'TaxRateList',
+        ],
+        Node: [
+            'Collection',
+            'Customer',
+            'Facet',
+            'Fulfillment',
+            'Job',
+            'Order',
+            'Product',
+            'ProductVariant',
+            'Address',
+            'Administrator',
+            'Asset',
+            'Channel',
+            'Country',
+            'CustomerGroup',
+            'FacetValue',
+            'HistoryEntry',
+            'OrderItem',
+            'OrderLine',
+            'Payment',
+            'Refund',
+            'PaymentMethod',
+            'ProductOptionGroup',
+            'ProductOption',
+            'Promotion',
+            'Role',
+            'ShippingMethod',
+            'StockAdjustment',
+            'Allocation',
+            'Sale',
+            'Cancellation',
+            'Return',
+            'Release',
+            'TaxCategory',
+            'TaxRate',
+            'User',
+            'AuthenticationMethod',
+            'Zone',
+        ],
+        ErrorResult: [
+            'MimeTypeError',
+            'LanguageNotAvailableError',
+            'ChannelDefaultLanguageError',
+            'SettlePaymentError',
+            'EmptyOrderLineSelectionError',
+            'ItemsAlreadyFulfilledError',
+            'MultipleOrderError',
+            'CancelActiveOrderError',
+            'PaymentOrderMismatchError',
+            'RefundOrderStateError',
+            'NothingToRefundError',
+            'AlreadyRefundedError',
+            'QuantityTooGreatError',
+            'RefundStateTransitionError',
+            'PaymentStateTransitionError',
+            'FulfillmentStateTransitionError',
+            'ProductOptionInUseError',
+            'MissingConditionsError',
+            'NativeAuthStrategyError',
+            'InvalidCredentialsError',
+            'OrderStateTransitionError',
+            'EmailAddressConflictError',
+        ],
+        CustomField: [
+            'StringCustomFieldConfig',
+            'LocaleStringCustomFieldConfig',
+            'IntCustomFieldConfig',
+            'FloatCustomFieldConfig',
+            'BooleanCustomFieldConfig',
+            'DateTimeCustomFieldConfig',
+        ],
+        CustomFieldConfig: [
+            'StringCustomFieldConfig',
+            'LocaleStringCustomFieldConfig',
+            'IntCustomFieldConfig',
+            'FloatCustomFieldConfig',
+            'BooleanCustomFieldConfig',
+            'DateTimeCustomFieldConfig',
+        ],
+        SearchResultPrice: ['PriceRange', 'SinglePrice'],
+        StockMovement: ['StockAdjustment', 'Allocation', 'Sale', 'Cancellation', 'Return', 'Release'],
+        StockMovementItem: ['StockAdjustment', 'Allocation', 'Sale', 'Cancellation', 'Return', 'Release'],
+    },
 };
-      export default result;
-    
+export default result;

+ 30 - 2
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -1463,6 +1463,7 @@ export type Product = Node & {
 export type ProductVariant = Node & {
     enabled: Scalars['Boolean'];
     stockOnHand: Scalars['Int'];
+    stockAllocated: Scalars['Int'];
     trackInventory: GlobalFlag;
     stockMovements: StockMovementList;
     id: Scalars['ID'];
@@ -1946,6 +1947,7 @@ export type NativeAuthStrategyError = ErrorResult & {
 export type InvalidCredentialsError = ErrorResult & {
     errorCode: ErrorCode;
     message: Scalars['String'];
+    authenticationError: Scalars['String'];
 };
 
 /** Returned if there is an error in transitioning the Order state */
@@ -3270,6 +3272,8 @@ export type ShippingMethodList = PaginatedList & {
 
 export enum StockMovementType {
     ADJUSTMENT = 'ADJUSTMENT',
+    ALLOCATION = 'ALLOCATION',
+    RELEASE = 'RELEASE',
     SALE = 'SALE',
     CANCELLATION = 'CANCELLATION',
     RETURN = 'RETURN',
@@ -3294,7 +3298,7 @@ export type StockAdjustment = Node &
         quantity: Scalars['Int'];
     };
 
-export type Sale = Node &
+export type Allocation = Node &
     StockMovement & {
         id: Scalars['ID'];
         createdAt: Scalars['DateTime'];
@@ -3305,6 +3309,17 @@ export type Sale = Node &
         orderLine: OrderLine;
     };
 
+export type Sale = Node &
+    StockMovement & {
+        id: Scalars['ID'];
+        createdAt: Scalars['DateTime'];
+        updatedAt: Scalars['DateTime'];
+        productVariant: ProductVariant;
+        type: StockMovementType;
+        quantity: Scalars['Int'];
+        orderItem: OrderItem;
+    };
+
 export type Cancellation = Node &
     StockMovement & {
         id: Scalars['ID'];
@@ -3327,7 +3342,18 @@ export type Return = Node &
         orderItem: OrderItem;
     };
 
-export type StockMovementItem = StockAdjustment | Sale | Cancellation | Return;
+export type Release = Node &
+    StockMovement & {
+        id: Scalars['ID'];
+        createdAt: Scalars['DateTime'];
+        updatedAt: Scalars['DateTime'];
+        productVariant: ProductVariant;
+        type: StockMovementType;
+        quantity: Scalars['Int'];
+        orderItem: OrderItem;
+    };
+
+export type StockMovementItem = StockAdjustment | Allocation | Sale | Cancellation | Return | Release;
 
 export type StockMovementList = {
     items: Array<StockMovementItem>;
@@ -3792,6 +3818,7 @@ export type TaxRateSortParameter = {
 export type ProductVariantFilterParameter = {
     enabled?: Maybe<BooleanOperators>;
     stockOnHand?: Maybe<NumberOperators>;
+    stockAllocated?: Maybe<NumberOperators>;
     trackInventory?: Maybe<StringOperators>;
     createdAt?: Maybe<DateOperators>;
     updatedAt?: Maybe<DateOperators>;
@@ -3806,6 +3833,7 @@ export type ProductVariantFilterParameter = {
 
 export type ProductVariantSortParameter = {
     stockOnHand?: Maybe<SortOrder>;
+    stockAllocated?: Maybe<SortOrder>;
     id?: Maybe<SortOrder>;
     productId?: Maybe<SortOrder>;
     createdAt?: Maybe<SortOrder>;

+ 28 - 2
packages/common/src/generated-shop-types.ts

@@ -2350,6 +2350,8 @@ export type ShippingMethodList = PaginatedList & {
 
 export enum StockMovementType {
     ADJUSTMENT = 'ADJUSTMENT',
+    ALLOCATION = 'ALLOCATION',
+    RELEASE = 'RELEASE',
     SALE = 'SALE',
     CANCELLATION = 'CANCELLATION',
     RETURN = 'RETURN',
@@ -2375,6 +2377,18 @@ export type StockAdjustment = Node &
         quantity: Scalars['Int'];
     };
 
+export type Allocation = Node &
+    StockMovement & {
+        __typename?: 'Allocation';
+        id: Scalars['ID'];
+        createdAt: Scalars['DateTime'];
+        updatedAt: Scalars['DateTime'];
+        productVariant: ProductVariant;
+        type: StockMovementType;
+        quantity: Scalars['Int'];
+        orderLine: OrderLine;
+    };
+
 export type Sale = Node &
     StockMovement & {
         __typename?: 'Sale';
@@ -2384,7 +2398,7 @@ export type Sale = Node &
         productVariant: ProductVariant;
         type: StockMovementType;
         quantity: Scalars['Int'];
-        orderLine: OrderLine;
+        orderItem: OrderItem;
     };
 
 export type Cancellation = Node &
@@ -2411,7 +2425,19 @@ export type Return = Node &
         orderItem: OrderItem;
     };
 
-export type StockMovementItem = StockAdjustment | Sale | Cancellation | Return;
+export type Release = Node &
+    StockMovement & {
+        __typename?: 'Release';
+        id: Scalars['ID'];
+        createdAt: Scalars['DateTime'];
+        updatedAt: Scalars['DateTime'];
+        productVariant: ProductVariant;
+        type: StockMovementType;
+        quantity: Scalars['Int'];
+        orderItem: OrderItem;
+    };
+
+export type StockMovementItem = StockAdjustment | Allocation | Sale | Cancellation | Return | Release;
 
 export type StockMovementList = {
     __typename?: 'StockMovementList';

+ 29 - 2
packages/common/src/generated-types.ts

@@ -1605,6 +1605,7 @@ export type ProductVariant = Node & {
   __typename?: 'ProductVariant';
   enabled: Scalars['Boolean'];
   stockOnHand: Scalars['Int'];
+  stockAllocated: Scalars['Int'];
   trackInventory: GlobalFlag;
   stockMovements: StockMovementList;
   id: Scalars['ID'];
@@ -3490,6 +3491,8 @@ export type ShippingMethodList = PaginatedList & {
 
 export enum StockMovementType {
   ADJUSTMENT = 'ADJUSTMENT',
+  ALLOCATION = 'ALLOCATION',
+  RELEASE = 'RELEASE',
   SALE = 'SALE',
   CANCELLATION = 'CANCELLATION',
   RETURN = 'RETURN'
@@ -3514,6 +3517,17 @@ export type StockAdjustment = Node & StockMovement & {
   quantity: Scalars['Int'];
 };
 
+export type Allocation = Node & StockMovement & {
+  __typename?: 'Allocation';
+  id: Scalars['ID'];
+  createdAt: Scalars['DateTime'];
+  updatedAt: Scalars['DateTime'];
+  productVariant: ProductVariant;
+  type: StockMovementType;
+  quantity: Scalars['Int'];
+  orderLine: OrderLine;
+};
+
 export type Sale = Node & StockMovement & {
   __typename?: 'Sale';
   id: Scalars['ID'];
@@ -3522,7 +3536,7 @@ export type Sale = Node & StockMovement & {
   productVariant: ProductVariant;
   type: StockMovementType;
   quantity: Scalars['Int'];
-  orderLine: OrderLine;
+  orderItem: OrderItem;
 };
 
 export type Cancellation = Node & StockMovement & {
@@ -3547,7 +3561,18 @@ export type Return = Node & StockMovement & {
   orderItem: OrderItem;
 };
 
-export type StockMovementItem = StockAdjustment | Sale | Cancellation | Return;
+export type Release = Node & StockMovement & {
+  __typename?: 'Release';
+  id: Scalars['ID'];
+  createdAt: Scalars['DateTime'];
+  updatedAt: Scalars['DateTime'];
+  productVariant: ProductVariant;
+  type: StockMovementType;
+  quantity: Scalars['Int'];
+  orderItem: OrderItem;
+};
+
+export type StockMovementItem = StockAdjustment | Allocation | Sale | Cancellation | Return | Release;
 
 export type StockMovementList = {
   __typename?: 'StockMovementList';
@@ -4019,6 +4044,7 @@ export type TaxRateSortParameter = {
 export type ProductVariantFilterParameter = {
   enabled?: Maybe<BooleanOperators>;
   stockOnHand?: Maybe<NumberOperators>;
+  stockAllocated?: Maybe<NumberOperators>;
   trackInventory?: Maybe<StringOperators>;
   createdAt?: Maybe<DateOperators>;
   updatedAt?: Maybe<DateOperators>;
@@ -4033,6 +4059,7 @@ export type ProductVariantFilterParameter = {
 
 export type ProductVariantSortParameter = {
   stockOnHand?: Maybe<SortOrder>;
+  stockAllocated?: Maybe<SortOrder>;
   id?: Maybe<SortOrder>;
   productId?: Maybe<SortOrder>;
   createdAt?: Maybe<SortOrder>;

+ 10 - 10
packages/core/e2e/__snapshots__/import.e2e-spec.ts.snap

@@ -55,7 +55,7 @@ Object {
         "id": "T_1",
         "name": "Standard Tax",
       },
-      "trackInventory": false,
+      "trackInventory": "FALSE",
     },
     Object {
       "assets": Array [],
@@ -75,7 +75,7 @@ Object {
         "id": "T_1",
         "name": "Standard Tax",
       },
-      "trackInventory": false,
+      "trackInventory": "FALSE",
     },
     Object {
       "assets": Array [],
@@ -101,7 +101,7 @@ Object {
         "id": "T_1",
         "name": "Standard Tax",
       },
-      "trackInventory": false,
+      "trackInventory": "FALSE",
     },
   ],
 }
@@ -144,7 +144,7 @@ Object {
         "id": "T_1",
         "name": "Standard Tax",
       },
-      "trackInventory": false,
+      "trackInventory": "FALSE",
     },
   ],
 }
@@ -199,7 +199,7 @@ Object {
         "id": "T_1",
         "name": "Standard Tax",
       },
-      "trackInventory": false,
+      "trackInventory": "FALSE",
     },
     Object {
       "assets": Array [
@@ -231,7 +231,7 @@ Object {
         "id": "T_1",
         "name": "Standard Tax",
       },
-      "trackInventory": false,
+      "trackInventory": "FALSE",
     },
   ],
 }
@@ -279,7 +279,7 @@ Object {
         "id": "T_2",
         "name": "Reduced Tax",
       },
-      "trackInventory": false,
+      "trackInventory": "FALSE",
     },
     Object {
       "assets": Array [],
@@ -299,7 +299,7 @@ Object {
         "id": "T_2",
         "name": "Reduced Tax",
       },
-      "trackInventory": false,
+      "trackInventory": "FALSE",
     },
     Object {
       "assets": Array [],
@@ -319,7 +319,7 @@ Object {
         "id": "T_2",
         "name": "Reduced Tax",
       },
-      "trackInventory": false,
+      "trackInventory": "FALSE",
     },
     Object {
       "assets": Array [],
@@ -339,7 +339,7 @@ Object {
         "id": "T_2",
         "name": "Reduced Tax",
       },
-      "trackInventory": false,
+      "trackInventory": "FALSE",
     },
   ],
 }

+ 1 - 0
packages/core/e2e/graphql/fragments.ts

@@ -469,6 +469,7 @@ export const VARIANT_WITH_STOCK_FRAGMENT = gql`
     fragment VariantWithStock on ProductVariant {
         id
         stockOnHand
+        stockAllocated
         stockMovements {
             items {
                 ... on StockMovement {

+ 74 - 42
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -1463,6 +1463,7 @@ export type Product = Node & {
 export type ProductVariant = Node & {
     enabled: Scalars['Boolean'];
     stockOnHand: Scalars['Int'];
+    stockAllocated: Scalars['Int'];
     trackInventory: GlobalFlag;
     stockMovements: StockMovementList;
     id: Scalars['ID'];
@@ -1946,6 +1947,7 @@ export type NativeAuthStrategyError = ErrorResult & {
 export type InvalidCredentialsError = ErrorResult & {
     errorCode: ErrorCode;
     message: Scalars['String'];
+    authenticationError: Scalars['String'];
 };
 
 /** Returned if there is an error in transitioning the Order state */
@@ -3270,6 +3272,8 @@ export type ShippingMethodList = PaginatedList & {
 
 export enum StockMovementType {
     ADJUSTMENT = 'ADJUSTMENT',
+    ALLOCATION = 'ALLOCATION',
+    RELEASE = 'RELEASE',
     SALE = 'SALE',
     CANCELLATION = 'CANCELLATION',
     RETURN = 'RETURN',
@@ -3294,7 +3298,7 @@ export type StockAdjustment = Node &
         quantity: Scalars['Int'];
     };
 
-export type Sale = Node &
+export type Allocation = Node &
     StockMovement & {
         id: Scalars['ID'];
         createdAt: Scalars['DateTime'];
@@ -3305,6 +3309,17 @@ export type Sale = Node &
         orderLine: OrderLine;
     };
 
+export type Sale = Node &
+    StockMovement & {
+        id: Scalars['ID'];
+        createdAt: Scalars['DateTime'];
+        updatedAt: Scalars['DateTime'];
+        productVariant: ProductVariant;
+        type: StockMovementType;
+        quantity: Scalars['Int'];
+        orderItem: OrderItem;
+    };
+
 export type Cancellation = Node &
     StockMovement & {
         id: Scalars['ID'];
@@ -3327,7 +3342,18 @@ export type Return = Node &
         orderItem: OrderItem;
     };
 
-export type StockMovementItem = StockAdjustment | Sale | Cancellation | Return;
+export type Release = Node &
+    StockMovement & {
+        id: Scalars['ID'];
+        createdAt: Scalars['DateTime'];
+        updatedAt: Scalars['DateTime'];
+        productVariant: ProductVariant;
+        type: StockMovementType;
+        quantity: Scalars['Int'];
+        orderItem: OrderItem;
+    };
+
+export type StockMovementItem = StockAdjustment | Allocation | Sale | Cancellation | Return | Release;
 
 export type StockMovementList = {
     items: Array<StockMovementItem>;
@@ -3792,6 +3818,7 @@ export type TaxRateSortParameter = {
 export type ProductVariantFilterParameter = {
     enabled?: Maybe<BooleanOperators>;
     stockOnHand?: Maybe<NumberOperators>;
+    stockAllocated?: Maybe<NumberOperators>;
     trackInventory?: Maybe<StringOperators>;
     createdAt?: Maybe<DateOperators>;
     updatedAt?: Maybe<DateOperators>;
@@ -3806,6 +3833,7 @@ export type ProductVariantFilterParameter = {
 
 export type ProductVariantSortParameter = {
     stockOnHand?: Maybe<SortOrder>;
+    stockAllocated?: Maybe<SortOrder>;
     id?: Maybe<SortOrder>;
     productId?: Maybe<SortOrder>;
     createdAt?: Maybe<SortOrder>;
@@ -3931,7 +3959,9 @@ export type AuthenticateMutationVariables = Exact<{
 }>;
 
 export type AuthenticateMutation = {
-    authenticate: CurrentUserFragment | Pick<InvalidCredentialsError, 'errorCode' | 'message'>;
+    authenticate:
+        | CurrentUserFragment
+        | Pick<InvalidCredentialsError, 'authenticationError' | 'errorCode' | 'message'>;
 };
 
 export type GetCustomersQueryVariables = Exact<{ [key: string]: never }>;
@@ -4518,13 +4548,15 @@ export type CurrentUserFragment = Pick<CurrentUser, 'id' | 'identifier'> & {
     channels: Array<Pick<CurrentUserChannel, 'code' | 'token' | 'permissions'>>;
 };
 
-export type VariantWithStockFragment = Pick<ProductVariant, 'id' | 'stockOnHand'> & {
+export type VariantWithStockFragment = Pick<ProductVariant, 'id' | 'stockOnHand' | 'stockAllocated'> & {
     stockMovements: Pick<StockMovementList, 'totalItems'> & {
         items: Array<
             | Pick<StockAdjustment, 'id' | 'type' | 'quantity'>
+            | Pick<Allocation, 'id' | 'type' | 'quantity'>
             | Pick<Sale, 'id' | 'type' | 'quantity'>
             | Pick<Cancellation, 'id' | 'type' | 'quantity'>
             | Pick<Return, 'id' | 'type' | 'quantity'>
+            | Pick<Release, 'id' | 'type' | 'quantity'>
         >;
     };
 };
@@ -5014,6 +5046,24 @@ export type AdminTransitionMutation = {
     >;
 };
 
+export type CancelOrderMutationVariables = Exact<{
+    input: CancelOrderInput;
+}>;
+
+export type CancelOrderMutation = {
+    cancelOrder:
+        | CanceledOrderFragment
+        | Pick<EmptyOrderLineSelectionError, 'errorCode' | 'message'>
+        | Pick<QuantityTooGreatError, 'errorCode' | 'message'>
+        | Pick<MultipleOrderError, 'errorCode' | 'message'>
+        | Pick<CancelActiveOrderError, 'errorCode' | 'message'>
+        | Pick<OrderStateTransitionError, 'errorCode' | 'message'>;
+};
+
+export type CanceledOrderFragment = Pick<Order, 'id'> & {
+    lines: Array<Pick<OrderLine, 'quantity'> & { items: Array<Pick<OrderItem, 'id' | 'cancelled'>> }>;
+};
+
 export type UpdateOptionGroupMutationVariables = Exact<{
     input: UpdateProductOptionGroupInput;
 }>;
@@ -5074,24 +5124,6 @@ export type GetOrderFulfillmentItemsQuery = {
     order?: Maybe<Pick<Order, 'id' | 'state'> & { fulfillments?: Maybe<Array<FulfillmentFragment>> }>;
 };
 
-export type CancelOrderMutationVariables = Exact<{
-    input: CancelOrderInput;
-}>;
-
-export type CancelOrderMutation = {
-    cancelOrder:
-        | CanceledOrderFragment
-        | Pick<EmptyOrderLineSelectionError, 'errorCode' | 'message'>
-        | Pick<QuantityTooGreatError, 'errorCode' | 'message'>
-        | Pick<MultipleOrderError, 'errorCode' | 'message'>
-        | Pick<CancelActiveOrderError, 'errorCode' | 'message'>
-        | Pick<OrderStateTransitionError, 'errorCode' | 'message'>;
-};
-
-export type CanceledOrderFragment = Pick<Order, 'id'> & {
-    lines: Array<Pick<OrderLine, 'quantity'> & { items: Array<Pick<OrderItem, 'id' | 'cancelled'>> }>;
-};
-
 export type RefundFragment = Pick<
     Refund,
     'id' | 'state' | 'items' | 'transactionId' | 'shipping' | 'total' | 'metadata'
@@ -5655,9 +5687,9 @@ export namespace Authenticate {
     export type Variables = AuthenticateMutationVariables;
     export type Mutation = AuthenticateMutation;
     export type Authenticate = NonNullable<AuthenticateMutation['authenticate']>;
-    export type ErrorResultInlineFragment = DiscriminateUnion<
+    export type InvalidCredentialsErrorInlineFragment = DiscriminateUnion<
         NonNullable<AuthenticateMutation['authenticate']>,
-        { __typename?: 'ErrorResult' }
+        { __typename?: 'InvalidCredentialsError' }
     >;
 }
 
@@ -6816,6 +6848,24 @@ export namespace AdminTransition {
     >;
 }
 
+export namespace CancelOrder {
+    export type Variables = CancelOrderMutationVariables;
+    export type Mutation = CancelOrderMutation;
+    export type CancelOrder = NonNullable<CancelOrderMutation['cancelOrder']>;
+    export type ErrorResultInlineFragment = DiscriminateUnion<
+        NonNullable<CancelOrderMutation['cancelOrder']>,
+        { __typename?: 'ErrorResult' }
+    >;
+}
+
+export namespace CanceledOrder {
+    export type Fragment = CanceledOrderFragment;
+    export type Lines = NonNullable<NonNullable<CanceledOrderFragment['lines']>[number]>;
+    export type Items = NonNullable<
+        NonNullable<NonNullable<NonNullable<CanceledOrderFragment['lines']>[number]>['items']>[number]
+    >;
+}
+
 export namespace UpdateOptionGroup {
     export type Variables = UpdateOptionGroupMutationVariables;
     export type Mutation = UpdateOptionGroupMutation;
@@ -6898,24 +6948,6 @@ export namespace GetOrderFulfillmentItems {
     >;
 }
 
-export namespace CancelOrder {
-    export type Variables = CancelOrderMutationVariables;
-    export type Mutation = CancelOrderMutation;
-    export type CancelOrder = NonNullable<CancelOrderMutation['cancelOrder']>;
-    export type ErrorResultInlineFragment = DiscriminateUnion<
-        NonNullable<CancelOrderMutation['cancelOrder']>,
-        { __typename?: 'ErrorResult' }
-    >;
-}
-
-export namespace CanceledOrder {
-    export type Fragment = CanceledOrderFragment;
-    export type Lines = NonNullable<NonNullable<CanceledOrderFragment['lines']>[number]>;
-    export type Items = NonNullable<
-        NonNullable<NonNullable<NonNullable<CanceledOrderFragment['lines']>[number]>['items']>[number]
-    >;
-}
-
 export namespace Refund {
     export type Fragment = RefundFragment;
 }

+ 31 - 3
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -2247,6 +2247,8 @@ export type ShippingMethodList = PaginatedList & {
 
 export enum StockMovementType {
     ADJUSTMENT = 'ADJUSTMENT',
+    ALLOCATION = 'ALLOCATION',
+    RELEASE = 'RELEASE',
     SALE = 'SALE',
     CANCELLATION = 'CANCELLATION',
     RETURN = 'RETURN',
@@ -2271,7 +2273,7 @@ export type StockAdjustment = Node &
         quantity: Scalars['Int'];
     };
 
-export type Sale = Node &
+export type Allocation = Node &
     StockMovement & {
         id: Scalars['ID'];
         createdAt: Scalars['DateTime'];
@@ -2282,6 +2284,17 @@ export type Sale = Node &
         orderLine: OrderLine;
     };
 
+export type Sale = Node &
+    StockMovement & {
+        id: Scalars['ID'];
+        createdAt: Scalars['DateTime'];
+        updatedAt: Scalars['DateTime'];
+        productVariant: ProductVariant;
+        type: StockMovementType;
+        quantity: Scalars['Int'];
+        orderItem: OrderItem;
+    };
+
 export type Cancellation = Node &
     StockMovement & {
         id: Scalars['ID'];
@@ -2304,7 +2317,18 @@ export type Return = Node &
         orderItem: OrderItem;
     };
 
-export type StockMovementItem = StockAdjustment | Sale | Cancellation | Return;
+export type Release = Node &
+    StockMovement & {
+        id: Scalars['ID'];
+        createdAt: Scalars['DateTime'];
+        updatedAt: Scalars['DateTime'];
+        productVariant: ProductVariant;
+        type: StockMovementType;
+        quantity: Scalars['Int'];
+        orderItem: OrderItem;
+    };
+
+export type StockMovementItem = StockAdjustment | Allocation | Sale | Cancellation | Return | Release;
 
 export type StockMovementList = {
     items: Array<StockMovementItem>;
@@ -2907,7 +2931,7 @@ export type AddPaymentToOrderMutation = {
         | Pick<OrderPaymentStateError, 'errorCode' | 'message'>
         | Pick<PaymentFailedError, 'errorCode' | 'message' | 'paymentErrorMessage'>
         | Pick<PaymentDeclinedError, 'errorCode' | 'message' | 'paymentErrorMessage'>
-        | Pick<OrderStateTransitionError, 'errorCode' | 'message'>
+        | Pick<OrderStateTransitionError, 'errorCode' | 'message' | 'transitionError'>
     >;
 };
 
@@ -3334,6 +3358,10 @@ export namespace AddPaymentToOrder {
         NonNullable<AddPaymentToOrderMutation['addPaymentToOrder']>,
         { __typename?: 'PaymentFailedError' }
     >;
+    export type OrderStateTransitionErrorInlineFragment = DiscriminateUnion<
+        NonNullable<AddPaymentToOrderMutation['addPaymentToOrder']>,
+        { __typename?: 'OrderStateTransitionError' }
+    >;
 }
 
 export namespace GetActiveOrderPayments {

+ 22 - 0
packages/core/e2e/graphql/shared-definitions.ts

@@ -669,3 +669,25 @@ export const ADMIN_TRANSITION_TO_STATE = gql`
     }
     ${ORDER_FRAGMENT}
 `;
+
+export const CANCEL_ORDER = gql`
+    mutation CancelOrder($input: CancelOrderInput!) {
+        cancelOrder(input: $input) {
+            ...CanceledOrder
+            ... on ErrorResult {
+                errorCode
+                message
+            }
+        }
+    }
+    fragment CanceledOrder on Order {
+        id
+        lines {
+            quantity
+            items {
+                id
+                cancelled
+            }
+        }
+    }
+`;

+ 3 - 0
packages/core/e2e/graphql/shop-definitions.ts

@@ -488,6 +488,9 @@ export const ADD_PAYMENT = gql`
             ... on PaymentFailedError {
                 paymentErrorMessage
             }
+            ... on OrderStateTransitionError {
+                transitionError
+            }
         }
     }
     ${TEST_ORDER_WITH_PAYMENTS_FRAGMENT}

+ 32 - 52
packages/core/e2e/order.e2e-spec.ts

@@ -10,7 +10,7 @@ import gql from 'graphql-tag';
 import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
+import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 
 import {
     failsToSettlePaymentMethod,
@@ -37,6 +37,7 @@ import {
     GetOrderWithPayments,
     GetProductWithVariants,
     GetStockMovement,
+    GlobalFlag,
     HistoryEntryType,
     PaymentFragment,
     RefundFragment,
@@ -53,18 +54,17 @@ import {
     AddItemToOrder,
     DeletionResult,
     GetActiveOrder,
-    GetActiveOrderWithPayments,
-    GetOrderByCode,
     GetOrderByCodeWithPayments,
     TestOrderFragmentFragment,
     UpdatedOrder,
 } from './graphql/generated-e2e-shop-types';
 import {
+    CANCEL_ORDER,
     CREATE_FULFILLMENT,
     GET_CUSTOMER_LIST,
     GET_ORDER,
-    GET_ORDER_FULFILLMENTS,
     GET_ORDERS_LIST,
+    GET_ORDER_FULFILLMENTS,
     GET_PRODUCT_WITH_VARIANTS,
     GET_STOCK_MOVEMENT,
     TRANSIT_FULFILLMENT,
@@ -73,10 +73,7 @@ import {
 import {
     ADD_ITEM_TO_ORDER,
     GET_ACTIVE_ORDER,
-    GET_ACTIVE_ORDER_WITH_PAYMENTS,
-    GET_ORDER_BY_CODE,
     GET_ORDER_BY_CODE_WITH_PAYMENTS,
-    TEST_ORDER_WITH_PAYMENTS_FRAGMENT,
 } from './graphql/shop-definitions';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { addPaymentToOrder, proceedToArrangingPayment } from './utils/test-order-utils';
@@ -735,10 +732,11 @@ describe('Orders resolver', () => {
                 },
             );
             let variant1 = result1.product!.variants[0];
-            expect(variant1.stockOnHand).toBe(98);
+            expect(variant1.stockOnHand).toBe(100);
+            expect(variant1.stockAllocated).toBe(2);
             expect(variant1.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
                 { type: StockMovementType.ADJUSTMENT, quantity: 100 },
-                { type: StockMovementType.SALE, quantity: -2 },
+                { type: StockMovementType.ALLOCATION, quantity: 2 },
             ]);
 
             const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(
@@ -775,11 +773,12 @@ describe('Orders resolver', () => {
             );
             variant1 = result2.product!.variants[0];
             expect(variant1.stockOnHand).toBe(100);
+            expect(variant1.stockAllocated).toBe(0);
             expect(variant1.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
                 { type: StockMovementType.ADJUSTMENT, quantity: 100 },
-                { type: StockMovementType.SALE, quantity: -2 },
-                { type: StockMovementType.CANCELLATION, quantity: 1 },
-                { type: StockMovementType.CANCELLATION, quantity: 1 },
+                { type: StockMovementType.ALLOCATION, quantity: 2 },
+                { type: StockMovementType.RELEASE, quantity: 1 },
+                { type: StockMovementType.RELEASE, quantity: 1 },
             ]);
         });
 
@@ -909,13 +908,14 @@ describe('Orders resolver', () => {
                 },
             );
             const variant1 = result1.product!.variants[0];
-            expect(variant1.stockOnHand).toBe(98);
+            expect(variant1.stockOnHand).toBe(100);
+            expect(variant1.stockAllocated).toBe(2);
             expect(variant1.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
                 { type: StockMovementType.ADJUSTMENT, quantity: 100 },
-                { type: StockMovementType.SALE, quantity: -2 },
-                { type: StockMovementType.CANCELLATION, quantity: 1 },
-                { type: StockMovementType.CANCELLATION, quantity: 1 },
-                { type: StockMovementType.SALE, quantity: -2 },
+                { type: StockMovementType.ALLOCATION, quantity: 2 },
+                { type: StockMovementType.RELEASE, quantity: 1 },
+                { type: StockMovementType.RELEASE, quantity: 1 },
+                { type: StockMovementType.ALLOCATION, quantity: 2 },
             ]);
 
             const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
@@ -954,14 +954,15 @@ describe('Orders resolver', () => {
                 },
             );
             const variant2 = result2.product!.variants[0];
-            expect(variant2.stockOnHand).toBe(99);
+            expect(variant2.stockOnHand).toBe(100);
+            expect(variant2.stockAllocated).toBe(1);
             expect(variant2.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
                 { type: StockMovementType.ADJUSTMENT, quantity: 100 },
-                { type: StockMovementType.SALE, quantity: -2 },
-                { type: StockMovementType.CANCELLATION, quantity: 1 },
-                { type: StockMovementType.CANCELLATION, quantity: 1 },
-                { type: StockMovementType.SALE, quantity: -2 },
-                { type: StockMovementType.CANCELLATION, quantity: 1 },
+                { type: StockMovementType.ALLOCATION, quantity: 2 },
+                { type: StockMovementType.RELEASE, quantity: 1 },
+                { type: StockMovementType.RELEASE, quantity: 1 },
+                { type: StockMovementType.ALLOCATION, quantity: 2 },
+                { type: StockMovementType.RELEASE, quantity: 1 },
             ]);
         });
 
@@ -1011,14 +1012,15 @@ describe('Orders resolver', () => {
             );
             const variant2 = result.product!.variants[0];
             expect(variant2.stockOnHand).toBe(100);
+            expect(variant2.stockAllocated).toBe(0);
             expect(variant2.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
                 { type: StockMovementType.ADJUSTMENT, quantity: 100 },
-                { type: StockMovementType.SALE, quantity: -2 },
-                { type: StockMovementType.CANCELLATION, quantity: 1 },
-                { type: StockMovementType.CANCELLATION, quantity: 1 },
-                { type: StockMovementType.SALE, quantity: -2 },
-                { type: StockMovementType.CANCELLATION, quantity: 1 },
-                { type: StockMovementType.CANCELLATION, quantity: 1 },
+                { type: StockMovementType.ALLOCATION, quantity: 2 },
+                { type: StockMovementType.RELEASE, quantity: 1 },
+                { type: StockMovementType.RELEASE, quantity: 1 },
+                { type: StockMovementType.ALLOCATION, quantity: 2 },
+                { type: StockMovementType.RELEASE, quantity: 1 },
+                { type: StockMovementType.RELEASE, quantity: 1 },
             ]);
         });
 
@@ -1488,7 +1490,7 @@ async function createTestOrder(
         input: [
             {
                 id: productVariantId,
-                trackInventory: true,
+                trackInventory: GlobalFlag.TRUE,
             },
         ],
     });
@@ -1556,28 +1558,6 @@ export const GET_ORDER_FULFILLMENT_ITEMS = gql`
     ${FULFILLMENT_FRAGMENT}
 `;
 
-export const CANCEL_ORDER = gql`
-    mutation CancelOrder($input: CancelOrderInput!) {
-        cancelOrder(input: $input) {
-            ...CanceledOrder
-            ... on ErrorResult {
-                errorCode
-                message
-            }
-        }
-    }
-    fragment CanceledOrder on Order {
-        id
-        lines {
-            quantity
-            items {
-                id
-                cancelled
-            }
-        }
-    }
-`;
-
 const REFUND_FRAGMENT = gql`
     fragment Refund on Refund {
         id

+ 145 - 11
packages/core/e2e/stock-control.e2e-spec.ts

@@ -1,6 +1,6 @@
 /* tslint:disable:no-non-null-assertion */
 import { mergeConfig, OrderState } from '@vendure/core';
-import { createTestEnvironment } from '@vendure/testing';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
 import path from 'path';
 
@@ -10,7 +10,10 @@ import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-conf
 import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
 import { VARIANT_WITH_STOCK_FRAGMENT } from './graphql/fragments';
 import {
+    CancelOrder,
     CreateAddressInput,
+    CreateFulfillment,
+    GetOrder,
     GetStockMovement,
     GlobalFlag,
     StockMovementType,
@@ -23,9 +26,15 @@ import {
     AddPaymentToOrder,
     PaymentInput,
     SetShippingAddress,
+    TestOrderFragmentFragment,
     TransitionToState,
 } from './graphql/generated-e2e-shop-types';
-import { GET_STOCK_MOVEMENT } from './graphql/shared-definitions';
+import {
+    CANCEL_ORDER,
+    CREATE_FULFILLMENT,
+    GET_ORDER,
+    GET_STOCK_MOVEMENT,
+} from './graphql/shared-definitions';
 import {
     ADD_ITEM_TO_ORDER,
     ADD_PAYMENT,
@@ -43,6 +52,10 @@ describe('Stock control', () => {
         }),
     );
 
+    const orderGuard: ErrorResultGuard<TestOrderFragmentFragment> = createErrorResultGuard<
+        TestOrderFragmentFragment
+    >(input => !!input.lines);
+
     beforeAll(async () => {
         await server.init({
             initialData,
@@ -146,6 +159,8 @@ describe('Stock control', () => {
     });
 
     describe('sales', () => {
+        let orderId: string;
+
         beforeAll(async () => {
             const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
                 GET_STOCK_MOVEMENT,
@@ -202,7 +217,7 @@ describe('Stock control', () => {
             );
         });
 
-        it('creates a Sale when order completed', async () => {
+        it('creates an Allocation when order completed', async () => {
             const { addPaymentToOrder } = await shopClient.query<
                 AddPaymentToOrder.Mutation,
                 AddPaymentToOrder.Variables
@@ -212,7 +227,9 @@ describe('Stock control', () => {
                     metadata: {},
                 } as PaymentInput,
             });
+            orderGuard.assertSuccess(addPaymentToOrder);
             expect(addPaymentToOrder).not.toBeNull();
+            orderId = addPaymentToOrder.id;
 
             const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
                 GET_STOCK_MOVEMENT,
@@ -221,19 +238,98 @@ describe('Stock control', () => {
             const [variant1, variant2, variant3] = product!.variants;
 
             expect(variant1.stockMovements.totalItems).toBe(2);
-            expect(variant1.stockMovements.items[1].type).toBe(StockMovementType.SALE);
-            expect(variant1.stockMovements.items[1].quantity).toBe(-2);
+            expect(variant1.stockMovements.items[1].type).toBe(StockMovementType.ALLOCATION);
+            expect(variant1.stockMovements.items[1].quantity).toBe(2);
 
             expect(variant2.stockMovements.totalItems).toBe(2);
-            expect(variant2.stockMovements.items[1].type).toBe(StockMovementType.SALE);
-            expect(variant2.stockMovements.items[1].quantity).toBe(-3);
+            expect(variant2.stockMovements.items[1].type).toBe(StockMovementType.ALLOCATION);
+            expect(variant2.stockMovements.items[1].quantity).toBe(3);
 
             expect(variant3.stockMovements.totalItems).toBe(2);
-            expect(variant3.stockMovements.items[1].type).toBe(StockMovementType.SALE);
-            expect(variant3.stockMovements.items[1].quantity).toBe(-4);
+            expect(variant3.stockMovements.items[1].type).toBe(StockMovementType.ALLOCATION);
+            expect(variant3.stockMovements.items[1].quantity).toBe(4);
         });
 
-        it('stockOnHand is updated according to trackInventory setting', async () => {
+        it('stockAllocated is updated according to trackInventory setting', async () => {
+            const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
+                GET_STOCK_MOVEMENT,
+                { id: 'T_2' },
+            );
+            const [variant1, variant2, variant3] = product!.variants;
+
+            // stockOnHand not changed yet
+            expect(variant1.stockOnHand).toBe(5);
+            expect(variant2.stockOnHand).toBe(5);
+            expect(variant3.stockOnHand).toBe(5);
+
+            expect(variant1.stockAllocated).toBe(0); // untracked inventory
+            expect(variant2.stockAllocated).toBe(3); // tracked inventory
+            expect(variant3.stockAllocated).toBe(0); // inherited untracked inventory
+        });
+
+        it('creates a Release on cancelling an allocated OrderItem and updates stockAllocated', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: orderId,
+            });
+
+            await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
+                input: {
+                    orderId: order!.id,
+                    lines: [{ orderLineId: order!.lines.find(l => l.quantity === 3)!.id, quantity: 1 }],
+                    reason: 'Not needed',
+                },
+            });
+
+            const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
+                GET_STOCK_MOVEMENT,
+                { id: 'T_2' },
+            );
+            const [_, variant2, __] = product!.variants;
+
+            expect(variant2.stockMovements.totalItems).toBe(3);
+            expect(variant2.stockMovements.items[2].type).toBe(StockMovementType.RELEASE);
+            expect(variant2.stockMovements.items[2].quantity).toBe(1);
+
+            expect(variant2.stockAllocated).toBe(2);
+        });
+
+        it('creates a Sale on Fulfillment creation', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: orderId,
+            });
+
+            await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
+                CREATE_FULFILLMENT,
+                {
+                    input: {
+                        lines: order?.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })) ?? [],
+                        method: 'test method',
+                        trackingCode: 'ABC123',
+                    },
+                },
+            );
+
+            const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
+                GET_STOCK_MOVEMENT,
+                { id: 'T_2' },
+            );
+            const [variant1, variant2, variant3] = product!.variants;
+
+            expect(variant1.stockMovements.totalItems).toBe(3);
+            expect(variant1.stockMovements.items[2].type).toBe(StockMovementType.SALE);
+            expect(variant1.stockMovements.items[2].quantity).toBe(-2);
+
+            // 4 rather than 3 since a Release was created in the previous test
+            expect(variant2.stockMovements.totalItems).toBe(4);
+            expect(variant2.stockMovements.items[3].type).toBe(StockMovementType.SALE);
+            expect(variant2.stockMovements.items[3].quantity).toBe(-2);
+
+            expect(variant3.stockMovements.totalItems).toBe(3);
+            expect(variant3.stockMovements.items[2].type).toBe(StockMovementType.SALE);
+            expect(variant3.stockMovements.items[2].quantity).toBe(-4);
+        });
+
+        it('updates stockOnHand and stockAllocated when Sales are created', async () => {
             const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
                 GET_STOCK_MOVEMENT,
                 { id: 'T_2' },
@@ -241,8 +337,46 @@ describe('Stock control', () => {
             const [variant1, variant2, variant3] = product!.variants;
 
             expect(variant1.stockOnHand).toBe(5); // untracked inventory
-            expect(variant2.stockOnHand).toBe(2); // tracked inventory
+            expect(variant2.stockOnHand).toBe(3); // tracked inventory
             expect(variant3.stockOnHand).toBe(5); // inherited untracked inventory
+
+            expect(variant1.stockAllocated).toBe(0); // untracked inventory
+            expect(variant2.stockAllocated).toBe(0); // tracked inventory
+            expect(variant3.stockAllocated).toBe(0); // inherited untracked inventory
+        });
+
+        it('creates Cancellations when cancelling items which are part of a Fulfillment', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: orderId,
+            });
+
+            await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
+                input: {
+                    orderId: order!.id,
+                    lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
+                    reason: 'Faulty',
+                },
+            });
+
+            const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
+                GET_STOCK_MOVEMENT,
+                { id: 'T_2' },
+            );
+            const [variant1, variant2, variant3] = product!.variants;
+
+            expect(variant1.stockMovements.totalItems).toBe(5);
+            expect(variant1.stockMovements.items[3].type).toBe(StockMovementType.CANCELLATION);
+            expect(variant1.stockMovements.items[4].type).toBe(StockMovementType.CANCELLATION);
+
+            expect(variant2.stockMovements.totalItems).toBe(6);
+            expect(variant2.stockMovements.items[4].type).toBe(StockMovementType.CANCELLATION);
+            expect(variant2.stockMovements.items[5].type).toBe(StockMovementType.CANCELLATION);
+
+            expect(variant3.stockMovements.totalItems).toBe(7);
+            expect(variant3.stockMovements.items[3].type).toBe(StockMovementType.CANCELLATION);
+            expect(variant3.stockMovements.items[4].type).toBe(StockMovementType.CANCELLATION);
+            expect(variant3.stockMovements.items[5].type).toBe(StockMovementType.CANCELLATION);
+            expect(variant3.stockMovements.items[6].type).toBe(StockMovementType.CANCELLATION);
         });
     });
 });

+ 4 - 0
packages/core/src/api/config/configure-graphql-module.ts

@@ -91,12 +91,16 @@ async function createGraphQLOptions(
             switch (value.type) {
                 case StockMovementType.ADJUSTMENT:
                     return 'StockAdjustment';
+                case StockMovementType.ALLOCATION:
+                    return 'Allocation';
                 case StockMovementType.SALE:
                     return 'Sale';
                 case StockMovementType.CANCELLATION:
                     return 'Cancellation';
                 case StockMovementType.RETURN:
                     return 'Return';
+                case StockMovementType.RELEASE:
+                    return 'Release';
             }
         },
     };

+ 1 - 0
packages/core/src/api/schema/admin-api/product.api.graphql

@@ -46,6 +46,7 @@ type Product implements Node {
 type ProductVariant implements Node {
     enabled: Boolean!
     stockOnHand: Int!
+    stockAllocated: Int!
     trackInventory: GlobalFlag!
     stockMovements(options: StockMovementListOptions): StockMovementList!
 }

+ 24 - 2
packages/core/src/api/schema/type/stock-movement.type.graphql

@@ -1,5 +1,7 @@
 enum StockMovementType {
     ADJUSTMENT
+    ALLOCATION
+    RELEASE
     SALE
     CANCELLATION
     RETURN
@@ -23,7 +25,7 @@ type StockAdjustment implements Node & StockMovement {
     quantity: Int!
 }
 
-type Sale implements Node & StockMovement {
+type Allocation implements Node & StockMovement {
     id: ID!
     createdAt: DateTime!
     updatedAt: DateTime!
@@ -33,6 +35,16 @@ type Sale implements Node & StockMovement {
     orderLine: OrderLine!
 }
 
+type Sale implements Node & StockMovement {
+    id: ID!
+    createdAt: DateTime!
+    updatedAt: DateTime!
+    productVariant: ProductVariant!
+    type: StockMovementType!
+    quantity: Int!
+    orderItem: OrderItem!
+}
+
 type Cancellation implements Node & StockMovement {
     id: ID!
     createdAt: DateTime!
@@ -53,7 +65,17 @@ type Return implements Node & StockMovement {
     orderItem: OrderItem!
 }
 
-union StockMovementItem = StockAdjustment | Sale | Cancellation | Return
+type Release implements Node & StockMovement {
+    id: ID!
+    createdAt: DateTime!
+    updatedAt: DateTime!
+    productVariant: ProductVariant!
+    type: StockMovementType!
+    quantity: Int!
+    orderItem: OrderItem!
+}
+
+union StockMovementItem = StockAdjustment | Allocation | Sale | Cancellation | Return | Release
 
 type StockMovementList {
     items: [StockMovementItem!]!

+ 27 - 27
packages/core/src/data-import/providers/import-parser/__snapshots__/import-parser.spec.ts.snap

@@ -38,7 +38,7 @@ Array [
         "sku": "PPS12",
         "stockOnHand": 10,
         "taxCategory": "standard",
-        "trackInventory": false,
+        "trackInventory": "FALSE",
       },
       Object {
         "assetPaths": Array [],
@@ -53,7 +53,7 @@ Array [
         "sku": "PPS14",
         "stockOnHand": 10,
         "taxCategory": "standard",
-        "trackInventory": false,
+        "trackInventory": "FALSE",
       },
       Object {
         "assetPaths": Array [],
@@ -68,7 +68,7 @@ Array [
         "sku": "PPSF",
         "stockOnHand": 10,
         "taxCategory": "standard",
-        "trackInventory": false,
+        "trackInventory": "FALSE",
       },
     ],
   },
@@ -108,7 +108,7 @@ Array [
         "sku": "PPS12",
         "stockOnHand": 10,
         "taxCategory": "standard",
-        "trackInventory": false,
+        "trackInventory": "FALSE",
       },
       Object {
         "assetPaths": Array [],
@@ -121,7 +121,7 @@ Array [
         "sku": "PPS14",
         "stockOnHand": 11,
         "taxCategory": "standard",
-        "trackInventory": true,
+        "trackInventory": "TRUE",
       },
       Object {
         "assetPaths": Array [],
@@ -134,7 +134,7 @@ Array [
         "sku": "PPSF",
         "stockOnHand": 12,
         "taxCategory": "standard",
-        "trackInventory": false,
+        "trackInventory": "FALSE",
       },
     ],
   },
@@ -158,7 +158,7 @@ Array [
         "sku": "M02",
         "stockOnHand": 13,
         "taxCategory": "standard",
-        "trackInventory": true,
+        "trackInventory": "TRUE",
       },
     ],
   },
@@ -192,7 +192,7 @@ Array [
         "sku": "225400",
         "stockOnHand": 14,
         "taxCategory": "standard",
-        "trackInventory": false,
+        "trackInventory": "FALSE",
       },
       Object {
         "assetPaths": Array [],
@@ -205,7 +205,7 @@ Array [
         "sku": "225600",
         "stockOnHand": 15,
         "taxCategory": "standard",
-        "trackInventory": true,
+        "trackInventory": "TRUE",
       },
     ],
   },
@@ -247,7 +247,7 @@ Array [
         "sku": "10112",
         "stockOnHand": 16,
         "taxCategory": "standard",
-        "trackInventory": false,
+        "trackInventory": "FALSE",
       },
       Object {
         "assetPaths": Array [],
@@ -261,7 +261,7 @@ Array [
         "sku": "10113",
         "stockOnHand": 17,
         "taxCategory": "standard",
-        "trackInventory": true,
+        "trackInventory": "TRUE",
       },
       Object {
         "assetPaths": Array [],
@@ -275,7 +275,7 @@ Array [
         "sku": "10114",
         "stockOnHand": 18,
         "taxCategory": "standard",
-        "trackInventory": false,
+        "trackInventory": "FALSE",
       },
       Object {
         "assetPaths": Array [],
@@ -289,7 +289,7 @@ Array [
         "sku": "10115",
         "stockOnHand": 19,
         "taxCategory": "standard",
-        "trackInventory": true,
+        "trackInventory": "TRUE",
       },
     ],
   },
@@ -329,7 +329,7 @@ Array [
         "sku": "PPS12",
         "stockOnHand": 10,
         "taxCategory": "standard",
-        "trackInventory": false,
+        "trackInventory": "FALSE",
       },
       Object {
         "assetPaths": Array [],
@@ -342,7 +342,7 @@ Array [
         "sku": "PPS14",
         "stockOnHand": 10,
         "taxCategory": "standard",
-        "trackInventory": false,
+        "trackInventory": "FALSE",
       },
       Object {
         "assetPaths": Array [],
@@ -355,7 +355,7 @@ Array [
         "sku": "PPSF",
         "stockOnHand": 10,
         "taxCategory": "standard",
-        "trackInventory": false,
+        "trackInventory": "FALSE",
       },
     ],
   },
@@ -401,7 +401,7 @@ Array [
         "sku": "PPS12",
         "stockOnHand": 10,
         "taxCategory": "standard",
-        "trackInventory": false,
+        "trackInventory": "FALSE",
       },
     ],
   },
@@ -441,7 +441,7 @@ Array [
         "sku": "PPS12",
         "stockOnHand": 10,
         "taxCategory": "standard",
-        "trackInventory": false,
+        "trackInventory": "FALSE",
       },
       Object {
         "assetPaths": Array [],
@@ -454,7 +454,7 @@ Array [
         "sku": "PPS14",
         "stockOnHand": 11,
         "taxCategory": "standard",
-        "trackInventory": true,
+        "trackInventory": "TRUE",
       },
       Object {
         "assetPaths": Array [],
@@ -467,7 +467,7 @@ Array [
         "sku": "PPSF",
         "stockOnHand": 12,
         "taxCategory": "standard",
-        "trackInventory": false,
+        "trackInventory": "FALSE",
       },
     ],
   },
@@ -491,7 +491,7 @@ Array [
         "sku": "M02",
         "stockOnHand": 13,
         "taxCategory": "standard",
-        "trackInventory": true,
+        "trackInventory": "TRUE",
       },
     ],
   },
@@ -525,7 +525,7 @@ Array [
         "sku": "225400",
         "stockOnHand": 14,
         "taxCategory": "standard",
-        "trackInventory": false,
+        "trackInventory": "FALSE",
       },
       Object {
         "assetPaths": Array [],
@@ -538,7 +538,7 @@ Array [
         "sku": "225600",
         "stockOnHand": 15,
         "taxCategory": "standard",
-        "trackInventory": true,
+        "trackInventory": "TRUE",
       },
     ],
   },
@@ -580,7 +580,7 @@ Array [
         "sku": "10112",
         "stockOnHand": 16,
         "taxCategory": "standard",
-        "trackInventory": false,
+        "trackInventory": "FALSE",
       },
       Object {
         "assetPaths": Array [],
@@ -594,7 +594,7 @@ Array [
         "sku": "10113",
         "stockOnHand": 17,
         "taxCategory": "standard",
-        "trackInventory": true,
+        "trackInventory": "TRUE",
       },
       Object {
         "assetPaths": Array [],
@@ -608,7 +608,7 @@ Array [
         "sku": "10114",
         "stockOnHand": 18,
         "taxCategory": "standard",
-        "trackInventory": false,
+        "trackInventory": "FALSE",
       },
       Object {
         "assetPaths": Array [],
@@ -622,7 +622,7 @@ Array [
         "sku": "10115",
         "stockOnHand": 19,
         "taxCategory": "standard",
-        "trackInventory": true,
+        "trackInventory": "TRUE",
       },
     ],
   },

+ 4 - 0
packages/core/src/entity/entities.ts

@@ -44,7 +44,9 @@ import { AnonymousSession } from './session/anonymous-session.entity';
 import { AuthenticatedSession } from './session/authenticated-session.entity';
 import { Session } from './session/session.entity';
 import { ShippingMethod } from './shipping-method/shipping-method.entity';
+import { Allocation } from './stock-movement/allocation.entity';
 import { Cancellation } from './stock-movement/cancellation.entity';
+import { Release } from './stock-movement/release.entity';
 import { Sale } from './stock-movement/sale.entity';
 import { StockAdjustment } from './stock-movement/stock-adjustment.entity';
 import { StockMovement } from './stock-movement/stock-movement.entity';
@@ -59,6 +61,7 @@ import { Zone } from './zone/zone.entity';
 export const coreEntitiesMap = {
     Address,
     Administrator,
+    Allocation,
     AnonymousSession,
     Asset,
     AuthenticatedSession,
@@ -101,6 +104,7 @@ export const coreEntitiesMap = {
     ProductVariantTranslation,
     Promotion,
     Refund,
+    Release,
     Role,
     Sale,
     Session,

+ 3 - 0
packages/core/src/entity/product-variant/product-variant.entity.ts

@@ -103,6 +103,9 @@ export class ProductVariant extends VendureEntity implements Translatable, HasCu
     @Column({ default: 0 })
     stockOnHand: number;
 
+    @Column({ default: 0 })
+    stockAllocated: number;
+
     @Column({ type: 'varchar', default: GlobalFlag.INHERIT })
     trackInventory: GlobalFlag;
 

+ 19 - 0
packages/core/src/entity/stock-movement/allocation.entity.ts

@@ -0,0 +1,19 @@
+import { StockMovementType } from '@vendure/common/lib/generated-types';
+import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { ChildEntity, ManyToOne } from 'typeorm';
+
+import { OrderLine } from '../order-line/order-line.entity';
+
+import { StockMovement } from './stock-movement.entity';
+
+@ChildEntity()
+export class Allocation extends StockMovement {
+    readonly type = StockMovementType.ALLOCATION;
+
+    constructor(input: DeepPartial<Allocation>) {
+        super(input);
+    }
+
+    @ManyToOne(type => OrderLine)
+    orderLine: OrderLine;
+}

+ 2 - 3
packages/core/src/entity/stock-movement/cancellation.entity.ts

@@ -1,6 +1,6 @@
 import { StockMovementType } from '@vendure/common/lib/generated-types';
 import { DeepPartial } from '@vendure/common/lib/shared-types';
-import { ChildEntity, JoinColumn, OneToOne } from 'typeorm';
+import { ChildEntity, ManyToOne } from 'typeorm';
 
 import { OrderItem } from '../order-item/order-item.entity';
 
@@ -14,7 +14,6 @@ export class Cancellation extends StockMovement {
         super(input);
     }
 
-    @OneToOne(type => OrderItem, orderItem => orderItem.cancellation)
-    @JoinColumn()
+    @ManyToOne(type => OrderItem, orderItem => orderItem.cancellation)
     orderItem: OrderItem;
 }

+ 19 - 0
packages/core/src/entity/stock-movement/release.entity.ts

@@ -0,0 +1,19 @@
+import { StockMovementType } from '@vendure/common/lib/generated-types';
+import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { ChildEntity, ManyToOne } from 'typeorm';
+
+import { OrderItem } from '../order-item/order-item.entity';
+
+import { StockMovement } from './stock-movement.entity';
+
+@ChildEntity()
+export class Release extends StockMovement {
+    readonly type = StockMovementType.RELEASE;
+
+    constructor(input: DeepPartial<Release>) {
+        super(input);
+    }
+
+    @ManyToOne(type => OrderItem)
+    orderItem: OrderItem;
+}

+ 2 - 2
packages/core/src/entity/stock-movement/sale.entity.ts

@@ -1,6 +1,6 @@
 import { StockMovementType } from '@vendure/common/lib/generated-types';
 import { DeepPartial } from '@vendure/common/lib/shared-types';
-import { ChildEntity, OneToOne } from 'typeorm';
+import { ChildEntity, ManyToOne } from 'typeorm';
 
 import { OrderLine } from '../order-line/order-line.entity';
 
@@ -14,6 +14,6 @@ export class Sale extends StockMovement {
         super(input);
     }
 
-    @OneToOne(type => OrderLine)
+    @ManyToOne(type => OrderLine)
     orderLine: OrderLine;
 }

+ 1 - 1
packages/core/src/service/helpers/order-state-machine/order-state-machine.ts

@@ -122,7 +122,7 @@ export class OrderStateMachine {
         if (toState === 'PaymentAuthorized' || toState === 'PaymentSettled') {
             data.order.active = false;
             data.order.orderPlacedAt = new Date();
-            await this.stockMovementService.createSalesForOrder(data.ctx, data.order);
+            await this.stockMovementService.createAllocationsForOrder(data.ctx, data.order);
             await this.promotionService.addPromotionsToOrder(data.ctx, data.order);
         }
         if (toState === 'Cancelled') {

+ 12 - 3
packages/core/src/service/services/order.service.ts

@@ -684,7 +684,11 @@ export class OrderService {
         ) {
             return new EmptyOrderLineSelectionError();
         }
-        const ordersAndItems = await this.getOrdersAndItemsFromLines(ctx, input.lines, i => !i.fulfillment);
+        const ordersAndItems = await this.getOrdersAndItemsFromLines(
+            ctx,
+            input.lines,
+            i => !i.fulfillment && !i.cancelled,
+        );
         if (!ordersAndItems) {
             return new ItemsAlreadyFulfilledError();
         }
@@ -695,6 +699,8 @@ export class OrderService {
             orderItems: ordersAndItems.items,
         });
 
+        await this.stockMovementService.createSalesForOrder(ctx, ordersAndItems.items);
+
         for (const order of ordersAndItems.orders) {
             await this.historyService.createHistoryEntryForOrder({
                 ctx,
@@ -790,9 +796,12 @@ export class OrderService {
         if (order.state === 'AddingItems' || order.state === 'ArrangingPayment') {
             return new CancelActiveOrderError(order.state);
         }
+        const fullOrder = await this.findOne(ctx, order.id);
 
-        // Perform the cancellation
-        await this.stockMovementService.createCancellationsForOrderItems(ctx, items);
+        const soldItems = items.filter(i => !!i.fulfillment);
+        const allocatedItems = items.filter(i => !i.fulfillment);
+        await this.stockMovementService.createCancellationsForOrderItems(ctx, soldItems);
+        await this.stockMovementService.createReleasesForOrderItems(ctx, allocatedItems);
         items.forEach(i => (i.cancelled = true));
         await this.connection.getRepository(ctx, OrderItem).save(items, { reload: false });
 

+ 85 - 6
packages/core/src/service/services/stock-movement.service.ts

@@ -7,10 +7,13 @@ import { InternalServerError } from '../../common/error/errors';
 import { ShippingCalculator } from '../../config/shipping-method/shipping-calculator';
 import { ShippingEligibilityChecker } from '../../config/shipping-method/shipping-eligibility-checker';
 import { OrderItem } from '../../entity/order-item/order-item.entity';
+import { OrderLine } from '../../entity/order-line/order-line.entity';
 import { Order } from '../../entity/order/order.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { ShippingMethod } from '../../entity/shipping-method/shipping-method.entity';
+import { Allocation } from '../../entity/stock-movement/allocation.entity';
 import { Cancellation } from '../../entity/stock-movement/cancellation.entity';
+import { Release } from '../../entity/stock-movement/release.entity';
 import { Sale } from '../../entity/stock-movement/sale.entity';
 import { StockAdjustment } from '../../entity/stock-movement/stock-adjustment.entity';
 import { StockMovement } from '../../entity/stock-movement/stock-movement.entity';
@@ -67,23 +70,62 @@ export class StockMovementService {
         return this.connection.getRepository(ctx, StockAdjustment).save(adjustment);
     }
 
-    async createSalesForOrder(ctx: RequestContext, order: Order): Promise<Sale[]> {
+    async createAllocationsForOrder(ctx: RequestContext, order: Order): Promise<Allocation[]> {
         if (order.active !== false) {
-            throw new InternalServerError('error.cannot-create-sales-for-active-order');
+            throw new InternalServerError('error.cannot-create-allocations-for-active-order');
         }
-        const sales: Sale[] = [];
+        const allocations: Allocation[] = [];
         const globalTrackInventory = (await this.globalSettingsService.getSettings(ctx)).trackInventory;
         for (const line of order.lines) {
             const { productVariant } = line;
-            const sale = new Sale({
+            const allocation = new Allocation({
                 productVariant,
-                quantity: line.quantity * -1,
+                quantity: line.quantity,
                 orderLine: line,
             });
+            allocations.push(allocation);
+
+            if (this.trackInventoryForVariant(productVariant, globalTrackInventory)) {
+                productVariant.stockAllocated += line.quantity;
+                await this.connection
+                    .getRepository(ctx, ProductVariant)
+                    .save(productVariant, { reload: false });
+            }
+        }
+        return this.connection.getRepository(ctx, Allocation).save(allocations);
+    }
+
+    async createSalesForOrder(ctx: RequestContext, orderItems: OrderItem[]): Promise<Sale[]> {
+        const sales: Sale[] = [];
+        const globalTrackInventory = (await this.globalSettingsService.getSettings(ctx)).trackInventory;
+        const orderItemsWithVariants = await this.connection.getRepository(ctx, OrderItem).findByIds(
+            orderItems.map(i => i.id),
+            {
+                relations: ['line', 'line.productVariant'],
+            },
+        );
+        const orderLinesMap = new Map<ID, { line: OrderLine; items: OrderItem[] }>();
+
+        for (const orderItem of orderItemsWithVariants) {
+            let value = orderLinesMap.get(orderItem.line.id);
+            if (!value) {
+                value = { line: orderItem.line, items: [] };
+                orderLinesMap.set(orderItem.line.id, value);
+            }
+            value.items.push(orderItem);
+        }
+        for (const lineRow of orderLinesMap.values()) {
+            const { productVariant } = lineRow.line;
+            const sale = new Sale({
+                productVariant,
+                quantity: lineRow.items.length * -1,
+                orderLine: lineRow.line,
+            });
             sales.push(sale);
 
             if (this.trackInventoryForVariant(productVariant, globalTrackInventory)) {
-                productVariant.stockOnHand -= line.quantity;
+                productVariant.stockOnHand -= lineRow.items.length;
+                productVariant.stockAllocated -= lineRow.items.length;
                 await this.connection
                     .getRepository(ctx, ProductVariant)
                     .save(productVariant, { reload: false });
@@ -129,6 +171,43 @@ export class StockMovementService {
         return this.connection.getRepository(ctx, Cancellation).save(cancellations);
     }
 
+    async createReleasesForOrderItems(ctx: RequestContext, items: OrderItem[]): Promise<Release[]> {
+        const orderItems = await this.connection.getRepository(ctx, OrderItem).findByIds(
+            items.map(i => i.id),
+            {
+                relations: ['line', 'line.productVariant'],
+            },
+        );
+        const releases: Release[] = [];
+        const globalTrackInventory = (await this.globalSettingsService.getSettings(ctx)).trackInventory;
+        const variantsMap = new Map<ID, ProductVariant>();
+        for (const item of orderItems) {
+            let productVariant: ProductVariant;
+            const productVariantId = item.line.productVariant.id;
+            if (variantsMap.has(productVariantId)) {
+                // tslint:disable-next-line:no-non-null-assertion
+                productVariant = variantsMap.get(productVariantId)!;
+            } else {
+                productVariant = item.line.productVariant;
+                variantsMap.set(productVariantId, productVariant);
+            }
+            const release = new Release({
+                productVariant,
+                quantity: 1,
+                orderItem: item,
+            });
+            releases.push(release);
+
+            if (this.trackInventoryForVariant(productVariant, globalTrackInventory)) {
+                productVariant.stockAllocated -= 1;
+                await this.connection
+                    .getRepository(ctx, ProductVariant)
+                    .save(productVariant, { reload: false });
+            }
+        }
+        return this.connection.getRepository(ctx, Release).save(releases);
+    }
+
     private trackInventoryForVariant(variant: ProductVariant, globalTrackInventory: boolean): boolean {
         return (
             variant.trackInventory === GlobalFlag.TRUE ||

+ 30 - 2
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -1463,6 +1463,7 @@ export type Product = Node & {
 export type ProductVariant = Node & {
     enabled: Scalars['Boolean'];
     stockOnHand: Scalars['Int'];
+    stockAllocated: Scalars['Int'];
     trackInventory: GlobalFlag;
     stockMovements: StockMovementList;
     id: Scalars['ID'];
@@ -1946,6 +1947,7 @@ export type NativeAuthStrategyError = ErrorResult & {
 export type InvalidCredentialsError = ErrorResult & {
     errorCode: ErrorCode;
     message: Scalars['String'];
+    authenticationError: Scalars['String'];
 };
 
 /** Returned if there is an error in transitioning the Order state */
@@ -3270,6 +3272,8 @@ export type ShippingMethodList = PaginatedList & {
 
 export enum StockMovementType {
     ADJUSTMENT = 'ADJUSTMENT',
+    ALLOCATION = 'ALLOCATION',
+    RELEASE = 'RELEASE',
     SALE = 'SALE',
     CANCELLATION = 'CANCELLATION',
     RETURN = 'RETURN',
@@ -3294,7 +3298,7 @@ export type StockAdjustment = Node &
         quantity: Scalars['Int'];
     };
 
-export type Sale = Node &
+export type Allocation = Node &
     StockMovement & {
         id: Scalars['ID'];
         createdAt: Scalars['DateTime'];
@@ -3305,6 +3309,17 @@ export type Sale = Node &
         orderLine: OrderLine;
     };
 
+export type Sale = Node &
+    StockMovement & {
+        id: Scalars['ID'];
+        createdAt: Scalars['DateTime'];
+        updatedAt: Scalars['DateTime'];
+        productVariant: ProductVariant;
+        type: StockMovementType;
+        quantity: Scalars['Int'];
+        orderItem: OrderItem;
+    };
+
 export type Cancellation = Node &
     StockMovement & {
         id: Scalars['ID'];
@@ -3327,7 +3342,18 @@ export type Return = Node &
         orderItem: OrderItem;
     };
 
-export type StockMovementItem = StockAdjustment | Sale | Cancellation | Return;
+export type Release = Node &
+    StockMovement & {
+        id: Scalars['ID'];
+        createdAt: Scalars['DateTime'];
+        updatedAt: Scalars['DateTime'];
+        productVariant: ProductVariant;
+        type: StockMovementType;
+        quantity: Scalars['Int'];
+        orderItem: OrderItem;
+    };
+
+export type StockMovementItem = StockAdjustment | Allocation | Sale | Cancellation | Return | Release;
 
 export type StockMovementList = {
     items: Array<StockMovementItem>;
@@ -3792,6 +3818,7 @@ export type TaxRateSortParameter = {
 export type ProductVariantFilterParameter = {
     enabled?: Maybe<BooleanOperators>;
     stockOnHand?: Maybe<NumberOperators>;
+    stockAllocated?: Maybe<NumberOperators>;
     trackInventory?: Maybe<StringOperators>;
     createdAt?: Maybe<DateOperators>;
     updatedAt?: Maybe<DateOperators>;
@@ -3806,6 +3833,7 @@ export type ProductVariantFilterParameter = {
 
 export type ProductVariantSortParameter = {
     stockOnHand?: Maybe<SortOrder>;
+    stockAllocated?: Maybe<SortOrder>;
     id?: Maybe<SortOrder>;
     productId?: Maybe<SortOrder>;
     createdAt?: Maybe<SortOrder>;

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
schema-admin.json


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
schema-shop.json


Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov