Skip to content

Commit

Permalink
[Feature] Scrap Build Outputs (#4800)
Browse files Browse the repository at this point in the history
* Update docs for status codes

* Adds API endpoint for scrapping individual build outputs

* Support 'buildorder' reference in stock tracking history

* Add page for build output documentation

* Build docs

* Add example build order process to docs

* remove debug statement

* JS lint cleanup

* Add migration file for stock status

* Add unit tests for build output scrapping

* Increment API version

* bug fix
  • Loading branch information
SchrodingersGat authored May 13, 2023
1 parent 634daa2 commit b2ceac2
Show file tree
Hide file tree
Showing 39 changed files with 784 additions and 244 deletions.
6 changes: 5 additions & 1 deletion InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@


# InvenTree API version
INVENTREE_API_VERSION = 112
INVENTREE_API_VERSION = 113

"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
v113 -> 2023-05-13 : https://github.com/inventree/InvenTree/pull/4800
- Adds API endpoints for scrapping a build output
v112 -> 2023-05-13: https://github.com/inventree/InvenTree/pull/4741
- Adds flag use_pack_size to the stock addition API, which allows addings packs
Expand All @@ -16,6 +19,7 @@
- Adds tags to the ManufacturerPart serializer
- Adds tags to the StockItem serializer
- Adds tags to the StockLocation serializer
v110 -> 2023-04-26 : https://github.com/inventree/InvenTree/pull/4698
- Adds 'order_currency' field for PurchaseOrder / SalesOrder endpoints
Expand Down
5 changes: 3 additions & 2 deletions InvenTree/InvenTree/status_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,13 +227,12 @@ class StockStatus(StatusCode):
LOST: _("Lost"),
REJECTED: _("Rejected"),
QUARANTINED: _("Quarantined"),
RETURNED: _("Returned"),
}

colors = {
OK: 'success',
ATTENTION: 'warning',
DAMAGED: 'danger',
DAMAGED: 'warning',
DESTROYED: 'danger',
LOST: 'dark',
REJECTED: 'danger',
Expand Down Expand Up @@ -289,6 +288,7 @@ class StockHistoryCode(StatusCode):
# Build order codes
BUILD_OUTPUT_CREATED = 50
BUILD_OUTPUT_COMPLETED = 55
BUILD_OUTPUT_REJECTED = 56
BUILD_CONSUMED = 57

# Sales order codes
Expand Down Expand Up @@ -337,6 +337,7 @@ class StockHistoryCode(StatusCode):

BUILD_OUTPUT_CREATED: _('Build order output created'),
BUILD_OUTPUT_COMPLETED: _('Build order output completed'),
BUILD_OUTPUT_REJECTED: _('Build order output rejected'),
BUILD_CONSUMED: _('Consumed by build order'),

SHIPPED_AGAINST_SALES_ORDER: _("Shipped against Sales Order"),
Expand Down
14 changes: 14 additions & 0 deletions InvenTree/build/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,19 @@ class BuildOutputCreate(BuildOrderContextMixin, CreateAPI):
serializer_class = build.serializers.BuildOutputCreateSerializer


class BuildOutputScrap(BuildOrderContextMixin, CreateAPI):
"""API endpoint for scrapping build output(s)."""

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

def get_serializer_context(self):
"""Add extra context information to the endpoint serializer."""
ctx = super().get_serializer_context()
ctx['to_complete'] = False
return ctx


class BuildOutputComplete(BuildOrderContextMixin, CreateAPI):
"""API endpoint for completing build outputs."""

Expand Down Expand Up @@ -489,6 +502,7 @@ class BuildAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI):
re_path(r'^complete/', BuildOutputComplete.as_view(), name='api-build-output-complete'),
re_path(r'^create-output/', BuildOutputCreate.as_view(), name='api-build-output-create'),
re_path(r'^delete-outputs/', BuildOutputDelete.as_view(), name='api-build-output-delete'),
re_path(r'^scrap-outputs/', BuildOutputScrap.as_view(), name='api-build-output-scrap'),
re_path(r'^finish/', BuildFinish.as_view(), name='api-build-finish'),
re_path(r'^cancel/', BuildCancel.as_view(), name='api-build-cancel'),
re_path(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'),
Expand Down
83 changes: 78 additions & 5 deletions InvenTree/build/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ def create_build_output(self, quantity, **kwargs):
location: Override location
auto_allocate: Automatically allocate stock with matching serial numbers
"""
user = kwargs.get('user', None)
batch = kwargs.get('batch', self.batch)
location = kwargs.get('location', self.destination)
serials = kwargs.get('serials', None)
Expand All @@ -630,6 +631,24 @@ def create_build_output(self, quantity, **kwargs):
or multiple outputs (with quantity = 1)
"""

def _add_tracking_entry(output, user):
"""Helper function to add a tracking entry to the newly created output"""
deltas = {
'quantity': float(output.quantity),
'buildorder': self.pk,
}

if output.batch:
deltas['batch'] = output.batch

if output.serial:
deltas['serial'] = output.serial

if output.location:
deltas['location'] = output.location.pk

output.add_tracking_entry(StockHistoryCode.BUILD_OUTPUT_CREATED, user, deltas)

multiple = False

# Serial numbers are provided? We need to split!
Expand Down Expand Up @@ -663,6 +682,8 @@ def create_build_output(self, quantity, **kwargs):
is_building=True,
)

_add_tracking_entry(output, user)

