Skip to content

Commit

Permalink
Merge pull request #135 from miggland/complete-po-so-bo
Browse files Browse the repository at this point in the history
Add cancel, complete, issue, ship, to SO/PO/BO
  • Loading branch information
SchrodingersGat authored Aug 20, 2022
2 parents 8f8ecf9 + a048d76 commit 54da628
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 23 deletions.
46 changes: 46 additions & 0 deletions inventree/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,3 +510,49 @@ def downloadImage(self, destination, **kwargs):
return self._api.downloadFile(self.image, destination, **kwargs)
else:
raise ValueError(f"Part '{self.name}' does not have an associated image")


class StatusMixin:
"""Class adding functionality to assign a new status by calling
- complete
- cancel
on supported items.
Other functions, such as
- ship
- finish
- issue
can be reached through _statusupdate function
"""

def _statusupdate(self, status: str, data=None, **kwargs):

# Check status
if status not in [
'complete',
'cancel',
'ship',
'issue',
'finish',
]:
raise ValueError(f"Order stats {status} not supported.")

# Set the url
URL = self.URL + f"/{self.pk}/{status}"

# Send data
response = self._api.post(URL, data, **kwargs)

# Reload
self.reload()

# Return
return response

def complete(self, **kwargs):

return self._statusupdate(status='complete')

def cancel(self, **kwargs):

return self._statusupdate(status='cancel')
29 changes: 28 additions & 1 deletion inventree/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import inventree.base


class Build(inventree.base.InventreeObject):
class Build(
inventree.base.InventreeObject,
inventree.base.StatusMixin,
):
""" Class representing the Build database model """

URL = 'build'
Expand All @@ -19,6 +22,30 @@ def uploadAttachment(self, attachment, comment=''):
build=self.pk
)

def complete(
self,
accept_overallocated='reject',
accept_unallocated=False,
accept_incomplete=False,
):
"""Finish a build order. Takes the following flags:
- accept_overallocated
- accept_unallocated
- accept_incomplete
"""
return self._statusupdate(
status='finish',
data={
'accept_overallocated': accept_overallocated,
'accept_unallocated': accept_unallocated,
'accept_incomplete': accept_incomplete,
}
)

def finish(self, *args, **kwargs):
"""Alias for complete"""
return self.complete(*args, **kwargs)


class BuildAttachment(inventree.base.Attachment):
"""Class representing an attachment against a Build object"""
Expand Down
40 changes: 27 additions & 13 deletions inventree/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
import inventree.company


class PurchaseOrder(inventree.base.MetadataMixin, inventree.base.InventreeObject):
class PurchaseOrder(
inventree.base.MetadataMixin,
inventree.base.InventreeObject,
inventree.base.StatusMixin
):
""" Class representing the PurchaseOrder database model """

URL = 'order/po'
Expand Down Expand Up @@ -47,6 +51,14 @@ def uploadAttachment(self, attachment, comment=''):
order=self.pk,
)

def issue(self):
"""
Issue the purchase order
"""

# Return
return self._statusupdate(status='issue')


class PurchaseOrderLineItem(inventree.base.InventreeObject):
""" Class representing the PurchaseOrderLineItem database model """
Expand Down Expand Up @@ -92,7 +104,11 @@ class PurchaseOrderAttachment(inventree.base.Attachment):
REQUIRED_KWARGS = ['order']


class SalesOrder(inventree.base.MetadataMixin, inventree.base.InventreeObject):
class SalesOrder(
inventree.base.MetadataMixin,
inventree.base.InventreeObject,
inventree.base.StatusMixin
):
""" Class respresenting the SalesOrder database model """

URL = 'order/so'
Expand Down Expand Up @@ -256,7 +272,10 @@ class SalesOrderAttachment(inventree.base.Attachment):
REQUIRED_KWARGS = ['order']


class SalesOrderShipment(inventree.base.InventreeObject):
class SalesOrderShipment(
inventree.base.InventreeObject,
inventree.base.StatusMixin
):
"""Class representing a shipment for a SalesOrder"""

