2
0
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:
Oliver 2024-11-28 00:09:18 +11:00 committed by GitHub
parent 81e87a65e2
commit a48d23b161
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 130 additions and 32 deletions

View File

@ -1,13 +1,16 @@
"""InvenTree API version information."""
# 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."""
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
- Adds "SelectionList" and "SelectionListEntry" API endpoints

View File

@ -1254,7 +1254,9 @@ class CurrencyAPITests(InvenTreeAPITestCase):
# Updating via the external exchange may not work every time
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
if Rate.objects.all().exists():

View File

@ -2363,10 +2363,19 @@ class ReturnOrder(TotalPriceMixin, Order):
# endregion
@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.
Rules:
Arguments:
line: ReturnOrderLineItem to receive
location: StockLocation to receive the item to
user: User performing the action
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
@ -2379,17 +2388,18 @@ class ReturnOrder(TotalPriceMixin, Order):
stock_item = line.item
deltas = {
'status': StockStatus.QUARANTINED.value,
'returnorder': self.pk,
'location': location.pk,
}
status = kwargs.get('status')
if status is None:
status = StockStatus.QUARANTINED.value
deltas = {'status': status, 'returnorder': self.pk, 'location': location.pk}
if stock_item.customer:
deltas['customer'] = stock_item.customer.pk
# Update the StockItem
stock_item.status = kwargs.get('status', StockStatus.QUARANTINED.value)
stock_item.status = status
stock_item.location = location
stock_item.customer = None
stock_item.sales_order = None
@ -2400,7 +2410,7 @@ class ReturnOrder(TotalPriceMixin, Order):
stock_item.add_tracking_entry(
StockHistoryCode.RETURNED_AGAINST_RETURN_ORDER,
user,
notes=note,
notes=kwargs.get('note', ''),
deltas=deltas,
location=location,
returnorder=self,

View File

@ -26,6 +26,7 @@ import part.filters as part_filters
import part.models as part_models
import stock.models
import stock.serializers
import stock.status_codes
from common.serializers import ProjectCodeSerializer
from company.serializers import (
AddressBriefSerializer,
@ -1923,7 +1924,7 @@ class ReturnOrderLineItemReceiveSerializer(serializers.Serializer):
class Meta:
"""Metaclass options."""
fields = ['item']
fields = ['item', 'status']
item = serializers.PrimaryKeyRelatedField(
queryset=order.models.ReturnOrderLineItem.objects.all(),
@ -1933,6 +1934,15 @@ class ReturnOrderLineItemReceiveSerializer(serializers.Serializer):
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):
"""Validation for a single line item."""
if item.order != self.context['order']:
@ -1950,7 +1960,7 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
class Meta:
"""Metaclass options."""
fields = ['items', 'location']
fields = ['items', 'location', 'note']
items = ReturnOrderLineItemReceiveSerializer(many=True)
@ -1963,6 +1973,14 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
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):
"""Perform data validation for this serializer."""
order = self.context['order']
@ -1993,7 +2011,14 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
with transaction.atomic():
for item in items:
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()

View File

@ -1217,8 +1217,16 @@ class StockItem(
def return_from_customer(self, location, user=None, **kwargs):
"""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.
Otherwise create the stock in the new location
Otherwise create the stock in the new location.
"""
notes = kwargs.get('notes', '')
@ -1228,6 +1236,17 @@ class StockItem(
tracking_info['customer'] = self.customer.id
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(
StockHistoryCode.RETURNED_FROM_CUSTOMER,
user,
@ -1236,13 +1255,6 @@ class StockItem(
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)
"""If new location is the same as the parent location, merge this stock back in the parent"""

View File

@ -968,7 +968,7 @@ class ReturnStockItemSerializer(serializers.Serializer):
class Meta:
"""Metaclass options."""
fields = ['location', 'note']
fields = ['location', 'status', 'notes']
location = serializers.PrimaryKeyRelatedField(
queryset=StockLocation.objects.all(),
@ -979,6 +979,15 @@ class ReturnStockItemSerializer(serializers.Serializer):
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(
label=_('Notes'),
help_text=_('Add transaction note (optional)'),
@ -994,9 +1003,13 @@ class ReturnStockItemSerializer(serializers.Serializer):
data = self.validated_data
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):

View File

@ -4,6 +4,7 @@ import { IconAddressBook, IconUser, IconUsers } from '@tabler/icons-react';
import { useMemo } from 'react';
import RemoveRowButton from '../components/buttons/RemoveRowButton';
import { StandaloneField } from '../components/forms/StandaloneField';
import type {
ApiFormAdjustFilterType,
ApiFormFieldSet
@ -11,8 +12,10 @@ import type {
import type { TableFieldRowProps } from '../components/forms/fields/TableField';
import { Thumbnail } from '../components/images/Thumbnail';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { ModelType } from '../enums/ModelType';
import { useCreateApiFormModal } from '../hooks/UseForm';
import { apiUrl } from '../states/ApiState';
import { StatusFilterOptions } from '../tables/Filter';
export function useReturnOrderFields({
duplicateOrderId
@ -133,6 +136,17 @@ function ReturnOrderLineItemFormRow({
props: TableFieldRowProps;
record: any;
}>) {
const statusOptions = useMemo(() => {
return (
StatusFilterOptions(ModelType.stockitem)()?.map((choice) => {
return {
value: choice.value,
display_name: choice.label
};
}) ?? []
);
}, []);
return (
<>
<Table.Tr>
@ -146,7 +160,21 @@ function ReturnOrderLineItemFormRow({
<div>{record.part_detail.name}</div>
</Flex>
</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>
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
</Table.Td>
@ -181,7 +209,7 @@ export function useReceiveReturnOrderLineItems(
/>
);
},
headers: [t`Part`, t`Serial Number`]
headers: [t`Part`, t`Stock Item`, t`Status`]
},
location: {
filters: {

View File

@ -639,10 +639,12 @@ export default function StockDetail() {
),
fields: {
location: {},
status: {},
notes: {}
},
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`,
onFormSuccess: () => {

View File

@ -152,6 +152,9 @@ export function ReturnOrderTable({
url: ApiEndpoints.return_order_list,
title: t`Add Return Order`,
fields: returnOrderFields,
initialData: {
customer: customerId
},
follow: true,
modelType: ModelType.returnorder
});