2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-03 13:58:47 +00:00

Transfer out of stock items (#7194)

* Use new setting to determine if item can be moved

* Add new setting to front-end

* Invert double inversion

* Prevent empty stock tracking entry

* Updated unit tests

* Fix rendering of FailedTasksTable
This commit is contained in:
Oliver 2024-05-10 12:04:26 +10:00 committed by GitHub
parent b88457a39e
commit 29fa5cfafa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 48 additions and 6 deletions

View File

@ -1781,6 +1781,14 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'STOCK_ALLOW_OUT_OF_STOCK_TRANSFER': {
'name': _('Allow Out of Stock Transfer'),
'description': _(
'Allow stock items which are not in stock to be transferred between stock locations'
),
'default': False,
'validator': bool,
},
'BUILDORDER_REFERENCE_PATTERN': { 'BUILDORDER_REFERENCE_PATTERN': {
'name': _('Build Order Reference Pattern'), 'name': _('Build Order Reference Pattern'),
'description': _( 'description': _(

View File

@ -1419,6 +1419,14 @@ class StockItem(
if deltas is None: if deltas is None:
deltas = {} deltas = {}
# Prevent empty entry
if (
entry_type == StockHistoryCode.STOCK_UPDATE
and len(deltas) == 0
and not notes
):
return
# Has a location been specified? # Has a location been specified?
location = kwargs.get('location', None) location = kwargs.get('location', None)
@ -1866,7 +1874,11 @@ class StockItem(
except InvalidOperation: except InvalidOperation:
return False return False
if not self.in_stock: allow_out_of_stock_transfer = common.models.InvenTreeSetting.get_setting(
'STOCK_ALLOW_OUT_OF_STOCK_TRANSFER', backup_value=False, cache=False
)
if not allow_out_of_stock_transfer and not self.in_stock:
raise ValidationError(_('StockItem cannot be moved as it is not in stock')) raise ValidationError(_('StockItem cannot be moved as it is not in stock'))
if quantity <= 0: if quantity <= 0:

View File

@ -1470,6 +1470,14 @@ class StocktakeTest(StockAPITestCase):
def test_transfer(self): def test_transfer(self):
"""Test stock transfers.""" """Test stock transfers."""
stock_item = StockItem.objects.get(pk=1234)
# Mark this stock item as "quarantined" (cannot be moved)
stock_item.status = StockStatus.QUARANTINED.value
stock_item.save()
InvenTreeSetting.set_setting('STOCK_ALLOW_OUT_OF_STOCK_TRANSFER', False)
data = { data = {
'items': [{'pk': 1234, 'quantity': 10}], 'items': [{'pk': 1234, 'quantity': 10}],
'location': 1, 'location': 1,
@ -1478,6 +1486,14 @@ class StocktakeTest(StockAPITestCase):
url = reverse('api-stock-transfer') url = reverse('api-stock-transfer')
# First attempt should *fail* - stock item is quarantined
response = self.post(url, data, expected_code=400)
self.assertIn('cannot be moved as it is not in stock', str(response.data))
# Now, allow transfer of "out of stock" items
InvenTreeSetting.set_setting('STOCK_ALLOW_OUT_OF_STOCK_TRANSFER', True)
# This should succeed # This should succeed
response = self.post(url, data, expected_code=201) response = self.post(url, data, expected_code=201)

View File

@ -23,6 +23,7 @@
{% include "InvenTree/settings/setting.html" with key="STOCK_LOCATION_DEFAULT_ICON" icon="fa-icons" %} {% include "InvenTree/settings/setting.html" with key="STOCK_LOCATION_DEFAULT_ICON" icon="fa-icons" %}
{% include "InvenTree/settings/setting.html" with key="STOCK_SHOW_INSTALLED_ITEMS" icon="fa-sitemap" %} {% include "InvenTree/settings/setting.html" with key="STOCK_SHOW_INSTALLED_ITEMS" icon="fa-sitemap" %}
{% include "InvenTree/settings/setting.html" with key="STOCK_ENFORCE_BOM_INSTALLATION" icon="fa-check-circle" %} {% include "InvenTree/settings/setting.html" with key="STOCK_ENFORCE_BOM_INSTALLATION" icon="fa-check-circle" %}
{% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_OUT_OF_STOCK_TRANSFER" icon="fa-dolly" %}
{% include "InvenTree/settings/setting.html" with key="TEST_STATION_DATA" icon="fa-network-wired" %} {% include "InvenTree/settings/setting.html" with key="TEST_STATION_DATA" icon="fa-network-wired" %}
</tbody> </tbody>

View File

@ -20,7 +20,7 @@ const FailedTasksTable = Loadable(
export default function TaskManagementPanel() { export default function TaskManagementPanel() {
return ( return (
<Accordion defaultValue="pending"> <Accordion defaultValue="pending">
<Accordion.Item value="pending"> <Accordion.Item value="pending" key="pending-tasks">
<Accordion.Control> <Accordion.Control>
<StylishText size="lg">{t`Pending Tasks`}</StylishText> <StylishText size="lg">{t`Pending Tasks`}</StylishText>
</Accordion.Control> </Accordion.Control>
@ -28,7 +28,7 @@ export default function TaskManagementPanel() {
<PendingTasksTable /> <PendingTasksTable />
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
<Accordion.Item value="scheduled"> <Accordion.Item value="scheduled" key="scheduled-tasks">
<Accordion.Control> <Accordion.Control>
<StylishText size="lg">{t`Scheduled Tasks`}</StylishText> <StylishText size="lg">{t`Scheduled Tasks`}</StylishText>
</Accordion.Control> </Accordion.Control>
@ -36,7 +36,7 @@ export default function TaskManagementPanel() {
<ScheduledTasksTable /> <ScheduledTasksTable />
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
<Accordion.Item value="failed"> <Accordion.Item value="failed" key="failed-tasks">
<Accordion.Control> <Accordion.Control>
<StylishText size="lg">{t`Failed Tasks`}</StylishText> <StylishText size="lg">{t`Failed Tasks`}</StylishText>
</Accordion.Control> </Accordion.Control>

View File

@ -212,6 +212,7 @@ export default function SystemSettings() {
'STOCK_LOCATION_DEFAULT_ICON', 'STOCK_LOCATION_DEFAULT_ICON',
'STOCK_SHOW_INSTALLED_ITEMS', 'STOCK_SHOW_INSTALLED_ITEMS',
'STOCK_ENFORCE_BOM_INSTALLATION', 'STOCK_ENFORCE_BOM_INSTALLATION',
'STOCK_ALLOW_OUT_OF_STOCK_TRANSFER',
'TEST_STATION_DATA' 'TEST_STATION_DATA'
]} ]}
/> />

View File

@ -57,8 +57,12 @@ export default function FailedTasksTable() {
title={<StylishText>{t`Error Details`}</StylishText>} title={<StylishText>{t`Error Details`}</StylishText>}
onClose={close} onClose={close}
> >
{error.split('\n').map((line: string) => { {error.split('\n').map((line: string, index: number) => {
return <Text size="sm">{line}</Text>; return (
<Text key={`error-${index}`} size="sm">
{line}
</Text>
);
})} })}
</Drawer> </Drawer>
<InvenTreeTable <InvenTreeTable