if auto_allocate and serial is not None:

# Get a list of BomItem objects which point to "trackable" parts
Expand Down Expand Up @@ -695,7 +716,7 @@ def create_build_output(self, quantity, **kwargs):
else:
"""Create a single build output of the given quantity."""

stock.models.StockItem.objects.create(
output = stock.models.StockItem.objects.create(
quantity=quantity,
location=location,
part=self.part,
Expand All @@ -704,6 +725,8 @@ def create_build_output(self, quantity, **kwargs):
is_building=True
)

_add_tracking_entry(output, user)

if self.status == BuildStatus.PENDING:
self.status = BuildStatus.PRODUCTION
self.save()
Expand Down Expand Up @@ -773,6 +796,50 @@ def subtract_allocated_stock(self, user):
# Delete allocation
items.all().delete()

@transaction.atomic
def scrap_build_output(self, output, location, **kwargs):
"""Mark a particular build output as scrapped / rejected
- Mark the output as "complete"
- *Do Not* update the "completed" count for this order
- Set the item status to "scrapped"
- Add a transaction entry to the stock item history
"""

if not output:
raise ValidationError(_("No build output specified"))

user = kwargs.get('user', None)
notes = kwargs.get('notes', '')
discard_allocations = kwargs.get('discard_allocations', False)

# Update build output item
output.is_building = False
output.status = StockStatus.REJECTED
output.location = location
output.save(add_note=False)

allocated_items = output.items_to_install.all()

# Complete or discard allocations
for build_item in allocated_items:
if not discard_allocations:
build_item.complete_allocation(user)

# Delete allocations
allocated_items.delete()

output.add_tracking_entry(
StockHistoryCode.BUILD_OUTPUT_REJECTED,
user,
notes=notes,
deltas={
'location': location.pk,
'status': StockStatus.REJECTED,
'buildorder': self.pk,
}
)

@transaction.atomic
def complete_build_output(self, output, user, **kwargs):
"""Complete a particular build output.
Expand Down Expand Up @@ -801,15 +868,21 @@ def complete_build_output(self, output, user, **kwargs):
output.location = location
output.status = status

output.save()
output.save(add_note=False)

deltas = {
'status': status,
'buildorder': self.pk
}

if location:
deltas['location'] = location.pk

output.add_tracking_entry(
StockHistoryCode.BUILD_OUTPUT_COMPLETED,
user,
notes=notes,
deltas={
'status': status,
}
deltas=deltas
)

# Increase the completed quantity for this build
Expand Down
72 changes: 72 additions & 0 deletions InvenTree/build/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,12 +302,14 @@ def save(self):
auto_allocate = data.get('auto_allocate', False)

build = self.get_build()
user = self.context['request'].user

build.create_build_output(
quantity,
serials=self.serials,
batch=batch_code,
auto_allocate=auto_allocate,
user=user,
)


Expand Down Expand Up @@ -349,6 +351,76 @@ def save(self):
build.delete_output(output)


class BuildOutputScrapSerializer(serializers.Serializer):
"""DRF serializer for scrapping one or more build outputs"""

class Meta:
"""Serializer metaclass"""
fields = [
'outputs',
'location',
'notes',
]

outputs = BuildOutputSerializer(
many=True,
required=True,
)

location = serializers.PrimaryKeyRelatedField(
queryset=StockLocation.objects.all(),
many=False,
allow_null=False,
required=True,
label=_('Location'),
help_text=_('Stock location for scrapped outputs'),
)

discard_allocations = serializers.BooleanField(
required=False,
default=False,
label=_('Discard Allocations'),
help_text=_('Discard any stock allocations for scrapped outputs'),
)

notes = serializers.CharField(
label=_('Notes'),
help_text=_('Reason for scrapping build output(s)'),
required=True,
allow_blank=False,
)

def validate(self, data):
"""Perform validation on the serializer data"""
super().validate(data)
outputs = data.get('outputs', [])

if len(outputs) == 0:
raise ValidationError(_("A list of build outputs must be provided"))

return data

def save(self):
"""Save the serializer to scrap the build outputs"""

build = self.context['build']
request = self.context['request']
data = self.validated_data
outputs = data.get('outputs', [])

# Scrap the build outputs
with transaction.atomic():
for item in outputs:
output = item['output']
build.scrap_build_output(
output,
data.get('location', None),
user=request.user,
notes=data.get('notes', ''),
discard_allocations=data.get('discard_allocations', False)
)


class BuildOutputCompleteSerializer(serializers.Serializer):
"""DRF serializer for completing one or more build outputs."""

Expand Down
5 changes: 5 additions & 0 deletions InvenTree/build/templates/build/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ <h4>{% trans "Incomplete Build Outputs" %}</h4>
<span class='fas fa-check-circle icon-green'></span> {% trans "Complete outputs" %}
</a></li>
{% endif %}
{% if roles.build.change %}
<li><a class='dropdown-item' href='#' id='multi-output-scrap' title='{% trans "Scrap selected build outputs" %}'>
<span class='fas fa-times-circle icon-red'></span> {% trans "Scrap outputs" %}
</a></li>
{% endif %}
{% if roles.build.delete %}
<li><a class='dropdown-item' href='#' id='multi-output-delete' title='{% trans "Delete selected build outputs" %}'>
<span class='fas fa-trash-alt icon-red'></span> {% trans "Delete outputs" %}
Expand Down
Loading

0 comments on commit b2ceac2

Please sign in to comment.