From 0fa8e3809e34c1d24c8483a4e012b254fbc58b78 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 15 Jun 2019 15:33:03 +1000 Subject: [PATCH 01/19] Limit PO selection to only those which are PENDING --- InvenTree/company/models.py | 4 ++++ InvenTree/order/templates/order/order_wizard/select_pos.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 68e9cbaf9b..9d09557f17 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -135,6 +135,10 @@ class Company(models.Model): """ Return purchase orders which are 'outstanding' """ return self.purchase_orders.filter(status__in=OrderStatus.OPEN) + def pending_purchase_orders(self): + """ Return purchase orders which are PENDING (not yet issued) """ + return self.purchase_orders.filter(status=OrderStatus.PENDING) + def closed_purchase_orders(self): """ Return purchase orders which are not 'outstanding' diff --git a/InvenTree/order/templates/order/order_wizard/select_pos.html b/InvenTree/order/templates/order/order_wizard/select_pos.html index f97ba95ee2..4ae0a89c95 100644 --- a/InvenTree/order/templates/order/order_wizard/select_pos.html +++ b/InvenTree/order/templates/order/order_wizard/select_pos.html @@ -53,7 +53,7 @@ id='id-purchase-order-{{ supplier.id }}' name='purchase-order-{{ supplier.id }}'> - {% for order in supplier.outstanding_purchase_orders %} + {% for order in supplier.pending_purchase_orders %} From 11d9312c45abb9c566050fa7866679e96c6505dc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 15 Jun 2019 15:33:10 +1000 Subject: [PATCH 02/19] Improve rendering of purchase order table --- .../order/purchase_order_detail.html | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 185eba4cad..1eee598fa8 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -74,20 +74,24 @@ InvenTree | {{ order }} {% endif %} + - - - - - + + + + + + {% if not order.status == OrderStatus.PENDING %} - + {% endif %} - + {% if order.status == OrderStatus.PENDING %} - + {% endif %} + + {% for line in order.lines.all %} + {% else %} - + {% endif %} @@ -124,6 +129,7 @@ InvenTree | {{ order }} {% endif %} {% endfor %} +
LinePartOrder CodeReferenceQuantityLinePartDescriptionOrder CodeReferenceQuantityReceivedReceivedNoteNote
@@ -98,9 +102,10 @@ InvenTree | {{ order }} {% include "hover_image.html" with image=line.part.part.image hover=True %} {{ line.part.part.full_name }} {{ line.part.part.description }} {{ line.part.SKU }}Warning: Part has been deleted.Warning: Part has been deleted.{{ line.reference }} {{ line.quantity }}
{% if order.notes %} @@ -182,6 +188,8 @@ $('#new-po-line').click(function() { {% endif %} $("#po-lines-table").bootstrapTable({ + search: true, + sortable: true, }); From bbd6b15089362a6ba9f04437787ff0eb6f8ddaa3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 15 Jun 2019 17:09:25 +1000 Subject: [PATCH 03/19] Add a VIew for receiving purchase order --- .../order/purchase_order_detail.html | 8 ++++++ .../order/templates/order/receive_parts.html | 10 +++++++ InvenTree/order/urls.py | 1 + InvenTree/order/views.py | 28 +++++++++++++++++++ 4 files changed, 47 insertions(+) create mode 100644 InvenTree/order/templates/order/receive_parts.html diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 1eee598fa8..f663df629e 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -63,6 +63,8 @@ InvenTree | {{ order }} {% if order.status == OrderStatus.PENDING and order.lines.count > 0 %} + {% elif order.status == OrderStatus.PLACED %} + {% endif %} @@ -159,6 +161,12 @@ $("#edit-order").click(function() { ); }); +$("#receive-order").click(function() { + launchModalForm("{% url 'purchase-order-receive' order.id %}", { + reload: true, + }); +}); + $("#export-order").click(function() { location.href = "{% url 'purchase-order-export' order.id %}"; }); diff --git a/InvenTree/order/templates/order/receive_parts.html b/InvenTree/order/templates/order/receive_parts.html new file mode 100644 index 0000000000..c81a016fd7 --- /dev/null +++ b/InvenTree/order/templates/order/receive_parts.html @@ -0,0 +1,10 @@ +{% extends "modal_form.html" %} + +{% block form %} + +
+ {% csrf_token %} + {% load crispy_forms_tags %} +
+ +{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 695628bad0..38687342c2 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -13,6 +13,7 @@ purchase_order_detail_urls = [ url(r'^edit/?', views.PurchaseOrderEdit.as_view(), name='purchase-order-edit'), url(r'^issue/?', views.PurchaseOrderIssue.as_view(), name='purchase-order-issue'), + url(r'^receive/?', views.PurchaseOrderReceive.as_view(), name='purchase-order-receive'), url(r'^export/?', views.PurchaseOrderExport.as_view(), name='purchase-order-export'), diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 578021b2cb..0281564474 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -165,6 +165,34 @@ class PurchaseOrderExport(AjaxView): return DownloadFile(filedata, filename) +class PurchaseOrderReceive(AjaxView): + """ View for receiving parts which are outstanding against a PurchaseOrder. + + Any parts which are outstanding are listed. + If all parts are marked as received, the order is closed out. + + """ + + ajax_form_title = "Receive Parts" + ajax_template_name = "order/receive_parts.html" + + def get_context_data(self): + + ctx = { + 'order': self.order, + } + + return ctx + + def get(self, request, *args, **kwargs): + + self.request = request + self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk']) + + return self.renderJsonResponse(request) + + + class OrderParts(AjaxView): """ View for adding various SupplierPart items to a Purchase Order. From e28a435c074b8e457056e237f505be3bfd24bf0e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 15 Jun 2019 17:13:28 +1000 Subject: [PATCH 04/19] Add entry point for order documentation --- docs/modules.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/modules.rst b/docs/modules.rst index 183e49bfe4..06078ad6e2 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -11,6 +11,7 @@ InvenTree Modules docs/build/index docs/company/index docs/part/index + docs/order/index docs/stock/index The InvenTree Django ecosystem provides the following 'apps' for core functionality: @@ -19,4 +20,5 @@ The InvenTree Django ecosystem provides the following 'apps' for core functional * `Build `_ - Part build projects * `Company `_ - Company management (suppliers / customers) * `Part `_ - Part management +* `Order `_ - Order management * `Stock `_ - Stock management \ No newline at end of file From c7ca9a3d8f6fa53a6285ed80da8d55193ff858b2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 15 Jun 2019 17:29:33 +1000 Subject: [PATCH 05/19] Display table of outstanding line items --- InvenTree/order/models.py | 12 +++++++ .../order/templates/order/receive_parts.html | 34 +++++++++++++++++++ InvenTree/order/views.py | 3 ++ 3 files changed, 49 insertions(+) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index e2a0efc36d..4cb58f401b 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -195,6 +195,13 @@ class PurchaseOrder(Order): line.save() + def pending_line_items(self): + """ Return a list of pending line items for this order. + Any line item where 'received' < 'quantity' will be returned. + """ + + return [line for line in self.lines.all() if line.quantity > line.received] + class OrderLineItem(models.Model): """ Abstract model for an order line item @@ -251,3 +258,8 @@ class PurchaseOrderLineItem(OrderLineItem): ) received = models.PositiveIntegerField(default=0, help_text=_('Number of items received')) + + def remaining(self): + """ Calculate the number of items remaining to be received """ + r = self.quantity - self.received + return max(r, 0) \ No newline at end of file diff --git a/InvenTree/order/templates/order/receive_parts.html b/InvenTree/order/templates/order/receive_parts.html index c81a016fd7..42c104e274 100644 --- a/InvenTree/order/templates/order/receive_parts.html +++ b/InvenTree/order/templates/order/receive_parts.html @@ -2,9 +2,43 @@ {% block form %} +

Receive parts for {{ order }}

+
{% csrf_token %} {% load crispy_forms_tags %} + + + + + + + + + + {% for line in lines %} + + {% if line.part %} + + + {% else %} + + {% endif %} + + + + + {% endfor %} +
PartOrder CodeOn OrderReceivedReceive
+ {% include "hover_image.html" with image=line.part.part.image hover=False %} + {{ line.part.part.full_name }} + {{ line.part.SKU }}Referenced part has been removed{{ line.quantity }}{{ line.received }} +
+
+ +
+
+
{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 0281564474..48fb466367 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -180,6 +180,7 @@ class PurchaseOrderReceive(AjaxView): ctx = { 'order': self.order, + 'lines': self.lines, } return ctx @@ -189,6 +190,8 @@ class PurchaseOrderReceive(AjaxView): self.request = request self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk']) + self.lines = self.order.pending_line_items() + return self.renderJsonResponse(request) From 1290e7f289cee9b697147e2e5fab1bbe3b5c5091 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 15 Jun 2019 19:39:57 +1000 Subject: [PATCH 06/19] Implement POST for receiving items - Create new StockItem in the correct location --- .../migrations/0011_auto_20190615_1928.py | 26 ++++++ InvenTree/order/models.py | 55 ++++++++++- .../order/purchase_order_detail.html | 12 ++- .../order/templates/order/receive_parts.html | 24 ++++- InvenTree/order/views.py | 92 ++++++++++++++++++- .../0006_stockitem_purchase_order.py | 20 ++++ InvenTree/stock/models.py | 9 ++ InvenTree/stock/templates/stock/item.html | 6 ++ 8 files changed, 234 insertions(+), 10 deletions(-) create mode 100644 InvenTree/order/migrations/0011_auto_20190615_1928.py create mode 100644 InvenTree/stock/migrations/0006_stockitem_purchase_order.py diff --git a/InvenTree/order/migrations/0011_auto_20190615_1928.py b/InvenTree/order/migrations/0011_auto_20190615_1928.py new file mode 100644 index 0000000000..cf6f1f61dc --- /dev/null +++ b/InvenTree/order/migrations/0011_auto_20190615_1928.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.2 on 2019-06-15 09:28 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('order', '0010_purchaseorderlineitem_notes'), + ] + + operations = [ + migrations.AddField( + model_name='purchaseorder', + name='complete_date', + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name='purchaseorder', + name='received_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 4cb58f401b..a8d8b12542 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -4,7 +4,7 @@ Order model definitions # -*- coding: utf-8 -*- -from django.db import models +from django.db import models, transaction from django.core.validators import MinValueValidator from django.core.exceptions import ValidationError from django.contrib.auth.models import User @@ -14,6 +14,7 @@ from django.utils.translation import ugettext as _ import tablib from datetime import datetime +from stock.models import StockItem from company.models import Company, SupplierPart from InvenTree.status_codes import OrderStatus @@ -33,6 +34,7 @@ class Order(models.Model): creation_date: Automatic date of order creation created_by: User who created this order (automatically captured) issue_date: Date the order was issued + complete_date: Date the order was completed """ @@ -70,6 +72,8 @@ class Order(models.Model): issue_date = models.DateField(blank=True, null=True) + complete_date = models.DateField(blank=True, null=True) + notes = models.TextField(blank=True, help_text=_('Order notes')) def place_order(self): @@ -80,13 +84,21 @@ class Order(models.Model): self.issue_date = datetime.now().date() self.save() + def complete_order(self): + """ Marks the order as COMPLETE. Order must be currently PLACED. """ + + if self.status == OrderStatus.PLACED: + self.status = OrderStatus.COMPLETE + self.complete_date = datetime.now().date() + self.save() + class PurchaseOrder(Order): """ A PurchaseOrder represents goods shipped inwards from an external supplier. Attributes: supplier: Reference to the company supplying the goods in the order - + received_by: User that received the goods """ ORDER_PREFIX = "PO" @@ -100,6 +112,12 @@ class PurchaseOrder(Order): help_text=_('Company') ) + received_by = models.ForeignKey(User, + on_delete=models.SET_NULL, + blank=True, null=True, + related_name='+' + ) + def export_to_file(self, **kwargs): """ Export order information to external file """ @@ -151,6 +169,7 @@ class PurchaseOrder(Order): def get_absolute_url(self): return reverse('purchase-order-detail', kwargs={'pk': self.id}) + @transaction.atomic def add_line_item(self, supplier_part, quantity, group=True, reference=''): """ Add a new line item to this purchase order. This function will check that: @@ -202,6 +221,38 @@ class PurchaseOrder(Order): return [line for line in self.lines.all() if line.quantity > line.received] + @transaction.atomic + def receive_line_item(self, line, location, quantity, user): + """ Receive a line item (or partial line item) against this PO + """ + + # Create a new stock item + if line.part: + stock = StockItem( + part=line.part.part, + location=location, + quantity=quantity, + purchase_order=self) + + stock.save() + + # Add a new transaction note to the newly created stock item + stock.addTransactionNote("Received items", user, "Received {q} items against order '{po}'".format( + q=line.receive_quantity, + po=str(self)) + ) + + + # Update the number of parts received against the particular line item + line.received += quantity + line.save() + + # Has this order been completed? + if len(self.pending_line_items()) == 0: + + self.received_by = user + self.complete_order() # This will save the model + class OrderLineItem(models.Model): """ Abstract model for an order line item diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index f663df629e..68370c9c22 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -41,11 +41,7 @@ InvenTree | {{ order }} Created - {{ order.creation_date }} - - - Created By - {{ order.created_by }} + {{ order.creation_date }}{{ order.created_by }} {% if order.issue_date %} @@ -53,6 +49,12 @@ InvenTree | {{ order }} {{ order.issue_date }} {% endif %} + {% if order.status == OrderStatus.COMPLETE %} + + Received + {{ order.complete_date }}{{ order.received_by }} + + {% endif %} diff --git a/InvenTree/order/templates/order/receive_parts.html b/InvenTree/order/templates/order/receive_parts.html index 42c104e274..e2d57e9c2a 100644 --- a/InvenTree/order/templates/order/receive_parts.html +++ b/InvenTree/order/templates/order/receive_parts.html @@ -2,12 +2,32 @@ {% block form %} -

