Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ jobs:
- name: Push Docker Images
id: push-docker
if: github.event_name != 'pull_request'
uses: depot/build-push-action@2583627a84956d07561420dcc1d0eb1f2af3fac0 # pin@v1
uses: depot/build-push-action@9785b135c3c76c33db102e45be96a25ab55cd507 # pin@v1
with:
project: jczzbjkk68
context: .
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/qc_checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # pin@v3
uses: github/codeql-action/upload-sarif@96f518a34f7a870018057716cc4d7a5c014bd61c # pin@v3
with:
sarif_file: results.sarif
category: zizmor
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
- name: Build frontend
run: cd src/frontend && npm run compile && npm run build
- name: Create SBOM for frontend
uses: anchore/sbom-action@7b36ad622f042cab6f59a75c2ac24ccb256e9b45 # pin@v0
uses: anchore/sbom-action@da167eac915b4e86f08b264dbdbc867b61be6f0c # pin@v0
with:
artifact-name: frontend-build.spdx
path: src/frontend
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/scorecard.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,6 @@ jobs:

# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.8
uses: github/codeql-action/upload-sarif@96f518a34f7a870018057716cc4d7a5c014bd61c # v3.29.10
with:
sarif_file: results.sarif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions docs/docs/manufacturing/allocate.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,49 @@ Here we can see that the incomplete build outputs (serial numbers 15 and 14) now
!!! note "Example: Tracked Stock"
Let's say we have 5 units of "Tracked Part" in stock - with 1 unit allocated to the build output. Once we complete the build output, there will be 4 units of "Tracked Part" in stock, with 1 unit being marked as "installed" within the assembled part

## Consuming Stock

Allocating stock items to a build order does not immediately remove them from stock. Instead, the stock items are marked as "allocated" against the build order, and are only removed from stock when they are "consumed" by the build order.

In the *Required Parts* tab, you can see the *consumed* vs *allocated* state of each line item in the BOM:

{{ image("build/parts_allocated_consumed.png", "Partially allocated and consumed") }}

Consuming items against the build order can be performed in two ways:

- Manually, by consuming selected stock allocations against the build order
- Automatically, by completing the build order

### Manual Consumption

Manual consuming stock items (before the build order is completed) can be performed at any point after stock has been allocated against the build order. Manual stock consumption may be desired in some situations, for example if the build order is being performed in stages, or to ensure that stock levels are kept up to date.

Manual consumption of stock items can be performed in the in the following ways:

#### Required Parts Tab

Consuming stock items can be performed against BOM line items in the *Required Parts* tab, either against a single line or multiple selected lines:

- Navigate to the *Required Parts* tab
- Select the individual line items which you wish to consume
- Click the *Consume Stock* button

#### Allocated Stock Tab

Consuming stock items can also be performed against the *Allocated Stock* tab, either against a single allocation or multiple allocations:

- Navigate to the *Allocated Stock* tab
- Select the individual stock allocations which you wish to consume
- Click the *Consume Stock* button

### Automatic Consumption

When a build order is completed, all remaining allocated stock items are automatically consumed by the build order.

### Returning Items to Stock

Consumed items may be manually returned into stock if required. This can be performed in the *Consumed Stock* tab.

## Completing a Build

!!! warning "Complete Build Outputs"
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,11 +1,15 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 385
INVENTREE_API_VERSION = 386

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

