diff --git a/inventree/build.py b/inventree/build.py index d20fb3d..6c60b03 100644 --- a/inventree/build.py +++ b/inventree/build.py @@ -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') @@ -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, @@ -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) diff --git a/test/test_build.py b/test/test_build.py index f7db883..914a34a 100644 --- a/test/test_build.py +++ b/test/test_build.py @@ -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)