mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-27 14:16:02 +00:00
Allocate mixin (#12914)
* Create AllocateMixin plugin mixin class * Add documentation * Add hook-in points for the new code * Add unit tests * Add CHANGELOG entry * Add loose typing
This commit is contained in:
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- [#12713](https://github.com/inventree/InvenTree/pull/12713) adds SCIM 2 provisioning support, allowing InvenTree to be integrated with external identity providers for user management.
|
- [#12713](https://github.com/inventree/InvenTree/pull/12713) adds SCIM 2 provisioning support, allowing InvenTree to be integrated with external identity providers for user management.
|
||||||
- [#12731](https://github.com/inventree/InvenTree/pull/12731) adds OIDC provider settings to the Admin Center - making all Identity Federation settings now available in one place without the need to use the database admin interface.
|
- [#12731](https://github.com/inventree/InvenTree/pull/12731) adds OIDC provider settings to the Admin Center - making all Identity Federation settings now available in one place without the need to use the database admin interface.
|
||||||
- [#12837](https://github.com/inventree/InvenTree/pull/12837) adds a user setting `ROTATE_TABLE_HEADERS` which rotates table headers by 90 degrees, improving readability for tables with long column titles.
|
- [#12837](https://github.com/inventree/InvenTree/pull/12837) adds a user setting `ROTATE_TABLE_HEADERS` which rotates table headers by 90 degrees, improving readability for tables with long column titles.
|
||||||
|
- [#12914](https://github.com/inventree/InvenTree/pull/12914) adds the AllocateMixin, allowing plugins to customize automatic stock allocation for build orders and sales orders.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ Supported mixin classes are:
|
|||||||
| Mixin | Description |
|
| Mixin | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [ActionMixin](./mixins/action.md) | Run custom actions |
|
| [ActionMixin](./mixins/action.md) | Run custom actions |
|
||||||
|
| [AllocateMixin](./mixins/allocate.md) | Customize automatic stock allocation |
|
||||||
| [APICallMixin](./mixins/api.md) | Perform calls to external APIs |
|
| [APICallMixin](./mixins/api.md) | Perform calls to external APIs |
|
||||||
| [AppMixin](./mixins/app.md) | Integrate additional database tables |
|
| [AppMixin](./mixins/app.md) | Integrate additional database tables |
|
||||||
| [BarcodeMixin](./mixins/barcode.md) | Support custom barcode actions |
|
| [BarcodeMixin](./mixins/barcode.md) | Support custom barcode actions |
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
title: Allocate Mixin
|
||||||
|
---
|
||||||
|
|
||||||
|
## AllocateMixin
|
||||||
|
|
||||||
|
The `AllocateMixin` class enables plugins to customize how stock items are automatically allocated against [build orders](../../manufacturing/build.md) and [sales orders](../../sales/sales_order.md).
|
||||||
|
|
||||||
|
When a user triggers "auto allocation" of stock against an order, InvenTree first determines a list of candidate stock items for each line, using the default allocation rules (e.g. in-stock, matching part / variant / substitute, location, serialization, etc). Before this candidate list is used to actually create the stock allocations, it is passed through any active plugins which implement the `AllocateMixin` class - allowing a plugin to filter, reorder, or otherwise adjust which stock items are used.
|
||||||
|
|
||||||
|
!!! info "Multi Plugin Support"
|
||||||
|
If multiple plugins are active which implement the `AllocateMixin` methods, they are called in turn - each plugin receives the (possibly already adjusted) output of the previous plugin.
|
||||||
|
|
||||||
|
!!! info "Default Behavior"
|
||||||
|
Neither method needs to be implemented by a plugin. If a method is not overridden - or if it returns `None` - the provided list of stock items is passed through unmodified.
|
||||||
|
|
||||||
|
### Build Order Allocation
|
||||||
|
|
||||||
|
The `filter_build_allocation` method is called when automatically allocating stock against a [build order](../../manufacturing/build.md) - for both "tracked" and "untracked" stock items.
|
||||||
|
|
||||||
|
Note that this method is called *once per candidate list* - once for each `BuildLine` (untracked stock), and once for each tracked build output.
|
||||||
|
|
||||||
|
::: plugin.base.integration.AllocateMixin.AllocateMixin.filter_build_allocation
|
||||||
|
options:
|
||||||
|
show_bases: False
|
||||||
|
show_root_heading: False
|
||||||
|
show_root_toc_entry: False
|
||||||
|
extra:
|
||||||
|
show_source: True
|
||||||
|
summary: False
|
||||||
|
members: []
|
||||||
|
|
||||||
|
### Sales Order Allocation
|
||||||
|
|
||||||
|
The `filter_sales_order_allocation` method is called when automatically allocating stock against a [sales order](../../sales/sales_order.md).
|
||||||
|
|
||||||
|
::: plugin.base.integration.AllocateMixin.AllocateMixin.filter_sales_order_allocation
|
||||||
|
options:
|
||||||
|
show_bases: False
|
||||||
|
show_root_heading: False
|
||||||
|
show_root_toc_entry: False
|
||||||
|
extra:
|
||||||
|
show_source: True
|
||||||
|
summary: False
|
||||||
|
members: []
|
||||||
|
|
||||||
|
### Sample Plugin
|
||||||
|
|
||||||
|
A sample plugin which implements custom allocation filtering is provided in the InvenTree source code. It excludes any stock item with a batch code of `REJECT` from being automatically allocated:
|
||||||
|
|
||||||
|
::: plugin.samples.integration.allocate_sample.SampleAllocatePlugin
|
||||||
|
options:
|
||||||
|
show_bases: False
|
||||||
|
show_root_heading: False
|
||||||
|
show_root_toc_entry: False
|
||||||
|
show_source: True
|
||||||
|
members: []
|
||||||
@@ -218,6 +218,7 @@ nav:
|
|||||||
- Unit Test: plugins/test.md
|
- Unit Test: plugins/test.md
|
||||||
- Plugin Mixins:
|
- Plugin Mixins:
|
||||||
- Action Mixin: plugins/mixins/action.md
|
- Action Mixin: plugins/mixins/action.md
|
||||||
|
- Allocate Mixin: plugins/mixins/allocate.md
|
||||||
- API Mixin: plugins/mixins/api.md
|
- API Mixin: plugins/mixins/api.md
|
||||||
- App Mixin: plugins/mixins/app.md
|
- App Mixin: plugins/mixins/app.md
|
||||||
- Barcode Mixin: plugins/mixins/barcode.md
|
- Barcode Mixin: plugins/mixins/barcode.md
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ from generic.states import (
|
|||||||
inventree_transition,
|
inventree_transition,
|
||||||
)
|
)
|
||||||
from InvenTree.helpers_db import bulk_create_and_fetch
|
from InvenTree.helpers_db import bulk_create_and_fetch
|
||||||
|
from plugin.base.integration.AllocateMixin import apply_allocate_mixin
|
||||||
from plugin.events import bulk_trigger_event, trigger_event
|
from plugin.events import bulk_trigger_event, trigger_event
|
||||||
from stock.events import StockEvents
|
from stock.events import StockEvents
|
||||||
from stock.status_codes import StockHistoryCode, StockStatus
|
from stock.status_codes import StockHistoryCode, StockStatus
|
||||||
@@ -1730,6 +1731,11 @@ class Build(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Allow plugins to filter / reorder the candidate stock items
|
||||||
|
available_items = apply_allocate_mixin(
|
||||||
|
'filter_build_allocation', line_item, available_items, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
if len(available_items) == 1:
|
if len(available_items) == 1:
|
||||||
allocations.append(
|
allocations.append(
|
||||||
BuildItem(
|
BuildItem(
|
||||||
@@ -1903,6 +1909,11 @@ class Build(
|
|||||||
key=lambda item, b=bom_item, v=variant_parts: stock_sort(item, b, v),
|
key=lambda item, b=bom_item, v=variant_parts: stock_sort(item, b, v),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Allow plugins to filter / reorder the candidate stock items
|
||||||
|
available_stock = apply_allocate_mixin(
|
||||||
|
'filter_build_allocation', line_item, available_stock, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
if len(available_stock) != 1 and not interchangeable:
|
if len(available_stock) != 1 and not interchangeable:
|
||||||
# Multiple stock items are available, but they are not interchangeable -
|
# Multiple stock items are available, but they are not interchangeable -
|
||||||
# the user must manually decide how to allocate them.
|
# the user must manually decide how to allocate them.
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ from order.status_codes import (
|
|||||||
TransferOrderStatusGroups,
|
TransferOrderStatusGroups,
|
||||||
)
|
)
|
||||||
from part import models as PartModels
|
from part import models as PartModels
|
||||||
|
from plugin.base.integration.AllocateMixin import apply_allocate_mixin
|
||||||
from plugin.events import bulk_trigger_event, trigger_event
|
from plugin.events import bulk_trigger_event, trigger_event
|
||||||
from stock.events import StockEvents
|
from stock.events import StockEvents
|
||||||
from stock.status_codes import StockHistoryCode, StockStatus
|
from stock.status_codes import StockHistoryCode, StockStatus
|
||||||
@@ -1611,7 +1612,12 @@ class SalesOrder(TotalPriceMixin, Order):
|
|||||||
else:
|
else:
|
||||||
available_stock = available_stock.order_by(stock_sort_by)
|
available_stock = available_stock.order_by(stock_sort_by)
|
||||||
|
|
||||||
stock_count = available_stock.count()
|
# Allow plugins to filter / reorder the candidate stock items
|
||||||
|
available_stock = apply_allocate_mixin(
|
||||||
|
'filter_sales_order_allocation', line_item, available_stock, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
stock_count = len(available_stock)
|
||||||
|
|
||||||
if stock_count == 0:
|
if stock_count == 0:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
"""Plugin mixin class for AllocateMixin."""
|
||||||
|
|
||||||
|
from django.db.models import Model
|
||||||
|
|
||||||
|
from InvenTree.exceptions import log_error
|
||||||
|
from plugin import PluginMixinEnum
|
||||||
|
|
||||||
|
|
||||||
|
class AllocateMixin:
|
||||||
|
"""Mixin which allows plugins to customize automatic stock allocation.
|
||||||
|
|
||||||
|
This mixin acts as a "shim" during the auto-allocation of stock items
|
||||||
|
against build orders and sales orders. It is called *after* the default
|
||||||
|
allocation logic has determined a list of candidate stock items, but
|
||||||
|
*before* those items are actually used for allocation.
|
||||||
|
|
||||||
|
This allows a plugin to filter, reorder, or otherwise adjust the list
|
||||||
|
of candidate stock items - for example, to implement a custom picking
|
||||||
|
strategy, or to exclude certain stock items from automatic allocation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class MixinMeta:
|
||||||
|
"""Meta options for this mixin."""
|
||||||
|
|
||||||
|
MIXIN_NAME = 'Allocate'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Register mixin."""
|
||||||
|
super().__init__()
|
||||||
|
self.add_mixin(PluginMixinEnum.ALLOCATE, True, __class__)
|
||||||
|
|
||||||
|
def filter_build_allocation(
|
||||||
|
self, build_line: Model, stock_items: list, **kwargs
|
||||||
|
) -> list:
|
||||||
|
"""Filter the stock items available for auto-allocation against a build order.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
build_line: The BuildLine object which is being allocated against
|
||||||
|
stock_items: A list of candidate StockItem objects, which have already
|
||||||
|
been filtered against the default allocation rules (e.g. in-stock,
|
||||||
|
matching part / variant / substitute, location, etc)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of StockItem objects to be used for auto-allocation.
|
||||||
|
|
||||||
|
The default implementation simply returns the provided list of stock items,
|
||||||
|
unmodified.
|
||||||
|
"""
|
||||||
|
return stock_items
|
||||||
|
|
||||||
|
def filter_sales_order_allocation(
|
||||||
|
self, order_line: Model, stock_items: list, **kwargs
|
||||||
|
) -> list:
|
||||||
|
"""Filter the stock items available for auto-allocation against a sales order.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
order_line: The SalesOrderLineItem object which is being allocated against
|
||||||
|
stock_items: A list of candidate StockItem objects, which have already
|
||||||
|
been filtered against the default allocation rules (e.g. in-stock,
|
||||||
|
matching part, location, serialization, etc)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of StockItem objects to be used for auto-allocation.
|
||||||
|
|
||||||
|
The default implementation simply returns the provided list of stock items,
|
||||||
|
unmodified.
|
||||||
|
"""
|
||||||
|
return stock_items
|
||||||
|
|
||||||
|
|
||||||
|
def apply_allocate_mixin(
|
||||||
|
hook_name: str, line_item, stock_items: list, **kwargs
|
||||||
|
) -> list:
|
||||||
|
"""Run the named AllocateMixin hook against every active implementing plugin.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
hook_name: Name of the AllocateMixin method to call
|
||||||
|
(e.g. 'filter_build_allocation' or 'filter_sales_order_allocation')
|
||||||
|
line_item: The BuildLine / SalesOrderLineItem being allocated against
|
||||||
|
stock_items: The current list of candidate StockItem objects
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The (possibly modified) list of candidate StockItem objects, after being
|
||||||
|
passed through every active plugin which implements the AllocateMixin.
|
||||||
|
|
||||||
|
Each active plugin is given the opportunity to filter / reorder the list,
|
||||||
|
receiving the output of the previous plugin as its input. If a plugin raises
|
||||||
|
an exception, or returns a non-list value, its result is discarded and the
|
||||||
|
list is passed unmodified to the next plugin.
|
||||||
|
"""
|
||||||
|
from plugin import registry
|
||||||
|
|
||||||
|
stock_items = list(stock_items)
|
||||||
|
|
||||||
|
for plg in registry.with_mixin(PluginMixinEnum.ALLOCATE):
|
||||||
|
try:
|
||||||
|
result = getattr(plg, hook_name)(line_item, stock_items, **kwargs)
|
||||||
|
except Exception:
|
||||||
|
log_error(hook_name, plugin=plg.slug)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if result is not None:
|
||||||
|
try:
|
||||||
|
stock_items = list(result)
|
||||||
|
except Exception:
|
||||||
|
log_error(hook_name, plugin=plg.slug)
|
||||||
|
|
||||||
|
return stock_items
|
||||||
@@ -13,6 +13,7 @@ from InvenTree.unit_test import InvenTreeTestCase
|
|||||||
from plugin import InvenTreePlugin
|
from plugin import InvenTreePlugin
|
||||||
from plugin.helpers import MixinNotImplementedError
|
from plugin.helpers import MixinNotImplementedError
|
||||||
from plugin.mixins import (
|
from plugin.mixins import (
|
||||||
|
AllocateMixin,
|
||||||
APICallMixin,
|
APICallMixin,
|
||||||
AppMixin,
|
AppMixin,
|
||||||
NavigationMixin,
|
NavigationMixin,
|
||||||
@@ -241,6 +242,33 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase):
|
|||||||
NavigationCls()
|
NavigationCls()
|
||||||
|
|
||||||
|
|
||||||
|
class AllocateMixinTest(BaseMixinDefinition, TestCase):
|
||||||
|
"""Tests for AllocateMixin."""
|
||||||
|
|
||||||
|
MIXIN_HUMAN_NAME = 'Allocate'
|
||||||
|
MIXIN_NAME = 'allocate'
|
||||||
|
MIXIN_ENABLE_CHECK = 'has_allocate'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Setup for all tests."""
|
||||||
|
|
||||||
|
class AllocateCls(AllocateMixin, InvenTreePlugin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.mixin = AllocateCls()
|
||||||
|
|
||||||
|
def test_function(self):
|
||||||
|
"""Test that the default hook implementations are pass-through."""
|
||||||
|
stock_items = ['a', 'b', 'c']
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.mixin.filter_build_allocation(None, stock_items), stock_items
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.mixin.filter_sales_order_allocation(None, stock_items), stock_items
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class APICallMixinTest(BaseMixinDefinition, TestCase):
|
class APICallMixinTest(BaseMixinDefinition, TestCase):
|
||||||
"""Tests for APICallMixin."""
|
"""Tests for APICallMixin."""
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from plugin.base.action.mixins import ActionMixin
|
|||||||
from plugin.base.barcodes.mixins import BarcodeMixin, SupplierBarcodeMixin
|
from plugin.base.barcodes.mixins import BarcodeMixin, SupplierBarcodeMixin
|
||||||
from plugin.base.event.mixins import EventMixin
|
from plugin.base.event.mixins import EventMixin
|
||||||
from plugin.base.icons.mixins import IconPackMixin
|
from plugin.base.icons.mixins import IconPackMixin
|
||||||
|
from plugin.base.integration.AllocateMixin import AllocateMixin
|
||||||
from plugin.base.integration.APICallMixin import APICallMixin
|
from plugin.base.integration.APICallMixin import APICallMixin
|
||||||
from plugin.base.integration.AppMixin import AppMixin
|
from plugin.base.integration.AppMixin import AppMixin
|
||||||
from plugin.base.integration.CurrencyExchangeMixin import CurrencyExchangeMixin
|
from plugin.base.integration.CurrencyExchangeMixin import CurrencyExchangeMixin
|
||||||
@@ -28,6 +29,7 @@ from plugin.base.ui.mixins import UserInterfaceMixin
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
'APICallMixin',
|
'APICallMixin',
|
||||||
'ActionMixin',
|
'ActionMixin',
|
||||||
|
'AllocateMixin',
|
||||||
'AppMixin',
|
'AppMixin',
|
||||||
'BarcodeMixin',
|
'BarcodeMixin',
|
||||||
'CurrencyExchangeMixin',
|
'CurrencyExchangeMixin',
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class PluginMixinEnum(StringEnum):
|
|||||||
BASE = 'base'
|
BASE = 'base'
|
||||||
|
|
||||||
ACTION = 'action'
|
ACTION = 'action'
|
||||||
|
ALLOCATE = 'allocate'
|
||||||
API_CALL = 'api_call'
|
API_CALL = 'api_call'
|
||||||
APP = 'app'
|
APP = 'app'
|
||||||
BARCODE = 'barcode'
|
BARCODE = 'barcode'
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""Sample plugin which demonstrates custom stock allocation functionality."""
|
||||||
|
|
||||||
|
from plugin import InvenTreePlugin
|
||||||
|
from plugin.mixins import AllocateMixin
|
||||||
|
|
||||||
|
# Batch code which marks a stock item as excluded from auto-allocation
|
||||||
|
REJECT_BATCH_CODE = 'REJECT'
|
||||||
|
|
||||||
|
|
||||||
|
class SampleAllocatePlugin(AllocateMixin, InvenTreePlugin):
|
||||||
|
"""A sample plugin for demonstrating custom auto-allocation behavior.
|
||||||
|
|
||||||
|
Any stock item with a batch code of 'REJECT' is excluded from
|
||||||
|
auto-allocation, for both build orders and sales orders.
|
||||||
|
"""
|
||||||
|
|
||||||
|
NAME = 'SampleAllocate'
|
||||||
|
SLUG = 'sampleallocate'
|
||||||
|
TITLE = 'Sample Allocate Plugin'
|
||||||
|
DESCRIPTION = (
|
||||||
|
'A sample plugin for demonstrating custom stock allocation functionality'
|
||||||
|
)
|
||||||
|
VERSION = '0.1.0'
|
||||||
|
|
||||||
|
def filter_build_allocation(self, build_line, stock_items, **kwargs):
|
||||||
|
"""Exclude any stock item with a 'REJECT' batch code."""
|
||||||
|
return [item for item in stock_items if item.batch != REJECT_BATCH_CODE]
|
||||||
|
|
||||||
|
def filter_sales_order_allocation(self, order_line, stock_items, **kwargs):
|
||||||
|
"""Exclude any stock item with a 'REJECT' batch code."""
|
||||||
|
return [item for item in stock_items if item.batch != REJECT_BATCH_CODE]
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"""Unit tests for the SampleAllocatePlugin class."""
|
||||||
|
|
||||||
|
from build.models import Build, BuildLine, generate_next_build_reference
|
||||||
|
from company.models import Company
|
||||||
|
from InvenTree.unit_test import InvenTreeTestCase
|
||||||
|
from order.models import SalesOrder, SalesOrderLineItem
|
||||||
|
from part.models import BomItem, Part
|
||||||
|
from plugin.registry import registry
|
||||||
|
from stock.models import StockItem
|
||||||
|
|
||||||
|
|
||||||
|
class SampleAllocatePluginTest(InvenTreeTestCase):
|
||||||
|
"""Tests for the SampleAllocatePlugin class."""
|
||||||
|
|
||||||
|
def enable_plugin(self, en: bool):
|
||||||
|
"""Enable or disable the SampleAllocatePlugin."""
|
||||||
|
registry.set_plugin_state('sampleallocate', en)
|
||||||
|
|
||||||
|
def test_build_auto_allocate(self):
|
||||||
|
"""The plugin should exclude 'REJECT' batches from build order allocation."""
|
||||||
|
assembly = Part.objects.create(name='Assembly', assembly=True)
|
||||||
|
component = Part.objects.create(name='Component', component=True)
|
||||||
|
|
||||||
|
BomItem.objects.create(part=assembly, sub_part=component, quantity=5)
|
||||||
|
|
||||||
|
build = Build.objects.create(
|
||||||
|
reference=generate_next_build_reference(), part=assembly, quantity=1
|
||||||
|
)
|
||||||
|
|
||||||
|
line = BuildLine.objects.get(build=build)
|
||||||
|
|
||||||
|
good_stock = StockItem.objects.create(part=component, quantity=10)
|
||||||
|
StockItem.objects.create(part=component, quantity=10, batch='REJECT')
|
||||||
|
|
||||||
|
# With the plugin disabled, either stock item may be selected - not interchangeable
|
||||||
|
self.enable_plugin(False)
|
||||||
|
build.auto_allocate_stock(interchangeable=False)
|
||||||
|
self.assertEqual(line.allocated_quantity(), 0)
|
||||||
|
|
||||||
|
# With the plugin enabled, the 'REJECT' item is filtered out, leaving a single
|
||||||
|
# (interchangeable) candidate, which can then be allocated
|
||||||
|
self.enable_plugin(True)
|
||||||
|
build.auto_allocate_stock(interchangeable=False)
|
||||||
|
|
||||||
|
line.refresh_from_db()
|
||||||
|
self.assertEqual(line.allocated_quantity(), 5)
|
||||||
|
self.assertEqual(
|
||||||
|
list(build.allocated_stock.values_list('stock_item', flat=True)),
|
||||||
|
[good_stock.pk],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.enable_plugin(False)
|
||||||
|
|
||||||
|
def test_sales_order_auto_allocate(self):
|
||||||
|
"""The plugin should exclude 'REJECT' batches from sales order allocation."""
|
||||||
|
customer = Company.objects.create(name='Customer', is_customer=True)
|
||||||
|
part = Part.objects.create(name='Widget', salable=True)
|
||||||
|
|
||||||
|
order = SalesOrder.objects.create(customer=customer)
|
||||||
|
line = SalesOrderLineItem.objects.create(order=order, part=part, quantity=5)
|
||||||
|
|
||||||
|
good_stock = StockItem.objects.create(part=part, quantity=10)
|
||||||
|
StockItem.objects.create(part=part, quantity=10, batch='REJECT')
|
||||||
|
|
||||||
|
self.enable_plugin(True)
|
||||||
|
order.auto_allocate_stock(interchangeable=False)
|
||||||
|
|
||||||
|
self.assertTrue(line.is_fully_allocated())
|
||||||
|
self.assertEqual(
|
||||||
|
list(order.stock_allocations.values_list('item', flat=True)),
|
||||||
|
[good_stock.pk],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.enable_plugin(False)
|
||||||
Reference in New Issue
Block a user