mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-02 13:28:49 +00:00
Add Expiry Date on Receive Line Item (#8867)
* Add expiry on line item receive from PO * add backend test * reset pre-commit * increment inventree api version * use None as default expiry date * check global setting STOCK_ENABLE_EXPIRY * check for default expiry in line item receive * use dayjs --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
parent
c75630d1bd
commit
e8c1417b15
@ -1,13 +1,17 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 298
|
||||
INVENTREE_API_VERSION = 299
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v299 - 2025-01-10 - https://github.com/inventree/InvenTree/pull/8867
|
||||
- Adds 'expiry_date' field to the PurchaseOrderReceive API endpoint
|
||||
- Adds 'default_expiry` field to the PartBriefSerializer, affecting API endpoints which use it
|
||||
|
||||
v298 - 2025-01-07 - https://github.com/inventree/InvenTree/pull/8848
|
||||
- Adds 'created_by' field to PurchaseOrder API endpoints
|
||||
- Adds 'created_by' field to SalesOrder API endpoints
|
||||
|
@ -410,6 +410,7 @@ class PurchaseOrderReceive(PurchaseOrderContextMixin, CreateAPI):
|
||||
- supplier_part: pk value of the supplier part
|
||||
- quantity: quantity to receive
|
||||
- status: stock item status
|
||||
- expiry_date: stock item expiry date (optional)
|
||||
- location: destination for stock item (optional)
|
||||
- batch_code: the batch code for this stock item
|
||||
- serial_numbers: serial numbers for this stock item
|
||||
|
@ -819,6 +819,9 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
# Extract optional batch code for the new stock item
|
||||
batch_code = kwargs.get('batch_code', '')
|
||||
|
||||
# Extract optional expiry date for the new stock item
|
||||
expiry_date = kwargs.get('expiry_date')
|
||||
|
||||
# Extract optional list of serial numbers
|
||||
serials = kwargs.get('serials')
|
||||
|
||||
@ -882,6 +885,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
purchase_order=self,
|
||||
status=status,
|
||||
batch=batch_code,
|
||||
expiry_date=expiry_date,
|
||||
packaging=packaging,
|
||||
serial=sn,
|
||||
purchase_price=unit_purchase_price,
|
||||
|
@ -717,6 +717,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
'quantity',
|
||||
'status',
|
||||
'batch_code',
|
||||
'expiry_date',
|
||||
'serial_numbers',
|
||||
'packaging',
|
||||
'note',
|
||||
@ -765,6 +766,13 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
allow_blank=True,
|
||||
)
|
||||
|
||||
expiry_date = serializers.DateField(
|
||||
label=_('Expiry Date'),
|
||||
help_text=_('Enter expiry date for incoming stock items'),
|
||||
required=False,
|
||||
default=None,
|
||||
)
|
||||
|
||||
serial_numbers = serializers.CharField(
|
||||
label=_('Serial Numbers'),
|
||||
help_text=_('Enter serial numbers for incoming stock items'),
|
||||
@ -967,6 +975,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
||||
status=item['status'],
|
||||
barcode=item.get('barcode', ''),
|
||||
batch_code=item.get('batch_code', ''),
|
||||
expiry_date=item.get('expiry_date', None),
|
||||
packaging=item.get('packaging', ''),
|
||||
serials=item.get('serials', None),
|
||||
notes=item.get('note', None),
|
||||
|
@ -3,7 +3,7 @@
|
||||
import base64
|
||||
import io
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import connection
|
||||
@ -1061,12 +1061,20 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertEqual(line_1.received, 0)
|
||||
self.assertEqual(line_2.received, 50)
|
||||
|
||||
one_week_from_today = date.today() + timedelta(days=7)
|
||||
|
||||
valid_data = {
|
||||
'items': [
|
||||
{'line_item': 1, 'quantity': 50, 'barcode': 'MY-UNIQUE-BARCODE-123'},
|
||||
{
|
||||
'line_item': 1,
|
||||
'quantity': 50,
|
||||
'expiry_date': one_week_from_today.strftime(r'%Y-%m-%d'),
|
||||
'barcode': 'MY-UNIQUE-BARCODE-123',
|
||||
},
|
||||
{
|
||||
'line_item': 2,
|
||||
'quantity': 200,
|
||||
'expiry_date': one_week_from_today.strftime(r'%Y-%m-%d'),
|
||||
'location': 2, # Explicit location
|
||||
'barcode': 'MY-UNIQUE-BARCODE-456',
|
||||
},
|
||||
@ -1111,6 +1119,10 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertEqual(stock_1.last().location.pk, 1)
|
||||
self.assertEqual(stock_2.last().location.pk, 2)
|
||||
|
||||
# Expiry dates should be set
|
||||
self.assertEqual(stock_1.last().expiry_date, one_week_from_today)
|
||||
self.assertEqual(stock_2.last().expiry_date, one_week_from_today)
|
||||
|
||||
# Barcodes should have been assigned to the stock items
|
||||
self.assertTrue(
|
||||
StockItem.objects.filter(barcode_data='MY-UNIQUE-BARCODE-123').exists()
|
||||
|
@ -352,6 +352,7 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
'barcode_hash',
|
||||
'category_default_location',
|
||||
'default_location',
|
||||
'default_expiry',
|
||||
'name',
|
||||
'revision',
|
||||
'full_name',
|
||||
|
@ -25,6 +25,8 @@ import {
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { IconCalendarExclamation } from '@tabler/icons-react';
|
||||
import dayjs from 'dayjs';
|
||||
import { api } from '../App';
|
||||
import { ActionButton } from '../components/buttons/ActionButton';
|
||||
import RemoveRowButton from '../components/buttons/RemoveRowButton';
|
||||
@ -49,7 +51,7 @@ import {
|
||||
useSerialNumberGenerator
|
||||
} from '../hooks/UseGenerator';
|
||||
import { apiUrl } from '../states/ApiState';
|
||||
|
||||
import { useGlobalSettingsState } from '../states/SettingsState';
|
||||
/*
|
||||
* Construct a set of fields for creating / editing a PurchaseOrderLineItem instance
|
||||
*/
|
||||
@ -246,6 +248,8 @@ function LineItemFormRow({
|
||||
[record]
|
||||
);
|
||||
|
||||
const settings = useGlobalSettingsState();
|
||||
|
||||
useEffect(() => {
|
||||
if (!!record.destination) {
|
||||
props.changeFn(props.idx, 'location', record.destination);
|
||||
@ -298,6 +302,23 @@ function LineItemFormRow({
|
||||
}
|
||||
});
|
||||
|
||||
const [expiryDateOpen, expiryDateHandlers] = useDisclosure(false, {
|
||||
onOpen: () => {
|
||||
// check the default part expiry. Assume expiry is relative to today
|
||||
const defaultExpiry = record.part_detail?.default_expiry;
|
||||
if (defaultExpiry !== undefined && defaultExpiry > 0) {
|
||||
props.changeFn(
|
||||
props.idx,
|
||||
'expiry_date',
|
||||
dayjs().add(defaultExpiry, 'day').format('YYYY-MM-DD')
|
||||
);
|
||||
}
|
||||
},
|
||||
onClose: () => {
|
||||
props.changeFn(props.idx, 'expiry_date', undefined);
|
||||
}
|
||||
});
|
||||
|
||||
// Status value
|
||||
const [statusOpen, statusHandlers] = useDisclosure(false, {
|
||||
onClose: () => props.changeFn(props.idx, 'status', undefined)
|
||||
@ -440,6 +461,16 @@ function LineItemFormRow({
|
||||
tooltipAlignment='top'
|
||||
variant={batchOpen ? 'filled' : 'transparent'}
|
||||
/>
|
||||
{settings.isSet('STOCK_ENABLE_EXPIRY') && (
|
||||
<ActionButton
|
||||
size='sm'
|
||||
onClick={() => expiryDateHandlers.toggle()}
|
||||
icon={<IconCalendarExclamation />}
|
||||
tooltip={t`Set Expiry Date`}
|
||||
tooltipAlignment='top'
|
||||
variant={expiryDateOpen ? 'filled' : 'transparent'}
|
||||
/>
|
||||
)}
|
||||
<ActionButton
|
||||
size='sm'
|
||||
icon={<InvenTreeIcon icon='packaging' />}
|
||||
@ -586,6 +617,21 @@ function LineItemFormRow({
|
||||
}}
|
||||
error={props.rowErrors?.serial_numbers?.message}
|
||||
/>
|
||||
{settings.isSet('STOCK_ENABLE_EXPIRY') && (
|
||||
<TableFieldExtraRow
|
||||
visible={expiryDateOpen}
|
||||
onValueChange={(value) =>
|
||||
props.changeFn(props.idx, 'expiry_date', value)
|
||||
}
|
||||
fieldDefinition={{
|
||||
field_type: 'date',
|
||||
label: t`Expiry Date`,
|
||||
description: t`Enter an expiry date for received items`,
|
||||
value: props.item.expiry_date
|
||||
}}
|
||||
error={props.rowErrors?.expiry_date?.message}
|
||||
/>
|
||||
)}
|
||||
<TableFieldExtraRow
|
||||
visible={packagingOpen}
|
||||
onValueChange={(value) => props.changeFn(props.idx, 'packaging', value)}
|
||||
@ -672,6 +718,7 @@ export function useReceiveLineItems(props: LineItemsForm) {
|
||||
line_item: elem.pk,
|
||||
location: elem.destination ?? elem.destination_detail?.pk ?? null,
|
||||
quantity: elem.quantity - elem.received,
|
||||
expiry_date: null,
|
||||
batch_code: '',
|
||||
serial_numbers: '',
|
||||
status: 10,
|
||||
|
Loading…
x
Reference in New Issue
Block a user