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
100 changes: 98 additions & 2 deletions inventree/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Build(
def issue(self):
"""Mark this build as 'issued'."""
return self._statusupdate(status='issue')

def hold(self):
"""Mark this build as 'on hold'."""
return self._statusupdate(status='hold')
Expand Down Expand Up @@ -54,6 +54,102 @@ def getLines(self, **kwargs):
""" Return the build line items associated with this build order """
return BuildLine.list(self._api, build=self.pk, **kwargs)

def getBuildOutputs(self, complete: bool = None, **kwargs):
""" Return the build output items associated with this build order

Arguments:
- complete: If not None, filter the build outputs by their 'complete' status
"""
if complete is not None:
kwargs['is_building'] = not complete

# Find stock items which are marked as 'outputs' of this build order
return inventree.stock.StockItem.list(
self._api,
build=self.pk,
**kwargs
)

def createBuildOutput(self, **kwargs):
""" Create a new build output (stock item) associated with this build order """
result = self._api.post(
f'{self.URL}{self.pk}/create-output/',
data={
**kwargs
}
)

# Note: The response is a list of created stock items
return [inventree.stock.StockItem(self._api, item['pk'], item) for item in result]

def cancelBuildOutputs(self, outputs):
""" Cancel a build output item associated with this build order

Arguments:
- outputs: The StockItem object (or list of StockItem objects) to cancel
"""

if not isinstance(outputs, list):
outputs = [outputs]

return self._api.post(
f'{self.URL}{self.pk}/delete-outputs/',
data={
'outputs': [
{'output': output.pk} for output in outputs
]
}
)

def scrapBuildOutput(self, output, **kwargs):
""" Scrap a single build output item associated with this build order

Arguments:
- output: The StockItem object to scrap
"""

data = {
**kwargs,
'outputs': [
{
'output': output.pk,
'quantity': kwargs.get('quantity', output.quantity),
}
]
}

data['location'] = kwargs.get('location', output.location)

return self._api.post(
f'{self.URL}{self.pk}/scrap-outputs/',
data=data
)

def completeBuildOutput(self, output, **kwargs):
""" Mark a single build output item as complete

Arguments:
- output: The StockItem object to mark as complete
"""

data = {
**kwargs,
'outputs': [
{
'output': output.pk,
'quantity': kwargs.get('quantity', output.quantity),
}
]
}

# If a location is not specified, use the current location of the stock item
data['location'] = kwargs.get('location', output.location)

return self._api.post(
f'{self.URL}{self.pk}/complete/',
data=data
)


class BuildLine(
inventree.base.InventreeObject,
Expand Down Expand Up @@ -83,7 +179,7 @@ def getBuild(self):
def getBuildLine(self):
"""Return the BuildLine object associated with this build item"""
return BuildLine(self._api, self.build_line)

def getStockItem(self):
"""Return the StockItem object associated with this build item"""
return inventree.stock.StockItem(self._api, self.stock_item)
134 changes: 134 additions & 0 deletions test/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,137 @@ def test_build_complete(self):
# Check status
self.assertEqual(build.status, 40)
self.assertEqual(build.status_text, 'Complete')


class BuildOrderOutputTests(InvenTreeTestCase):
""" Unit tests for build output functionality """

def setUp(self):
""" Ensure we have a base build order to work with """

super().setUp()

builds = Build.list(self.api)

self.build = Build.create(
self.api,
{
"title": "A new build order",
"part": 25,
"quantity": 10,
"reference": f"BO-{len(builds) + 1:04d}"
}
)

def test_create_build_output(self):
"""Test that we can create a build output item"""

# Initially, there should be no build outputs
outputs = self.build.getBuildOutputs()
self.assertEqual(len(outputs), 0)

# Let's create 3 new outputs (with serial numbers)
outputs = self.build.createBuildOutput(
quantity=3,
batch_code='TEST-BATCH-001',
serial_numbers='400+'
)

self.assertEqual(len(outputs), 3)
self.assertEqual(len(self.build.getBuildOutputs()), 3)

for output in outputs:
self.assertIsNotNone(output)
self.assertEqual(output.quantity, 1)
self.assertEqual(output.batch, 'TEST-BATCH-001')
self.assertEqual(output.build, self.build.pk)
self.assertEqual(output.part, self.build.part)
self.assertTrue(output.is_building)

# Directly delete the build output
output.delete()

# There should now be no build outputs again
self.assertEqual(len(self.build.getBuildOutputs()), 0)

def test_cancel_build_output(self):
""" Test that we can cancel a build output item """

self.assertEqual(len(self.build.getBuildOutputs()), 0)

# Create a new build output
output = self.build.createBuildOutput(
quantity=1,
batch_code='TEST-BATCH-001',
serial_numbers='456'
)[0]

self.assertEqual(len(self.build.getBuildOutputs()), 1)

self.build.cancelBuildOutputs(output)
self.assertEqual(len(self.build.getBuildOutputs()), 0)

def test_complete_build_output(self):
""" Test that we can complete a build output item """

self.assertEqual(len(self.build.getBuildOutputs()), 0)

# Create a new build output
output = self.build.createBuildOutput(
quantity=1,
batch_code='TEST-BATCH-001',
serial_numbers='457'
)[0]

q = self.build.completed

self.assertTrue(output.is_building)
self.assertEqual(len(self.build.getBuildOutputs()), 1)

# Complete the build output
self.build.completeBuildOutput(output, location=1)

self.assertEqual(len(self.build.getBuildOutputs()), 1)
output.reload()
self.assertFalse(output.is_building)

# Remove the output
output.delete()
self.assertEqual(len(self.build.getBuildOutputs()), 0)

# The number of "completed" items should have increased by 1
self.build.reload()
self.assertEqual(self.build.completed, q + 1)

def test_scrap_build_output(self):
"""Test that we can scrap a build output item"""

self.assertEqual(len(self.build.getBuildOutputs()), 0)

# Create a new build output
output = self.build.createBuildOutput(
quantity=1,
batch_code='TEST-BATCH-001',
serial_numbers='468'
)[0]

q = self.build.completed

self.assertTrue(output.is_building)
self.assertEqual(len(self.build.getBuildOutputs()), 1)

# Scrap the build output
self.build.scrapBuildOutput(output, location=1, notes='Test scrap')
self.assertEqual(len(self.build.getBuildOutputs()), 1)
self.assertEqual(len(self.build.getBuildOutputs(complete=False)), 0)
self.assertEqual(len(self.build.getBuildOutputs(complete=True)), 1)

output.reload()
self.assertFalse(output.is_building)

# Remove the build output
output.delete()

# The number of "completed" items should not have increased
self.build.reload()
self.assertEqual(self.build.completed, q)