Skip to content

Commit e5dcb25

Browse files
Merge pull request #293 from inventree/build-outputs
Build Outputs
2 parents 5cc60a4 + e32b12d commit e5dcb25

2 files changed

Lines changed: 232 additions & 2 deletions

File tree

inventree/build.py

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class Build(
2121
def issue(self):
2222
"""Mark this build as 'issued'."""
2323
return self._statusupdate(status='issue')
24-
24+
2525
def hold(self):
2626
"""Mark this build as 'on hold'."""
2727
return self._statusupdate(status='hold')
@@ -54,6 +54,102 @@ def getLines(self, **kwargs):
5454
""" Return the build line items associated with this build order """
5555
return BuildLine.list(self._api, build=self.pk, **kwargs)
5656

57+
def getBuildOutputs(self, complete: bool = None, **kwargs):
58+
""" Return the build output items associated with this build order
59+
60+
Arguments:
61+
- complete: If not None, filter the build outputs by their 'complete' status
62+
"""
63+
if complete is not None:
64+
kwargs['is_building'] = not complete
65+
66+
# Find stock items which are marked as 'outputs' of this build order
67+
return inventree.stock.StockItem.list(
68+
self._api,
69+
build=self.pk,
70+
**kwargs
71+
)
72+
73+
def createBuildOutput(self, **kwargs):
74+
""" Create a new build output (stock item) associated with this build order """
75+
result = self._api.post(
76+
f'{self.URL}{self.pk}/create-output/',
77+
data={
78+
**kwargs
79+
}
80+
)
81+
82+
# Note: The response is a list of created stock items
83+
return [inventree.stock.StockItem(self._api, item['pk'], item) for item in result]
84+
85+
def cancelBuildOutputs(self, outputs):
86+
""" Cancel a build output item associated with this build order
87+
88+
Arguments:
89+
- outputs: The StockItem object (or list of StockItem objects) to cancel
90+
"""
91+
92+
if not isinstance(outputs, list):
93+
outputs = [outputs]
94+
95+
return self._api.post(
96+
f'{self.URL}{self.pk}/delete-outputs/',
97+
data={
98+
'outputs': [
99+
{'output': output.pk} for output in outputs
100+
]
101+
}
102+
)
103+
104+
def scrapBuildOutput(self, output, **kwargs):
105+
""" Scrap a single build output item associated with this build order
106+
107+
Arguments:
108+
- output: The StockItem object to scrap
109+
"""
110+
111+
data = {
112+
**kwargs,
113+
'outputs': [
114+
{
115+
'output': output.pk,
116+
'quantity': kwargs.get('quantity', output.quantity),
117+
}
118+
]
119+
}
120+
121+
data['location'] = kwargs.get('location', output.location)
122+
123+
return self._api.post(
124+
f'{self.URL}{self.pk}/scrap-outputs/',
125+
data=data
126+
)
127+
128+
def completeBuildOutput(self, output, **kwargs):
129+
""" Mark a single build output item as complete
130+
131+
Arguments:
132+
- output: The StockItem object to mark as complete
133+
"""
134+
135+
data = {
136+
**kwargs,
137+
'outputs': [
138+
{
139+
'output': output.pk,
140+
'quantity': kwargs.get('quantity', output.quantity),
141+
}
142+
]
143+
}
144+
145+
# If a location is not specified, use the current location of the stock item
146+
data['location'] = kwargs.get('location', output.location)
147+
148+
return self._api.post(
149+
f'{self.URL}{self.pk}/complete/',
150+
data=data
151+
)
152+
57153

58154
class BuildLine(
59155
inventree.base.InventreeObject,
@@ -83,7 +179,7 @@ def getBuild(self):
83179
def getBuildLine(self):
84180
"""Return the BuildLine object associated with this build item"""
85181
return BuildLine(self._api, self.build_line)
86-
182+
87183
def getStockItem(self):
88184
"""Return the StockItem object associated with this build item"""
89185
return inventree.stock.StockItem(self._api, self.stock_item)

test/test_build.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,137 @@ def test_build_complete(self):
144144
# Check status
145145
self.assertEqual(build.status, 40)
146146
self.assertEqual(build.status_text, 'Complete')
147+
148+
149+
class BuildOrderOutputTests(InvenTreeTestCase):
150+
""" Unit tests for build output functionality """
151+
152+
def setUp(self):
153+
""" Ensure we have a base build order to work with """
154+
155+
super().setUp()
156+
157+
builds = Build.list(self.api)
158+
159+
self.build = Build.create(
160+
self.api,
161+
{
162+
"title": "A new build order",
163+
"part": 25,
164+
"quantity": 10,
165+
"reference": f"BO-{len(builds) + 1:04d}"
166+
}
167+
)
168+
169+
def test_create_build_output(self):
170+
"""Test that we can create a build output item"""
171+
172+
# Initially, there should be no build outputs
173+
outputs = self.build.getBuildOutputs()
174+
self.assertEqual(len(outputs), 0)
175+
176+
# Let's create 3 new outputs (with serial numbers)
177+
outputs = self.build.createBuildOutput(
178+
quantity=3,
179+
batch_code='TEST-BATCH-001',
180+
serial_numbers='400+'
181+
)
182+
183+
self.assertEqual(len(outputs), 3)
184+
self.assertEqual(len(self.build.getBuildOutputs()), 3)
185+
186+
for output in outputs:
187+
self.assertIsNotNone(output)
188+
self.assertEqual(output.quantity, 1)
189+
self.assertEqual(output.batch, 'TEST-BATCH-001')
190+
self.assertEqual(output.build, self.build.pk)
191+
self.assertEqual(output.part, self.build.part)
192+
self.assertTrue(output.is_building)
193+
194+
# Directly delete the build output
195+
output.delete()
196+
197+
# There should now be no build outputs again
198+
self.assertEqual(len(self.build.getBuildOutputs()), 0)
199+
200+
def test_cancel_build_output(self):
201+
""" Test that we can cancel a build output item """
202+
203+
self.assertEqual(len(self.build.getBuildOutputs()), 0)
204+
205+
# Create a new build output
206+
output = self.build.createBuildOutput(
207+
quantity=1,
208+
batch_code='TEST-BATCH-001',
209+
serial_numbers='456'
210+
)[0]
211+
212+
self.assertEqual(len(self.build.getBuildOutputs()), 1)
213+
214+
self.build.cancelBuildOutputs(output)
215+
self.assertEqual(len(self.build.getBuildOutputs()), 0)
216+
217+
def test_complete_build_output(self):
218+
""" Test that we can complete a build output item """
219+
220+
self.assertEqual(len(self.build.getBuildOutputs()), 0)
221+
222+
# Create a new build output
223+
output = self.build.createBuildOutput(
224+
quantity=1,
225+
batch_code='TEST-BATCH-001',
226+
serial_numbers='457'
227+
)[0]
228+
229+
q = self.build.completed
230+
231+
self.assertTrue(output.is_building)
232+
self.assertEqual(len(self.build.getBuildOutputs()), 1)
233+
234+
# Complete the build output
235+
self.build.completeBuildOutput(output, location=1)
236+
237+
self.assertEqual(len(self.build.getBuildOutputs()), 1)
238+
output.reload()
239+
self.assertFalse(output.is_building)
240+
241+
# Remove the output
242+
output.delete()
243+
self.assertEqual(len(self.build.getBuildOutputs()), 0)
244+
245+
# The number of "completed" items should have increased by 1
246+
self.build.reload()
247+
self.assertEqual(self.build.completed, q + 1)
248+
249+
def test_scrap_build_output(self):
250+
"""Test that we can scrap a build output item"""
251+
252+
self.assertEqual(len(self.build.getBuildOutputs()), 0)
253+
254+
# Create a new build output
255+
output = self.build.createBuildOutput(
256+
quantity=1,
257+
batch_code='TEST-BATCH-001',
258+
serial_numbers='468'
259+
)[0]
260+
261+
q = self.build.completed
262+
263+
self.assertTrue(output.is_building)
264+
self.assertEqual(len(self.build.getBuildOutputs()), 1)
265+
266+
# Scrap the build output
267+
self.build.scrapBuildOutput(output, location=1, notes='Test scrap')
268+
self.assertEqual(len(self.build.getBuildOutputs()), 1)
269+
self.assertEqual(len(self.build.getBuildOutputs(complete=False)), 0)
270+
self.assertEqual(len(self.build.getBuildOutputs(complete=True)), 1)
271+
272+
output.reload()
273+
self.assertFalse(output.is_building)
274+
275+
# Remove the build output
276+
output.delete()
277+
278+
# The number of "completed" items should not have increased
279+
self.build.reload()
280+
self.assertEqual(self.build.completed, q)

0 commit comments

Comments
 (0)