INVENTREE_API_TEXT = """
v386 -> 2025-08-11 : https://github.com/inventree/InvenTree/pull/8191
- Adds "consumed" field to the BuildItem API
- Adds API endpoint to consume stock against a BuildOrder

v385 -> 2025-08-15 : https://github.com/inventree/InvenTree/pull/10174
- Adjust return type of PurchaseOrderReceive API serializer
- Now returns list of of the created stock items when receiving
Expand Down
24 changes: 21 additions & 3 deletions src/backend/InvenTree/build/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,14 @@ def filter_allocated(self, queryset, name, value):
return queryset.filter(allocated__gte=F('quantity'))
return queryset.filter(allocated__lt=F('quantity'))

consumed = rest_filters.BooleanFilter(label=_('Consumed'), method='filter_consumed')

def filter_consumed(self, queryset, name, value):
"""Filter by whether each BuildLine is fully consumed."""
if str2bool(value):
return queryset.filter(consumed__gte=F('quantity'))
return queryset.filter(consumed__lt=F('quantity'))

available = rest_filters.BooleanFilter(
label=_('Available'), method='filter_available'
)
Expand All @@ -494,6 +502,7 @@ def filter_available(self, queryset, name, value):
"""
flt = Q(
quantity__lte=F('allocated')
+ F('consumed')
+ F('available_stock')
+ F('available_substitute_stock')
+ F('available_variant_stock')
Expand All @@ -504,7 +513,7 @@ def filter_available(self, queryset, name, value):
return queryset.exclude(flt)


class BuildLineEndpoint:
class BuildLineMixin:
"""Mixin class for BuildLine API endpoints."""

queryset = BuildLine.objects.all()
Expand Down Expand Up @@ -553,7 +562,7 @@ def get_queryset(self):
)


class BuildLineList(BuildLineEndpoint, DataExportViewMixin, ListCreateAPI):
class BuildLineList(BuildLineMixin, DataExportViewMixin, ListCreateAPI):
"""API endpoint for accessing a list of BuildLine objects."""

