From faab6fa0536fe5731868add1e2379f6f7620717a Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 1 Sep 2022 12:38:51 +1000 Subject: [PATCH 1/7] Fixes for docker compose --- tasks.py | 13 +++++++++---- test/docker-compose.yml | 8 ++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/tasks.py b/tasks.py index 88f6b3d..8de0d40 100644 --- a/tasks.py +++ b/tasks.py @@ -32,9 +32,11 @@ def reset_data(c, debug=False): # Reset the database to a known state print("Reset test database to a known state (this might take a little while...)") - c.run("docker-compose -f test/docker-compose.yml run inventree-py-test-server invoke migrate", hide=None if debug else 'both') - c.run("docker-compose -f test/docker-compose.yml run inventree-py-test-server invoke delete-data -f", hide=None if debug else 'both') - c.run("docker-compose -f test/docker-compose.yml run inventree-py-test-server invoke import-fixtures", hide=None if debug else 'both') + hide = None if debug else 'both' + + c.run("docker-compose -f test/docker-compose.yml run inventree-py-test-server invoke migrate", hide=hide) + c.run("docker-compose -f test/docker-compose.yml run inventree-py-test-server invoke delete-data -f", hide=hide) + c.run("docker-compose -f test/docker-compose.yml run inventree-py-test-server invoke import-fixtures", hide=hide) @task(post=[reset_data]) @@ -45,7 +47,10 @@ def update_image(c, debug=True): print("Pulling latest InvenTree image from docker hub (maybe grab a coffee!)") - c.run("docker-compose -f test/docker-compose.yml pull", hide=None if debug else 'both') + hide = None if debug else 'both' + + c.run("docker-compose -f test/docker-compose.yml pull", hide=hide) + c.run("docker-compose -f test/docker-compose.yml run inventree-py-test-server invoke update", hide=hide) @task diff --git a/test/docker-compose.yml b/test/docker-compose.yml index b3a8e51..13c2991 100644 --- a/test/docker-compose.yml +++ b/test/docker-compose.yml @@ -17,7 +17,7 @@ services: environment: - INVENTREE_DEBUG=True - INVENTREE_DB_ENGINE=sqlite - - INVENTREE_DB_NAME=/home/inventree/db/test_db.sqlite3 + - INVENTREE_DB_NAME=/home/inventree/data/db/test_db.sqlite3 - INVENTREE_DEBUG_LEVEL=error - INVENTREE_ADMIN_USER=testuser - INVENTREE_ADMIN_PASSWORD=testpassword @@ -25,6 +25,6 @@ services: restart: unless-stopped volumes: # Note: media and static files are ephemeral - - ./data/media:/home/inventree/media/ - - ./data/static:/home/inventree/static/ - - ./data/db:/home/inventree/db/ + - ./data/media:/home/inventree/data/media/ + - ./data/static:/home/inventree/data/static/ + - ./data/db:/home/inventree/data/db/ From 1cd9a6a13910317139ce2d26522f844bb9abd65b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 3 Sep 2022 14:18:43 +1000 Subject: [PATCH 2/7] Further updates to docker-compose file for testing --- test/docker-compose.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/docker-compose.yml b/test/docker-compose.yml index 13c2991..dba49b3 100644 --- a/test/docker-compose.yml +++ b/test/docker-compose.yml @@ -17,14 +17,11 @@ services: environment: - INVENTREE_DEBUG=True - INVENTREE_DB_ENGINE=sqlite - - INVENTREE_DB_NAME=/home/inventree/data/db/test_db.sqlite3 + - INVENTREE_DB_NAME=/home/inventree/data/test_db.sqlite3 - INVENTREE_DEBUG_LEVEL=error - INVENTREE_ADMIN_USER=testuser - INVENTREE_ADMIN_PASSWORD=testpassword - INVENTREE_ADMIN_EMAIL=test@test.com restart: unless-stopped volumes: - # Note: media and static files are ephemeral - - ./data/media:/home/inventree/data/media/ - - ./data/static:/home/inventree/data/static/ - - ./data/db:/home/inventree/data/db/ + - ./data:/home/inventree/data From d848370b4c63138b41a0cda0462ed79d81d47531 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 5 Sep 2022 21:06:38 +1000 Subject: [PATCH 3/7] Fixes for various unit tests - Required a change to sales order allocation - Improved filtering when auto-selecting stock items for allocation - remove status_check_helper function - Cleaner unit test code --- inventree/base.py | 16 +++-- inventree/order.py | 14 ++-- inventree/part.py | 8 +-- test/test_order.py | 159 +++++++++++++++++++++++---------------------- 4 files changed, 105 insertions(+), 92 deletions(-) diff --git a/inventree/base.py b/inventree/base.py index 30b8210..e327c60 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -525,7 +525,7 @@ class StatusMixin: can be reached through _statusupdate function """ - def _statusupdate(self, status: str, data=None, **kwargs): + def _statusupdate(self, status: str, reload=True, data=None, **kwargs): # Check status if status not in [ @@ -540,19 +540,25 @@ def _statusupdate(self, status: str, data=None, **kwargs): # Set the url URL = self.URL + f"/{self.pk}/{status}" + if data is None: + data = {} + + data.update(kwargs) + # Send data - response = self._api.post(URL, data, **kwargs) + response = self._api.post(URL, data) # Reload - self.reload() + if reload: + self.reload() # Return return response def complete(self, **kwargs): - return self._statusupdate(status='complete') + return self._statusupdate(status='complete', **kwargs) def cancel(self, **kwargs): - return self._statusupdate(status='cancel') + return self._statusupdate(status='cancel', **kwargs) diff --git a/inventree/order.py b/inventree/order.py index ff663c0..2bbee32 100644 --- a/inventree/order.py +++ b/inventree/order.py @@ -51,13 +51,13 @@ def uploadAttachment(self, attachment, comment=''): order=self.pk, ) - def issue(self): + def issue(self, **kwargs): """ Issue the purchase order """ # Return - return self._statusupdate(status='issue') + return self._statusupdate(status='issue', **kwargs) class PurchaseOrderLineItem(inventree.base.InventreeObject): @@ -202,9 +202,9 @@ def allocateToShipment(self, shipment, stockitems=None, quantity=None): possibly because no stock items are available. """ - # If stockitems are not defined, get the list + # If stockitems are not defined, get the list of available stock items if stockitems is None: - stockitems = self.getPart().getStockItems() + stockitems = self.getPart().getStockItems(include_variants=False, in_stock=True, available=True) # If no quantity is defined, calculate the number of required items # This is the number of sold items not yet allocated, but can not @@ -225,6 +225,7 @@ def allocateToShipment(self, shipment, stockitems=None, quantity=None): # Look through stock items, assign items until the required amount # is reached items = list() + for SI in stockitems: # Check if we are done @@ -323,7 +324,8 @@ def complete( shipment_date=None, tracking_number='', invoice_number='', - link='' + link='', + **kwargs ): """ Complete the shipment, with given shipment_date, or reasonable @@ -339,7 +341,7 @@ def complete( } # Return - return self._statusupdate(status='ship', data=data) + return self._statusupdate(status='ship', data=data, **kwargs) def ship(self, *args, **kwargs): """Alias for complete function""" diff --git a/inventree/part.py b/inventree/part.py index c8f3f19..53016ed 100644 --- a/inventree/part.py +++ b/inventree/part.py @@ -74,9 +74,9 @@ def getSupplierParts(self): """ Return the supplier parts associated with this part """ return inventree.company.SupplierPart.list(self._api, part=self.pk) - def getBomItems(self): + def getBomItems(self, **kwargs): """ Return the items required to make this part """ - return BomItem.list(self._api, part=self.pk) + return BomItem.list(self._api, part=self.pk, **kwargs) def isUsedIn(self): """ Return a list of all the parts this part is used in """ @@ -86,9 +86,9 @@ def getBuilds(self, **kwargs): """ Return the builds associated with this part """ return inventree.build.Build.list(self._api, part=self.pk, **kwargs) - def getStockItems(self): + def getStockItems(self, **kwargs): """ Return the stock items associated with this part """ - return inventree.stock.StockItem.list(self._api, part=self.pk) + return inventree.stock.StockItem.list(self._api, part=self.pk, **kwargs) def getParameters(self): """ Return parameters associated with this part """ diff --git a/test/test_order.py b/test/test_order.py index 3e1181d..9af8e09 100644 --- a/test/test_order.py +++ b/test/test_order.py @@ -12,44 +12,7 @@ from inventree import part # noqa: E402 from inventree import order # noqa: E402 from inventree import company # noqa: E402 - - -def status_check_helper( - orderlist, - applymethod, - target_status, - target_status_text, - **kwargs -): - """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)(**kwargs) - - 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 +from inventree import stock # noqa: E402 class POTest(InvenTreeTestCase): @@ -198,31 +161,57 @@ 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', - accept_incomplete=True, - )) - # Go through purchase orders, try to cancel one - self.assertTrue(status_check_helper( - order.PurchaseOrder.list(self.api), - 'cancel', - 40, - 'Cancelled' - )) + def test_order_cancel(self): + """Test that we can cancel a PurchaseOrder via the API""" + ... + + def test_order_complete(self): + """Test that we can complete an order via the API""" + + # First, let's create a new PurchaseOrder + po = order.PurchaseOrder.create(self.api, data={ + 'supplier': 1, + 'description': 'A new purchase order', + }) + + # Add some line items + for p in company.SupplierPart.list(self.api, supplier=1, limit=5): + + po.addLineItem( + part=p.pk, + quantity=10, + ) + + # Check that lines have actually been added + lines = po.getLineItems() + self.assertTrue(len(lines) > 0) + + # Initial status code is "PENDING" + self.assertEqual(po.status, 10) + self.assertEqual(po.status_text, "Pending") + + # Issue the order + po.issue() + po.reload() + + self.assertEqual(po.status, 20) + self.assertEqual(po.status_text, "Placed") + + # Try to complete the order (should fail, as lines have not been received) + with self.assertRaises(HTTPError): + po.complete() + + po.reload() + + # Check that order status has *not* changed + self.assertEqual(po.status, 20) + + # Now, try to complete the order again, accepting incomplete + po.complete(accept_incomplete=True) + po.reload() + + # Check that the order is now complete + self.assertEqual(po.status, 30) def test_purchase_order_delete(self): """ @@ -415,21 +404,36 @@ def test_so_attachment(self): self.assertEqual(len(attachments), n + 1) def test_so_shipment(self): - """ - Test shipment functionality for a SalesOrder - """ + """Test shipment functionality for a SalesOrder.""" - # Grab the last available SalesOrder - should not have a shipment yet - orders = order.SalesOrder.list(self.api) + # Construct a new SalesOrder instance + so = order.SalesOrder.create(self.api, { + 'customer': 4, + "description": "Selling some stuff", + }) - if len(orders) > 0: - so = orders[-1] - else: - so = order.SalesOrder.create(self.api, { - 'customer': 4, - 'reference': "SO-4444", - "description": "Selling some stuff", - }) + # Add some line items to the SalesOrder + for p in part.Part.list(self.api, is_template=False, salable=True, limit=5): + + # Create a line item matching the part + order.SalesOrderLineItem.create( + self.api, + data={ + 'part': p.pk, + 'order': so.pk, + 'quantity': 10, + } + ) + + # Ensure there is available stock + stock.StockItem.create( + self.api, + data={ + 'part': p.pk, + 'quantity': 25, + 'location': 1, + } + ) # The shipments list should return something which is not none self.assertIsNotNone(so.getShipments()) @@ -485,6 +489,7 @@ def test_so_shipment(self): # (which should be overwritten) notes = f'Test shipment number {num_shipments+1} for order {so.pk}' tracking_number = '93414134343' + shipment_2 = so.addShipment( reference=f'Package {num_shipments+1}', order=10103413, @@ -535,7 +540,7 @@ def test_so_shipment(self): allocated_quantities[so_part.pk] ) - # Complete the shipment, with minimum information + # Attempt to complete the shipment, but no items have been allocated shipment_2.complete() # Make sure date is not None From a8aabd6d160ee091ed10a459221aad5696d4fa51 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 5 Sep 2022 21:08:13 +1000 Subject: [PATCH 4/7] Reimplement unit test for cancelling a PurchaseOrder --- test/test_order.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/test_order.py b/test/test_order.py index 9af8e09..e9a4db4 100644 --- a/test/test_order.py +++ b/test/test_order.py @@ -163,7 +163,21 @@ def test_po_create(self): def test_order_cancel(self): """Test that we can cancel a PurchaseOrder via the API""" - ... + + # Create a new PO + po = order.PurchaseOrder.create(self.api, data={ + 'supplier': 1, + 'description': 'Some new order' + }) + + self.assertEqual(po.status, 10) + self.assertEqual(po.status_text, "Pending") + + # Cancel the order + po.cancel() + + self.assertEqual(po.status, 40) + self.assertEqual(po.status_text, "Cancelled") def test_order_complete(self): """Test that we can complete an order via the API""" From f9423e96016c59248fada4163ed6cea90c76e9b7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 5 Sep 2022 21:13:03 +1000 Subject: [PATCH 5/7] Adjust parameters for stock unit test --- test/test_stock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_stock.py b/test/test_stock.py index 9ebaf4e..82fd1cf 100644 --- a/test/test_stock.py +++ b/test/test_stock.py @@ -86,7 +86,7 @@ def test_location_stock(self): items = location.getStockItems() - self.assertGreaterEqual(len(items), 19) + self.assertGreaterEqual(len(items), 15) # Check specific part stock in location 1 (initially empty) items = location.getStockItems(part=1) From 3f7f321b272836143a00c1e6d2f010b33be2ef25 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 5 Sep 2022 21:21:08 +1000 Subject: [PATCH 6/7] Define a reference value when creating new Purchase Orders --- test/test_order.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/test_order.py b/test/test_order.py index e9a4db4..9607320 100644 --- a/test/test_order.py +++ b/test/test_order.py @@ -164,9 +164,13 @@ def test_po_create(self): def test_order_cancel(self): """Test that we can cancel a PurchaseOrder via the API""" + n = len(order.PurchaseOrder.list(self.api)) + 1 + ref = f"PO-{n}" + # Create a new PO po = order.PurchaseOrder.create(self.api, data={ 'supplier': 1, + 'reference': ref, 'description': 'Some new order' }) @@ -182,9 +186,13 @@ def test_order_cancel(self): def test_order_complete(self): """Test that we can complete an order via the API""" + n = len(order.PurchaseOrder.list(self.api)) + 1 + ref = f"PO-{n}" + # First, let's create a new PurchaseOrder po = order.PurchaseOrder.create(self.api, data={ 'supplier': 1, + 'reference': ref, 'description': 'A new purchase order', }) From 57949b4992ab2db11b7d5960e7bf66a7343ec170 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 5 Sep 2022 21:27:13 +1000 Subject: [PATCH 7/7] Include more debug information when waiting for server --- .github/workflows/ci.yaml | 2 +- tasks.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4cb41a2..1e8918e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -47,7 +47,7 @@ jobs: invoke wait - name: Run Tests run: | - invoke check-server + invoke check-server -d coverage run -m unittest discover -s test/ - name: Upload Report run: | diff --git a/tasks.py b/tasks.py index 8de0d40..9e3f355 100644 --- a/tasks.py +++ b/tasks.py @@ -75,11 +75,14 @@ def check_server(c, host="http://localhost:12345", username="testuser", password print("Error:", str(e)) if response is None: - timeout -= 1 - time.sleep(1) + if timeout > 0: + if debug: + print(f"No response from server. {timeout} seconds remaining") + timeout -= 1 + time.sleep(1) - if timeout <= 0: + else: return False if response.status_code != 200: