2
0
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:
Jacob Felknor 2025-01-11 19:56:30 -07:00 committed by GitHub
parent c75630d1bd
commit e8c1417b15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 82 additions and 4 deletions

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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),

View File

@ -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()

View File

@ -352,6 +352,7 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
'barcode_hash',
'category_default_location',
'default_location',
'default_expiry',
'name',
'revision',
'full_name',

View File

@ -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,