filterset_class = BuildLineFilter
Expand All @@ -562,6 +571,7 @@ class BuildLineList(BuildLineEndpoint, DataExportViewMixin, ListCreateAPI):
ordering_fields = [
'part',
'allocated',
'consumed',
'reference',
'quantity',
'consumable',
Expand Down Expand Up @@ -605,7 +615,7 @@ def get_source_build(self) -> Build | None:
return source_build


class BuildLineDetail(BuildLineEndpoint, RetrieveUpdateDestroyAPI):
class BuildLineDetail(BuildLineMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a BuildLine object."""

def get_source_build(self) -> Build | None:
Expand Down Expand Up @@ -734,6 +744,13 @@ class BuildAllocate(BuildOrderContextMixin, CreateAPI):
serializer_class = build.serializers.BuildAllocationSerializer


class BuildConsume(BuildOrderContextMixin, CreateAPI):
"""API endpoint to consume stock against a build order."""

queryset = Build.objects.none()
serializer_class = build.serializers.BuildConsumeSerializer


class BuildIssue(BuildOrderContextMixin, CreateAPI):
"""API endpoint for issuing a BuildOrder."""

Expand Down Expand Up @@ -953,6 +970,7 @@ def filter_queryset(self, queryset):
'<int:pk>/',
include([
path('allocate/', BuildAllocate.as_view(), name='api-build-allocate'),
path('consume/', BuildConsume.as_view(), name='api-build-consume'),
path(
'auto-allocate/',
BuildAutoAllocate.as_view(),
Expand Down
19 changes: 19 additions & 0 deletions src/backend/InvenTree/build/migrations/0058_buildline_consumed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.15 on 2024-09-26 10:11

import django.core.validators
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('build', '0057_build_external'),
]

operations = [
migrations.AddField(
model_name='buildline',
name='consumed',
field=models.DecimalField(decimal_places=5, default=0, help_text='Quantity of consumed stock', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Consumed'),
),
]
56 changes: 49 additions & 7 deletions src/backend/InvenTree/build/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1106,7 +1106,7 @@ def subtract_allocated_stock(self, user):

# Remove stock
for item in items:
item.complete_allocation(user)
item.complete_allocation(user=user)

# Delete allocation
items.all().delete()
Expand Down Expand Up @@ -1151,7 +1151,7 @@ def scrap_build_output(self, output, quantity, location, **kwargs):
# Complete or discard allocations
for build_item in allocated_items:
if not discard_allocations:
build_item.complete_allocation(user)
build_item.complete_allocation(user=user)

# Delete allocations
allocated_items.delete()
Expand Down Expand Up @@ -1200,7 +1200,7 @@ def complete_build_output(self, output, user, **kwargs):

for build_item in allocated_items:
# Complete the allocation of stock for that item
build_item.complete_allocation(user)
build_item.complete_allocation(user=user)

# Delete the BuildItem objects from the database
allocated_items.all().delete()
Expand Down Expand Up @@ -1569,6 +1569,7 @@ class BuildLine(report.mixins.InvenTreeReportMixin, InvenTree.models.InvenTreeMo
build: Link to a Build object
bom_item: Link to a BomItem object
quantity: Number of units required for the Build
consumed: Number of units which have been consumed against this line item
"""

class Meta:
Expand Down Expand Up @@ -1614,6 +1615,15 @@ def report_context(self) -> BuildLineReportContext:
help_text=_('Required quantity for build order'),
)

consumed = models.DecimalField(
decimal_places=5,
max_digits=15,
default=0,
validators=[MinValueValidator(0)],
verbose_name=_('Consumed'),
help_text=_('Quantity of consumed stock'),
)

@property
def part(self):
"""Return the sub_part reference from the link bom_item."""
Expand Down Expand Up @@ -1645,6 +1655,10 @@ def is_overallocated(self):
"""Return True if this BuildLine is over-allocated."""
return self.allocated_quantity() > self.quantity

def is_fully_consumed(self):
"""Return True if this BuildLine is fully consumed."""
return self.consumed >= self.quantity


class BuildItem(InvenTree.models.InvenTreeMetadataModel):
"""A BuildItem links multiple StockItem objects to a Build.
Expand Down Expand Up @@ -1812,20 +1826,36 @@ def bom_item(self):
return self.build_line.bom_item if self.build_line else None

@transaction.atomic
def complete_allocation(self, user, notes=''):
def complete_allocation(self, quantity=None, notes='', user=None):
"""Complete the allocation of this BuildItem into the output stock item.

Arguments:
quantity: The quantity to allocate (default is the full quantity)
notes: Additional notes to add to the transaction
user: The user completing the allocation

- If the referenced part is trackable, the stock item will be *installed* into the build output
- If the referenced part is *not* trackable, the stock item will be *consumed* by the build order

TODO: This is quite expensive (in terms of number of database hits) - and requires some thought
TODO: Revisit, and refactor!

"""
# If the quantity is not provided, use the quantity of this BuildItem
if quantity is None:
quantity = self.quantity

item = self.stock_item

# Ensure we are not allocating more than available
if quantity > item.quantity:
raise ValidationError({
'quantity': _('Allocated quantity exceeds available stock quantity')
})

# Split the allocated stock if there are more available than allocated
if item.quantity > self.quantity:
item = item.splitStock(self.quantity, None, user, notes=notes)
if item.quantity > quantity:
item = item.splitStock(quantity, None, user, notes=notes)

# For a trackable part, special consideration needed!
if item.part.trackable:
Expand All @@ -1835,7 +1865,7 @@ def complete_allocation(self, user, notes=''):

# Install the stock item into the output
self.install_into.installStockItem(
item, self.quantity, user, notes, build=self.build
item, quantity, user, notes, build=self.build
)

else:
Expand All @@ -1851,6 +1881,18 @@ def complete_allocation(self, user, notes=''):
deltas={'buildorder': self.build.pk, 'quantity': float(item.quantity)},
)

# Increase the "consumed" count for the associated BuildLine
self.build_line.consumed += quantity
self.build_line.save()

# Decrease the allocated quantity
self.quantity = max(0, self.quantity - quantity)

if self.quantity <= 0:
self.delete()
else:
self.save()

build_line = models.ForeignKey(
BuildLine, on_delete=models.CASCADE, null=True, related_name='allocations'
)
Expand Down
Loading
Loading