2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-07-10 00:50:57 +00:00

Merge branch 'master' of https://github.com/inventree/InvenTree into matmair/issue6281

This commit is contained in:
Matthias Mair
2025-01-12 04:58:35 +01:00
87 changed files with 40208 additions and 40479 deletions
@@ -11,6 +11,10 @@ v299 - 2025-01-10 : https://github.com/inventree/InvenTree/pull/6293
- Removes a considerable amount of old auth endpoints - Removes a considerable amount of old auth endpoints
- Introduces allauth based REST API - Introduces allauth based REST API
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 v298 - 2025-01-07 - https://github.com/inventree/InvenTree/pull/8848
- Adds 'created_by' field to PurchaseOrder API endpoints - Adds 'created_by' field to PurchaseOrder API endpoints
- Adds 'created_by' field to SalesOrder API endpoints - Adds 'created_by' field to SalesOrder API endpoints
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -410,6 +410,7 @@ class PurchaseOrderReceive(PurchaseOrderContextMixin, CreateAPI):
- supplier_part: pk value of the supplier part - supplier_part: pk value of the supplier part
- quantity: quantity to receive - quantity: quantity to receive
- status: stock item status - status: stock item status
- expiry_date: stock item expiry date (optional)
- location: destination for stock item (optional) - location: destination for stock item (optional)
- batch_code: the batch code for this stock item - batch_code: the batch code for this stock item
- serial_numbers: serial numbers for this stock item - serial_numbers: serial numbers for this stock item
+24 -1
View File
@@ -796,10 +796,32 @@ class PurchaseOrder(TotalPriceMixin, Order):
def receive_line_item( def receive_line_item(
self, line, location, quantity, user, status=StockStatus.OK.value, **kwargs self, line, location, quantity, user, status=StockStatus.OK.value, **kwargs
): ):
"""Receive a line item (or partial line item) against this PurchaseOrder.""" """Receive a line item (or partial line item) against this PurchaseOrder.
Arguments:
line: The PurchaseOrderLineItem to receive against
location: The StockLocation to receive the item into
quantity: The quantity to receive
user: The User performing the action
status: The StockStatus to assign to the item (default: StockStatus.OK)
Keyword Arguments:
barch_code: Optional batch code for the new StockItem
serials: Optional list of serial numbers to assign to the new StockItem(s)
notes: Optional notes field for the StockItem
packaging: Optional packaging field for the StockItem
barcode: Optional barcode field for the StockItem
Raises:
ValidationError: If the quantity is negative or otherwise invalid
ValidationError: If the order is not in the 'PLACED' state
"""
# Extract optional batch code for the new stock item # Extract optional batch code for the new stock item
batch_code = kwargs.get('batch_code', '') 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 # Extract optional list of serial numbers
serials = kwargs.get('serials') serials = kwargs.get('serials')
@@ -863,6 +885,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
purchase_order=self, purchase_order=self,
status=status, status=status,
batch=batch_code, batch=batch_code,
expiry_date=expiry_date,
packaging=packaging, packaging=packaging,
serial=sn, serial=sn,
purchase_price=unit_purchase_price, purchase_price=unit_purchase_price,
@@ -717,6 +717,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
'quantity', 'quantity',
'status', 'status',
'batch_code', 'batch_code',
'expiry_date',
'serial_numbers', 'serial_numbers',
'packaging', 'packaging',
'note', 'note',
@@ -765,6 +766,13 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
allow_blank=True, 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( serial_numbers = serializers.CharField(
label=_('Serial Numbers'), label=_('Serial Numbers'),
help_text=_('Enter serial numbers for incoming stock items'), help_text=_('Enter serial numbers for incoming stock items'),
@@ -967,6 +975,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
status=item['status'], status=item['status'],
barcode=item.get('barcode', ''), barcode=item.get('barcode', ''),
batch_code=item.get('batch_code', ''), batch_code=item.get('batch_code', ''),
expiry_date=item.get('expiry_date', None),
packaging=item.get('packaging', ''), packaging=item.get('packaging', ''),
serials=item.get('serials', None), serials=item.get('serials', None),
notes=item.get('note', None), notes=item.get('note', None),
+14 -2
View File
@@ -3,7 +3,7 @@
import base64 import base64
import io import io
import json import json
from datetime import datetime, timedelta from datetime import date, datetime, timedelta
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import connection from django.db import connection
@@ -1061,12 +1061,20 @@ class PurchaseOrderReceiveTest(OrderTest):
self.assertEqual(line_1.received, 0) self.assertEqual(line_1.received, 0)
self.assertEqual(line_2.received, 50) self.assertEqual(line_2.received, 50)
one_week_from_today = date.today() + timedelta(days=7)
valid_data = { valid_data = {
'items': [ '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, 'line_item': 2,
'quantity': 200, 'quantity': 200,
'expiry_date': one_week_from_today.strftime(r'%Y-%m-%d'),
'location': 2, # Explicit location 'location': 2, # Explicit location
'barcode': 'MY-UNIQUE-BARCODE-456', 'barcode': 'MY-UNIQUE-BARCODE-456',
}, },
@@ -1111,6 +1119,10 @@ class PurchaseOrderReceiveTest(OrderTest):
self.assertEqual(stock_1.last().location.pk, 1) self.assertEqual(stock_1.last().location.pk, 1)
self.assertEqual(stock_2.last().location.pk, 2) 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 # Barcodes should have been assigned to the stock items
self.assertTrue( self.assertTrue(
StockItem.objects.filter(barcode_data='MY-UNIQUE-BARCODE-123').exists() StockItem.objects.filter(barcode_data='MY-UNIQUE-BARCODE-123').exists()
@@ -352,6 +352,7 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
'barcode_hash', 'barcode_hash',
'category_default_location', 'category_default_location',
'default_location', 'default_location',
'default_expiry',
'name', 'name',
'revision', 'revision',
'full_name', 'full_name',
@@ -47,6 +47,7 @@ export const getSupportedLanguages = (): Record<string, string> => {
ru: t`Russian`, ru: t`Russian`,
sk: t`Slovak`, sk: t`Slovak`,
sl: t`Slovenian`, sl: t`Slovenian`,
sr: t`Serbian`,
sv: t`Swedish`, sv: t`Swedish`,
th: t`Thai`, th: t`Thai`,
tr: t`Turkish`, tr: t`Turkish`,
+48 -1
View File
@@ -25,6 +25,8 @@ import {
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { IconCalendarExclamation } from '@tabler/icons-react';
import dayjs from 'dayjs';
import { api } from '../App'; import { api } from '../App';
import { ActionButton } from '../components/buttons/ActionButton'; import { ActionButton } from '../components/buttons/ActionButton';
import RemoveRowButton from '../components/buttons/RemoveRowButton'; import RemoveRowButton from '../components/buttons/RemoveRowButton';
@@ -49,7 +51,7 @@ import {
useSerialNumberGenerator useSerialNumberGenerator
} from '../hooks/UseGenerator'; } from '../hooks/UseGenerator';
import { apiUrl } from '../states/ApiState'; import { apiUrl } from '../states/ApiState';
import { useGlobalSettingsState } from '../states/SettingsState';
/* /*
* Construct a set of fields for creating / editing a PurchaseOrderLineItem instance * Construct a set of fields for creating / editing a PurchaseOrderLineItem instance
*/ */
@@ -246,6 +248,8 @@ function LineItemFormRow({
[record] [record]
); );
const settings = useGlobalSettingsState();
useEffect(() => { useEffect(() => {
if (!!record.destination) { if (!!record.destination) {
props.changeFn(props.idx, 'location', 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 // Status value
const [statusOpen, statusHandlers] = useDisclosure(false, { const [statusOpen, statusHandlers] = useDisclosure(false, {
onClose: () => props.changeFn(props.idx, 'status', undefined) onClose: () => props.changeFn(props.idx, 'status', undefined)
@@ -440,6 +461,16 @@ function LineItemFormRow({
tooltipAlignment='top' tooltipAlignment='top'
variant={batchOpen ? 'filled' : 'transparent'} 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 <ActionButton
size='sm' size='sm'
icon={<InvenTreeIcon icon='packaging' />} icon={<InvenTreeIcon icon='packaging' />}
@@ -586,6 +617,21 @@ function LineItemFormRow({
}} }}
error={props.rowErrors?.serial_numbers?.message} 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 <TableFieldExtraRow
visible={packagingOpen} visible={packagingOpen}
onValueChange={(value) => props.changeFn(props.idx, 'packaging', value)} onValueChange={(value) => props.changeFn(props.idx, 'packaging', value)}
@@ -672,6 +718,7 @@ export function useReceiveLineItems(props: LineItemsForm) {
line_item: elem.pk, line_item: elem.pk,
location: elem.destination ?? elem.destination_detail?.pk ?? null, location: elem.destination ?? elem.destination_detail?.pk ?? null,
quantity: elem.quantity - elem.received, quantity: elem.quantity - elem.received,
expiry_date: null,
batch_code: '', batch_code: '',
serial_numbers: '', serial_numbers: '',
status: 10, status: 10,
+22 -1
View File
@@ -13,6 +13,7 @@ import {
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { Suspense, useEffect, useMemo, useState } from 'react'; import { Suspense, useEffect, useMemo, useState } from 'react';
import dayjs from 'dayjs';
import { api } from '../App'; import { api } from '../App';
import { ActionButton } from '../components/buttons/ActionButton'; import { ActionButton } from '../components/buttons/ActionButton';
import RemoveRowButton from '../components/buttons/RemoveRowButton'; import RemoveRowButton from '../components/buttons/RemoveRowButton';
@@ -68,6 +69,8 @@ export function useStockFields({
const [nextBatchCode, setNextBatchCode] = useState<string>(''); const [nextBatchCode, setNextBatchCode] = useState<string>('');
const [nextSerialNumber, setNextSerialNumber] = useState<string>(''); const [nextSerialNumber, setNextSerialNumber] = useState<string>('');
const [expiryDate, setExpiryDate] = useState<string | null>(null);
const batchGenerator = useBatchCodeGenerator((value: any) => { const batchGenerator = useBatchCodeGenerator((value: any) => {
if (value) { if (value) {
setNextBatchCode(`${t`Next batch code`}: ${value}`); setNextBatchCode(`${t`Next batch code`}: ${value}`);
@@ -106,6 +109,14 @@ export function useStockFields({
// Clear the 'supplier_part' field if the part is changed // Clear the 'supplier_part' field if the part is changed
setSupplierPart(null); setSupplierPart(null);
// Adjust the 'expiry date' for the stock item
const expiry_days = record?.default_expiry ?? 0;
if (expiry_days && expiry_days > 0) {
// Adjust the expiry date based on the part default expiry
setExpiryDate(dayjs().add(expiry_days, 'days').toISOString());
}
} }
}, },
supplier_part: { supplier_part: {
@@ -171,7 +182,11 @@ export function useStockFields({
}, },
expiry_date: { expiry_date: {
icon: <IconCalendarExclamation />, icon: <IconCalendarExclamation />,
hidden: !globalSettings.isSet('STOCK_ENABLE_EXPIRY') hidden: !globalSettings.isSet('STOCK_ENABLE_EXPIRY'),
value: expiryDate,
onValueChange: (value) => {
setExpiryDate(value);
}
}, },
purchase_price: { purchase_price: {
icon: <IconCurrencyDollar /> icon: <IconCurrencyDollar />
@@ -194,9 +209,15 @@ export function useStockFields({
// TODO: Handle custom field management based on provided options // TODO: Handle custom field management based on provided options
// TODO: refer to stock.py in original codebase // TODO: refer to stock.py in original codebase
// Remove the expiry date field if it is not enabled
if (!globalSettings.isSet('STOCK_ENABLE_EXPIRY')) {
delete fields.expiry_date;
}
return fields; return fields;
}, [ }, [
stockItem, stockItem,
expiryDate,
partInstance, partInstance,
partId, partId,
globalSettings, globalSettings,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff