mirror of
https://github.com/inventree/InvenTree.git
synced 2026-07-05 06:32:55 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into matmair/issue6281
This commit is contained in:
@@ -11,6 +11,10 @@ v299 - 2025-01-10 : https://github.com/inventree/InvenTree/pull/6293
|
||||
- Removes a considerable amount of old auth endpoints
|
||||
- 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
|
||||
- Adds 'created_by' field to PurchaseOrder 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
@@ -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
|
||||
|
||||
@@ -796,10 +796,32 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
def receive_line_item(
|
||||
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
|
||||
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')
|
||||
|
||||
@@ -863,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',
|
||||
|
||||
@@ -47,6 +47,7 @@ export const getSupportedLanguages = (): Record<string, string> => {
|
||||
ru: t`Russian`,
|
||||
sk: t`Slovak`,
|
||||
sl: t`Slovenian`,
|
||||
sr: t`Serbian`,
|
||||
sv: t`Swedish`,
|
||||
th: t`Thai`,
|
||||
tr: t`Turkish`,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { Suspense, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { api } from '../App';
|
||||
import { ActionButton } from '../components/buttons/ActionButton';
|
||||
import RemoveRowButton from '../components/buttons/RemoveRowButton';
|
||||
@@ -68,6 +69,8 @@ export function useStockFields({
|
||||
const [nextBatchCode, setNextBatchCode] = useState<string>('');
|
||||
const [nextSerialNumber, setNextSerialNumber] = useState<string>('');
|
||||
|
||||
const [expiryDate, setExpiryDate] = useState<string | null>(null);
|
||||
|
||||
const batchGenerator = useBatchCodeGenerator((value: any) => {
|
||||
if (value) {
|
||||
setNextBatchCode(`${t`Next batch code`}: ${value}`);
|
||||
@@ -106,6 +109,14 @@ export function useStockFields({
|
||||
|
||||
// Clear the 'supplier_part' field if the part is changed
|
||||
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: {
|
||||
@@ -171,7 +182,11 @@ export function useStockFields({
|
||||
},
|
||||
expiry_date: {
|
||||
icon: <IconCalendarExclamation />,
|
||||
hidden: !globalSettings.isSet('STOCK_ENABLE_EXPIRY')
|
||||
hidden: !globalSettings.isSet('STOCK_ENABLE_EXPIRY'),
|
||||
value: expiryDate,
|
||||
onValueChange: (value) => {
|
||||
setExpiryDate(value);
|
||||
}
|
||||
},
|
||||
purchase_price: {
|
||||
icon: <IconCurrencyDollar />
|
||||
@@ -194,9 +209,15 @@ export function useStockFields({
|
||||
// TODO: Handle custom field management based on provided options
|
||||
// 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;
|
||||
}, [
|
||||
stockItem,
|
||||
expiryDate,
|
||||
partInstance,
|
||||
partId,
|
||||
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
+2363
-2373
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
Reference in New Issue
Block a user