[Feature] Scrap Build Outputs (#4800)
* 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
@ -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
|
||||
|
||||
@ -16,6 +19,7 @@ v111 -> 2023-05-02 : https://github.com/inventree/InvenTree/pull/4367
|
||||
- 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
|
||||
|
||||
|
@ -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',
|
||||
@ -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
|
||||
@ -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"),
|
||||
|
@ -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."""
|
||||
|
||||
@ -489,6 +502,7 @@ build_api_urls = [
|
||||
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'),
|
||||
|
@ -620,6 +620,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
||||
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)
|
||||
@ -630,6 +631,24 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
||||
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!
|
||||
@ -663,6 +682,8 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
||||
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
|
||||
@ -695,7 +716,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
||||
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,
|
||||
@ -704,6 +725,8 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
||||
is_building=True
|
||||
)
|
||||
|
||||
_add_tracking_entry(output, user)
|
||||
|
||||
if self.status == BuildStatus.PENDING:
|
||||
self.status = BuildStatus.PRODUCTION
|
||||
self.save()
|
||||
@ -773,6 +796,50 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
||||
# 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.
|
||||
@ -801,15 +868,21 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
||||
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
|
||||
|
@ -302,12 +302,14 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
@ -349,6 +351,76 @@ class BuildOutputDeleteSerializer(serializers.Serializer):
|
||||
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."""
|
||||
|
||||
|
@ -261,6 +261,11 @@
|
||||
<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" %}
|
||||
|
@ -10,7 +10,7 @@ from part.models import Part
|
||||
from build.models import Build, BuildItem
|
||||
from stock.models import StockItem
|
||||
|
||||
from InvenTree.status_codes import BuildStatus
|
||||
from InvenTree.status_codes import BuildStatus, StockStatus
|
||||
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||
|
||||
|
||||
@ -924,3 +924,127 @@ class BuildListTest(BuildAPITest):
|
||||
builds = response.data
|
||||
|
||||
self.assertEqual(len(builds), 20)
|
||||
|
||||
|
||||
class BuildOutputScrapTest(BuildAPITest):
|
||||
"""Unit tests for scrapping build outputs"""
|
||||
|
||||
def scrap(self, build_id, data, expected_code=None):
|
||||
"""Helper method to POST to the scrap API"""
|
||||
|
||||
url = reverse('api-build-output-scrap', kwargs={'pk': build_id})
|
||||
|
||||
response = self.post(url, data, expected_code=expected_code)
|
||||
|
||||
return response.data
|
||||
|
||||
def test_invalid_scraps(self):
|
||||
"""Test that invalid scrap attempts are rejected"""
|
||||
|
||||
# Test with missing required fields
|
||||
response = self.scrap(1, {}, expected_code=400)
|
||||
|
||||
for field in ['outputs', 'location', 'notes']:
|
||||
self.assertIn('This field is required', str(response[field]))
|
||||
|
||||
# Scrap with no outputs specified
|
||||
response = self.scrap(
|
||||
1,
|
||||
{
|
||||
'outputs': [],
|
||||
'location': 1,
|
||||
'notes': 'Should fail',
|
||||
}
|
||||
)
|
||||
|
||||
self.assertIn('A list of build outputs must be provided', str(response))
|
||||
|
||||
# Scrap with an invalid output ID
|
||||
response = self.scrap(
|
||||
1,
|
||||
{
|
||||
'outputs': [
|
||||
{
|
||||
'output': 9999,
|
||||
}
|
||||
],
|
||||
'location': 1,
|
||||
'notes': 'Should fail',
|
||||
},
|
||||
expected_code=400
|
||||
)
|
||||
|
||||
self.assertIn('object does not exist', str(response['outputs']))
|
||||
|
||||
# Create a build output, for a different build
|
||||
build = Build.objects.get(pk=2)
|
||||
output = StockItem.objects.create(
|
||||
part=build.part,
|
||||
quantity=10,
|
||||
batch='BATCH-TEST',
|
||||
is_building=True,
|
||||
build=build,
|
||||
)
|
||||
|
||||
response = self.scrap(
|
||||
1,
|
||||
{
|
||||
'outputs': [
|
||||
{
|
||||
'output': output.pk,
|
||||
},
|
||||
],
|
||||
'location': 1,
|
||||
'notes': 'Should fail',
|
||||
},
|
||||
expected_code=400
|
||||
)
|
||||
|
||||
self.assertIn("Build output does not match the parent build", str(response['outputs']))
|
||||
|
||||
def test_valid_scraps(self):
|
||||
"""Test that valid scrap attempts succeed"""
|
||||
|
||||
# Create a build output
|
||||
build = Build.objects.get(pk=1)
|
||||
|
||||
for _ in range(3):
|
||||
build.create_build_output(2)
|
||||
|
||||
outputs = build.build_outputs.all()
|
||||
|
||||
self.assertEqual(outputs.count(), 3)
|
||||
self.assertEqual(StockItem.objects.filter(build=build).count(), 3)
|
||||
|
||||
for output in outputs:
|
||||
self.assertEqual(output.status, StockStatus.OK)
|
||||
self.assertTrue(output.is_building)
|
||||
|
||||
# Scrap all three outputs
|
||||
self.scrap(
|
||||
1,
|
||||
{
|
||||
'outputs': [
|
||||
{
|
||||
'output': outputs[0].pk,
|
||||
},
|
||||
{
|
||||
'output': outputs[1].pk,
|
||||
},
|
||||
{
|
||||
'output': outputs[2].pk,
|
||||
},
|
||||
],
|
||||
'location': 1,
|
||||
'notes': 'Should succeed',
|
||||
},
|
||||
expected_code=201
|
||||
)
|
||||
|
||||
# There should still be three outputs associated with this build
|
||||
self.assertEqual(StockItem.objects.filter(build=build).count(), 3)
|
||||
|
||||
for output in outputs:
|
||||
output.refresh_from_db()
|
||||
self.assertEqual(output.status, StockStatus.REJECTED)
|
||||
self.assertFalse(output.is_building)
|
||||
|
@ -164,7 +164,7 @@ class PluginsRegistry:
|
||||
if not _maintenance:
|
||||
set_maintenance_mode(False)
|
||||
|
||||
logger.info('Finished loading plugins')
|
||||
logger.debug('Finished loading plugins')
|
||||
|
||||
def unload_plugins(self, force_reload: bool = False):
|
||||
"""Unload and deactivate all IntegrationPlugins.
|
||||
@ -335,7 +335,7 @@ class PluginsRegistry:
|
||||
return True
|
||||
|
||||
try:
|
||||
output = str(subprocess.check_output(['pip', 'install', '-U', '-r', settings.PLUGIN_FILE], cwd=settings.BASE_DIR.parent), 'utf-8')
|
||||
subprocess.check_output(['pip', 'install', '-U', '-r', settings.PLUGIN_FILE], cwd=settings.BASE_DIR.parent)
|
||||
except subprocess.CalledProcessError as error: # pragma: no cover
|
||||
logger.error(f'Ran into error while trying to install plugins!\n{str(error)}')
|
||||
return False
|
||||
@ -343,8 +343,6 @@ class PluginsRegistry:
|
||||
# System most likely does not have 'git' installed
|
||||
return False
|
||||
|
||||
logger.info(f'plugin requirements were run\n{output}')
|
||||
|
||||
# do not run again
|
||||
settings.PLUGIN_FILE_CHECKED = True
|
||||
return 'first_run'
|
||||
@ -503,7 +501,7 @@ class PluginsRegistry:
|
||||
if hasattr(mixin, '_activate_mixin'):
|
||||
mixin._activate_mixin(self, plugins, force_reload=force_reload, full_reload=full_reload)
|
||||
|
||||
logger.info('Done activating')
|
||||
logger.debug('Done activating')
|
||||
|
||||
def _deactivate_plugins(self, force_reload: bool = False):
|
||||
"""Run deactivation functions for all plugins.
|
||||
|
@ -19,6 +19,7 @@ import common.models
|
||||
import common.settings
|
||||
import stock.serializers as StockSerializers
|
||||
from build.models import Build
|
||||
from build.serializers import BuildSerializer
|
||||
from company.models import Company, SupplierPart
|
||||
from company.serializers import CompanySerializer, SupplierPartSerializer
|
||||
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
||||
@ -1308,6 +1309,15 @@ class StockTrackingList(ListAPI):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Add BuildOrder detail
|
||||
if 'buildorder' in deltas:
|
||||
try:
|
||||
order = Build.objects.get(pk=deltas['buildorder'])
|
||||
serializer = BuildSerializer(order)
|
||||
deltas['buildorder_detail'] = serializer.data
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if page is not None:
|
||||
return self.get_paginated_response(data)
|
||||
if request.is_ajax():
|
||||
|
19
InvenTree/stock/migrations/0099_alter_stockitem_status.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2.18 on 2023-05-13 05:15
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stock', '0098_auto_20230427_2033'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='status',
|
||||
field=models.PositiveIntegerField(choices=[(10, 'OK'), (50, 'Attention needed'), (55, 'Damaged'), (60, 'Destroyed'), (70, 'Lost'), (65, 'Rejected'), (75, 'Quarantined')], default=10, validators=[django.core.validators.MinValueValidator(0)]),
|
||||
),
|
||||
]
|
@ -397,9 +397,17 @@ function makeBuildOutputButtons(output_id, build_info, options={}) {
|
||||
'{% trans "Complete build output" %}',
|
||||
);
|
||||
|
||||
// Add a button to "delete" this build output
|
||||
// Add a button to "scrap" the build output
|
||||
html += makeIconButton(
|
||||
'fa-times-circle icon-red',
|
||||
'button-output-scrap',
|
||||
output_id,
|
||||
'{% trans "Scrap build output" %}',
|
||||
);
|
||||
|
||||
// Add a button to "remove" this build output
|
||||
html += makeDeleteButton(
|
||||
'button-output-delete',
|
||||
'button-output-remove',
|
||||
output_id,
|
||||
'{% trans "Delete build output" %}',
|
||||
);
|
||||
@ -452,6 +460,51 @@ function unallocateStock(build_id, options={}) {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Helper function to render a single build output in a modal form
|
||||
*/
|
||||
function renderBuildOutput(output, opts={}) {
|
||||
let pk = output.pk;
|
||||
|
||||
let output_html = imageHoverIcon(output.part_detail.thumbnail);
|
||||
|
||||
if (output.quantity == 1 && output.serial) {
|
||||
output_html += `{% trans "Serial Number" %}: ${output.serial}`;
|
||||
} else {
|
||||
output_html += `{% trans "Quantity" %}: ${output.quantity}`;
|
||||
if (output.part_detail && output.part_detail.units) {
|
||||
output_html += ` ${output.part_detail.units} `;
|
||||
}
|
||||
}
|
||||
|
||||
let buttons = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
buttons += makeRemoveButton('button-row-remove', pk, '{% trans "Remove row" %}');
|
||||
|
||||
buttons += '</div>';
|
||||
|
||||
let field = constructField(
|
||||
`outputs_output_${pk}`,
|
||||
{
|
||||
type: 'raw',
|
||||
html: output_html,
|
||||
},
|
||||
{
|
||||
hideLabels: true,
|
||||
}
|
||||
);
|
||||
|
||||
let html = `
|
||||
<tr id='output_row_${pk}'>
|
||||
<td>${field}</td>
|
||||
<td>${output.part_detail.full_name}</td>
|
||||
<td>${buttons}</td>
|
||||
</tr>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Launch a modal form to complete selected build outputs
|
||||
*/
|
||||
@ -465,48 +518,6 @@ function completeBuildOutputs(build_id, outputs, options={}) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Render a single build output (StockItem)
|
||||
function renderBuildOutput(output, opts={}) {
|
||||
var pk = output.pk;
|
||||
|
||||
var output_html = imageHoverIcon(output.part_detail.thumbnail);
|
||||
|
||||
if (output.quantity == 1 && output.serial) {
|
||||
output_html += `{% trans "Serial Number" %}: ${output.serial}`;
|
||||
} else {
|
||||
output_html += `{% trans "Quantity" %}: ${output.quantity}`;
|
||||
if (output.part_detail && output.part_detail.units) {
|
||||
output_html += ` ${output.part_detail.units} `;
|
||||
}
|
||||
}
|
||||
|
||||
var buttons = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
buttons += makeRemoveButton('button-row-remove', pk, '{% trans "Remove row" %}');
|
||||
|
||||
buttons += '</div>';
|
||||
|
||||
var field = constructField(
|
||||
`outputs_output_${pk}`,
|
||||
{
|
||||
type: 'raw',
|
||||
html: output_html,
|
||||
},
|
||||
{
|
||||
hideLabels: true,
|
||||
}
|
||||
);
|
||||
|
||||
var html = `
|
||||
<tr id='output_row_${pk}'>
|
||||
<td>${field}</td>
|
||||
<td>${output.part_detail.full_name}</td>
|
||||
<td>${buttons}</td>
|
||||
</tr>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// Construct table entries
|
||||
var table_entries = '';
|
||||
|
||||
@ -515,6 +526,9 @@ function completeBuildOutputs(build_id, outputs, options={}) {
|
||||
});
|
||||
|
||||
var html = `
|
||||
<div class='alert alert-block alert-success'>
|
||||
{% trans "Selected build outputs will be marked as complete" %}
|
||||
</div>
|
||||
<table class='table table-striped table-condensed' id='build-complete-table'>
|
||||
<thead>
|
||||
<th colspan='2'>{% trans "Output" %}</th>
|
||||
@ -613,8 +627,122 @@ function completeBuildOutputs(build_id, outputs, options={}) {
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Launch a modal form to scrap selected build outputs.
|
||||
* Scrapped outputs are marked as "complete", but with the "rejected" code
|
||||
* These outputs are not included in build completion calculations.
|
||||
*/
|
||||
function scrapBuildOutputs(build_id, outputs, options={}) {
|
||||
|
||||
if (outputs.length == 0) {
|
||||
showAlertDialog(
|
||||
'{% trans "Select Build Outputs" %}',
|
||||
'{% trans "At least one build output must be selected" %}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let table_entries = '';
|
||||
|
||||
outputs.forEach(function(output) {
|
||||
table_entries += renderBuildOutput(output);
|
||||
});
|
||||
|
||||
var html = `
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Selected build outputs will be marked as scrapped" %}
|
||||
<ul>
|
||||
<li>{% trans "Scrapped output are given the 'rejected' status" %}</li>
|
||||
<li>{% trans "Allocated stock items will no longer be available" %}</li>
|
||||
<li>{% trans "The completion status of the build order will not be adjusted" %}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<table class='table table-striped table-condensed' id='build-scrap-table'>
|
||||
<thead>
|
||||
<th colspan='2'>{% trans "Output" %}</th>
|
||||
<th><!-- Actions --></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
${table_entries}
|
||||
</tbody>
|
||||
</table>`;
|
||||
|
||||
constructForm(`{% url "api-build-list" %}${build_id}/scrap-outputs/`, {
|
||||
method: 'POST',
|
||||
preFormContent: html,
|
||||
fields: {
|
||||
location: {},
|
||||
notes: {},
|
||||
discard_allocations: {},
|
||||
},
|
||||
confirm: true,
|
||||
title: '{% trans "Scrap Build Outputs" %}',
|
||||
afterRender: function(fields, opts) {
|
||||
// Setup callbacks to remove outputs
|
||||
$(opts.modal).find('.button-row-remove').click(function() {
|
||||
let pk = $(this).attr('pk');
|
||||
$(opts.modal).find(`#output_row_${pk}`).remove();
|
||||
});
|
||||
},
|
||||
onSubmit: function(fields, opts) {
|
||||
let data = {
|
||||
outputs: [],
|
||||
location: getFormFieldValue('location', {}, opts),
|
||||
notes: getFormFieldValue('notes', {}, opts),
|
||||
discard_allocations: getFormFieldValue('discard_allocations', {type: 'boolean'}, opts),
|
||||
};
|
||||
|
||||
let output_pk_values = [];
|
||||
|
||||
outputs.forEach(function(output) {
|
||||
let pk = output.pk;
|
||||
let row = $(opts.modal).find(`#output_row_${pk}`);
|
||||
|
||||
if (row.exists()) {
|
||||
data.outputs.push({
|
||||
output: pk,
|
||||
});
|
||||
output_pk_values.push(pk);
|
||||
}
|
||||
});
|
||||
|
||||
opts.nested = {
|
||||
'outputs': output_pk_values,
|
||||
};
|
||||
|
||||
inventreePut(
|
||||
opts.url,
|
||||
data,
|
||||
{
|
||||
method: 'POST',
|
||||
success: function(response) {
|
||||
$(opts.modal).modal('hide');
|
||||
|
||||
if (options.success) {
|
||||
options.success(response);
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
switch (xhr.status) {
|
||||
case 400:
|
||||
handleFormErrors(xhr.responseJSON, fields, opts);
|
||||
break;
|
||||
default:
|
||||
$(opts.modal).modal('hide');
|
||||
showApiError(xhr, opts.url);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Launch a modal form to delete selected build outputs
|
||||
* Launch a modal form to delete selected build outputs.
|
||||
* Deleted outputs are expunged from the database.
|
||||
*/
|
||||
function deleteBuildOutputs(build_id, outputs, options={}) {
|
||||
|
||||
@ -626,48 +754,6 @@ function deleteBuildOutputs(build_id, outputs, options={}) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Render a single build output (StockItem)
|
||||
function renderBuildOutput(output, opts={}) {
|
||||
var pk = output.pk;
|
||||
|
||||
var output_html = imageHoverIcon(output.part_detail.thumbnail);
|
||||
|
||||
if (output.quantity == 1 && output.serial) {
|
||||
output_html += `{% trans "Serial Number" %}: ${output.serial}`;
|
||||
} else {
|
||||
output_html += `{% trans "Quantity" %}: ${output.quantity}`;
|
||||
if (output.part_detail && output.part_detail.units) {
|
||||
output_html += ` ${output.part_detail.units} `;
|
||||
}
|
||||
}
|
||||
|
||||
var buttons = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
buttons += makeRemoveButton('button-row-remove', pk, '{% trans "Remove row" %}');
|
||||
|
||||
buttons += '</div>';
|
||||
|
||||
var field = constructField(
|
||||
`outputs_output_${pk}`,
|
||||
{
|
||||
type: 'raw',
|
||||
html: output_html,
|
||||
},
|
||||
{
|
||||
hideLabels: true,
|
||||
}
|
||||
);
|
||||
|
||||
var html = `
|
||||
<tr id='output_row_${pk}'>
|
||||
<td>${field}</td>
|
||||
<td>${output.part_detail.full_name}</td>
|
||||
<td>${buttons}</td>
|
||||
</tr>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// Construct table entries
|
||||
var table_entries = '';
|
||||
|
||||
@ -676,6 +762,13 @@ function deleteBuildOutputs(build_id, outputs, options={}) {
|
||||
});
|
||||
|
||||
var html = `
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Selected build outputs will be deleted" %}
|
||||
<ul>
|
||||
<li>{% trans "Build output data will be permanently deleted" %}</li>
|
||||
<li>{% trans "Allocated stock items will be returned to stock" %}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<table class='table table-striped table-condensed' id='build-complete-table'>
|
||||
<thead>
|
||||
<th colspan='2'>{% trans "Output" %}</th>
|
||||
@ -952,8 +1045,25 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
);
|
||||
});
|
||||
|
||||
// Callback for the "delete" button
|
||||
$(table).find('.button-output-delete').click(function() {
|
||||
// Callback for the "scrap" button
|
||||
$(table).find('.button-output-scrap').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
var output = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||
|
||||
scrapBuildOutputs(
|
||||
build_info.pk,
|
||||
[output],
|
||||
{
|
||||
success: function() {
|
||||
$(table).bootstrapTable('refresh');
|
||||
$('#build-stock-table').bootstrapTable('refresh');
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Callback for the "remove" button
|
||||
$(table).find('.button-output-remove').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
var output = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||
@ -1368,6 +1478,25 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
|
||||
// Add callbacks for the various table menubar buttons
|
||||
|
||||
// Scrap multiple outputs
|
||||
$('#multi-output-scrap').click(function() {
|
||||
var outputs = getTableData(table);
|
||||
|
||||
scrapBuildOutputs(
|
||||
build_info.pk,
|
||||
outputs,
|
||||
{
|
||||
success: function() {
|
||||
// Reload the "in progress" table
|
||||
$('#build-output-table').bootstrapTable('refresh');
|
||||
|
||||
// Reload the "completed" table
|
||||
$('#build-stock-table').bootstrapTable('refresh');
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Complete multiple outputs
|
||||
$('#multi-output-complete').click(function() {
|
||||
var outputs = getTableData(table);
|
||||
|
@ -3,8 +3,6 @@
|
||||
{% load status_codes %}
|
||||
|
||||
/* globals
|
||||
attachSelect,
|
||||
closeModal,
|
||||
constructField,
|
||||
constructFormBody,
|
||||
getFormFieldValue,
|
||||
@ -18,12 +16,6 @@
|
||||
loadTableFilters,
|
||||
makeIconBadge,
|
||||
makeIconButton,
|
||||
makeOptionsList,
|
||||
modalEnable,
|
||||
modalSetContent,
|
||||
modalSetTitle,
|
||||
modalSubmit,
|
||||
openModal,
|
||||
renderLink,
|
||||
scanItemsIntoLocation,
|
||||
showAlertDialog,
|
||||
@ -54,7 +46,6 @@
|
||||
serializeStockItem,
|
||||
stockItemFields,
|
||||
stockLocationFields,
|
||||
stockStatusCodes,
|
||||
uninstallStockItem,
|
||||
*/
|
||||
|
||||
@ -603,22 +594,6 @@ function findStockItemBySerialNumber(part_id) {
|
||||
}
|
||||
|
||||
|
||||
/* Stock API functions
|
||||
* Requires api.js to be loaded first
|
||||
*/
|
||||
|
||||
function stockStatusCodes() {
|
||||
return [
|
||||
{% for code in StockStatus.list %}
|
||||
{
|
||||
key: {{ code.key }},
|
||||
text: '{{ code.value }}',
|
||||
},
|
||||
{% endfor %}
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Assign multiple stock items to a customer
|
||||
*/
|
||||
@ -2261,93 +2236,6 @@ function loadStockTable(table, options) {
|
||||
orderParts(parts, {});
|
||||
});
|
||||
|
||||
$('#multi-item-set-status').click(function() {
|
||||
// Select and set the STATUS field for selected stock items
|
||||
var selections = getTableData(table);
|
||||
|
||||
// Select stock status
|
||||
var modal = '#modal-form';
|
||||
|
||||
var status_list = makeOptionsList(
|
||||
stockStatusCodes(),
|
||||
function(item) {
|
||||
return item.text;
|
||||
},
|
||||
function(item) {
|
||||
return item.key;
|
||||
}
|
||||
);
|
||||
|
||||
// Add an empty option at the start of the list
|
||||
status_list.unshift('<option value="">---------</option>');
|
||||
|
||||
// Construct form
|
||||
var html = `
|
||||
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
|
||||
<div class='form-group'>
|
||||
<label class='control-label requiredField' for='id_status'>
|
||||
{% trans "Stock Status" %}
|
||||
</label>
|
||||
<div class='controls'>
|
||||
<select id='id_status' class='select form-control' name='label'>
|
||||
${status_list}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>`;
|
||||
|
||||
openModal({
|
||||
modal: modal,
|
||||
});
|
||||
|
||||
modalEnable(modal, true);
|
||||
modalSetTitle(modal, '{% trans "Set Stock Status" %}');
|
||||
modalSetContent(modal, html);
|
||||
|
||||
attachSelect(modal);
|
||||
|
||||
modalSubmit(modal, function() {
|
||||
var label = $(modal).find('#id_status');
|
||||
|
||||
var status_code = label.val();
|
||||
|
||||
closeModal(modal);
|
||||
|
||||
if (!status_code) {
|
||||
showAlertDialog(
|
||||
'{% trans "Select Status Code" %}',
|
||||
'{% trans "Status code must be selected" %}'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var requests = [];
|
||||
|
||||
selections.forEach(function(item) {
|
||||
var url = `/api/stock/${item.pk}/`;
|
||||
|
||||
requests.push(
|
||||
inventreePut(
|
||||
url,
|
||||
{
|
||||
status: status_code,
|
||||
},
|
||||
{
|
||||
method: 'PATCH',
|
||||
success: function() {
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
$.when.apply($, requests).done(function() {
|
||||
$(table).bootstrapTable('refresh');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$('#multi-item-delete').click(function() {
|
||||
var selections = getTableData(table);
|
||||
|
||||
@ -2697,11 +2585,24 @@ function loadStockTrackingTable(table, options) {
|
||||
html += '</td></tr>';
|
||||
}
|
||||
|
||||
// BuildOrder Information
|
||||
if (details.buildorder) {
|
||||
html += `<tr><th>{% trans "Build Order" %}</th>`;
|
||||
html += `<td>`;
|
||||
|
||||
if (details.buildorder_detail) {
|
||||
html += renderLink(
|
||||
details.buildorder_detail.reference,
|
||||
`/build/${details.buildorder}/`
|
||||
);
|
||||
} else {
|
||||
html += `<i>{% trans "Build order no longer exists" %}</i>`;
|
||||
}
|
||||
}
|
||||
|
||||
// PurchaseOrder Information
|
||||
if (details.purchaseorder) {
|
||||
|
||||
html += `<tr><th>{% trans "Purchase Order" %}</th>`;
|
||||
|
||||
html += '<td>';
|
||||
|
||||
if (details.purchaseorder_detail) {
|
||||
|
@ -405,6 +405,12 @@ function getStockTestTableFilters() {
|
||||
}
|
||||
|
||||
|
||||
// Return a dictionary of filters for the "stocktracking" table
|
||||
function getStockTrackingTableFilters() {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
// Return a dictionary of filters for the "part tests" table
|
||||
function getPartTestTemplateFilters() {
|
||||
return {
|
||||
@ -741,6 +747,8 @@ function getAvailableTableFilters(tableKey) {
|
||||
return getStockTableFilters();
|
||||
case 'stocktests':
|
||||
return getStockTestTableFilters();
|
||||
case 'stocktracking':
|
||||
return getStockTrackingTableFilters();
|
||||
case 'supplierpart':
|
||||
return getSupplierPartFilters();
|
||||
case 'usedin':
|
||||
|
@ -37,7 +37,6 @@
|
||||
<li><a class='dropdown-item' href='#' id='multi-item-merge' title='{% trans "Merge selected stock items" %}'><span class='fas fa-object-group'></span> {% trans "Merge stock" %}</a></li>
|
||||
<li><a class='dropdown-item' href='#' id='multi-item-order' title='{% trans "Order selected items" %}'><span class='fas fa-shopping-cart'></span> {% trans "Order stock" %}</a></li>
|
||||
<li><a class='dropdown-item' href='#' id='multi-item-assign' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
|
||||
<li><a class='dropdown-item' href='#' id='multi-item-set-status' title='{% trans "Change status" %}'><span class='fas fa-exclamation-circle'></span> {% trans "Change stock status" %}</a></li>
|
||||
{% endif %}
|
||||
{% if roles.stock.delete %}
|
||||
<li><a class='dropdown-item' href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete stock" %}</a></li>
|
||||
|
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 30 KiB |
BIN
docs/docs/assets/images/build/build_example_complete_outputs.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
docs/docs/assets/images/build/build_example_create.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
docs/docs/assets/images/build/build_example_create_outputs.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
docs/docs/assets/images/build/build_example_incomplete_list.png
Normal file
After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 31 KiB |
BIN
docs/docs/assets/images/build/build_output_create.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
docs/docs/assets/images/build/build_output_delete.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
docs/docs/assets/images/build/build_output_scrap.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
docs/docs/assets/images/build/build_outputs_complete.png
Normal file
After Width: | Height: | Size: 161 KiB |
BIN
docs/docs/assets/images/build/build_outputs_incomplete.png
Normal file
After Width: | Height: | Size: 149 KiB |
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 45 KiB |
BIN
docs/docs/assets/images/stock/stock_status_edit.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
docs/docs/assets/images/stock/stock_status_edit_multiple.png
Normal file
After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 60 KiB |
12
docs/docs/build/allocate.md
vendored
@ -131,16 +131,6 @@ Here we can see that the incomplete build outputs (serial numbers 15 and 14) now
|
||||
- Serial number 15 has been fully allocated, and can be completed
|
||||
- Serial number 14 has not been fully allocated, and cannot yet be completed
|
||||
|
||||
## Completing a Build Output
|
||||
|
||||
An individual build output is completed by selecting the "Complete build output" button associated with that build output:
|
||||
|
||||
{% with id="build_output_complete", url="build/build_output_complete.png", description="Complete build output" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
Here the user can select the destination location for the build output, as well as the stock item status.
|
||||
|
||||
### Allocated Stock
|
||||
|
||||
*Tracked* stock items which are allocated against the selected build output will be removed from stock, and installed "inside" the output assembly. The allocated stock items will still exist in the InvenTree database, however will no longer be available for regular stock actions.
|
||||
@ -151,7 +141,7 @@ Here the user can select the destination location for the build output, as well
|
||||
## Completing a Build
|
||||
|
||||
!!! warning "Complete Build Outputs"
|
||||
A build order cannot be completed if there are outstanding build outputs. Ensure that all build outputs are completed first.
|
||||
A build order cannot be completed if there are outstanding build outputs. Ensure that all [build outputs](./output.md) are completed first.
|
||||
|
||||
Once all build outputs have been completed, the build order itself can be completed by selecting the *Complete Build* button:
|
||||
|
||||
|
5
docs/docs/build/build.md
vendored
@ -66,8 +66,7 @@ The following parameters are available for each Build Order, and can be edited b
|
||||
|
||||
A *Build Output* creates a new stock instance of the assembly part, of a specified quantity. Each *Build Order* requires at least one build output. Multiple build outputs can be specified if the build is completed in batches.
|
||||
|
||||
!!! info "Example - Build Outputs"
|
||||
For example, let's say we wish to create 10 new "Widgets". We create a new build for the widget, which signals an *intent* to assemble the "Widget" in quantity 10. We can produce 5 widgets in a single day, and so we create 2 build outputs, each of quantity 5.
|
||||
Read more about build outputs [here](./output.md).
|
||||
|
||||
### Build Status
|
||||
|
||||
@ -125,7 +124,7 @@ The allocation table (as shown above) shows the stock allocation progress for th
|
||||
|
||||
### Build Outputs
|
||||
|
||||
The *Build Outputs* tab shows the outputs (created stock items) associated with this build.
|
||||
The *Build Outputs* tab shows the [build outputs](./output.md) (created stock items) associated with this build.
|
||||
|
||||
As shown below, there are separate panels for *incomplete* and *completed* build outputs.
|
||||
|
||||
|
57
docs/docs/build/example.md
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
---
|
||||
title: Build Order Example
|
||||
---
|
||||
|
||||
## Build Order Example
|
||||
|
||||
For example, let's say we wish to create 10 new "Widgets". We create a new build for the widget, which signals an *intent* to assemble the "Widget" in quantity 10. As the *Widget* is a serialized part, with tracked subcomponents, the build outputs must themselves be serialized. This means that we need to generate 10 separate build outputs for this build order.
|
||||
|
||||
### Create Build Order
|
||||
|
||||
First, create a new build order for the *Widget* assembly:
|
||||
|
||||
{% with id="build_example_create", url="build/build_example_create.png", description="Create build order" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
### Generate Build Outputs
|
||||
|
||||
Generate build outputs for this build order. As this is a tracked item, with tracked subcomponents, the build outputs must be serialized:
|
||||
|
||||
{% with id="build_example_create_outputs", url="build/build_example_create_outputs.png", description="Create build outputs" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
A list of new build outputs will have now been generated:
|
||||
|
||||
{% with id="build_example_incomplete_list", url="build/build_example_incomplete_list.png", description="Incomplete build outputs" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
### Allocate Untracked Stock
|
||||
|
||||
Untracked stock items are allocated to the build order in the *Allocate Stock* tab:
|
||||
|
||||
{% with id="build_example_allocate_untracked", url="build/build_example_allocate_untracked.png", description="Allocated Untracked Stock" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
### Allocate Tracked Stock
|
||||
|
||||
Tracked stock items are allocated to individual build outputs:
|
||||
|
||||
{% with id="build_example_allocate_tracked", url="build/build_example_allocated_tracked.png", description="Allocated Tracked Stock" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
### Complete Build Outputs
|
||||
|
||||
Mark each build output as complete:
|
||||
|
||||
{% with id="build_example_complete_outputs", url="build/build_example_complete_outputs.png", description="Complete Build Outputs" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
### Complete Build Order
|
||||
|
||||
Once the build outputs have been completed, and all stock has been allocated, the build order can be completed.
|
119
docs/docs/build/output.md
vendored
Normal file
@ -0,0 +1,119 @@
|
||||
---
|
||||
title: Build Outputs
|
||||
---
|
||||
|
||||
## Build Outputs
|
||||
|
||||
With reference to a [build order](./build.md), a *Build Output* is a finished product which is expected to be produced by completing the order.
|
||||
|
||||
- A single build order may have multiple build outputs which are produced at different times or by different operators.
|
||||
- An individual build output may be a single unit, or a batch of units
|
||||
- Serial numbers and batch codes can be associated with a build output
|
||||
|
||||
### Incomplete Outputs
|
||||
|
||||
The *Incomplete Outputs* tab displays any outstanding / in-progress build outputs for the current build order.
|
||||
|
||||
{% with id="build-outputs-incomplete", url="build/build_outputs_incomplete.png", description="Incomplete build outputs" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
### Completed Outputs
|
||||
|
||||
The *Completed Outputs* tab displays any [completed](#complete-build-output) or [scrapped](#scrap-build-output) outputs for the current build order.
|
||||
|
||||
{% with id="build-outputs-complete", url="build/build_outputs_complete.png", description="Complete build outputs" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
## Create Build Output
|
||||
|
||||
Create a new build output by pressing the <span class="badge inventree add"><span class='fas fa-plus-circle'></span> New Build Output</span> button under the [incomplete outputs](#incomplete-outputs) tab:
|
||||
|
||||
{% with id="build_output_create", url="build/build_output_create.png", description="Create build output" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
### Create Options
|
||||
|
||||
The following options are available when creating a new build output:
|
||||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| Quantity | The number of items to create as part of this build output |
|
||||
| Serial Numbers | If this is a tracked build output, the serial numbers for each of the generated outputs |
|
||||
| Batch Code | Batch code identifier for the generated output(s) |
|
||||
| Auto Allocate Serial Numbers | If selected, any available tracked subcomponents which already have serial numbers assigned, will be automatically assigned to matching build outputs |
|
||||
|
||||
### Specifying Serial Numbers
|
||||
|
||||
Refer to the [serial number generation guide](../stock/tracking.md#generating-serial-numbers) for further information on serial number input.
|
||||
|
||||
## Complete Build Output
|
||||
|
||||
*Completing* a build output marks that output as finished, in the context of the given build order.
|
||||
|
||||
An individual build output is completed by selecting the "Complete build output" button associated with that build output:
|
||||
|
||||
{% with id="build_output_complete", url="build/build_output_complete.png", description="Complete build output" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
Here the user can select the destination location for the build output, as well as the stock item status.
|
||||
|
||||
Marking the build output(s) as complete performs the following actions:
|
||||
|
||||
- The completed build quantity is increased by the quantity of the selected build output(s)
|
||||
- The build output(s) are marked as "completed", and available for stock actions
|
||||
- Any [tracked BOM items](./allocate.md#allocating-tracked-stock) which are allocated to the build output are *installed* into that build output.
|
||||
|
||||
### Complete Options
|
||||
|
||||
The following options are available when completing a build output:
|
||||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| Status | The [stock status](../stock/status.md) for the completed outputs |
|
||||
| Location | The [stock location](../stock/stock.md#stock-location) where the outputs will be located |
|
||||
| Notes | Any additional notes associated with the completion of these outputs |
|
||||
| Accept Incomplete Allocation | If selected, this option allows [tracked build outputs](./allocate.md#tracked-build-outputs) to be completed in the case where required BOM items have not been fully allocated |
|
||||
|
||||
## Scrap Build Output
|
||||
|
||||
*Scrapping* a build output marks the particular output as rejected, in the context of the given build order.
|
||||
|
||||
An individual build output is completed by selecting the *Scrap build output* button associated with that build output:
|
||||
|
||||
{% with id="build_output_scrap", url="build/build_output_scrap.png", description="Scrap build output" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
Marking the build output(s) as scrapped performs the following actions:
|
||||
|
||||
- The build outputs are marked as "rejected" and removed from the build
|
||||
- The completed build quantity *does not increase*
|
||||
- The build outputs are not available for any further stock actions
|
||||
- Optionally, any [tracked BOM items](./allocate.md#allocating-tracked-stock) which are allocated to the build output are *installed* into the rejected build output
|
||||
|
||||
### Scrap Options
|
||||
|
||||
The following options are available when scrapping a build order:
|
||||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| Location | The stock location where the scrapped build output(s) will be located |
|
||||
| Notes | Any additional notes associated with the scrapping of these outputs |
|
||||
| Discard Allocations | If selected, any installed BOM items will be removed first, before marking the build output as scrapped. Use this option if the installed items are recoverable and can be used elsewhere |
|
||||
|
||||
## Delete Build Output
|
||||
|
||||
*Deleting* a build output causes the build output to be cancelled, and removed from the database entirely. Use this option when the build output does not physically exist (or was never built) and should not be tracked in the database.
|
||||
|
||||
{% with id="build_output_delete", url="build/build_output_delete.png", description="Delete build output" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
Marking the build output(s) as deleted performs the following actions:
|
||||
|
||||
- Any allocated stock items are returned to stock
|
||||
- The build output is removed from the database
|
@ -4,39 +4,36 @@ title: Stock Status
|
||||
|
||||
## Stock Status
|
||||
|
||||
Stock status serves at categorizing and identifying the state of stock items.
|
||||
Each [Stock Item](./stock.md#stock-item) has a *status* attribute, which serves to identify the current condition of the individual stock item.
|
||||
|
||||
Below is the current list of stock status and their proposed meaning:
|
||||
Certain stock item status codes will restrict the availability of the stock item.
|
||||
|
||||
| Status | Description |
|
||||
| ----------- | ----------- |
|
||||
| OK | Stock item is healthy, nothing wrong to report |
|
||||
| Attention needed | Stock item hasn't been checked or tested yet |
|
||||
| Damaged | Stock item is not functional in its present state |
|
||||
| Destroyed | Stock item has been destroyed |
|
||||
| Lost | Stock item has been lost |
|
||||
| Rejected | Stock item did not pass the quality control standards |
|
||||
| Returned | Stock item was returned to seller (if bought) or is a customer return (if sold) |
|
||||
| Quarantined | Stock item has been intentionally isolated and it unavailable |
|
||||
Below is the list of available stock status codes and their meaning:
|
||||
|
||||
Stock status code will remove the stock from certain operations. For instance, users can't add "destroyed" or "lost" stock to a sales order.
|
||||
| Status | Description | Available |
|
||||
| ----------- | ----------- | --- |
|
||||
| <span class='badge inventree success'>OK</span> | Stock item is healthy, nothing wrong to report | <span class='badge inventree success'>Yes</span> |
|
||||
| <span class='badge inventree warning'>Attention needed</span> | Stock item hasn't been checked or tested yet | <span class='badge inventree success'>Yes</span> |
|
||||
| <span class='badge inventree warning'>Damaged</span> | Stock item is not functional in its present state | <span class='badge inventree success'>Yes</span> |
|
||||
| <span class='badge inventree danger'>Destroyed</span> | Stock item has been destroyed | <span class='badge inventree danger'>No</span> |
|
||||
| <span class='badge inventree'>Lost</span> | Stock item has been lost | <span class='badge inventree danger'>No</span> |
|
||||
| <span class='badge inventree danger'>Rejected</span> | Stock item did not pass the quality control standards | <span class='badge inventree danger'>No</span> |
|
||||
| <span class='badge inventree info'>Quarantined</span> | Stock item has been intentionally isolated and it unavailable | <span class='badge inventree danger'>No</span> |
|
||||
|
||||
The stock status is displayed as a label in the header of each stock item detail page, for instance here the stock status is "OK":
|
||||
The *status* of a given stock item is displayed on the stock item detail page:
|
||||
|
||||
{% with id="stock_status_label", url="stock/stock_status_label.png", description="Stock Status Label" %}
|
||||
{% include 'img.html' %}
|
||||
{% endwith %}
|
||||
|
||||
### Default Status Code
|
||||
|
||||
The default status code for any newly created Stock Item is <span class='badge inventree success'>OK</span>
|
||||
|
||||
## Update Status
|
||||
|
||||
In the "Stock" tab of the part view, select all stock items which stock status needs to be updated:
|
||||
To update the status code for an individual stock item, open the *Edit Stock Item* dialog and then select the required status code in the *Status* field
|
||||
|
||||
{% with id="stock_status_change_multiple", url="stock/stock_status_change_multiple.png", description="Stock Status Status Multiple" %}
|
||||
{% include 'img.html' %}
|
||||
{% endwith %}
|
||||
|
||||
Click on `Stock Options > Change stock status`, select the new status then submit. All selected stock items status will be automatically updated:
|
||||
|
||||
{% with id="stock_status_change_multiple_done", url="stock/stock_status_change_multiple_done.png", description="Stock Status Status Multiple Done" %}
|
||||
{% with id="stock_status_edit", url="stock/stock_status_edit.png", description="Edit stock item status" %}
|
||||
{% include 'img.html' %}
|
||||
{% endwith %}
|
||||
|
@ -135,3 +135,22 @@ div:nth-of-type(1) p {
|
||||
margin: 5px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.badge.inventree.success {
|
||||
background-color: #5cb85c;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.badge.inventree.warning {
|
||||
background-color: #f0ad4e;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.badge.inventree.danger {
|
||||
background-color: #d9534f;
|
||||
}
|
||||
|
||||
.badge.inventree.info {
|
||||
background-color: #5bc0de;
|
||||
color: #555;
|
||||
}
|
||||
|
@ -99,7 +99,9 @@ nav:
|
||||
- Test Results: stock/test.md
|
||||
- Build:
|
||||
- Build Orders: build/build.md
|
||||
- Build Outputs: build/output.md
|
||||
- Allocating Stock: build/allocate.md
|
||||
- Example Build Order: build/example.md
|
||||
- Bill of Materials: build/bom.md
|
||||
- Importing BOM Data: build/bom_import.md
|
||||
- Exporting BOM Data: build/bom_export.md
|
||||
|