fix(core-flows, dashboard, types): improve allocation flows on Admin (#12572)
**What** - handle single inventory items with `required_quantity > 1` correctly in the allocate UI flow - display correct availability amount in the create fulullment form for inventory kit items - fix check in the create fulfillment flow that would allow negative reservation because `required_quantity` wasn't included in the check - show the most recent reservations first in the reservations table - display an error in the allocation form if a reservation is not created for some inventory items - display inventory kit in the order summary if the product has multiple same inventory items --- CLOSES SUP-1655
This commit is contained in:
1 parent
67d3660abf
commit
2f594291ad
15 files changed
+293
-53
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/dashboard": patch
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(core-flows, dashboard, types): improve allocation flows in Admin
|
||||||
@@ -13,7 +13,12 @@ jest.setTimeout(300000)
|
|||||||
|
|
||||||
medusaIntegrationTestRunner({
|
medusaIntegrationTestRunner({
|
||||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
let order, seeder, inventoryItemOverride3, productOverride3, shippingProfile
|
let order,
|
||||||
|
seeder,
|
||||||
|
inventoryItemOverride3,
|
||||||
|
productOverride3,
|
||||||
|
shippingProfile,
|
||||||
|
productOverride4
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const container = getContainer()
|
const container = getContainer()
|
||||||
@@ -836,6 +841,56 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
).data.product
|
).data.product
|
||||||
|
|
||||||
|
const inventoryItemOverride4 = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "test-variant-4-no-shipping", requires_shipping: false },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items/${inventoryItemOverride4.id}/location-levels`,
|
||||||
|
{
|
||||||
|
location_id: stockChannelOverride.id,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
productOverride4 = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
{
|
||||||
|
title: `Test override 4`,
|
||||||
|
shipping_profile_id: shippingProfile.id,
|
||||||
|
options: [{ title: "size", values: ["large"] }],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "Test variant 4",
|
||||||
|
sku: "test-variant-4-override",
|
||||||
|
inventory_items: [
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItemOverride4.id,
|
||||||
|
required_quantity: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
size: "large",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
shippingProfileOverride = (
|
shippingProfileOverride = (
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/shipping-profiles`,
|
`/admin/shipping-profiles`,
|
||||||
@@ -889,6 +944,7 @@ medusaIntegrationTestRunner({
|
|||||||
additionalProducts: [
|
additionalProducts: [
|
||||||
{ variant_id: productOverride2.variants[0].id, quantity: 1 },
|
{ variant_id: productOverride2.variants[0].id, quantity: 1 },
|
||||||
{ variant_id: productOverride3.variants[0].id, quantity: 3 },
|
{ variant_id: productOverride3.variants[0].id, quantity: 3 },
|
||||||
|
{ variant_id: productOverride4.variants[0].id, quantity: 1 },
|
||||||
{
|
{
|
||||||
variant_id:
|
variant_id:
|
||||||
productOverride4WithOverrideShippingProfile.variants[0].id,
|
productOverride4WithOverrideShippingProfile.variants[0].id,
|
||||||
@@ -1024,6 +1080,50 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should throw if trying to fulfillment more items than it is reserved when item has required quantity", async () => {
|
||||||
|
const orderItemId = order.items.find(
|
||||||
|
(i) => i.variant_id === productOverride4.variants[0].id
|
||||||
|
).id
|
||||||
|
|
||||||
|
let reservation = (
|
||||||
|
await api.get(
|
||||||
|
`/admin/reservations?line_item_id=${orderItemId}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.reservations[0]
|
||||||
|
|
||||||
|
expect(reservation.quantity).toBe(3) // one item with required quantity 3
|
||||||
|
|
||||||
|
reservation = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/reservations/${reservation.id}`,
|
||||||
|
{
|
||||||
|
quantity: 2,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.reservation
|
||||||
|
|
||||||
|
expect(reservation.quantity).toBe(2)
|
||||||
|
|
||||||
|
const res = await api
|
||||||
|
.post(
|
||||||
|
`/admin/orders/${order.id}/fulfillments`,
|
||||||
|
{
|
||||||
|
shipping_option_id: seeder.shippingOption.id,
|
||||||
|
location_id: seeder.stockLocation.id,
|
||||||
|
items: [{ id: orderItemId, quantity: 1 }], // fulfill 1 orer item which requires 3 inventor items
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(res.response.status).toBe(400)
|
||||||
|
expect(res.response.data.message).toBe(
|
||||||
|
`Quantity to fulfill exceeds the reserved quantity for the item: ${orderItemId}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it("should throw if shipping profile of the product doesn't match the shipping profile of the shipping option", async () => {
|
it("should throw if shipping profile of the product doesn't match the shipping profile of the shipping option", async () => {
|
||||||
const orderItemId = order.items.find(
|
const orderItemId = order.items.find(
|
||||||
(i) =>
|
(i) =>
|
||||||
|
|||||||
@@ -3816,16 +3816,31 @@
|
|||||||
"credit": {
|
"credit": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"creditDescription": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"debit": {
|
"debit": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"debitDescription": {
|
"debitDescription": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"creditDescription": {
|
"required": [
|
||||||
"type": "string"
|
"title",
|
||||||
}
|
"total",
|
||||||
}
|
"creditOrDebit",
|
||||||
|
"createCreditLine",
|
||||||
|
"createCreditLineSuccess",
|
||||||
|
"createCreditLineError",
|
||||||
|
"createCreditLineDescription",
|
||||||
|
"operation",
|
||||||
|
"credit",
|
||||||
|
"creditDescription",
|
||||||
|
"debit",
|
||||||
|
"debitDescription"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"balanceSettlement": {
|
"balanceSettlement": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -3851,9 +3866,18 @@
|
|||||||
"creditLineDescription": {
|
"creditLineDescription": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"paymentMethod",
|
||||||
|
"paymentMethodDescription",
|
||||||
|
"creditLine",
|
||||||
|
"creditLineDescription"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
"required": ["title", "settlementType", "settlementTypes"],
|
||||||
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"domain": {
|
"domain": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -4876,9 +4900,12 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"created": {
|
"created": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["created"],
|
"required": ["created", "error"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -5539,6 +5566,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
"giftCardsStoreCreditLines",
|
||||||
|
"creditLines",
|
||||||
|
"balanceSettlement",
|
||||||
"domain",
|
"domain",
|
||||||
"claim",
|
"claim",
|
||||||
"exchange",
|
"exchange",
|
||||||
@@ -11229,6 +11259,8 @@
|
|||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"amount",
|
"amount",
|
||||||
|
"reference",
|
||||||
|
"reference_id",
|
||||||
"refundAmount",
|
"refundAmount",
|
||||||
"name",
|
"name",
|
||||||
"default",
|
"default",
|
||||||
@@ -11315,6 +11347,7 @@
|
|||||||
"account",
|
"account",
|
||||||
"total",
|
"total",
|
||||||
"paidTotal",
|
"paidTotal",
|
||||||
|
"creditTotal",
|
||||||
"totalExclTax",
|
"totalExclTax",
|
||||||
"subtotal",
|
"subtotal",
|
||||||
"shipping",
|
"shipping",
|
||||||
@@ -11497,6 +11530,7 @@
|
|||||||
"invite",
|
"invite",
|
||||||
"resetPassword",
|
"resetPassword",
|
||||||
"workflowExecutions",
|
"workflowExecutions",
|
||||||
|
"shippingOptionTypes",
|
||||||
"productTypes",
|
"productTypes",
|
||||||
"productTags",
|
"productTags",
|
||||||
"notifications",
|
"notifications",
|
||||||
|
|||||||
@@ -1305,7 +1305,8 @@
|
|||||||
"consistsOf": "Consists of {{num}}x inventory items",
|
"consistsOf": "Consists of {{num}}x inventory items",
|
||||||
"requires": "Requires {{num}} per variant",
|
"requires": "Requires {{num}} per variant",
|
||||||
"toast": {
|
"toast": {
|
||||||
"created": "Items successfully allocated"
|
"created": "Items successfully allocated",
|
||||||
|
"error": "Failed to allocate following items: {{items}}"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"quantityNotAllocated": "There are unallocated items."
|
"quantityNotAllocated": "There are unallocated items."
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ const EditSalesChannelsSchema = zod.object({
|
|||||||
sales_channels: zod.array(zod.string()).optional(),
|
sales_channels: zod.array(zod.string()).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const PAGE_SIZE = 50
|
const PAGE_SIZE = 20
|
||||||
const PREFIX = "sc"
|
const PREFIX = "sc"
|
||||||
|
|
||||||
export const LocationEditSalesChannelsForm = ({
|
export const LocationEditSalesChannelsForm = ({
|
||||||
|
|||||||
+19
-7
@@ -19,6 +19,7 @@ import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
|||||||
import { queryClient } from "../../../../../lib/query-client"
|
import { queryClient } from "../../../../../lib/query-client"
|
||||||
import { AllocateItemsSchema } from "./constants"
|
import { AllocateItemsSchema } from "./constants"
|
||||||
import { OrderAllocateItemsItem } from "./order-allocate-items-item"
|
import { OrderAllocateItemsItem } from "./order-allocate-items-item"
|
||||||
|
import { checkInventoryKit } from "./utils"
|
||||||
|
|
||||||
type OrderAllocateItemsFormProps = {
|
type OrderAllocateItemsFormProps = {
|
||||||
order: AdminOrder
|
order: AdminOrder
|
||||||
@@ -84,16 +85,18 @@ export function OrderAllocateItemsForm({ order }: OrderAllocateItemsFormProps) {
|
|||||||
const promises = payload.map(([itemId, inventoryId, quantity]) =>
|
const promises = payload.map(([itemId, inventoryId, quantity]) =>
|
||||||
allocateItems({
|
allocateItems({
|
||||||
location_id: data.location_id,
|
location_id: data.location_id,
|
||||||
inventory_item_id: inventoryId,
|
inventory_item_id: inventoryId as string,
|
||||||
line_item_id: itemId,
|
line_item_id: itemId as string,
|
||||||
quantity,
|
quantity: Number(quantity),
|
||||||
})
|
})
|
||||||
|
.then(() => ({ success: true, inventory_item_id: inventoryId }))
|
||||||
|
.catch(() => ({ success: false, inventory_item_id: inventoryId }))
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: we should have bulk endpoint for this so this is executed in a workflow and can be reverted
|
* TODO: we should have bulk endpoint for this so this is executed in a workflow and can be reverted
|
||||||
*/
|
*/
|
||||||
await Promise.all(promises)
|
const results = await Promise.all(promises)
|
||||||
|
|
||||||
// invalidate order details so we get new item.variant.inventory items
|
// invalidate order details so we get new item.variant.inventory items
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
@@ -102,10 +105,19 @@ export function OrderAllocateItemsForm({ order }: OrderAllocateItemsFormProps) {
|
|||||||
|
|
||||||
handleSuccess(`/orders/${order.id}`)
|
handleSuccess(`/orders/${order.id}`)
|
||||||
|
|
||||||
toast.success(t("general.success"), {
|
if (results.some((r) => !r.success)) {
|
||||||
description: t("orders.allocateItems.toast.created"),
|
const failedItems = results
|
||||||
|
.filter((r) => !r.success)
|
||||||
|
.map((r) => r.inventory_item_id)
|
||||||
|
.join(", ")
|
||||||
|
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: t("orders.allocateItems.toast.error", {
|
||||||
|
items: failedItems,
|
||||||
|
}),
|
||||||
dismissLabel: t("actions.close"),
|
dismissLabel: t("actions.close"),
|
||||||
})
|
})
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(t("general.error"), {
|
toast.error(t("general.error"), {
|
||||||
description: e.message,
|
description: e.message,
|
||||||
@@ -313,7 +325,7 @@ function defaultAllocations(items: OrderLineItemDTO) {
|
|||||||
const ret = {}
|
const ret = {}
|
||||||
|
|
||||||
items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
const hasInventoryKit = item.variant?.inventory_items.length > 1
|
const hasInventoryKit = checkInventoryKit(item)
|
||||||
|
|
||||||
ret[
|
ret[
|
||||||
hasInventoryKit
|
hasInventoryKit
|
||||||
|
|||||||
+2
-2
@@ -14,6 +14,7 @@ import { Thumbnail } from "../../../../../components/common/thumbnail"
|
|||||||
import { getFulfillableQuantity } from "../../../../../lib/order-item"
|
import { getFulfillableQuantity } from "../../../../../lib/order-item"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import { AllocateItemsSchema } from "./constants"
|
import { AllocateItemsSchema } from "./constants"
|
||||||
|
import { checkInventoryKit } from "./utils"
|
||||||
|
|
||||||
type OrderEditItemProps = {
|
type OrderEditItemProps = {
|
||||||
item: OrderLineItemDTO
|
item: OrderLineItemDTO
|
||||||
@@ -46,8 +47,7 @@ export function OrderAllocateItemsItem({
|
|||||||
name: "quantity",
|
name: "quantity",
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasInventoryKit =
|
const hasInventoryKit = checkInventoryKit(item)
|
||||||
!!variant?.inventory_items.length && variant?.inventory_items.length > 1
|
|
||||||
|
|
||||||
const { availableQuantity, inStockQuantity } = useMemo(() => {
|
const { availableQuantity, inStockQuantity } = useMemo(() => {
|
||||||
if (!variant || !locationId) {
|
if (!variant || !locationId) {
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
import {
|
||||||
|
AdminProductVariant,
|
||||||
|
AdminProductVariantInventoryItemLink,
|
||||||
|
OrderLineItemDTO,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the line item has inventory kit.
|
||||||
|
*/
|
||||||
|
export function checkInventoryKit(
|
||||||
|
item: OrderLineItemDTO & {
|
||||||
|
variant?: AdminProductVariant & {
|
||||||
|
inventory_items: AdminProductVariantInventoryItemLink[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const variant = item.variant
|
||||||
|
|
||||||
|
if (!variant) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
(!!variant.inventory_items.length && variant.inventory_items.length > 1) ||
|
||||||
|
(variant.inventory_items.length === 1 &&
|
||||||
|
variant.inventory_items[0].required_quantity! > 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-9
@@ -57,12 +57,6 @@ export function OrderCreateFulfillmentForm({
|
|||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
|
|
||||||
const itemReservedQuantitiesMap = useMemo(
|
|
||||||
() =>
|
|
||||||
new Map((reservations || []).map((r) => [r.line_item_id, r.quantity])),
|
|
||||||
[reservations]
|
|
||||||
)
|
|
||||||
|
|
||||||
const [fulfillableItems, setFulfillableItems] = useState(() =>
|
const [fulfillableItems, setFulfillableItems] = useState(() =>
|
||||||
(order.items || []).filter(
|
(order.items || []).filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
@@ -364,9 +358,7 @@ export function OrderCreateFulfillmentForm({
|
|||||||
disabled={
|
disabled={
|
||||||
requiresShipping && !isShippingProfileMatching
|
requiresShipping && !isShippingProfileMatching
|
||||||
}
|
}
|
||||||
itemReservedQuantitiesMap={
|
reservations={reservations}
|
||||||
itemReservedQuantitiesMap
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
+71
-16
@@ -17,7 +17,7 @@ type OrderEditItemProps = {
|
|||||||
currencyCode: string
|
currencyCode: string
|
||||||
locationId?: string
|
locationId?: string
|
||||||
onItemRemove: (itemId: string) => void
|
onItemRemove: (itemId: string) => void
|
||||||
itemReservedQuantitiesMap: Map<string, number>
|
reservations: HttpTypes.AdminReservation[]
|
||||||
form: UseFormReturn<zod.infer<typeof CreateFulfillmentSchema>>
|
form: UseFormReturn<zod.infer<typeof CreateFulfillmentSchema>>
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ export function OrderCreateFulfillmentItem({
|
|||||||
item,
|
item,
|
||||||
form,
|
form,
|
||||||
locationId,
|
locationId,
|
||||||
itemReservedQuantitiesMap,
|
reservations,
|
||||||
disabled,
|
disabled,
|
||||||
}: OrderEditItemProps) {
|
}: OrderEditItemProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -35,7 +35,7 @@ export function OrderCreateFulfillmentItem({
|
|||||||
item.product_id,
|
item.product_id,
|
||||||
item.variant_id,
|
item.variant_id,
|
||||||
{
|
{
|
||||||
fields: "*inventory,*inventory.location_levels",
|
fields: "*inventory,*inventory.location_levels,*inventory_items",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!item.variant,
|
enabled: !!item.variant,
|
||||||
@@ -43,28 +43,83 @@ export function OrderCreateFulfillmentItem({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { availableQuantity, inStockQuantity } = useMemo(() => {
|
const { availableQuantity, inStockQuantity } = useMemo(() => {
|
||||||
if (!variant || !locationId) {
|
if (
|
||||||
|
!variant?.inventory_items?.length ||
|
||||||
|
!variant?.inventory?.length ||
|
||||||
|
!locationId
|
||||||
|
) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { inventory } = variant
|
const { inventory, inventory_items } = variant
|
||||||
|
|
||||||
const locationInventory = inventory[0]?.location_levels?.find(
|
const locationHasEveryInventoryItem = inventory.every((i) =>
|
||||||
|
i.location_levels?.find((inv) => inv.location_id === locationId)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!locationHasEveryInventoryItem) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inventoryItemRequiredQuantityMap = new Map(
|
||||||
|
inventory_items.map((i) => [i.inventory_item_id, i.required_quantity])
|
||||||
|
)
|
||||||
|
|
||||||
|
// since we don't allow split fulifllments only one reservation from inventory kit is enough to calculate avalabel product quantity
|
||||||
|
const reservation = reservations?.find((r) => r.line_item_id === item.id)
|
||||||
|
const iitemRequiredQuantity = inventory_items.find(
|
||||||
|
(i) => i.inventory_item_id === reservation?.inventory_item_id
|
||||||
|
)?.required_quantity
|
||||||
|
|
||||||
|
const reservedQuantityForItem = !reservation
|
||||||
|
? 0
|
||||||
|
: reservation?.quantity / (iitemRequiredQuantity || 1)
|
||||||
|
|
||||||
|
const locationInventoryLevels = inventory.map((i) => {
|
||||||
|
const level = i.location_levels?.find(
|
||||||
(inv) => inv.location_id === locationId
|
(inv) => inv.location_id === locationId
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!locationInventory) {
|
const requiredQuantity = inventoryItemRequiredQuantityMap.get(i.id)
|
||||||
|
|
||||||
|
if (!level || !requiredQuantity) {
|
||||||
|
return {
|
||||||
|
availableQuantity: Number.MAX_SAFE_INTEGER,
|
||||||
|
stockedQuantity: Number.MAX_SAFE_INTEGER,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableQuantity = level.available_quantity / requiredQuantity
|
||||||
|
const stockedQuantity = level.stocked_quantity / requiredQuantity
|
||||||
|
|
||||||
|
return {
|
||||||
|
availableQuantity,
|
||||||
|
stockedQuantity,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const maxAvailableQuantity = Math.min(
|
||||||
|
...locationInventoryLevels.map((i) => i.availableQuantity)
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxStockedQuantity = Math.min(
|
||||||
|
...locationInventoryLevels.map((i) => i.stockedQuantity)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
maxAvailableQuantity === Number.MAX_SAFE_INTEGER ||
|
||||||
|
maxStockedQuantity === Number.MAX_SAFE_INTEGER
|
||||||
|
) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reservedQuantityForItem = itemReservedQuantitiesMap.get(item.id) ?? 0
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
availableQuantity:
|
availableQuantity: Math.floor(
|
||||||
locationInventory.available_quantity + reservedQuantityForItem,
|
maxAvailableQuantity + reservedQuantityForItem
|
||||||
inStockQuantity: locationInventory.stocked_quantity,
|
),
|
||||||
|
inStockQuantity: Math.floor(maxStockedQuantity),
|
||||||
}
|
}
|
||||||
}, [variant, locationId, itemReservedQuantitiesMap])
|
}, [variant, locationId, reservations])
|
||||||
|
|
||||||
const minValue = 0
|
const minValue = 0
|
||||||
const maxValue = Math.min(
|
const maxValue = Math.min(
|
||||||
@@ -76,7 +131,7 @@ export function OrderCreateFulfillmentItem({
|
|||||||
<div className="bg-ui-bg-subtle shadow-elevation-card-rest my-2 rounded-xl">
|
<div className="bg-ui-bg-subtle shadow-elevation-card-rest my-2 rounded-xl">
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
{disabled && (
|
{disabled && (
|
||||||
<div className="inline-flex items-center ml-4">
|
<div className="ml-4 inline-flex items-center">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={t("orders.fulfillment.disabledItemTooltip")}
|
content={t("orders.fulfillment.disabledItemTooltip")}
|
||||||
side="top"
|
side="top"
|
||||||
@@ -88,8 +143,8 @@ export function OrderCreateFulfillmentItem({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={clx(
|
className={clx(
|
||||||
"flex flex-col flex-1 gap-x-2 gap-y-2 border-b p-3 text-sm sm:flex-row",
|
"flex flex-1 flex-col gap-x-2 gap-y-2 border-b p-3 text-sm sm:flex-row",
|
||||||
disabled && "opacity-50 pointer-events-none"
|
disabled && "pointer-events-none opacity-50"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-1 items-center gap-x-3">
|
<div className="flex flex-1 items-center gap-x-3">
|
||||||
|
|||||||
+3
-1
@@ -397,7 +397,9 @@ const Item = ({
|
|||||||
|
|
||||||
const isInventoryManaged = item.variant?.manage_inventory
|
const isInventoryManaged = item.variant?.manage_inventory
|
||||||
const hasInventoryKit =
|
const hasInventoryKit =
|
||||||
isInventoryManaged && (item.variant?.inventory_items?.length || 0) > 1
|
isInventoryManaged &&
|
||||||
|
((item.variant?.inventory_items?.length || 0) > 1 ||
|
||||||
|
item.variant?.inventory_items?.some((i) => i.required_quantity > 1))
|
||||||
const hasUnfulfilledItems = item.quantity - item.detail.fulfilled_quantity > 0
|
const hasUnfulfilledItems = item.quantity - item.detail.fulfilled_quantity > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+2
-1
@@ -13,7 +13,7 @@ export const useReservationTableQuery = ({
|
|||||||
prefix
|
prefix
|
||||||
)
|
)
|
||||||
|
|
||||||
const { location_id, created_at, updated_at, quantity, offset, ...rest } = raw
|
const { location_id, created_at, updated_at, order, offset, ...rest } = raw
|
||||||
|
|
||||||
const searchParams: HttpTypes.AdminGetReservationsParams = {
|
const searchParams: HttpTypes.AdminGetReservationsParams = {
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
@@ -21,6 +21,7 @@ export const useReservationTableQuery = ({
|
|||||||
location_id: location_id,
|
location_id: location_id,
|
||||||
created_at: created_at ? JSON.parse(created_at) : undefined,
|
created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||||
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||||
|
order: order ?? "-created_at",
|
||||||
...rest,
|
...rest,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -306,13 +306,6 @@ function prepareInventoryUpdate({
|
|||||||
const inputQuantity = inputItemsMap[item.id]?.quantity ?? item.quantity
|
const inputQuantity = inputItemsMap[item.id]?.quantity ?? item.quantity
|
||||||
|
|
||||||
reservations.forEach((reservation) => {
|
reservations.forEach((reservation) => {
|
||||||
if (MathBN.gt(inputQuantity, reservation.quantity)) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_DATA,
|
|
||||||
`Quantity to fulfill exceeds the reserved quantity for the item: ${item.id}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const iItem = orderItem?.variant?.inventory_items.find(
|
const iItem = orderItem?.variant?.inventory_items.find(
|
||||||
(ii) => ii.inventory.id === reservation.inventory_item_id
|
(ii) => ii.inventory.id === reservation.inventory_item_id
|
||||||
)
|
)
|
||||||
@@ -327,6 +320,13 @@ function prepareInventoryUpdate({
|
|||||||
adjustemntQuantity
|
adjustemntQuantity
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (MathBN.lt(remainingReservationQuantity, 0)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Quantity to fulfill exceeds the reserved quantity for the item: ${item.id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
inventoryAdjustment.push({
|
inventoryAdjustment.push({
|
||||||
inventory_item_id: reservation.inventory_item_id,
|
inventory_item_id: reservation.inventory_item_id,
|
||||||
location_id: input.location_id ?? reservation.location_id,
|
location_id: input.location_id ?? reservation.location_id,
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ export interface AdminProductVariantInventoryItemLink {
|
|||||||
* The inventory item that is linked to the variant.
|
* The inventory item that is linked to the variant.
|
||||||
*/
|
*/
|
||||||
inventory?: AdminInventoryItem
|
inventory?: AdminInventoryItem
|
||||||
|
/**
|
||||||
|
* The quantity of the inventory item that is required to fulfill the variant.
|
||||||
|
*/
|
||||||
|
required_quantity?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminProductVariant extends BaseProductVariant {
|
export interface AdminProductVariant extends BaseProductVariant {
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ export interface AdminGetReservationsParams {
|
|||||||
* reservations for.
|
* reservations for.
|
||||||
*/
|
*/
|
||||||
line_item_id?: string | string[]
|
line_item_id?: string | string[]
|
||||||
|
/**
|
||||||
|
* Sort the reservations by the given field.
|
||||||
|
*/
|
||||||
|
order_id?: string | string[]
|
||||||
/**
|
/**
|
||||||
* Filter by the ID(s) of the user(s) to retrieve the
|
* Filter by the ID(s) of the user(s) to retrieve the
|
||||||
* reservations they created.
|
* reservations they created.
|
||||||
|
|||||||
Reference in new issue
Block a user