mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-03 13:58:47 +00:00
Set status when returning from customer (#8571)
* Set status when returning from customer * Fix default customer for return order table * Set status when receiving items against a ReturnOrder * Bump max query time for currency endpoint * Bump API version
This commit is contained in:
parent
81e87a65e2
commit
a48d23b161
@ -1,13 +1,16 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 286
|
INVENTREE_API_VERSION = 287
|
||||||
|
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
v287 - 2024-11-27 : https://github.com/inventree/InvenTree/pull/8571
|
||||||
|
- Adds ability to set stock status when returning items from a customer
|
||||||
|
|
||||||
v286 - 2024-11-26 : https://github.com/inventree/InvenTree/pull/8054
|
v286 - 2024-11-26 : https://github.com/inventree/InvenTree/pull/8054
|
||||||
- Adds "SelectionList" and "SelectionListEntry" API endpoints
|
- Adds "SelectionList" and "SelectionListEntry" API endpoints
|
||||||
|
|
||||||
|
@ -1254,7 +1254,9 @@ class CurrencyAPITests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
# Updating via the external exchange may not work every time
|
# Updating via the external exchange may not work every time
|
||||||
for _idx in range(5):
|
for _idx in range(5):
|
||||||
self.post(reverse('api-currency-refresh'), expected_code=200)
|
self.post(
|
||||||
|
reverse('api-currency-refresh'), expected_code=200, max_query_time=30
|
||||||
|
)
|
||||||
|
|
||||||
# There should be some new exchange rate objects now
|
# There should be some new exchange rate objects now
|
||||||
if Rate.objects.all().exists():
|
if Rate.objects.all().exists():
|
||||||
|
@ -2363,14 +2363,23 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def receive_line_item(self, line, location, user, note='', **kwargs):
|
def receive_line_item(self, line, location, user, **kwargs):
|
||||||
"""Receive a line item against this ReturnOrder.
|
"""Receive a line item against this ReturnOrder.
|
||||||
|
|
||||||
Rules:
|
Arguments:
|
||||||
- Transfers the StockItem to the specified location
|
line: ReturnOrderLineItem to receive
|
||||||
- Marks the StockItem as "quarantined"
|
location: StockLocation to receive the item to
|
||||||
- Adds a tracking entry to the StockItem
|
user: User performing the action
|
||||||
- Removes the 'customer' reference from the StockItem
|
|
||||||
|
Keyword Arguments:
|
||||||
|
note: Additional notes to add to the tracking entry
|
||||||
|
status: Status to set the StockItem to (default: StockStatus.QUARANTINED)
|
||||||
|
|
||||||
|
Performs the following actions:
|
||||||
|
- Transfers the StockItem to the specified location
|
||||||
|
- Marks the StockItem as "quarantined"
|
||||||
|
- Adds a tracking entry to the StockItem
|
||||||
|
- Removes the 'customer' reference from the StockItem
|
||||||
"""
|
"""
|
||||||
# Prevent an item from being "received" multiple times
|
# Prevent an item from being "received" multiple times
|
||||||
if line.received_date is not None:
|
if line.received_date is not None:
|
||||||
@ -2379,17 +2388,18 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
|
|
||||||
stock_item = line.item
|
stock_item = line.item
|
||||||
|
|
||||||
deltas = {
|
status = kwargs.get('status')
|
||||||
'status': StockStatus.QUARANTINED.value,
|
|
||||||
'returnorder': self.pk,
|
if status is None:
|
||||||
'location': location.pk,
|
status = StockStatus.QUARANTINED.value
|
||||||
}
|
|
||||||
|
deltas = {'status': status, 'returnorder': self.pk, 'location': location.pk}
|
||||||
|
|
||||||
if stock_item.customer:
|
if stock_item.customer:
|
||||||
deltas['customer'] = stock_item.customer.pk
|
deltas['customer'] = stock_item.customer.pk
|
||||||
|
|
||||||
# Update the StockItem
|
# Update the StockItem
|
||||||
stock_item.status = kwargs.get('status', StockStatus.QUARANTINED.value)
|
stock_item.status = status
|
||||||
stock_item.location = location
|
stock_item.location = location
|
||||||
stock_item.customer = None
|
stock_item.customer = None
|
||||||
stock_item.sales_order = None
|
stock_item.sales_order = None
|
||||||
@ -2400,7 +2410,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
stock_item.add_tracking_entry(
|
stock_item.add_tracking_entry(
|
||||||
StockHistoryCode.RETURNED_AGAINST_RETURN_ORDER,
|
StockHistoryCode.RETURNED_AGAINST_RETURN_ORDER,
|
||||||
user,
|
user,
|
||||||
notes=note,
|
notes=kwargs.get('note', ''),
|
||||||
deltas=deltas,
|
deltas=deltas,
|
||||||
location=location,
|
location=location,
|
||||||
returnorder=self,
|
returnorder=self,
|
||||||
|
@ -26,6 +26,7 @@ import part.filters as part_filters
|
|||||||
import part.models as part_models
|
import part.models as part_models
|
||||||
import stock.models
|
import stock.models
|
||||||
import stock.serializers
|
import stock.serializers
|
||||||
|
import stock.status_codes
|
||||||
from common.serializers import ProjectCodeSerializer
|
from common.serializers import ProjectCodeSerializer
|
||||||
from company.serializers import (
|
from company.serializers import (
|
||||||
AddressBriefSerializer,
|
AddressBriefSerializer,
|
||||||
@ -1923,7 +1924,7 @@ class ReturnOrderLineItemReceiveSerializer(serializers.Serializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
"""Metaclass options."""
|
"""Metaclass options."""
|
||||||
|
|
||||||
fields = ['item']
|
fields = ['item', 'status']
|
||||||
|
|
||||||
item = serializers.PrimaryKeyRelatedField(
|
item = serializers.PrimaryKeyRelatedField(
|
||||||
queryset=order.models.ReturnOrderLineItem.objects.all(),
|
queryset=order.models.ReturnOrderLineItem.objects.all(),
|
||||||
@ -1933,6 +1934,15 @@ class ReturnOrderLineItemReceiveSerializer(serializers.Serializer):
|
|||||||
label=_('Return order line item'),
|
label=_('Return order line item'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
status = serializers.ChoiceField(
|
||||||
|
choices=stock.status_codes.StockStatus.items(),
|
||||||
|
default=None,
|
||||||
|
label=_('Status'),
|
||||||
|
help_text=_('Stock item status code'),
|
||||||
|
required=False,
|
||||||
|
allow_blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
def validate_line_item(self, item):
|
def validate_line_item(self, item):
|
||||||
"""Validation for a single line item."""
|
"""Validation for a single line item."""
|
||||||
if item.order != self.context['order']:
|
if item.order != self.context['order']:
|
||||||
@ -1950,7 +1960,7 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
"""Metaclass options."""
|
"""Metaclass options."""
|
||||||
|
|
||||||
fields = ['items', 'location']
|
fields = ['items', 'location', 'note']
|
||||||
|
|
||||||
items = ReturnOrderLineItemReceiveSerializer(many=True)
|
items = ReturnOrderLineItemReceiveSerializer(many=True)
|
||||||
|
|
||||||
@ -1963,6 +1973,14 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
|
|||||||
help_text=_('Select destination location for received items'),
|
help_text=_('Select destination location for received items'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
note = serializers.CharField(
|
||||||
|
label=_('Note'),
|
||||||
|
help_text=_('Additional note for incoming stock items'),
|
||||||
|
required=False,
|
||||||
|
default='',
|
||||||
|
allow_blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
"""Perform data validation for this serializer."""
|
"""Perform data validation for this serializer."""
|
||||||
order = self.context['order']
|
order = self.context['order']
|
||||||
@ -1993,7 +2011,14 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
|
|||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for item in items:
|
for item in items:
|
||||||
line_item = item['item']
|
line_item = item['item']
|
||||||
order.receive_line_item(line_item, location, request.user)
|
|
||||||
|
order.receive_line_item(
|
||||||
|
line_item,
|
||||||
|
location,
|
||||||
|
request.user,
|
||||||
|
note=data.get('note', ''),
|
||||||
|
status=item.get('status', None),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@register_importer()
|
@register_importer()
|
||||||
|
@ -1217,8 +1217,16 @@ class StockItem(
|
|||||||
def return_from_customer(self, location, user=None, **kwargs):
|
def return_from_customer(self, location, user=None, **kwargs):
|
||||||
"""Return stock item from customer, back into the specified location.
|
"""Return stock item from customer, back into the specified location.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
location: The location to return the stock item to
|
||||||
|
user: The user performing the action
|
||||||
|
|
||||||
|
Keyword Arguments:
|
||||||
|
notes: Additional notes to add to the tracking entry
|
||||||
|
status: Optionally set the status of the stock item
|
||||||
|
|
||||||
If the selected location is the same as the parent, merge stock back into the parent.
|
If the selected location is the same as the parent, merge stock back into the parent.
|
||||||
Otherwise create the stock in the new location
|
Otherwise create the stock in the new location.
|
||||||
"""
|
"""
|
||||||
notes = kwargs.get('notes', '')
|
notes = kwargs.get('notes', '')
|
||||||
|
|
||||||
@ -1228,6 +1236,17 @@ class StockItem(
|
|||||||
tracking_info['customer'] = self.customer.id
|
tracking_info['customer'] = self.customer.id
|
||||||
tracking_info['customer_name'] = self.customer.name
|
tracking_info['customer_name'] = self.customer.name
|
||||||
|
|
||||||
|
# Clear out allocation information for the stock item
|
||||||
|
self.customer = None
|
||||||
|
self.belongs_to = None
|
||||||
|
self.sales_order = None
|
||||||
|
self.location = location
|
||||||
|
self.clearAllocations()
|
||||||
|
|
||||||
|
if status := kwargs.get('status'):
|
||||||
|
self.status = status
|
||||||
|
tracking_info['status'] = status
|
||||||
|
|
||||||
self.add_tracking_entry(
|
self.add_tracking_entry(
|
||||||
StockHistoryCode.RETURNED_FROM_CUSTOMER,
|
StockHistoryCode.RETURNED_FROM_CUSTOMER,
|
||||||
user,
|
user,
|
||||||
@ -1236,13 +1255,6 @@ class StockItem(
|
|||||||
location=location,
|
location=location,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Clear out allocation information for the stock item
|
|
||||||
self.customer = None
|
|
||||||
self.belongs_to = None
|
|
||||||
self.sales_order = None
|
|
||||||
self.location = location
|
|
||||||
self.clearAllocations()
|
|
||||||
|
|
||||||
trigger_event('stockitem.returnedfromcustomer', id=self.id)
|
trigger_event('stockitem.returnedfromcustomer', id=self.id)
|
||||||
|
|
||||||
"""If new location is the same as the parent location, merge this stock back in the parent"""
|
"""If new location is the same as the parent location, merge this stock back in the parent"""
|
||||||
|
@ -968,7 +968,7 @@ class ReturnStockItemSerializer(serializers.Serializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
"""Metaclass options."""
|
"""Metaclass options."""
|
||||||
|
|
||||||
fields = ['location', 'note']
|
fields = ['location', 'status', 'notes']
|
||||||
|
|
||||||
location = serializers.PrimaryKeyRelatedField(
|
location = serializers.PrimaryKeyRelatedField(
|
||||||
queryset=StockLocation.objects.all(),
|
queryset=StockLocation.objects.all(),
|
||||||
@ -979,6 +979,15 @@ class ReturnStockItemSerializer(serializers.Serializer):
|
|||||||
help_text=_('Destination location for returned item'),
|
help_text=_('Destination location for returned item'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
status = serializers.ChoiceField(
|
||||||
|
choices=stock.status_codes.StockStatus.items(),
|
||||||
|
default=None,
|
||||||
|
label=_('Status'),
|
||||||
|
help_text=_('Stock item status code'),
|
||||||
|
required=False,
|
||||||
|
allow_blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
notes = serializers.CharField(
|
notes = serializers.CharField(
|
||||||
label=_('Notes'),
|
label=_('Notes'),
|
||||||
help_text=_('Add transaction note (optional)'),
|
help_text=_('Add transaction note (optional)'),
|
||||||
@ -994,9 +1003,13 @@ class ReturnStockItemSerializer(serializers.Serializer):
|
|||||||
data = self.validated_data
|
data = self.validated_data
|
||||||
|
|
||||||
location = data['location']
|
location = data['location']
|
||||||
notes = data.get('notes', '')
|
|
||||||
|
|
||||||
item.return_from_customer(location, user=request.user, notes=notes)
|
item.return_from_customer(
|
||||||
|
location,
|
||||||
|
user=request.user,
|
||||||
|
notes=data.get('notes', ''),
|
||||||
|
status=data.get('status', None),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class StockChangeStatusSerializer(serializers.Serializer):
|
class StockChangeStatusSerializer(serializers.Serializer):
|
||||||
|
@ -4,6 +4,7 @@ import { IconAddressBook, IconUser, IconUsers } from '@tabler/icons-react';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import RemoveRowButton from '../components/buttons/RemoveRowButton';
|
import RemoveRowButton from '../components/buttons/RemoveRowButton';
|
||||||
|
import { StandaloneField } from '../components/forms/StandaloneField';
|
||||||
import type {
|
import type {
|
||||||
ApiFormAdjustFilterType,
|
ApiFormAdjustFilterType,
|
||||||
ApiFormFieldSet
|
ApiFormFieldSet
|
||||||
@ -11,8 +12,10 @@ import type {
|
|||||||
import type { TableFieldRowProps } from '../components/forms/fields/TableField';
|
import type { TableFieldRowProps } from '../components/forms/fields/TableField';
|
||||||
import { Thumbnail } from '../components/images/Thumbnail';
|
import { Thumbnail } from '../components/images/Thumbnail';
|
||||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||||
|
import { ModelType } from '../enums/ModelType';
|
||||||
import { useCreateApiFormModal } from '../hooks/UseForm';
|
import { useCreateApiFormModal } from '../hooks/UseForm';
|
||||||
import { apiUrl } from '../states/ApiState';
|
import { apiUrl } from '../states/ApiState';
|
||||||
|
import { StatusFilterOptions } from '../tables/Filter';
|
||||||
|
|
||||||
export function useReturnOrderFields({
|
export function useReturnOrderFields({
|
||||||
duplicateOrderId
|
duplicateOrderId
|
||||||
@ -133,6 +136,17 @@ function ReturnOrderLineItemFormRow({
|
|||||||
props: TableFieldRowProps;
|
props: TableFieldRowProps;
|
||||||
record: any;
|
record: any;
|
||||||
}>) {
|
}>) {
|
||||||
|
const statusOptions = useMemo(() => {
|
||||||
|
return (
|
||||||
|
StatusFilterOptions(ModelType.stockitem)()?.map((choice) => {
|
||||||
|
return {
|
||||||
|
value: choice.value,
|
||||||
|
display_name: choice.label
|
||||||
|
};
|
||||||
|
}) ?? []
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
@ -146,7 +160,21 @@ function ReturnOrderLineItemFormRow({
|
|||||||
<div>{record.part_detail.name}</div>
|
<div>{record.part_detail.name}</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>{record.item_detail.serial}</Table.Td>
|
<Table.Td># {record.item_detail.serial}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<StandaloneField
|
||||||
|
fieldDefinition={{
|
||||||
|
field_type: 'choice',
|
||||||
|
label: t`Status`,
|
||||||
|
choices: statusOptions,
|
||||||
|
onValueChange: (value) => {
|
||||||
|
props.changeFn(props.idx, 'status', value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
defaultValue={record.item_detail?.status}
|
||||||
|
error={props.rowErrors?.status?.message}
|
||||||
|
/>
|
||||||
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
|
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
@ -181,7 +209,7 @@ export function useReceiveReturnOrderLineItems(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
headers: [t`Part`, t`Serial Number`]
|
headers: [t`Part`, t`Stock Item`, t`Status`]
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
filters: {
|
filters: {
|
||||||
|
@ -639,10 +639,12 @@ export default function StockDetail() {
|
|||||||
),
|
),
|
||||||
fields: {
|
fields: {
|
||||||
location: {},
|
location: {},
|
||||||
|
status: {},
|
||||||
notes: {}
|
notes: {}
|
||||||
},
|
},
|
||||||
initialData: {
|
initialData: {
|
||||||
location: stockitem.location ?? stockitem.part_detail?.default_location
|
location: stockitem.location ?? stockitem.part_detail?.default_location,
|
||||||
|
status: stockitem.status_custom_key ?? stockitem.status
|
||||||
},
|
},
|
||||||
successMessage: t`Item returned to stock`,
|
successMessage: t`Item returned to stock`,
|
||||||
onFormSuccess: () => {
|
onFormSuccess: () => {
|
||||||
|
@ -152,6 +152,9 @@ export function ReturnOrderTable({
|
|||||||
url: ApiEndpoints.return_order_list,
|
url: ApiEndpoints.return_order_list,
|
||||||
title: t`Add Return Order`,
|
title: t`Add Return Order`,
|
||||||
fields: returnOrderFields,
|
fields: returnOrderFields,
|
||||||
|
initialData: {
|
||||||
|
customer: customerId
|
||||||
|
},
|
||||||
follow: true,
|
follow: true,
|
||||||
modelType: ModelType.returnorder
|
modelType: ModelType.returnorder
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user