Receive parts for {{ order }}

+Receive outstanding parts for {{ order }} - {{ order.description }}
{% csrf_token %} {% load crispy_forms_tags %} +
+ +
+ + {% if not destination %} + Select location to receive parts + {% else %} +

Location of received parts

+ {% endif %} +
+
+ + +

Select parts to receive against this order.

+ @@ -32,7 +52,7 @@ diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 48fb466367..6e2cdf4a98 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -5,6 +5,7 @@ Django views for interacting with Order app # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.db import transaction from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext as _ from django.views.generic import DetailView, ListView @@ -15,7 +16,7 @@ import logging from .models import PurchaseOrder, PurchaseOrderLineItem from build.models import Build from company.models import Company, SupplierPart -from stock.models import StockItem +from stock.models import StockItem, StockLocation from part.models import Part from . import forms as order_forms @@ -176,24 +177,113 @@ class PurchaseOrderReceive(AjaxView): ajax_form_title = "Receive Parts" ajax_template_name = "order/receive_parts.html" + # Where the parts will be going (selected in POST request) + destination = None + def get_context_data(self): ctx = { 'order': self.order, 'lines': self.lines, + 'locations': StockLocation.objects.all(), + 'destination': self.destination, } return ctx def get(self, request, *args, **kwargs): + """ Respond to a GET request. Determines which parts are outstanding, + and presents a list of these parts to the user. + """ self.request = request self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk']) self.lines = self.order.pending_line_items() + for line in self.lines: + # Pre-fill the remaining quantity + line.receive_quantity = line.remaining() + return self.renderJsonResponse(request) + def post(self, request, *args, **kwargs): + """ Respond to a POST request. Data checking and error handling. + If the request is valid, new StockItem objects will be made + for each received item. + """ + + self.request = request + self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk']) + + self.lines = [] + self.destination = None + + # Extract the destination for received parts + if 'receive_location' in request.POST: + pk = request.POST['receive_location'] + try: + self.destination = StockLocation.objects.get(id=pk) + except (StockLocation.DoesNotExist, ValueError): + pass + + errors = self.destination is None + + # Extract information on all submitted line items + for item in request.POST: + if item.startswith('line-'): + pk = item.replace('line-', '') + + try: + line = PurchaseOrderLineItem.objects.get(id=pk) + except (PurchaseOrderLineItem.DoesNotExist, ValueError): + continue + + # Ignore a part that doesn't map to a SupplierPart + try: + if line.part is None: + continue + except SupplierPart.DoesNotExist: + continue + + receive = self.request.POST[item] + + try: + receive = int(receive) + except ValueError: + # In the case on an invalid input, reset to default + receive = line.remaining() + errors = True + + if receive < 0: + receive = 0 + errors = True + + line.receive_quantity = receive + self.lines.append(line) + + # No errors? Receive the submitted parts! + if errors is False: + self.receive_parts() + + data = { + 'form_valid': errors is False, + 'success': 'Items marked as received', + } + + return self.renderJsonResponse(request, data=data) + + def receive_parts(self): + """ Called once the form has been validated. + Create new stockitems against received parts. + """ + + for line in self.lines: + + if not line.part: + continue + + self.order.receive_line_item(line, self.destination, line.receive_quantity, self.request.user) class OrderParts(AjaxView): diff --git a/InvenTree/stock/migrations/0006_stockitem_purchase_order.py b/InvenTree/stock/migrations/0006_stockitem_purchase_order.py new file mode 100644 index 0000000000..08d7f4de01 --- /dev/null +++ b/InvenTree/stock/migrations/0006_stockitem_purchase_order.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.2 on 2019-06-15 09:06 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0010_purchaseorderlineitem_notes'), + ('stock', '0005_auto_20190602_1944'), + ] + + operations = [ + migrations.AddField( + model_name='stockitem', + name='purchase_order', + field=models.ForeignKey(blank=True, help_text='Purchase order for this stock item', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='order.PurchaseOrder'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 50a0f39e72..a3a0c1d6ef 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -96,6 +96,7 @@ class StockItem(models.Model): delete_on_deplete: If True, StockItem will be deleted when the stock level gets to zero status: Status of this StockItem (ref: InvenTree.status_codes.StockStatus) notes: Extra notes field + purchase_order: Link to a PurchaseOrder (if this stock item was created from a PurchaseOrder) infinite: If True this StockItem can never be exhausted """ @@ -249,6 +250,14 @@ class StockItem(models.Model): updated = models.DateField(auto_now=True, null=True) + purchase_order = models.ForeignKey( + 'order.PurchaseOrder', + on_delete=models.SET_NULL, + related_name='stock_items', + blank=True, null=True, + help_text='Purchase order for this stock item' + ) + # last time the stock was checked / counted stocktake_date = models.DateField(blank=True, null=True) diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index 40d78eb7e1..7300855389 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -71,6 +71,12 @@ {% endif %} + {% if item.purchase_order %} + + + + + {% endif %} {% if item.customer %} From 3405fb93a94d76187b03d661f6a8106be2d5eb20 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 15 Jun 2019 19:42:09 +1000 Subject: [PATCH 07/19] Peppy fixes --- InvenTree/order/models.py | 6 +++--- InvenTree/order/views.py | 3 +-- InvenTree/stock/models.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index a8d8b12542..bc58a2afa3 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -112,7 +112,8 @@ class PurchaseOrder(Order): help_text=_('Company') ) - received_by = models.ForeignKey(User, + received_by = models.ForeignKey( + User, on_delete=models.SET_NULL, blank=True, null=True, related_name='+' @@ -242,7 +243,6 @@ class PurchaseOrder(Order): po=str(self)) ) - # Update the number of parts received against the particular line item line.received += quantity line.save() @@ -313,4 +313,4 @@ class PurchaseOrderLineItem(OrderLineItem): def remaining(self): """ Calculate the number of items remaining to be received """ r = self.quantity - self.received - return max(r, 0) \ No newline at end of file + return max(r, 0) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 6e2cdf4a98..b5b9540fed 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -5,7 +5,6 @@ Django views for interacting with Order app # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import transaction from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext as _ from django.views.generic import DetailView, ListView @@ -252,7 +251,7 @@ class PurchaseOrderReceive(AjaxView): receive = int(receive) except ValueError: # In the case on an invalid input, reset to default - receive = line.remaining() + receive = line.remaining() errors = True if receive < 0: diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index a3a0c1d6ef..a53e61f86d 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -251,7 +251,7 @@ class StockItem(models.Model): updated = models.DateField(auto_now=True, null=True) purchase_order = models.ForeignKey( - 'order.PurchaseOrder', + 'order.PurchaseOrder', on_delete=models.SET_NULL, related_name='stock_items', blank=True, null=True, From 93667f942f09c465d95111ff6b5fb956364df757 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 15 Jun 2019 19:47:16 +1000 Subject: [PATCH 08/19] Add button to remove line items from the 'receive' form --- InvenTree/order/templates/order/receive_parts.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/InvenTree/order/templates/order/receive_parts.html b/InvenTree/order/templates/order/receive_parts.html index e2d57e9c2a..68a6533b2e 100644 --- a/InvenTree/order/templates/order/receive_parts.html +++ b/InvenTree/order/templates/order/receive_parts.html @@ -37,7 +37,7 @@ Receive outstanding parts for {{ order }} - {{ order.description }}Receive {% for line in lines %} - + {% if line.part %} + {% endfor %}
Part
- +
{{ item.batch }}
Purchase Order{{ item.purchase_order }}
Customer
{% include "hover_image.html" with image=line.part.part.image hover=False %} @@ -56,6 +56,11 @@ Receive outstanding parts for {{ order }} - {{ order.description }} + +
From 78bfc0b6a8f9937b778caaf54b94a575e8803f6e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Jun 2019 19:44:36 +1000 Subject: [PATCH 09/19] Improve the 'creation date' for order - Automatically set on first save --- .../migrations/0012_auto_20190617_1943.py | 18 ++++++++++++++++++ InvenTree/order/models.py | 8 +++++++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 InvenTree/order/migrations/0012_auto_20190617_1943.py diff --git a/InvenTree/order/migrations/0012_auto_20190617_1943.py b/InvenTree/order/migrations/0012_auto_20190617_1943.py new file mode 100644 index 0000000000..b47d73d134 --- /dev/null +++ b/InvenTree/order/migrations/0012_auto_20190617_1943.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.2 on 2019-06-17 09:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0011_auto_20190615_1928'), + ] + + operations = [ + migrations.AlterField( + model_name='purchaseorder', + name='creation_date', + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index bc58a2afa3..e1e2a67614 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -50,6 +50,12 @@ class Order(models.Model): return " ".join(el) + def save(self, *args, **kwargs): + if not self.creation_date: + self.creation_date = dateimt.now().date() + + super().save(*args, **kwargs) + class Meta: abstract = True @@ -59,7 +65,7 @@ class Order(models.Model): URL = models.URLField(blank=True, help_text=_('Link to external page')) - creation_date = models.DateField(auto_now=True, editable=False) + creation_date = models.DateField(blank=True, null=True) status = models.PositiveIntegerField(default=OrderStatus.PENDING, choices=OrderStatus.items(), help_text='Order status') From 0857ec61fd594b65be98b9bdfd415ea122da8745 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Jun 2019 19:47:16 +1000 Subject: [PATCH 10/19] Add a test fixture for orders --- InvenTree/company/fixtures/company.yaml | 3 ++ InvenTree/order/__init__.py | 3 ++ InvenTree/order/fixtures/order.yaml | 43 +++++++++++++++++++++++++ InvenTree/order/tests.py | 34 ++++++++++++++++++- 4 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 InvenTree/order/fixtures/order.yaml diff --git a/InvenTree/company/fixtures/company.yaml b/InvenTree/company/fixtures/company.yaml index 7f95b6ec26..e83d886812 100644 --- a/InvenTree/company/fixtures/company.yaml +++ b/InvenTree/company/fixtures/company.yaml @@ -1,14 +1,17 @@ # Sample company data - model: company.company + pk: 1 fields: name: ACME description: A Cool Military Enterprise - model: company.company + pk: 2 fields: name: Appel Computers description: Think more differenter - model: company.company + pk: 3 fields: name: Zerg Corp description: We eat the competition \ No newline at end of file diff --git a/InvenTree/order/__init__.py b/InvenTree/order/__init__.py index e69de29bb2..c7ef040fb8 100644 --- a/InvenTree/order/__init__.py +++ b/InvenTree/order/__init__.py @@ -0,0 +1,3 @@ +""" +The Order module is responsible for managing Orders +""" \ No newline at end of file diff --git a/InvenTree/order/fixtures/order.yaml b/InvenTree/order/fixtures/order.yaml new file mode 100644 index 0000000000..bbf2c47a74 --- /dev/null +++ b/InvenTree/order/fixtures/order.yaml @@ -0,0 +1,43 @@ +# PurchaseOrder and PurchaseOrderLineItem objects for testing + +# Ordering some screws from ACME +- model: order.purchaseorder + pk: 1 + fields: + reference: 0001 + description: "Ordering some screws" + supplier: 1 + +# Ordering some screws from Zerg Corp +- model: order.purchaseorder + pk: 2 + fields: + reference: 0002 + description: "Ordering some more screws" + supplier: 3 + +# Add some line items against PO 0001 + +# 100 x ACME0001 (M2x4 LPHS) +- model: order.purchaseorderlineitem + fields: + order: 1 + part: 1 + quantity: 100 + +# 250 x ACME0002 (M2x4 LPHS) +# Partially received (50) +- model: order.purchaseorderlineitem + fields: + order: 1 + part: 2 + quantity: 250 + received: 50 + +# 100 x ZERGLPHS (M2x4 LPHS) +- model: order.purchaseorderlineitem + fields: + order: 2 + part: 3 + quantity: 100 + diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py index ce215e567d..116b10e89d 100644 --- a/InvenTree/order/tests.py +++ b/InvenTree/order/tests.py @@ -1 +1,33 @@ -# TODO - Implement tests for the order app +from django.test import TestCase + +from part.models import Part + +class OrderTest(TestCase): + """ + Tests to ensure that the order models are functioning correctly. + """ + + fixtures = [ + 'company', + 'supplier_part', + 'category', + 'part', + 'location', + 'stock', + 'order' + ] + + def test_on_order(self): + """ There should be 3 separate items on order for the M2x4 LPHS part """ + + part = Part.objects.get(name='M2x4 LPHS') + + open_orders = [] + + for supplier in part.supplier_parts.all(): + open_orders += supplier.open_orders() + + self.assertEqual(len(open_orders), 3) + + # Test the total on-order quantity + self.assertEqual(part.on_order, 400) From 9c299765117e58f4a6780136cec46f76864c8afd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Jun 2019 21:55:51 +1000 Subject: [PATCH 11/19] More tests for order module - Also fixed some bugs along the way! --- InvenTree/order/fixtures/order.yaml | 3 +- InvenTree/order/models.py | 14 ++++++- InvenTree/order/tests.py | 61 +++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/InvenTree/order/fixtures/order.yaml b/InvenTree/order/fixtures/order.yaml index bbf2c47a74..6fab8a705d 100644 --- a/InvenTree/order/fixtures/order.yaml +++ b/InvenTree/order/fixtures/order.yaml @@ -20,6 +20,7 @@ # 100 x ACME0001 (M2x4 LPHS) - model: order.purchaseorderlineitem + pk: 1 fields: order: 1 part: 1 @@ -40,4 +41,4 @@ order: 2 part: 3 quantity: 100 - + diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index e1e2a67614..ad8a4c0918 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -52,7 +52,7 @@ class Order(models.Model): def save(self, *args, **kwargs): if not self.creation_date: - self.creation_date = dateimt.now().date() + self.creation_date = datetime.now().date() super().save(*args, **kwargs) @@ -233,6 +233,16 @@ class PurchaseOrder(Order): """ Receive a line item (or partial line item) against this PO """ + if not self.status == OrderStatus.PLACED: + raise ValidationError({"status": _("Lines can only be received against an order marked as 'Placed'")}) + + try: + quantity = int(quantity) + if quantity <= 0: + raise ValidationError({"quantity": _("Quantity must be greater than zero")}) + except ValueError: + raise ValidationError({"quantity": _("Invalid quantity provided")}) + # Create a new stock item if line.part: stock = StockItem( @@ -245,7 +255,7 @@ class PurchaseOrder(Order): # Add a new transaction note to the newly created stock item stock.addTransactionNote("Received items", user, "Received {q} items against order '{po}'".format( - q=line.receive_quantity, + q=quantity, po=str(self)) ) diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py index 116b10e89d..872023c885 100644 --- a/InvenTree/order/tests.py +++ b/InvenTree/order/tests.py @@ -1,6 +1,12 @@ from django.test import TestCase +import django.core.exceptions as django_exceptions from part.models import Part +from .models import PurchaseOrder, PurchaseOrderLineItem +from stock.models import StockLocation + +from InvenTree.status_codes import OrderStatus + class OrderTest(TestCase): """ @@ -17,6 +23,18 @@ class OrderTest(TestCase): 'order' ] + def test_basics(self): + + order = PurchaseOrder.objects.get(pk=1) + + self.assertEqual(order.get_absolute_url(), '/order/purchase-order/1/') + + self.assertEqual(str(order), 'PO 1') + + line = PurchaseOrderLineItem.objects.get(pk=1) + + self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO 1)") + def test_on_order(self): """ There should be 3 separate items on order for the M2x4 LPHS part """ @@ -31,3 +49,46 @@ class OrderTest(TestCase): # Test the total on-order quantity self.assertEqual(part.on_order, 400) + + def test_receive(self): + + part = Part.objects.get(name='M2x4 LPHS') + + # Receive some items + line = PurchaseOrderLineItem.objects.get(id=1) + + order = line.order + loc = StockLocation.objects.get(id=1) + + # There should be two lines against this order + self.assertEqual(len(order.pending_line_items()), 2) + + # Should fail, as order is 'PENDING' not 'PLACED" + self.assertEqual(order.status, OrderStatus.PENDING) + + with self.assertRaises(django_exceptions.ValidationError): + order.receive_line_item(line, loc, 50, user=None) + + order.place_order() + + self.assertEqual(order.status, OrderStatus.PLACED) + + order.receive_line_item(line, loc, 50, user=None) + + self.assertEqual(part.on_order, 350) + + # Try to order some invalid things + with self.assertRaises(django_exceptions.ValidationError): + order.receive_line_item(line, loc, -10, user=None) + + with self.assertRaises(django_exceptions.ValidationError): + order.receive_line_item(line, loc, 'not a number', user=None) + + # Receive the rest of the items + order.receive_line_item(line, loc, 50, user=None) + + line = PurchaseOrderLineItem.objects.get(id=2) + order.receive_line_item(line, loc, 2 * line.quantity, user=None) + + self.assertEqual(part.on_order, 100) + self.assertEqual(order.status, OrderStatus.COMPLETE) From b9dc4a0c2fb34105b31438f76141673e18800594 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Jun 2019 22:10:37 +1000 Subject: [PATCH 12/19] Further tests for order --- InvenTree/company/fixtures/supplier_part.yaml | 16 ++++++ InvenTree/company/tests.py | 4 +- InvenTree/order/tests.py | 52 +++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/InvenTree/company/fixtures/supplier_part.yaml b/InvenTree/company/fixtures/supplier_part.yaml index f0076974d5..b9649704e1 100644 --- a/InvenTree/company/fixtures/supplier_part.yaml +++ b/InvenTree/company/fixtures/supplier_part.yaml @@ -15,6 +15,22 @@ supplier: 1 SKU: 'ACME0002' +# Widget purchaseable from ACME +- model: company.supplierpart + pk: 100 + fields: + part: 25 + supplier: 1 + SKU: 'ACME-WIDGET' + +# Widget purchaseable from Zerg +- model: company.supplierpart + pk: 101 + fields: + part: 25 + supplier: 2 + SKU: 'ZERG-WIDGET' + # M2x4 LPHS from Zerg Corp - model: company.supplierpart pk: 3 diff --git a/InvenTree/company/tests.py b/InvenTree/company/tests.py index b100710fc4..b4baec2976 100644 --- a/InvenTree/company/tests.py +++ b/InvenTree/company/tests.py @@ -56,10 +56,10 @@ class CompanySimpleTest(TestCase): zerg = Company.objects.get(pk=3) self.assertTrue(acme.has_parts) - self.assertEqual(acme.part_count, 2) + self.assertEqual(acme.part_count, 3) self.assertTrue(appel.has_parts) - self.assertEqual(appel.part_count, 1) + self.assertEqual(appel.part_count, 2) self.assertTrue(zerg.has_parts) self.assertEqual(zerg.part_count, 1) diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py index 872023c885..3a773ad0e9 100644 --- a/InvenTree/order/tests.py +++ b/InvenTree/order/tests.py @@ -4,6 +4,7 @@ import django.core.exceptions as django_exceptions from part.models import Part from .models import PurchaseOrder, PurchaseOrderLineItem from stock.models import StockLocation +from company.models import SupplierPart from InvenTree.status_codes import OrderStatus @@ -24,6 +25,7 @@ class OrderTest(TestCase): ] def test_basics(self): + """ Basic tests e.g. repr functions etc """ order = PurchaseOrder.objects.get(pk=1) @@ -50,7 +52,45 @@ class OrderTest(TestCase): # Test the total on-order quantity self.assertEqual(part.on_order, 400) + def test_add_items(self): + """ Test functions for adding line items to an order """ + + order = PurchaseOrder.objects.get(pk=1) + + self.assertEqual(order.status, OrderStatus.PENDING) + self.assertEqual(order.lines.count(), 2) + + sku = SupplierPart.objects.get(SKU='ACME-WIDGET') + part = sku.part + + # Try to order some invalid things + with self.assertRaises(django_exceptions.ValidationError): + order.add_line_item(sku, -999) + + with self.assertRaises(django_exceptions.ValidationError): + order.add_line_item(sku, 'not a number') + + # Order the part + self.assertEqual(part.on_order, 0) + + order.add_line_item(sku, 100) + + self.assertEqual(part.on_order, 100) + self.assertEqual(order.lines.count(), 3) + + # Order the same part again (it should be merged) + order.add_line_item(sku, 50) + self.assertEqual(order.lines.count(), 3) + self.assertEqual(part.on_order, 150) + + # Try to order a supplier part from the wrong supplier + sku = SupplierPart.objects.get(SKU='ZERG-WIDGET') + + with self.assertRaises(django_exceptions.ValidationError): + order.add_line_item(sku, 99) + def test_receive(self): + """ Test order receiving functions """ part = Part.objects.get(name='M2x4 LPHS') @@ -75,6 +115,8 @@ class OrderTest(TestCase): order.receive_line_item(line, loc, 50, user=None) + self.assertEqual(line.remaining(), 50) + self.assertEqual(part.on_order, 350) # Try to order some invalid things @@ -92,3 +134,13 @@ class OrderTest(TestCase): self.assertEqual(part.on_order, 100) self.assertEqual(order.status, OrderStatus.COMPLETE) + + def test_export(self): + """ Test order exporting """ + + order = PurchaseOrder.objects.get(pk=1) + + output = order.export_to_file(format='csv') + + self.assertIn('M2x4 LPHS', output) + self.assertIn('Line,Part,Description', output) From 08358f69614b6dc0908640b22fee5880620cf8ef Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Jun 2019 22:18:00 +1000 Subject: [PATCH 13/19] Run tests on custom validators --- InvenTree/InvenTree/tests.py | 38 ++++++++++++++++++++++++++++++++++++ InvenTree/order/__init__.py | 2 +- Makefile | 2 +- 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 InvenTree/InvenTree/tests.py diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py new file mode 100644 index 0000000000..1631d7bd96 --- /dev/null +++ b/InvenTree/InvenTree/tests.py @@ -0,0 +1,38 @@ +from django.test import TestCase +import django.core.exceptions as django_exceptions + +from .validators import validate_overage, validate_part_name + +class ValidatorTest(TestCase): + + """ Simple tests for custom field validators """ + + def test_part_name(self): + """ Test part name validator """ + + validate_part_name('hello world') + + with self.assertRaises(django_exceptions.ValidationError): + validate_part_name('This | name is not } valid') + + def test_overage(self): + """ Test overage validator """ + + validate_overage("100%") + validate_overage("10") + validate_overage("45.2 %") + + with self.assertRaises(django_exceptions.ValidationError): + validate_overage("-1") + + with self.assertRaises(django_exceptions.ValidationError): + validate_overage("-2.04 %") + + with self.assertRaises(django_exceptions.ValidationError): + validate_overage("105%") + + with self.assertRaises(django_exceptions.ValidationError): + validate_overage("xxx %") + + with self.assertRaises(django_exceptions.ValidationError): + validate_overage("aaaa") diff --git a/InvenTree/order/__init__.py b/InvenTree/order/__init__.py index c7ef040fb8..896e9facd5 100644 --- a/InvenTree/order/__init__.py +++ b/InvenTree/order/__init__.py @@ -1,3 +1,3 @@ """ The Order module is responsible for managing Orders -""" \ No newline at end of file +""" diff --git a/Makefile b/Makefile index e97ddc9732..cdef6b87c4 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ test: coverage: python3 InvenTree/manage.py check - coverage run InvenTree/manage.py test build company part stock order + coverage run InvenTree/manage.py test build company part stock order InvenTree coverage html documentation: From 38ef9b2b13f77e9157128ef7eb284ca35fed349d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Jun 2019 22:36:17 +1000 Subject: [PATCH 14/19] Tests for helper functions --- .coveragerc | 1 + InvenTree/InvenTree/helpers.py | 61 +------------------------------ InvenTree/InvenTree/tests.py | 65 ++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 60 deletions(-) diff --git a/.coveragerc b/.coveragerc index e953c6c86c..1a9e67e9a6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,6 +4,7 @@ omit = # Do not run coverage on migration files */migrations/* InvenTree/manage.py + InvenTree/keygen.py Inventree/InvenTree/middleware.py Inventree/InvenTree/utils.py Inventree/InvenTree/wsgi.py \ No newline at end of file diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 02f8159c6e..fe2fbe5046 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -30,67 +30,8 @@ def TestIfImageURL(url): '.jpg', '.jpeg', '.png', '.bmp', '.tif', '.tiff', - '.webp', + '.webp', '.gif', ] - - -def DownloadExternalFile(url, **kwargs): - """ Attempt to download an external file - - Args: - url - External URL - - """ - - result = { - 'status': False, - 'url': url, - 'file': None, - 'status_code': 200, - } - - headers = {'User-Agent': 'Mozilla/5.0'} - max_size = kwargs.get('max_size', 1048576) # 1MB default limit - - # Get the HEAD for the file - try: - head = requests.head(url, stream=True, headers=headers) - except: - result['error'] = 'Error retrieving HEAD data' - return result - - if not head.status_code == 200: - result['error'] = 'Incorrect HEAD status code' - result['status_code'] = head.status_code - return result - - try: - filesize = int(head.headers['Content-Length']) - except ValueError: - result['error'] = 'Could not decode filesize' - result['extra'] = head.headers['Content-Length'] - return result - - if filesize > max_size: - result['error'] = 'File size too large ({s})'.format(s=filesize) - return result - - # All checks have passed - download the file - - try: - request = requests.get(url, stream=True, headers=headers) - except: - result['error'] = 'Error retriving GET data' - return result - - try: - dl_file = io.StringIO(request.text) - result['status'] = True - result['file'] = dl_file - return result - except: - result['error'] = 'Could not convert downloaded data to file' - return result def str2bool(text, test=True): diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 1631d7bd96..fad8f888a3 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -2,6 +2,8 @@ from django.test import TestCase import django.core.exceptions as django_exceptions from .validators import validate_overage, validate_part_name +from . import helpers + class ValidatorTest(TestCase): @@ -36,3 +38,66 @@ class ValidatorTest(TestCase): with self.assertRaises(django_exceptions.ValidationError): validate_overage("aaaa") + + +class TestHelpers(TestCase): + """ Tests for InvenTree helper functions """ + + def test_image_url(self): + """ Test if a filename looks like an image """ + + for name in ['ape.png', 'bat.GiF', 'apple.WeBP', 'BiTMap.Bmp']: + self.assertTrue(helpers.TestIfImageURL(name)) + + for name in ['no.doc', 'nah.pdf', 'whatpng']: + self.assertFalse(helpers.TestIfImageURL(name)) + + def test_str2bool(self): + """ Test string to boolean conversion """ + + for s in ['yes', 'Y', 'ok', '1', 'OK', 'Ok', 'tRuE', 'oN']: + self.assertTrue(helpers.str2bool(s)) + self.assertFalse(helpers.str2bool(s, test=False)) + + for s in ['nO', '0', 'none', 'noNE', None, False, 'falSe', 'off']: + self.assertFalse(helpers.str2bool(s)) + self.assertTrue(helpers.str2bool(s, test=False)) + + for s in ['wombat', '', 'xxxx']: + self.assertFalse(helpers.str2bool(s)) + self.assertFalse(helpers.str2bool(s, test=False)) + + +class TestQuoteWrap(TestCase): + """ Tests for string wrapping """ + + def test_single(self): + + self.assertEqual(helpers.WrapWithQuotes('hello'), '"hello"') + self.assertEqual(helpers.WrapWithQuotes('hello"'), '"hello"') + + +class TestMakeBarcoede(TestCase): + """ Tests for barcode string creation """ + + def test_barcode(self): + + data = { + 'animal': 'cat', + 'legs': 3, + 'noise': 'purr' + } + + bc = helpers.MakeBarcode("part", 3, "www.google.com", data) + + self.assertIn('animal', bc) + self.assertIn('tool', bc) + self.assertIn('"tool": "InvenTree"', bc) + + +class TestDownloadFile(TestCase): + + + def test_download(self): + helpers.DownloadFile("hello world", "out.txt") + helpers.DownloadFile(bytes("hello world".encode("utf8")), "out.bin") From 760d08608fc0f6280dff9f713012b19b74db4f35 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Jun 2019 22:41:44 +1000 Subject: [PATCH 15/19] Minor test addition for builds --- InvenTree/build/tests.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py index 1030a4e198..36e8223a9b 100644 --- a/InvenTree/build/tests.py +++ b/InvenTree/build/tests.py @@ -14,6 +14,7 @@ class BuildTestSimple(TestCase): def setUp(self): part = Part.objects.create(name='Test part', description='Simple description') + Build.objects.create(part=part, batch='B1', status=BuildStatus.PENDING, @@ -35,6 +36,8 @@ class BuildTestSimple(TestCase): self.assertEqual(b.batch, 'B2') self.assertEqual(b.quantity, 21) + self.assertEqual(str(b), 'Build 21 x Test part - Simple description') + def test_url(self): b1 = Build.objects.get(pk=1) self.assertEqual(b1.get_absolute_url(), '/build/1/') @@ -46,6 +49,8 @@ class BuildTestSimple(TestCase): self.assertEqual(b1.is_complete, False) self.assertEqual(b2.is_complete, True) + self.assertEqual(b2.status, BuildStatus.COMPLETE) + def test_is_active(self): b1 = Build.objects.get(pk=1) b2 = Build.objects.get(pk=2) @@ -56,3 +61,14 @@ class BuildTestSimple(TestCase): def test_required_parts(self): # TODO - Generate BOM for test part pass + + def cancel_build(self): + """ Test build cancellation function """ + + build = Build.objects.get(id=1) + + self.assertEqual(build.status, BuildStatus.PENDING) + + build.cancelBuild() + + self.assertEqual(build.status, BuildStatus.CANCELLED) From eb1d534ebd10e67e7acd6ca39c51f5cc58009095 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Jun 2019 22:45:01 +1000 Subject: [PATCH 16/19] Ignore some more files --- .coveragerc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 1a9e67e9a6..948d835f41 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,4 +7,5 @@ omit = InvenTree/keygen.py Inventree/InvenTree/middleware.py Inventree/InvenTree/utils.py - Inventree/InvenTree/wsgi.py \ No newline at end of file + Inventree/InvenTree/wsgi.py + InvenTree/users/apps.py \ No newline at end of file From a147a704bf96a3d8d72bc28891ff5832128e6034 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Jun 2019 22:45:48 +1000 Subject: [PATCH 17/19] Pepper --- InvenTree/InvenTree/helpers.py | 1 - InvenTree/InvenTree/tests.py | 1 - 2 files changed, 2 deletions(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index fe2fbe5046..ddb4e35fee 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -6,7 +6,6 @@ import io import json import os.path from PIL import Image -import requests from wsgiref.util import FileWrapper from django.http import StreamingHttpResponse diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index fad8f888a3..ccdf2e2d43 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -97,7 +97,6 @@ class TestMakeBarcoede(TestCase): class TestDownloadFile(TestCase): - def test_download(self): helpers.DownloadFile("hello world", "out.txt") helpers.DownloadFile(bytes("hello world".encode("utf8")), "out.bin") From 008a55d3d7b3836a2e809f6249ad6695a24b9f1d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Jun 2019 23:02:44 +1000 Subject: [PATCH 18/19] Squashed a bug --- InvenTree/build/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 28c7ecfcd3..2825731efc 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -110,7 +110,7 @@ class Build(models.Model): self.completion_date = datetime.now().date() self.completed_by = user - self.status = self.CANCELLED + self.status = BuildStatus.CANCELLED self.save() def getAutoAllocations(self): From 7fbc1b71bc439d261ee3902e42ba37eccd562d47 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Jun 2019 23:22:51 +1000 Subject: [PATCH 19/19] Delete some unused models --- wip/order_models.py | 69 --------------------------------------------- 1 file changed, 69 deletions(-) delete mode 100644 wip/order_models.py diff --git a/wip/order_models.py b/wip/order_models.py deleted file mode 100644 index 018dba21bc..0000000000 --- a/wip/order_models.py +++ /dev/null @@ -1,69 +0,0 @@ -class SupplierOrder(models.Model): - """ - An order of parts from a supplier, made up of multiple line items - """ - - def get_absolute_url(self): - return "/supplier/order/{id}/".format(id=self.id) - - # Interal reference for this order - internal_ref = models.CharField(max_length=25, unique=True) - - supplier = models.ForeignKey(Company, on_delete=models.CASCADE, - related_name='orders') - - created_date = models.DateField(auto_now_add=True, editable=False) - - issued_date = models.DateField(blank=True, null=True, help_text="Date the purchase order was issued") - - notes = models.TextField(blank=True, help_text="Order notes") - - def __str__(self): - return "PO {ref} ({status})".format(ref=self.internal_ref, - status=self.get_status_display) - - PENDING = 10 # Order is pending (not yet placed) - PLACED = 20 # Order has been placed - RECEIVED = 30 # Order has been received - CANCELLED = 40 # Order was cancelled - LOST = 50 # Order was lost - - ORDER_STATUS_CODES = {PENDING: _("Pending"), - PLACED: _("Placed"), - CANCELLED: _("Cancelled"), - RECEIVED: _("Received"), - LOST: _("Lost") - } - - status = models.PositiveIntegerField(default=PENDING, - choices=ORDER_STATUS_CODES.items()) - - delivery_date = models.DateField(blank=True, null=True) - - - -class SupplierOrderLineItem(models.Model): - """ - A line item in a supplier order, corresponding to some quantity of part - """ - - class Meta: - unique_together = [ - ('order', 'line_number'), - ('order', 'supplier_part'), - ('order', 'internal_part'), - ] - - order = models.ForeignKey(SupplierOrder, on_delete=models.CASCADE) - - line_number = models.PositiveIntegerField(default=1) - - internal_part = models.ForeignKey(Part, null=True, blank=True, on_delete=models.SET_NULL) - - supplier_part = models.ForeignKey(SupplierPart, null=True, blank=True, on_delete=models.SET_NULL) - - quantity = models.PositiveIntegerField(default=1) - - notes = models.TextField(blank=True) - - received = models.BooleanField(default=False)