Skip to content

Commit

Permalink
[Feature] Build allocation export (#7611)
Browse files Browse the repository at this point in the history
* CUI: Add "allocated stock" panel to build order page

* Implement CUI table for build order allocations

* Add "bulk delete" option for build order allocations

* Add row actions

* Add extra fields for data export

* Add build allocation table in PUI

* Add 'batch' column

* Bump API version

* Add playwright tests

* Fix missing renderer

* Update build docs

* Update playwright tests

* Update playwright tests
  • Loading branch information
SchrodingersGat authored Jul 11, 2024
1 parent 4e68794 commit 6650f3e
Show file tree
Hide file tree
Showing 18 changed files with 500 additions and 68 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 9 additions & 12 deletions docs/docs/build/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,6 @@ To navigate to the Build Order display, select *Build* from the main navigation
{% include "img.html" %}
{% endwith %}

#### Tree View

*Tree View* also provides a tabulated view of Build Orders. Orders are displayed in a hierarchical manner, showing any parent / child relationships between different build orders.

{% with id="build_tree", url="build/build_tree.png", description="Build Tree" %}
{% include "img.html" %}
{% endwith %}

#### Calendar View

*Calendar View* shows a calendar display with upcoming build orders, based on the various dates specified for each build.
Expand Down Expand Up @@ -121,18 +113,23 @@ The *Build Details* tab provides an overview of the Build Order:
{% include "img.html" %}
{% endwith %}

### Allocate Stock
### Line Items

The *Allocate Stock* tab provides an interface to allocate required stock (as specified by the BOM) to the build:
The *Line Items* tab provides an interface to allocate required stock (as specified by the BOM) to the build:

{% with id="build_allocate", url="build/build_allocate.png", description="Allocation tab" %}
{% include "img.html" %}
{% endwith %}

The allocation table (as shown above) shows the stock allocation progress for this build. In the example above, there are two BOM lines, which have been partially allocated.

!!! info "Completed Builds"
The *Allocate Stock* tab is not available if the build has been completed!
### Allocated Stock

The *Allocated Stock* tab displays all stock items which have been *allocated* to this build order. These stock items are reserved for this build, and will be consumed when the build is completed:

{% with id="allocated_stock_table", url="build/allocated_stock_table.png", description="Allocated Stock Table" %}
{% include "img.html" %}
{% endwith %}

### Consumed Stock

Expand Down
6 changes: 5 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 218
INVENTREE_API_VERSION = 219

"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""


INVENTREE_API_TEXT = """
v219 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7611
- Adds new fields to the BuildItem API endpoints
- Adds new ordering / filtering options to the BuildItem API endpoints
v218 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7619
- Adds "can_build" field to the BomItem API
Expand Down
30 changes: 24 additions & 6 deletions src/backend/InvenTree/build/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@

from rest_framework.exceptions import ValidationError

from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as rest_filters

from importer.mixins import DataExportViewMixin

from InvenTree.api import MetadataView
from InvenTree.api import BulkDeleteMixin, MetadataView
from generic.states.api import StatusView
from InvenTree.helpers import str2bool, isNull
from build.status_codes import BuildStatus, BuildStatusGroups
Expand Down Expand Up @@ -546,15 +545,17 @@ def filter_tracked(self, queryset, name, value):
return queryset.filter(install_into=None)


class BuildItemList(DataExportViewMixin, ListCreateAPI):
class BuildItemList(DataExportViewMixin, BulkDeleteMixin, ListCreateAPI):
"""API endpoint for accessing a list of BuildItem objects.
- GET: Return list of objects
- POST: Create a new BuildItem object
"""

queryset = BuildItem.objects.all()
serializer_class = build.serializers.BuildItemSerializer
filterset_class = BuildItemFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS

def get_serializer(self, *args, **kwargs):
"""Returns a BuildItemSerializer instance based on the request."""
Expand All @@ -571,7 +572,7 @@ def get_serializer(self, *args, **kwargs):

def get_queryset(self):
"""Override the queryset method, to allow filtering by stock_item.part."""
queryset = BuildItem.objects.all()
queryset = super().get_queryset()

queryset = queryset.select_related(
'build_line',
Expand Down Expand Up @@ -607,8 +608,25 @@ def filter_queryset(self, queryset):

return queryset

filter_backends = [
DjangoFilterBackend,
ordering_fields = [
'part',
'sku',
'quantity',
'location',
'reference',
]

ordering_field_aliases = {
'part': 'stock_item__part__name',
'sku': 'stock_item__supplier_part__SKU',
'location': 'stock_item__location__name',
'reference': 'build_line__bom_item__reference',
}

search_fields = [
'stock_item__supplier_part__SKU',
'stock_item__part__name',
'build_line__bom_item__reference',
]


Expand Down
64 changes: 40 additions & 24 deletions src/backend/InvenTree/build/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@
import common.models
from common.serializers import ProjectCodeSerializer
from importer.mixins import DataImportExportSerializerMixin
import company.serializers
import part.filters
from part.serializers import BomItemSerializer, PartSerializer, PartBriefSerializer
import part.serializers as part_serializers
from users.serializers import OwnerSerializer

from .models import Build, BuildLine, BuildItem
Expand Down Expand Up @@ -85,7 +86,7 @@ class Meta:

status_text = serializers.CharField(source='get_status_display', read_only=True)

part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
part_detail = part_serializers.PartBriefSerializer(source='part', many=False, read_only=True)

part_name = serializers.CharField(source='part.name', read_only=True, label=_('Part Name'))

Expand Down Expand Up @@ -1062,10 +1063,13 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
# These fields are only used for data export
export_only_fields = [
'build_reference',
'bom_reference',
'sku',
'mpn',
'location_name',
'part_id',
'part_name',
'part_ipn',
'available_quantity',
]

class Meta:
Expand All @@ -1085,34 +1089,20 @@ class Meta:
'location_detail',
'part_detail',
'stock_item_detail',
'supplier_part_detail',

# The following fields are only used for data export
'bom_reference',
'build_reference',
'location_name',
'mpn',
'sku',
'part_id',
'part_name',
'part_ipn',
'available_quantity',
]

# Export-only fields
sku = serializers.CharField(source='stock_item.supplier_part.SKU', label=_('Supplier Part Number'), read_only=True)
mpn = serializers.CharField(source='stock_item.supplier_part.manufacturer_part.MPN', label=_('Manufacturer Part Number'), read_only=True)
location_name = serializers.CharField(source='stock_item.location.name', label=_('Location Name'), read_only=True)
build_reference = serializers.CharField(source='build.reference', label=_('Build Reference'), read_only=True)
bom_reference = serializers.CharField(source='build_line.bom_item.reference', label=_('BOM Reference'), read_only=True)

# Annotated fields
build = serializers.PrimaryKeyRelatedField(source='build_line.build', many=False, read_only=True)

# Extra (optional) detail fields
part_detail = PartBriefSerializer(source='stock_item.part', many=False, read_only=True, pricing=False)
stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True)
location = serializers.PrimaryKeyRelatedField(source='stock_item.location', many=False, read_only=True)
location_detail = LocationSerializer(source='stock_item.location', read_only=True)
build_detail = BuildSerializer(source='build_line.build', many=False, read_only=True)

quantity = InvenTreeDecimalField()

def __init__(self, *args, **kwargs):
"""Determine which extra details fields should be included"""
part_detail = kwargs.pop('part_detail', True)
Expand All @@ -1134,6 +1124,32 @@ def __init__(self, *args, **kwargs):
if not build_detail:
self.fields.pop('build_detail', None)

# Export-only fields
sku = serializers.CharField(source='stock_item.supplier_part.SKU', label=_('Supplier Part Number'), read_only=True)
mpn = serializers.CharField(source='stock_item.supplier_part.manufacturer_part.MPN', label=_('Manufacturer Part Number'), read_only=True)
location_name = serializers.CharField(source='stock_item.location.name', label=_('Location Name'), read_only=True)
build_reference = serializers.CharField(source='build.reference', label=_('Build Reference'), read_only=True)
bom_reference = serializers.CharField(source='build_line.bom_item.reference', label=_('BOM Reference'), read_only=True)

# Part detail fields
part_id = serializers.PrimaryKeyRelatedField(source='stock_item.part', label=_('Part ID'), many=False, read_only=True)
part_name = serializers.CharField(source='stock_item.part.name', label=_('Part Name'), read_only=True)
part_ipn = serializers.CharField(source='stock_item.part.IPN', label=_('Part IPN'), read_only=True)

# Annotated fields
build = serializers.PrimaryKeyRelatedField(source='build_line.build', many=False, read_only=True)

# Extra (optional) detail fields
part_detail = part_serializers.PartBriefSerializer(source='stock_item.part', many=False, read_only=True, pricing=False)
stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True)
location = serializers.PrimaryKeyRelatedField(source='stock_item.location', many=False, read_only=True)
location_detail = LocationSerializer(source='stock_item.location', read_only=True)
build_detail = BuildSerializer(source='build_line.build', many=False, read_only=True)
supplier_part_detail = company.serializers.SupplierPartSerializer(source='stock_item.supplier_part', many=False, read_only=True)

quantity = InvenTreeDecimalField(label=_('Allocated Quantity'))
available_quantity = InvenTreeDecimalField(source='stock_item.quantity', read_only=True, label=_('Available Quantity'))


class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
"""Serializer for a BuildItem object."""
Expand Down Expand Up @@ -1217,8 +1233,8 @@ class Meta:
bom_item = serializers.PrimaryKeyRelatedField(label=_('BOM Item'), read_only=True)

# Foreign key fields
bom_item_detail = BomItemSerializer(source='bom_item', many=False, read_only=True, pricing=False)
part_detail = PartSerializer(source='bom_item.sub_part', many=False, read_only=True, pricing=False)
bom_item_detail = part_serializers.BomItemSerializer(source='bom_item', many=False, read_only=True, pricing=False)
part_detail = part_serializers.PartSerializer(source='bom_item.sub_part', many=False, read_only=True, pricing=False)
allocations = BuildItemSerializer(many=True, read_only=True)

# Annotated (calculated) fields
Expand Down
18 changes: 17 additions & 1 deletion src/backend/InvenTree/build/templates/build/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ <h4>{% trans "Child Build Orders" %}</h4>
<div class='panel panel-hidden' id='panel-allocate'>
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
<h4>{% trans "Allocate Stock to Build" %}</h4>
<h4>{% trans "Build Order Line Items" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% if roles.build.add and build.active %}
Expand Down Expand Up @@ -231,6 +231,18 @@ <h4>{% trans "Incomplete Build Outputs" %}</h4>
</div>
</div>

<div class='panel panel-hidden' id='panel-allocated'>
<div class='panel-heading'>
<h4>{% trans "Allocated Stock" %}</h4>
</div>
<div class='panel-content'>
<div id='build-allocated-stock-toolbar'>
{% include "filter_list.html" with id='buildorderallocatedstock' %}
</div>
<table class='table table-striped table-condensed' id='allocated-stock-table' data-toolbar='#build-allocated-stock-toolbar'></table>
</div>
</div>

<div class='panel panel-hidden' id='panel-consumed'>
<div class='panel-heading'>
<h4>
Expand Down Expand Up @@ -290,6 +302,10 @@ <h4>{% trans "Build Notes" %}</h4>
{% block js_ready %}
{{ block.super }}

onPanelLoad('allocated', function() {
loadBuildOrderAllocatedStockTable($('#allocated-stock-table'), {{ build.pk }});
});

onPanelLoad('consumed', function() {
loadStockTable($('#consumed-stock-table'), {
filterTarget: '#filter-list-consumed-stock',
Expand Down
10 changes: 7 additions & 3 deletions src/backend/InvenTree/build/templates/build/sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
{% trans "Build Order Details" as text %}
{% include "sidebar_item.html" with label='details' text=text icon="fa-info-circle" %}
{% if build.is_active %}
{% trans "Allocate Stock" as text %}
{% include "sidebar_item.html" with label='allocate' text=text icon="fa-tasks" %}
{% trans "Line Items" as text %}
{% include "sidebar_item.html" with label='allocate' text=text icon="fa-list-ol" %}
{% trans "Incomplete Outputs" as text %}
{% include "sidebar_item.html" with label='outputs' text=text icon="fa-tools" %}
{% endif %}
{% trans "Completed Outputs" as text %}
{% include "sidebar_item.html" with label='completed' text=text icon="fa-boxes" %}
{% if build.is_active %}
{% trans "Allocated Stock" as text %}
{% include "sidebar_item.html" with label='allocated' text=text icon="fa-list" %}
{% endif %}
{% trans "Consumed Stock" as text %}
{% include "sidebar_item.html" with label='consumed' text=text icon="fa-list" %}
{% include "sidebar_item.html" with label='consumed' text=text icon="fa-tasks" %}
{% trans "Child Build Orders" as text %}
{% include "sidebar_item.html" with label='children' text=text icon="fa-sitemap" %}
{% trans "Attachments" as text %}
Expand Down
Loading

0 comments on commit 6650f3e

Please sign in to comment.