URL = 'order/so/shipment'
Expand Down Expand Up @@ -311,9 +330,6 @@ def complete(
defaults.
"""

# Customise URL
url = f'order/so/shipment/{self.pk}/ship'

# Create data from given inputs
data = {
'shipment_date': shipment_date,
Expand All @@ -322,11 +338,9 @@ def complete(
'link': link
}

# Send data
response = self._api.post(url, data)

# Reload
self.reload()

# Return
return response
return self._statusupdate(status='ship', data=data)

def ship(self, *args, **kwargs):
"""Alias for complete function"""
self.complete(*args, **kwargs)
53 changes: 52 additions & 1 deletion test/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ def get_build(self):
build = Build.create(
self.api,
{
"title": "Automated test build",
"part": 25,
"quantity": 100,
"reference": f"{n+1}"
"reference": f"BO-{n+1:04d}",
}
)
else:
Expand Down Expand Up @@ -116,3 +117,53 @@ def test_build_attachment_bulk_delete(self):

# All attachments for this Build should have been deleted
self.assertEqual(len(BuildAttachment.list(self.api, build=build.pk)), 0)

def test_build_cancel(self):
"""
Test cancelling a build order.
"""

n = len(Build.list(self.api))

# Create a new build order
build = Build.create(
self.api,
{
"title": "Automated test build",
"part": 25,
"quantity": 100,
"reference": f"BO-{n+1:04d}"
}
)

# Cancel
build.cancel()

# Check status
self.assertEqual(build.status, 30)
self.assertEqual(build.status_text, 'Cancelled')

def test_build_complete(self):
"""
Test completing a build order.
"""

n = len(Build.list(self.api))

# Create a new build order
build = Build.create(
self.api,
{
"title": "Automated test build",
"part": 25,
"quantity": 100,
"reference": f"BO-{n+1:04d}"
}
)

# Complete the build, even though it is not completed
build.complete(accept_unallocated=True, accept_incomplete=True)

# Check status
self.assertEqual(build.status, 40)
self.assertEqual(build.status_text, 'Complete')
109 changes: 101 additions & 8 deletions test/test_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,43 @@
from inventree import company # noqa: E402


def status_check_helper(
orderlist,
applymethod,
target_status,
target_status_text
):
"""Apply function to order list, check for status and
status_text until one is confirmed - then quit
"""
for o in orderlist:

# If order not complete, try to mark it as such
if o.status < target_status:
try:
# Use try-else so that only successful calls lead
# to next step - errors can occur due to orders
# which are not ready for completion yet
response = getattr(o, applymethod)()

except HTTPError:
continue
else:

# Expected response is {} if order was marked as complete
# Status should now be 20, status_text is shipped
if isinstance(response, dict) and len(response) == 0:
if (
o.status == target_status and o.status_text == target_status_text
):
# exit the function
return True

# End of loop reached without exit - this means function
# has not been completed successfully, which is not good
return False


class POTest(InvenTreeTestCase):
"""
Unit tests for PurchaseOrder
Expand Down Expand Up @@ -160,6 +197,31 @@ def test_po_create(self):
# Now there should be 0 lines left
self.assertEqual(len(po.getExtraLineItems()), 0)

def test_order_cancel_complete(self):
"""Test cancel and completing purchase orders"""

# Go through purchase orders, try to issue one
self.assertTrue(status_check_helper(
order.PurchaseOrder.list(self.api),
'issue',
20,
'Placed'
))
# Go through purchase orders, try to complete one
self.assertTrue(status_check_helper(
order.PurchaseOrder.list(self.api),
'complete',
30,
'Complete'
))
# Go through purchase orders, try to cancel one
self.assertTrue(status_check_helper(
order.PurchaseOrder.list(self.api),
'cancel',
40,
'Cancelled'
))

def test_purchase_order_delete(self):
"""
Test that we can delete existing purchase orders
Expand Down Expand Up @@ -363,7 +425,7 @@ def test_so_shipment(self):
else:
so = order.SalesOrder.create(self.api, {
'customer': 4,
'reference': "My new sales order",
'reference': "SO-4444",
"description": "Selling some stuff",
})

Expand Down Expand Up @@ -450,13 +512,17 @@ def test_so_shipment(self):

# Assign each line item to this shipment
for si in so.getLineItems():
response = si.allocateToShipment(shipment_2)
# Remember what we are doing for later check
# a response of None means nothing was allocated
if response is not None:
allocated_quantities[si.pk] = (
{x['stock_item']: float(x['quantity']) for x in response['items']}
)
# If there is no stock available, delete this line
if si.available_stock == 0:
si.delete()
else:
response = si.allocateToShipment(shipment_2)
# Remember what we are doing for later check
# a response of None means nothing was allocated
if response is not None:
allocated_quantities[si.pk] = (
{x['stock_item']: float(x['quantity']) for x in response['items']}
)

# Check saved values
for so_part in so.getLineItems():
Expand All @@ -472,3 +538,30 @@ def test_so_shipment(self):

# Make sure date is not None
self.assertIsNotNone(shipment_2.shipment_date)

# Try to complete this order
# Ship remaining shipments first
for shp in so.getShipments():
# Delete shipment if it has no allocations
if len(shp.allocations) == 0:
shp.delete()
continue
# If the shipment has no date, try to mark it shipped
if shp.shipment_date is None:
shp.ship()
so.complete()
self.assertEqual(so.status, 20)
self.assertEqual(so.status_text, 'Shipped')

def test_order_cancel(self):
"""Test cancel sales order"""

so = order.SalesOrder.create(self.api, {
'customer': 4,
"description": "Selling some more stuff",
})

so.cancel()

self.assertEqual(so.status, 40)
self.assertEqual(so.status_text, 'Cancelled')

0 comments on commit 54da628

Please sign in to comment.