From d840b44f7a7b6cb71a6118857f163620eac7568c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 4 Jun 2019 22:19:04 +1000 Subject: [PATCH 01/41] Create initial models for 'Order' - PurchaseOrder - PurchaseOrderLineItem (These are based on some abstract model classes) --- InvenTree/InvenTree/settings.py | 1 + .../migrations/0005_auto_20190604_2217.py | 19 ++++ InvenTree/order/__init__.py | 0 InvenTree/order/admin.py | 29 +++++ InvenTree/order/apps.py | 5 + InvenTree/order/migrations/0001_initial.py | 48 ++++++++ InvenTree/order/migrations/__init__.py | 0 InvenTree/order/models.py | 103 ++++++++++++++++++ InvenTree/order/tests.py | 3 + InvenTree/order/views.py | 3 + 10 files changed, 211 insertions(+) create mode 100644 InvenTree/build/migrations/0005_auto_20190604_2217.py create mode 100644 InvenTree/order/__init__.py create mode 100644 InvenTree/order/admin.py create mode 100644 InvenTree/order/apps.py create mode 100644 InvenTree/order/migrations/0001_initial.py create mode 100644 InvenTree/order/migrations/__init__.py create mode 100644 InvenTree/order/models.py create mode 100644 InvenTree/order/tests.py create mode 100644 InvenTree/order/views.py diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 74c9eac71a..d5ff5749ba 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -62,6 +62,7 @@ INSTALLED_APPS = [ 'stock.apps.StockConfig', 'company.apps.CompanyConfig', 'build.apps.BuildConfig', + 'order.apps.OrderConfig', # Third part add-ons 'django_filters', # Extended filter functionality diff --git a/InvenTree/build/migrations/0005_auto_20190604_2217.py b/InvenTree/build/migrations/0005_auto_20190604_2217.py new file mode 100644 index 0000000000..019399ef12 --- /dev/null +++ b/InvenTree/build/migrations/0005_auto_20190604_2217.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2019-06-04 12:17 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0004_auto_20190525_2356'), + ] + + operations = [ + migrations.AlterField( + model_name='build', + name='part', + field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'assembly': True, 'is_template': False}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part'), + ), + ] diff --git a/InvenTree/order/__init__.py b/InvenTree/order/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py new file mode 100644 index 0000000000..2fdf71950d --- /dev/null +++ b/InvenTree/order/admin.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.contrib import admin +from import_export.admin import ImportExportModelAdmin + +from .models import PurchaseOrder, PurchaseOrderLineItem + + +class PurchaseOrderAdmin(admin.ModelAdmin): + + list_display = ( + 'reference', + 'description', + 'creation_date' + ) + + +class PurchaseOrderLineItemAdmin(admin.ModelAdmin): + + list_display = ( + 'order', + 'quantity', + 'reference' + ) + + +admin.site.register(PurchaseOrder, PurchaseOrderAdmin) +admin.site.register(PurchaseOrderLineItem, PurchaseOrderLineItemAdmin) diff --git a/InvenTree/order/apps.py b/InvenTree/order/apps.py new file mode 100644 index 0000000000..821e6d872c --- /dev/null +++ b/InvenTree/order/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class OrderConfig(AppConfig): + name = 'order' diff --git a/InvenTree/order/migrations/0001_initial.py b/InvenTree/order/migrations/0001_initial.py new file mode 100644 index 0000000000..642b321d47 --- /dev/null +++ b/InvenTree/order/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# Generated by Django 2.2 on 2019-06-04 12:17 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('company', '0005_auto_20190525_2356'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='PurchaseOrder', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reference', models.CharField(help_text='Order reference', max_length=64, unique=True)), + ('description', models.CharField(help_text='Order description', max_length=250)), + ('creation_date', models.DateField(auto_now=True)), + ('issue_date', models.DateField(blank=True, null=True)), + ('notes', models.TextField(blank=True, help_text='Order notes')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('supplier', models.ForeignKey(help_text='Company', on_delete=django.db.models.deletion.CASCADE, related_name='Orders', to='company.Company')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PurchaseOrderLineItem', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1, help_text='Item quantity', validators=[django.core.validators.MinValueValidator(0)])), + ('reference', models.CharField(blank=True, help_text='Line item reference', max_length=100)), + ('received', models.PositiveIntegerField(default=0, help_text='Number of items received')), + ('order', models.ForeignKey(help_text='Purchase Order', on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='order.PurchaseOrder')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/InvenTree/order/migrations/__init__.py b/InvenTree/order/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py new file mode 100644 index 0000000000..cc97506bfd --- /dev/null +++ b/InvenTree/order/models.py @@ -0,0 +1,103 @@ +from django.db import models, transaction +from django.core.validators import MinValueValidator +from django.contrib.auth.models import User + +from django.utils.translation import ugettext as _ + +from part.models import Part +from company.models import Company +from stock.models import StockItem + + +class Order(models.Model): + """ Abstract model for an order. + + Instances of this class: + + - PuchaseOrder + + Attributes: + reference: Unique order number / reference / code + description: Long form description (required) + notes: Extra note field (optional) + creation_date: Automatic date of order creation + created_by: User who created this order (automatically captured) + issue_date: Date the order was issued + + """ + + # Order status codes + 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 + RETURNED = 60 # Order was returned + + class Meta: + abstract = True + + reference = models.CharField(unique=True, max_length=64, blank=False, help_text=_('Order reference')) + + description = models.CharField(max_length=250, blank=False, help_text=_('Order description')) + + creation_date = models.DateField(auto_now=True, editable=False) + + created_by = models.ForeignKey(User, + on_delete=models.SET_NULL, + blank=True, null=True, + related_name='+' + ) + + issue_date = models.DateField(blank=True, null=True) + + + notes = models.TextField(blank=True, help_text=_('Order notes')) + + +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 + + """ + + supplier = models.ForeignKey(Company, on_delete=models.CASCADE, + related_name=_('Orders'), + help_text=_('Company') + ) + +class OrderLineItem(models.Model): + """ Abstract model for an order line item + + Attributes: + quantity: Number of items + note: Annotation for the item + + """ + + class Meta: + abstract = True + + quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)], default=1, help_text=_('Item quantity')) + + reference = models.CharField(max_length=100, blank=True, help_text=_('Line item reference')) + + +class PurchaseOrderLineItem(OrderLineItem): + """ Model for a purchase order line item. + + Attributes: + order: Reference to a PurchaseOrder object + + """ + + order = models.ForeignKey(PurchaseOrder, on_delete=models.CASCADE, + related_name='lines', + help_text=_('Purchase Order') + ) + + # TODO - foreign key references to part and stockitem objects + + received = models.PositiveIntegerField(default=0, help_text=_('Number of items received')) diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py new file mode 100644 index 0000000000..7ce503c2dd --- /dev/null +++ b/InvenTree/order/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py new file mode 100644 index 0000000000..91ea44a218 --- /dev/null +++ b/InvenTree/order/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 68d514d4787ffc448a6476fe7d93fc0a2d08ab3b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 4 Jun 2019 22:20:49 +1000 Subject: [PATCH 02/41] Limit choices for supplier in PurchaseOrder --- InvenTree/order/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index cc97506bfd..ce9370e45e 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -64,6 +64,9 @@ class PurchaseOrder(Order): """ supplier = models.ForeignKey(Company, on_delete=models.CASCADE, + limit_choices_to={ + 'is_supplier': True, + }, related_name=_('Orders'), help_text=_('Company') ) From 54b1ccd585573ba54073e6f3f293d9d93802fb11 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 4 Jun 2019 22:26:19 +1000 Subject: [PATCH 03/41] Allow blank PO description and add URL field --- InvenTree/order/admin.py | 1 + .../migrations/0002_auto_20190604_2224.py | 29 +++++++++++++++++++ InvenTree/order/models.py | 5 ++-- Makefile | 5 ++-- 4 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 InvenTree/order/migrations/0002_auto_20190604_2224.py diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py index 2fdf71950d..18f1dd32f8 100644 --- a/InvenTree/order/admin.py +++ b/InvenTree/order/admin.py @@ -11,6 +11,7 @@ class PurchaseOrderAdmin(admin.ModelAdmin): list_display = ( 'reference', + 'supplier', 'description', 'creation_date' ) diff --git a/InvenTree/order/migrations/0002_auto_20190604_2224.py b/InvenTree/order/migrations/0002_auto_20190604_2224.py new file mode 100644 index 0000000000..2565eae2a2 --- /dev/null +++ b/InvenTree/order/migrations/0002_auto_20190604_2224.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2 on 2019-06-04 12:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='purchaseorder', + name='URL', + field=models.URLField(blank=True, help_text='Link to external page'), + ), + migrations.AlterField( + model_name='purchaseorder', + name='description', + field=models.CharField(blank=True, help_text='Order description', max_length=250), + ), + migrations.AlterField( + model_name='purchaseorder', + name='supplier', + field=models.ForeignKey(help_text='Company', limit_choices_to={'is_supplier': True}, on_delete=django.db.models.deletion.CASCADE, related_name='Orders', to='company.Company'), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index ce9370e45e..25eccd4df2 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -39,7 +39,9 @@ class Order(models.Model): reference = models.CharField(unique=True, max_length=64, blank=False, help_text=_('Order reference')) - description = models.CharField(max_length=250, blank=False, help_text=_('Order description')) + description = models.CharField(max_length=250, blank=True, help_text=_('Order description')) + + URL = models.URLField(blank=True, help_text=_('Link to external page')) creation_date = models.DateField(auto_now=True, editable=False) @@ -51,7 +53,6 @@ class Order(models.Model): issue_date = models.DateField(blank=True, null=True) - notes = models.TextField(blank=True, help_text=_('Order notes')) diff --git a/Makefile b/Makefile index c7fdddc1e2..70d3781629 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ migrate: python InvenTree/manage.py makemigrations part python InvenTree/manage.py makemigrations stock python InvenTree/manage.py makemigrations build + python InvenTree/manage.py makemigrations order python InvenTree/manage.py migrate --run-syncdb python InvenTree/manage.py check @@ -27,11 +28,11 @@ style: test: python InvenTree/manage.py check - python InvenTree/manage.py test build company part stock + python InvenTree/manage.py test build company part stock order coverage: python InvenTree/manage.py check - coverage run InvenTree/manage.py test build company part stock + coverage run InvenTree/manage.py test build company part stock order coverage html documentation: From 0e29f9b88ce7ea0f897f246109f95db118ac3c9d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 4 Jun 2019 22:26:40 +1000 Subject: [PATCH 04/41] Fix related name --- .../migrations/0003_auto_20190604_2226.py | 19 +++++++++++++++++++ InvenTree/order/models.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 InvenTree/order/migrations/0003_auto_20190604_2226.py diff --git a/InvenTree/order/migrations/0003_auto_20190604_2226.py b/InvenTree/order/migrations/0003_auto_20190604_2226.py new file mode 100644 index 0000000000..612166c2f7 --- /dev/null +++ b/InvenTree/order/migrations/0003_auto_20190604_2226.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2019-06-04 12:26 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0002_auto_20190604_2224'), + ] + + operations = [ + migrations.AlterField( + model_name='purchaseorder', + name='supplier', + field=models.ForeignKey(help_text='Company', limit_choices_to={'is_supplier': True}, on_delete=django.db.models.deletion.CASCADE, related_name='purchase_orders', to='company.Company'), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 25eccd4df2..d4bc6627b1 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -68,7 +68,7 @@ class PurchaseOrder(Order): limit_choices_to={ 'is_supplier': True, }, - related_name=_('Orders'), + related_name='purchase_orders', help_text=_('Company') ) From cc2e3351ff9dad9ddaccc15b02889b1703265cf5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 4 Jun 2019 22:34:58 +1000 Subject: [PATCH 05/41] Search for company --- InvenTree/templates/InvenTree/search.html | 27 +++++++++++++++++++ .../templates/InvenTree/search_company.html | 14 ++++++++++ 2 files changed, 41 insertions(+) create mode 100644 InvenTree/templates/InvenTree/search_company.html diff --git a/InvenTree/templates/InvenTree/search.html b/InvenTree/templates/InvenTree/search.html index 70915a49fd..a31e4ed1a8 100644 --- a/InvenTree/templates/InvenTree/search.html +++ b/InvenTree/templates/InvenTree/search.html @@ -25,6 +25,8 @@ InvenTree | Search Results {% include "InvenTree/search_parts.html" with collapse_id='parts' %} +{% include "InvenTree/search_company.html" with collapse_id='companies' %} + {% include "InvenTree/search_supplier_parts.html" with collapse_id='supplier_parts' %} {% include "InvenTree/search_stock_location.html" with collapse_id="locations" %} @@ -77,6 +79,8 @@ InvenTree | Search Results onSearchResults('#part-results-table', '#part-result-count'); + onSearchResults('#company-results-table', '#company-result-count'); + onSearchResults('#supplier-part-results-table', '#supplier-part-result-count'); $("#category-results-table").bootstrapTable({ @@ -130,6 +134,29 @@ InvenTree | Search Results } ); + $("#company-results-table").bootstrapTable({ + url: "{% url 'api-company-list' %}", + queryParams: { + search: "{{ query }}", + }, + pagination: true, + pageSize: 25, + search: true, + columns: [ + { + field: 'name', + title: 'Name', + formatter: function(value, row, index, field) { + return imageHoverIcon(row.image) + renderLink(value, row.url); + }, + }, + { + field: 'description', + title: 'Description', + }, + ] + }); + $("#supplier-part-results-table").bootstrapTable({ url: "{% url 'api-part-supplier-list' %}", queryParams: { diff --git a/InvenTree/templates/InvenTree/search_company.html b/InvenTree/templates/InvenTree/search_company.html new file mode 100644 index 0000000000..776f661819 --- /dev/null +++ b/InvenTree/templates/InvenTree/search_company.html @@ -0,0 +1,14 @@ +{% extends "collapse.html" %} + +{% block collapse_title %} +

Companies

+{% endblock %} + +{% block collapse_heading %} +

{% include "InvenTree/searching.html" %}

+{% endblock %} + +{% block collapse_content %} + +
+{% endblock %} \ No newline at end of file From c45d8a57828d8e6a28fe4ff0b10cee086e8e4695 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 4 Jun 2019 22:35:34 +1000 Subject: [PATCH 06/41] Add 'purchase orders' tab for company --- InvenTree/company/templates/company/tabs.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/InvenTree/company/templates/company/tabs.html b/InvenTree/company/templates/company/tabs.html index 211b56e1d1..8196e48132 100644 --- a/InvenTree/company/templates/company/tabs.html +++ b/InvenTree/company/templates/company/tabs.html @@ -9,12 +9,10 @@ Stock {{ company.stock_count }} - {% if 0 %} - Purchase Orders + Purchase Orders {{ company.purchase_orders.count }} {% endif %} - {% endif %} {% if company.is_customer %} {% if 0 %} From da53de844aadfbf7f065695cb885231192a3d36b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 4 Jun 2019 22:39:46 +1000 Subject: [PATCH 07/41] Add page for detailing company purchase orders --- .../company/templates/company/detail_purchase_orders.html | 7 +++++++ InvenTree/company/templates/company/tabs.html | 2 +- InvenTree/company/urls.py | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 InvenTree/company/templates/company/detail_purchase_orders.html diff --git a/InvenTree/company/templates/company/detail_purchase_orders.html b/InvenTree/company/templates/company/detail_purchase_orders.html new file mode 100644 index 0000000000..f7e3c6c5f9 --- /dev/null +++ b/InvenTree/company/templates/company/detail_purchase_orders.html @@ -0,0 +1,7 @@ +{% extends "company/company_base.html" %} +{% load static %} +{% block details %} + +{% include 'company/tabs.html' with tab='po' %} + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/company/templates/company/tabs.html b/InvenTree/company/templates/company/tabs.html index 8196e48132..e52f22fab2 100644 --- a/InvenTree/company/templates/company/tabs.html +++ b/InvenTree/company/templates/company/tabs.html @@ -10,7 +10,7 @@ Stock {{ company.stock_count }} - Purchase Orders {{ company.purchase_orders.count }} + Purchase Orders {{ company.purchase_orders.count }} {% endif %} {% if company.is_customer %} diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py index 7f617baafd..de4a5e7c5f 100644 --- a/InvenTree/company/urls.py +++ b/InvenTree/company/urls.py @@ -17,6 +17,7 @@ company_detail_urls = [ url(r'parts/?', views.CompanyDetail.as_view(template_name='company/detail_part.html'), name='company-detail-parts'), url(r'stock/?', views.CompanyDetail.as_view(template_name='company/detail_stock.html'), name='company-detail-stock'), + url(r'purchase-orders/?', views.CompanyDetail.as_view(template_name='company/detail_purchase_orders.html'), name='company-detail-purchase-orders'), url(r'thumbnail/?', views.CompanyImage.as_view(), name='company-image'), From 76a72be9269c912422df42dec21289222d9bf063 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 4 Jun 2019 23:09:51 +1000 Subject: [PATCH 08/41] Add order status field - Display status field in PurchaseOrder list view --- InvenTree/InvenTree/status_codes.py | 28 +++++++++++++++++++ InvenTree/company/models.py | 22 +++++++++++++++ .../company/templates/company/detail.html | 2 +- .../templates/company/detail_part.html | 2 +- .../company/detail_purchase_orders.html | 13 +++++++++ .../templates/company/detail_stock.html | 2 +- .../company/templates/company/po_list.html | 7 +++++ InvenTree/order/admin.py | 1 + .../migrations/0004_purchaseorder_status.py | 18 ++++++++++++ InvenTree/order/models.py | 25 ++++++++++++----- .../templates/order}/order_status.html | 4 +-- 11 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 InvenTree/InvenTree/status_codes.py create mode 100644 InvenTree/company/templates/company/po_list.html create mode 100644 InvenTree/order/migrations/0004_purchaseorder_status.py rename InvenTree/{company/templates/company => order/templates/order}/order_status.html (70%) diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py new file mode 100644 index 0000000000..061d0da99e --- /dev/null +++ b/InvenTree/InvenTree/status_codes.py @@ -0,0 +1,28 @@ +from django.utils.translation import ugettext as _ + + +class StatusCode: + + @classmethod + def items(cls): + return cls.options.items() + + +class OrderStatus(StatusCode): + + # Order status codes + PENDING = 10 # Order is pending (not yet placed) + PLACED = 20 # Order has been placed + COMPLETE = 30 # Order has been completed + CANCELLED = 40 # Order was cancelled + LOST = 50 # Order was lost + RETURNED = 60 # Order was returned + + options = { + PENDING: _("Pending"), + PLACED: _("Placed"), + COMPLETE: _("Complete"), + CANCELLED: _("Cancelled"), + LOST: _("Lost"), + RETURNED: _("Returned"), + } diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 245264900d..de8d2d0458 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -17,6 +17,8 @@ from django.urls import reverse from django.conf import settings from django.contrib.staticfiles.templatetags.staticfiles import static +from InvenTree.status_codes import OrderStatus + def rename_company_image(instance, filename): """ Function to rename a company image after upload @@ -128,6 +130,26 @@ class Company(models.Model): stock = apps.get_model('stock', 'StockItem') return stock.objects.filter(supplier_part__supplier=self.id).count() + def outstanding_purchase_orders(self): + """ Return purchase orders which are 'outstanding' """ + return self.purchase_orders.filter(status__in=[ + OrderStatus.PENDING, + OrderStatus.PLACED + ]) + + def complete_purchase_orders(self): + return self.purchase_orders.filter(status=OrderStatus.COMPLETE) + + def failed_purchase_orders(self): + """ Return any purchase orders which were not successful """ + + return self.purchase_orders.filter(status__in=[ + OrderStatus.CANCELLED, + OrderStatus.LOST, + OrderStatus.RETURNED + ]) + + class Contact(models.Model): """ A Contact represents a person who works at a particular company. diff --git a/InvenTree/company/templates/company/detail.html b/InvenTree/company/templates/company/detail.html index 8e26d85a89..fa40913f98 100644 --- a/InvenTree/company/templates/company/detail.html +++ b/InvenTree/company/templates/company/detail.html @@ -6,7 +6,7 @@
-

Company Details

+

Company Details

diff --git a/InvenTree/company/templates/company/detail_part.html b/InvenTree/company/templates/company/detail_part.html index ec6f80c652..5e0a28541c 100644 --- a/InvenTree/company/templates/company/detail_part.html +++ b/InvenTree/company/templates/company/detail_part.html @@ -4,7 +4,7 @@ {% include 'company/tabs.html' with tab='parts' %} -

Supplier Parts

+

Supplier Parts

diff --git a/InvenTree/company/templates/company/detail_purchase_orders.html b/InvenTree/company/templates/company/detail_purchase_orders.html index f7e3c6c5f9..28d7becd1a 100644 --- a/InvenTree/company/templates/company/detail_purchase_orders.html +++ b/InvenTree/company/templates/company/detail_purchase_orders.html @@ -4,4 +4,17 @@ {% include 'company/tabs.html' with tab='po' %} +

Purchase Orders

+ + + + + + + + {% include "company/po_list.html" with orders=company.outstanding_purchase_orders %} + {% include "company/po_list.html" with orders=company.complete_purchase_orders %} + {% include "company/po_list.html" with orders=company.failed_purchase_orders %} +
ReferenceDescriptionStatus
+ {% endblock %} \ No newline at end of file diff --git a/InvenTree/company/templates/company/detail_stock.html b/InvenTree/company/templates/company/detail_stock.html index b3682043dd..108342b2b1 100644 --- a/InvenTree/company/templates/company/detail_stock.html +++ b/InvenTree/company/templates/company/detail_stock.html @@ -5,7 +5,7 @@ {% include "company/tabs.html" with tab='stock' %} -

Supplier Stock

+

Supplier Stock

{% include "stock_table.html" %} diff --git a/InvenTree/company/templates/company/po_list.html b/InvenTree/company/templates/company/po_list.html new file mode 100644 index 0000000000..8b4204996f --- /dev/null +++ b/InvenTree/company/templates/company/po_list.html @@ -0,0 +1,7 @@ +{% for order in orders %} + + {{ order }} + {{ order.description }} + {% include "order/order_status.html" with order=order %} + +{% endfor %} \ No newline at end of file diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py index 18f1dd32f8..0ea5e8c599 100644 --- a/InvenTree/order/admin.py +++ b/InvenTree/order/admin.py @@ -12,6 +12,7 @@ class PurchaseOrderAdmin(admin.ModelAdmin): list_display = ( 'reference', 'supplier', + 'status', 'description', 'creation_date' ) diff --git a/InvenTree/order/migrations/0004_purchaseorder_status.py b/InvenTree/order/migrations/0004_purchaseorder_status.py new file mode 100644 index 0000000000..d1ad90eb38 --- /dev/null +++ b/InvenTree/order/migrations/0004_purchaseorder_status.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-06-04 12:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0003_auto_20190604_2226'), + ] + + operations = [ + migrations.AddField( + model_name='purchaseorder', + name='status', + field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Placed'), (30, 'Complete'), (40, 'Cancelled'), (50, 'Lost'), (60, 'Returned')], default=10, help_text='Order status'), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index d4bc6627b1..4679f02eea 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -8,6 +8,8 @@ from part.models import Part from company.models import Company from stock.models import StockItem +from InvenTree.status_codes import OrderStatus + class Order(models.Model): """ Abstract model for an order. @@ -26,13 +28,17 @@ class Order(models.Model): """ - # Order status codes - 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 - RETURNED = 60 # Order was returned + ORDER_PREFIX = "" + + def __str__(self): + el = [] + + if self.ORDER_PREFIX: + el.append(self.ORDER_PREFIX) + + el.append(self.reference) + + return " ".join(el) class Meta: abstract = True @@ -45,6 +51,9 @@ class Order(models.Model): creation_date = models.DateField(auto_now=True, editable=False) + status = models.PositiveIntegerField(default=OrderStatus.PENDING, choices=OrderStatus.items(), + help_text='Order status') + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, @@ -64,6 +73,8 @@ class PurchaseOrder(Order): """ + ORDER_PREFIX = "PO" + supplier = models.ForeignKey(Company, on_delete=models.CASCADE, limit_choices_to={ 'is_supplier': True, diff --git a/InvenTree/company/templates/company/order_status.html b/InvenTree/order/templates/order/order_status.html similarity index 70% rename from InvenTree/company/templates/company/order_status.html rename to InvenTree/order/templates/order/order_status.html index 49ccc7f170..46359e562d 100644 --- a/InvenTree/company/templates/company/order_status.html +++ b/InvenTree/order/templates/order/order_status.html @@ -2,9 +2,9 @@ {% elif order.status == order.PLACED %} -{% elif order.status == order.RECEIVED %} +{% elif order.status == order.COMPLETE %} -{% elif order.status == order.CANCELLED %} +{% elif order.status == order.CANCELLED or order.status == order.RETURNED %} {% else %} From 8d70d2f28a15519e672ad79a6839680d9bbad035 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 4 Jun 2019 23:14:57 +1000 Subject: [PATCH 09/41] Fix rendering of purchase order status codes --- InvenTree/company/views.py | 7 +++++++ InvenTree/order/templates/order/order_status.html | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index cce518676a..f9e032cbe9 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -11,6 +11,7 @@ from django.views.generic import DetailView, ListView from django.forms import HiddenInput from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView +from InvenTree.status_codes import OrderStatus from .models import Company from .models import SupplierPart @@ -57,6 +58,12 @@ class CompanyDetail(DetailView): queryset = Company.objects.all() model = Company + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + ctx['OrderStatus'] = OrderStatus + + return ctx + class CompanyImage(AjaxUpdateView): """ View for uploading an image for the Company """ diff --git a/InvenTree/order/templates/order/order_status.html b/InvenTree/order/templates/order/order_status.html index 46359e562d..3e3d2a1e08 100644 --- a/InvenTree/order/templates/order/order_status.html +++ b/InvenTree/order/templates/order/order_status.html @@ -1,10 +1,10 @@ -{% if order.status == order.PENDING %} +{% if order.status == OrderStatus.PENDING %} -{% elif order.status == order.PLACED %} +{% elif order.status == OrderStatus.PLACED %} -{% elif order.status == order.COMPLETE %} +{% elif order.status == OrderStatus.COMPLETE %} -{% elif order.status == order.CANCELLED or order.status == order.RETURNED %} +{% elif order.status == OrderStatus.CANCELLED or order.status == OrderStatus.RETURNED %} {% else %} From f731c45ce872d442239b8a47eb5369c38e6d13b9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 4 Jun 2019 23:38:52 +1000 Subject: [PATCH 10/41] Replace other choice fields with commonized status code --- InvenTree/InvenTree/status_codes.py | 50 +++++++++++++++++++++++++++++ InvenTree/build/models.py | 24 ++++---------- InvenTree/part/models.py | 8 +++-- InvenTree/stock/api.py | 3 +- InvenTree/stock/models.py | 21 +++--------- 5 files changed, 67 insertions(+), 39 deletions(-) diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index 061d0da99e..c8fddc5b2c 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -7,6 +7,11 @@ class StatusCode: def items(cls): return cls.options.items() + @classmethod + def label(cls, value): + """ Return the status code label associated with the provided value """ + return cls.options.get(value, '') + class OrderStatus(StatusCode): @@ -26,3 +31,48 @@ class OrderStatus(StatusCode): LOST: _("Lost"), RETURNED: _("Returned"), } + + +class StockStatus(StatusCode): + + OK = 10 # Item is OK + ATTENTION = 50 # Item requires attention + DAMAGED = 55 # Item is damaged + DESTROYED = 60 # Item is destroyed + LOST = 70 # Item has been lost + + options = { + OK: _("OK"), + ATTENTION: _("Attention needed"), + DAMAGED: _("Damaged"), + DESTROYED: _("Destroyed"), + LOST: _("Lost"), + } + + # The following codes correspond to parts that are 'available' + AVAILABLE_CODES = [ + OK, + ATTENTION, + DAMAGED + ] + + +class BuildStatus(StatusCode): + + # Build status codes + PENDING = 10 # Build is pending / active + ALLOCATED = 20 # Parts have been removed from stock + CANCELLED = 30 # Build was cancelled + COMPLETE = 40 # Build is complete + + options = { + PENDING: _("Pending"), + ALLOCATED: _("Allocated"), + CANCELLED: _("Cancelled"), + COMPLETE: _("Complete"), + } + + ACTIVE_CODES = [ + PENDING, + ALLOCATED + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index f9ffc99953..b40f270fbc 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -16,6 +16,8 @@ from django.db import models, transaction from django.db.models import Sum from django.core.validators import MinValueValidator +from InvenTree.status_codes import BuildStatus + from stock.models import StockItem from part.models import Part, BomItem @@ -68,22 +70,10 @@ class Build(models.Model): validators=[MinValueValidator(1)], help_text='Number of parts to build' ) - - # Build status codes - PENDING = 10 # Build is pending / active - ALLOCATED = 20 # Parts have been removed from stock - CANCELLED = 30 # Build was cancelled - COMPLETE = 40 # Build is complete - #: Build status codes - BUILD_STATUS_CODES = {PENDING: _("Pending"), - ALLOCATED: _("Allocated"), - CANCELLED: _("Cancelled"), - COMPLETE: _("Complete"), - } - status = models.PositiveIntegerField(default=PENDING, - choices=BUILD_STATUS_CODES.items(), + status = models.PositiveIntegerField(default=BuildStatus.PENDING, + choices=BuildStatus.items(), validators=[MinValueValidator(0)], help_text='Build status') @@ -325,14 +315,12 @@ class Build(models.Model): - HOLDING """ - return self.status in [ - self.PENDING, - ] + return self.status in BuildStatus.ACTIVE_CODES @property def is_complete(self): """ Returns True if the build status is COMPLETE """ - return self.status == self.COMPLETE + return self.status == self.BuildStatus.COMPLETE class BuildItem(models.Model): diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index f311aea1f1..29a11125ab 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -33,6 +33,8 @@ from InvenTree import helpers from InvenTree import validators from InvenTree.models import InvenTreeTree +from InvenTree.status_codes import BuildStatus, StockStatus + from company.models import SupplierPart @@ -454,14 +456,14 @@ class Part(models.Model): Builds marked as 'complete' or 'cancelled' are ignored """ - return [b for b in self.builds.all() if b.is_active] + return self.builds.filter(status__in=BuildStatus.ACTIVE_CODES) @property def inactive_builds(self): """ Return a list of inactive builds """ - return [b for b in self.builds.all() if not b.is_active] + return self.builds.exclude(status__in=BuildStatus.ACTIVE_CODES) @property def quantity_being_built(self): @@ -531,7 +533,7 @@ class Part(models.Model): if self.is_template: total = sum([variant.total_stock for variant in self.variants.all()]) else: - total = self.stock_entries.aggregate(total=Sum('quantity'))['total'] + total = self.stock_entries.filter(status__in=StockStatus.AVAILABLE_CODES).aggregate(total=Sum('quantity'))['total'] if total: return total diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index b1c746cdd6..092724407f 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -20,6 +20,7 @@ from .serializers import StockTrackingSerializer from InvenTree.views import TreeSerializer from InvenTree.helpers import str2bool +from InvenTree.status_codes import StockStatus import os @@ -311,7 +312,7 @@ class StockList(generics.ListCreateAPIView): else: item['location__path'] = None - item['status_text'] = StockItem.ITEM_STATUS_CODES[item['status']] + item['status_text'] = StockStatus.label(item['status']) return Response(data) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 4434741186..b9773f9d77 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -19,6 +19,7 @@ from django.dispatch import receiver from datetime import datetime from InvenTree import helpers +from InvenTree.status_codes import StockStatus from InvenTree.models import InvenTreeTree from part.models import Part @@ -93,7 +94,7 @@ class StockItem(models.Model): stocktake_user: User that performed the most recent stocktake review_needed: Flag if StockItem needs review delete_on_deplete: If True, StockItem will be deleted when the stock level gets to zero - status: Status of this StockItem (ref: ITEM_STATUS_CODES) + status: Status of this StockItem (ref: InvenTree.status_codes.StockStatus) notes: Extra notes field infinite: If True this StockItem can never be exhausted """ @@ -256,23 +257,9 @@ class StockItem(models.Model): delete_on_deplete = models.BooleanField(default=True, help_text='Delete this Stock Item when stock is depleted') - ITEM_OK = 10 - ITEM_ATTENTION = 50 - ITEM_DAMAGED = 55 - ITEM_DESTROYED = 60 - ITEM_LOST = 70 - - ITEM_STATUS_CODES = { - ITEM_OK: _("OK"), - ITEM_ATTENTION: _("Attention needed"), - ITEM_DAMAGED: _("Damaged"), - ITEM_DESTROYED: _("Destroyed"), - ITEM_LOST: _("Lost") - } - status = models.PositiveIntegerField( - default=ITEM_OK, - choices=ITEM_STATUS_CODES.items(), + default=StockStatus.OK, + choices=StockStatus.items(), validators=[MinValueValidator(0)]) notes = models.CharField(max_length=250, blank=True, help_text='Stock Item Notes') From 4f1acddb5d7d18e9554d5ec6b5aa31bd61690ab0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 4 Jun 2019 23:42:48 +1000 Subject: [PATCH 11/41] Fix some build status code thingies --- InvenTree/build/tests.py | 6 ++++-- InvenTree/build/views.py | 9 ++++++--- InvenTree/templates/build_status.html | 8 ++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py index 1918ce9b27..1030a4e198 100644 --- a/InvenTree/build/tests.py +++ b/InvenTree/build/tests.py @@ -6,6 +6,8 @@ from django.test import TestCase from .models import Build from part.models import Part +from InvenTree.status_codes import BuildStatus + class BuildTestSimple(TestCase): @@ -14,14 +16,14 @@ class BuildTestSimple(TestCase): description='Simple description') Build.objects.create(part=part, batch='B1', - status=Build.PENDING, + status=BuildStatus.PENDING, title='Building 7 parts', quantity=7, notes='Some simple notes') Build.objects.create(part=part, batch='B2', - status=Build.COMPLETE, + status=BuildStatus.COMPLETE, title='Building 21 parts', quantity=21, notes='Some simple notes') diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index ace53ecca6..028546a6d4 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -15,6 +15,7 @@ from stock.models import StockLocation, StockItem from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView from InvenTree.helpers import str2bool +from InvenTree.status_codes import BuildStatus class BuildIndex(ListView): @@ -32,10 +33,12 @@ class BuildIndex(ListView): context = super(BuildIndex, self).get_context_data(**kwargs).copy() - context['active'] = self.get_queryset().filter(status__in=[Build.PENDING, ]) + context['BuildStatus'] = BuildStatus - context['completed'] = self.get_queryset().filter(status=Build.COMPLETE) - context['cancelled'] = self.get_queryset().filter(status=Build.CANCELLED) + context['active'] = self.get_queryset().filter(status__in=BuildStatus.ACTIVE_CODES) + + context['completed'] = self.get_queryset().filter(status=BuildStatus.COMPLETE) + context['cancelled'] = self.get_queryset().filter(status=BuildStatus.CANCELLED) return context diff --git a/InvenTree/templates/build_status.html b/InvenTree/templates/build_status.html index 793e3ea231..b18b81e16f 100644 --- a/InvenTree/templates/build_status.html +++ b/InvenTree/templates/build_status.html @@ -1,10 +1,10 @@ -{% if build.status == build.PENDING %} +{% if build.status == BuildStatus.PENDING %} -{% elif build.status == build.ALLOCATED %} +{% elif build.status == BuildStatus.ALLOCATED %} -{% elif build.status == build.CANCELLED %} +{% elif build.status == BuildStatus.CANCELLED %} -{% elif build.status == build.COMPLETE %} +{% elif build.status == BuildStatus.COMPLETE %} {% endif %} {{ build.get_status_display }} From c49b8546f079f2ca9533d845fdb2dbfcf6766c90 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 4 Jun 2019 23:59:15 +1000 Subject: [PATCH 12/41] Index page for showing all purchase orders --- InvenTree/InvenTree/urls.py | 4 +-- .../templates/order/purchase_orders.html | 27 +++++++++++++++++++ InvenTree/order/urls.py | 20 ++++++++++++++ InvenTree/order/views.py | 27 +++++++++++++++++-- InvenTree/templates/navbar.html | 1 + 5 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 InvenTree/order/templates/order/purchase_orders.html create mode 100644 InvenTree/order/urls.py diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 1bffd90229..716ff25767 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -15,10 +15,9 @@ from company.urls import supplier_part_urls from company.urls import price_break_urls from part.urls import part_urls - from stock.urls import stock_urls - from build.urls import build_urls +from order.urls import order_urls from part.api import part_api_urls, bom_api_urls from company.api import company_api_urls @@ -56,6 +55,7 @@ urlpatterns = [ url(r'^stock/', include(stock_urls)), url(r'^company/', include(company_urls)), + url(r'^order/', include(order_urls)), url(r'^build/', include(build_urls)), diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html new file mode 100644 index 0000000000..9c056608da --- /dev/null +++ b/InvenTree/order/templates/order/purchase_orders.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% load static %} + +{% block content %} + +

Purchase Orders

+ + + + + + + + + + {% for order in orders %} + + + + + + + {% endfor %} +
ReferenceCompanyDescriptionStatus
{{ order }}{{ order.supplier }}{{ order.description }}{% include "order/order_status.html" %}
+ +{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py new file mode 100644 index 0000000000..905b01adb7 --- /dev/null +++ b/InvenTree/order/urls.py @@ -0,0 +1,20 @@ +""" +URL lookup for the Order app. Provides URL endpoints for: + +- List view of Purchase Orders +- Detail view of Purchase Orders +""" + +from django.conf.urls import url, include + +from . import views + +purchase_order_urls = [ + + # Display complete list of purchase orders + url(r'^.*$', views.PurchaseOrderIndex.as_view(), name='purchase-order-index'), +] + +order_urls = [ + url(r'^purchase-order/', include(purchase_order_urls)), +] diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 91ea44a218..fdbde9b8c0 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1,3 +1,26 @@ -from django.shortcuts import render +""" +Django views for interacting with Order app +""" -# Create your views here. +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.views.generic import DetailView, ListView + +from .models import PurchaseOrder + +from InvenTree.status_codes import OrderStatus + + +class PurchaseOrderIndex(ListView): + + model = PurchaseOrder + template_name = 'order/purchase_orders.html' + context_object_name = 'orders' + + def get_context_data(self): + ctx = super().get_context_data() + + ctx['OrderStatus'] = OrderStatus + + return ctx diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index e7785abcd5..2118bd2197 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -10,6 +10,7 @@
  • Stock
  • Build
  • Suppliers
  • +
  • Orders
  • +
    + +
    + +

    Order Items

    + + + + + + + + + + + + {% for line in order.lines.all %} + + + + + + + + {% endfor %} +
    LinePartReferenceQuantityReceived
    {{ forloop.counter }}{{ line.reference }}{{ line.quantity }}{{ line.received }}
    + +{% if order.notes %} +
    +
    +
    Notes
    +
    {{ order.notes }}
    +
    +{% endif %} + +{% endblock %} + +{% block js_ready %} + +$('#new-po-line').click(function() { + launchModalForm("{% url 'po-line-item-create' %}", + { + reload: true, + data: { + order: {{ order.id }}, + }, + } + ); +}); + +$("#po-lines-table").bootstrapTable({ +}); {% endblock %} \ No newline at end of file diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html index f85de6c962..4e48effa59 100644 --- a/InvenTree/order/templates/order/purchase_orders.html +++ b/InvenTree/order/templates/order/purchase_orders.html @@ -2,6 +2,10 @@ {% load static %} +{% block page_title %} +InvenTree | Purchase Orders +{% endblock %} + {% block content %}

    Purchase Orders

    diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 961cbb6857..9aa350fc54 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -14,11 +14,18 @@ purchase_order_detail_urls = [ url(r'^.*$', views.PurchaseOrderDetail.as_view(), name='purchase-order-detail'), ] +po_line_urls = [ + + url(r'^new/', views.POLineItemCreate.as_view(), name='po-line-item-create'), +] + purchase_order_urls = [ # Display detail view for a single purchase order url(r'^(?P\d+)/', include(purchase_order_detail_urls)), + url(r'^line/', include(po_line_urls)), + # Display complete list of purchase orders url(r'^.*$', views.PurchaseOrderIndex.as_view(), name='purchase-order-index'), ] diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index add27967d7..ac84ee1f4f 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -7,7 +7,10 @@ from __future__ import unicode_literals from django.views.generic import DetailView, ListView -from .models import PurchaseOrder +from .models import PurchaseOrder, PurchaseOrderLineItem +from .forms import EditPurchaseOrderLineItemForm + +from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.status_codes import OrderStatus @@ -19,8 +22,8 @@ class PurchaseOrderIndex(ListView): template_name = 'order/purchase_orders.html' context_object_name = 'orders' - def get_context_data(self): - ctx = super().get_context_data() + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) ctx['OrderStatus'] = OrderStatus @@ -33,3 +36,76 @@ class PurchaseOrderDetail(DetailView): context_object_name = 'order' queryset = PurchaseOrder.objects.all().prefetch_related('lines') template_name = 'order/purchase_order_detail.html' + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + ctx['OrderStatus'] = OrderStatus + + return ctx + + +class POLineItemCreate(AjaxCreateView): + """ AJAX view for creating a new PurchaseOrderLineItem object + """ + + model = PurchaseOrderLineItem + context_object_name = 'line' + form_class = EditPurchaseOrderLineItemForm + ajax_template_name = 'modal_form.html' + ajax_form_action = 'Add Line Item' + + def get_form(self): + """ Limit choice options based on the selected order, etc + """ + + form = super().get_form() + + order_id = form['order'].value() + + try: + order = PurchaseOrder.objects.get(id=order_id) + + query = form.fields['part'].queryset + + # Only allow parts from the selected supplier + query = query.filter(supplier=order.supplier.id) + + print('limiting queryset') + + form.fields['part'].queryset = query + except PurchaseOrder.DoesNotExist: + print('error') + pass + + return form + + + def get_initial(self): + """ Extract initial data for the line item. + + - The 'order' will be passed as a query parameter + - Use this to set the 'order' field and limit the options for 'part' + """ + + initials = super().get_initial().copy() + + order_id = self.request.GET.get('order', None) + + if order_id: + try: + order = PurchaseOrder.objects.get(id=order_id) + initials['order'] = order + + except PurchaseOrder.DoesNotExist: + pass + + return initials + + +class POLineItemEdit(AjaxUpdateView): + + model = PurchaseOrderLineItem + form_class = EditPurchaseOrderLineItemForm + ajax_template_name = 'modal_form.html' + ajax_form_action = 'Edit Line Item' diff --git a/InvenTree/static/css/inventree.css b/InvenTree/static/css/inventree.css index da7d38d250..794676ca68 100644 --- a/InvenTree/static/css/inventree.css +++ b/InvenTree/static/css/inventree.css @@ -297,6 +297,10 @@ margin-bottom: 5px; } +.panel-body { + padding: 10px; +} + .panel-group .panel { border-radius: 2px; } From f4abfc158f54bce3d07599bdd5664d2a151fdd32 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 5 Jun 2019 21:02:51 +1000 Subject: [PATCH 18/41] Improve rendering of purchase order list --- InvenTree/order/templates/order/purchase_order_detail.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 2d40de6459..9cee52bed0 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -62,6 +62,7 @@ InvenTree | {{ order }} Line Part + Order Code Reference Quantity Received @@ -69,7 +70,8 @@ InvenTree | {{ order }} {% for line in order.lines.all %} {{ forloop.counter }} - + {{ line.part.part.full_name }} + {{ line.part.SKU }} {{ line.reference }} {{ line.quantity }} {{ line.received }} From aee1ea9e35ce6b0fb382182f6c97ddb535d5520e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 5 Jun 2019 21:13:08 +1000 Subject: [PATCH 19/41] Limit queryset - Only parts from the supplier - Exclude parts already in the order --- InvenTree/order/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index ac84ee1f4f..d7d08988bd 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -71,11 +71,11 @@ class POLineItemCreate(AjaxCreateView): # Only allow parts from the selected supplier query = query.filter(supplier=order.supplier.id) - print('limiting queryset') + # Remove parts that are already in the order + query = query.exclude(id__in=[line.part.id for line in order.lines.all()]) form.fields['part'].queryset = query except PurchaseOrder.DoesNotExist: - print('error') pass return form From 8aa5452dd4cd2c0d984321cc9cb5c6579d547c1b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 5 Jun 2019 21:17:29 +1000 Subject: [PATCH 20/41] Add secondary modal to create a new supplier part --- .../templates/order/purchase_order_detail.html | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 9cee52bed0..30eb48849c 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -70,8 +70,12 @@ InvenTree | {{ order }} {% for line in order.lines.all %} {{ forloop.counter }} + {% if line.part %} {{ line.part.part.full_name }} {{ line.part.SKU }} + {% else %} + Warning: Part has been deleted. + {% endif %} {{ line.reference }} {{ line.quantity }} {{ line.received }} @@ -98,6 +102,17 @@ $('#new-po-line').click(function() { data: { order: {{ order.id }}, }, + secondary: [ + { + field: 'part', + label: 'New Supplier Part', + title: 'Create new supplier part', + url: "{% url 'supplier-part-create' %}", + data: { + supplier: {{ order.supplier.id }}, + }, + }, + ], } ); }); From 9b2b2841d9c98d919d2c5b1355883dd2d7b2fc43 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 5 Jun 2019 21:19:41 +1000 Subject: [PATCH 21/41] Hide 'order' input --- InvenTree/order/views.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index d7d08988bd..569dfc488f 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -6,6 +6,7 @@ Django views for interacting with Order app from __future__ import unicode_literals from django.views.generic import DetailView, ListView +from django.forms import HiddenInput from .models import PurchaseOrder, PurchaseOrderLineItem from .forms import EditPurchaseOrderLineItemForm @@ -53,7 +54,7 @@ class POLineItemCreate(AjaxCreateView): context_object_name = 'line' form_class = EditPurchaseOrderLineItemForm ajax_template_name = 'modal_form.html' - ajax_form_action = 'Add Line Item' + ajax_form_title = 'Add Line Item' def get_form(self): """ Limit choice options based on the selected order, etc @@ -71,10 +72,17 @@ class POLineItemCreate(AjaxCreateView): # Only allow parts from the selected supplier query = query.filter(supplier=order.supplier.id) + exclude = [] + + for line in order.lines.all(): + if line.part and line.part.id not in exclude: + exclude.append(line.part.id) + # Remove parts that are already in the order - query = query.exclude(id__in=[line.part.id for line in order.lines.all()]) + query = query.exclude(id__in=exclude) form.fields['part'].queryset = query + form.fields['order'].widget = HiddenInput() except PurchaseOrder.DoesNotExist: pass From 67248ec4dd2069dc93bd15b5755d913a27962e8b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 5 Jun 2019 21:47:22 +1000 Subject: [PATCH 22/41] List purchase orders for a given part --- InvenTree/company/models.py | 5 +++++ .../migrations/0007_auto_20190605_2138.py | 19 +++++++++++++++++++ .../migrations/0008_auto_20190605_2140.py | 19 +++++++++++++++++++ InvenTree/order/models.py | 2 +- InvenTree/order/templates/order/po_table.html | 16 ++++++++++++++++ .../templates/order/purchase_orders.html | 18 +----------------- InvenTree/part/models.py | 12 ++++++++++++ InvenTree/part/templates/part/orders.html | 18 ++++++++++++++++++ InvenTree/part/templates/part/tabs.html | 10 ++++++++-- InvenTree/part/urls.py | 1 + InvenTree/part/views.py | 3 +++ 11 files changed, 103 insertions(+), 20 deletions(-) create mode 100644 InvenTree/order/migrations/0007_auto_20190605_2138.py create mode 100644 InvenTree/order/migrations/0008_auto_20190605_2140.py create mode 100644 InvenTree/order/templates/order/po_table.html create mode 100644 InvenTree/part/templates/part/orders.html diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 880d37869b..d054aff040 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -307,6 +307,11 @@ class SupplierPart(models.Model): else: return None + def purchase_orders(self): + """ Returns a list of purchase orders relating to this supplier part """ + + return [line.order for line in self.purchase_order_line_items.all().prefetch_related('order')] + def __str__(self): s = "{supplier} ({sku})".format( sku=self.SKU, diff --git a/InvenTree/order/migrations/0007_auto_20190605_2138.py b/InvenTree/order/migrations/0007_auto_20190605_2138.py new file mode 100644 index 0000000000..ce2119f258 --- /dev/null +++ b/InvenTree/order/migrations/0007_auto_20190605_2138.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2019-06-05 11:38 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0006_auto_20190605_2056'), + ] + + operations = [ + migrations.AlterField( + model_name='purchaseorderlineitem', + name='part', + field=models.ForeignKey(blank=True, help_text='Supplier part', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_line_items', to='company.SupplierPart'), + ), + ] diff --git a/InvenTree/order/migrations/0008_auto_20190605_2140.py b/InvenTree/order/migrations/0008_auto_20190605_2140.py new file mode 100644 index 0000000000..688c8cf15c --- /dev/null +++ b/InvenTree/order/migrations/0008_auto_20190605_2140.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2019-06-05 11:40 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0007_auto_20190605_2138'), + ] + + operations = [ + migrations.AlterField( + model_name='purchaseorderlineitem', + name='part', + field=models.ForeignKey(blank=True, help_text='Supplier part', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_order_line_items', to='company.SupplierPart'), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 56ef5952fb..dc72522e67 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -124,7 +124,7 @@ class PurchaseOrderLineItem(OrderLineItem): part = models.ForeignKey( SupplierPart, on_delete=models.SET_NULL, blank=True, null=True, - related_name='orders', + related_name='purchase_order_line_items', help_text=_("Supplier part"), ) diff --git a/InvenTree/order/templates/order/po_table.html b/InvenTree/order/templates/order/po_table.html new file mode 100644 index 0000000000..5d5295b25e --- /dev/null +++ b/InvenTree/order/templates/order/po_table.html @@ -0,0 +1,16 @@ + + + + + + + + {% for order in orders %} + + + + + + + {% endfor %} +
    CompanyOrder ReferenceDescriptionStatus
    {% include "hover_image.html" with image=order.supplier.image hover=True %}{{ order.supplier.name }}{{ order }}{{ order.description }}{% include "order/order_status.html" %}
    \ No newline at end of file diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html index 4e48effa59..c599bfcd20 100644 --- a/InvenTree/order/templates/order/purchase_orders.html +++ b/InvenTree/order/templates/order/purchase_orders.html @@ -10,22 +10,6 @@ InvenTree | Purchase Orders

    Purchase Orders

    - - - - - - - - - {% for order in orders %} - - - - - - - {% endfor %} -
    CompanyOrder ReferenceDescriptionStatus
    {% include "hover_image.html" with image=order.supplier.image hover=True %}{{ order.supplier.name }}{{ order }}{{ order.description }}{% include "order/order_status.html" %}
    +{% include "order/po_table.html" %} {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 29a11125ab..9a2486798d 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -794,6 +794,18 @@ class Part(models.Model): return n + def purchase_orders(self): + """ Return a list of purchase orders which reference this part """ + + orders = [] + + for part in self.supplier_parts.all().prefetch_related('purchase_order_line_items'): + for order in part.purchase_orders(): + if order not in orders: + orders.append(order) + + return orders + def attach_file(instance, filename): """ Function for storing a file for a PartAttachment diff --git a/InvenTree/part/templates/part/orders.html b/InvenTree/part/templates/part/orders.html new file mode 100644 index 0000000000..ed8beb9b42 --- /dev/null +++ b/InvenTree/part/templates/part/orders.html @@ -0,0 +1,18 @@ +{% extends "part/part_base.html" %} +{% load static %} + +{% block details %} + +{% include 'part/tabs.html' with tab='orders' %} + +
    +
    +

    Part Orders

    +
    +
    +
    +
    + +{% include "order/po_table.html" with orders=part.purchase_orders %} + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index dd5034040d..c7dbb2418f 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -25,11 +25,17 @@ Used In{% if part.used_in_count > 0 %}{{ part.used_in_count }}{% endif %} {% endif %} - {% if part.purchaseable and part.is_template == False %} + {% if part.purchaseable %} + {% if part.is_template == False %} Suppliers {{ part.supplier_count }} - + + + {% endif %} + + Purchase Orders {{ part.purchase_orders|length }} + {% endif %} {% if part.trackable and 0 %} diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 0147cd4d07..e8a0175503 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -34,6 +34,7 @@ part_detail_urls = [ url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'), url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'), url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'), + url(r'^orders/?', views.PartDetail.as_view(template_name='part/orders.html'), name='part-orders'), url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 4f61036f95..9f8b8652bd 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -24,6 +24,7 @@ from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDelete from InvenTree.views import QRCodeView from InvenTree.helpers import DownloadFile, str2bool +from InvenTree.status_codes import OrderStatus class PartIndex(ListView): @@ -446,6 +447,8 @@ class PartDetail(DetailView): context['starred'] = part.isStarredBy(self.request.user) + context['OrderStatus'] = OrderStatus + return context From 04abe2b3d110af37778443578ac80d221cfc7aaa Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 5 Jun 2019 21:50:11 +1000 Subject: [PATCH 23/41] Display list of purchase orders against a particular supplier par --- InvenTree/company/templates/company/partdetail.html | 5 ++++- InvenTree/company/views.py | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/InvenTree/company/templates/company/partdetail.html b/InvenTree/company/templates/company/partdetail.html index dd43d0c5e4..681ba25074 100644 --- a/InvenTree/company/templates/company/partdetail.html +++ b/InvenTree/company/templates/company/partdetail.html @@ -101,7 +101,10 @@ InvenTree | {{ company.name }} - Parts
    -
    +
    + +

    Purchase Orders

    +{% include "order/po_table.html" with orders=part.purchase_orders %} {% endblock %} diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index f9e032cbe9..c3cda22e39 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -128,6 +128,12 @@ class SupplierPartDetail(DetailView): context_object_name = 'part' queryset = SupplierPart.objects.all() + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + ctx['OrderStatus'] = OrderStatus + + return ctx + class SupplierPartEdit(AjaxUpdateView): """ Update view for editing SupplierPart """ From eced012ece600871803d8d216ca94a0f5de25ddf Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 5 Jun 2019 21:56:52 +1000 Subject: [PATCH 24/41] PEP fixes --- InvenTree/order/forms.py | 4 ++-- InvenTree/order/templates/order/purchase_order_detail.html | 1 + InvenTree/order/views.py | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 627b5f130e..f80a4436a5 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals from InvenTree.forms import HelperForm -from .models import PurchaseOrder, PurchaseOrderLineItem +from .models import PurchaseOrderLineItem class EditPurchaseOrderLineItemForm(HelperForm): @@ -20,4 +20,4 @@ class EditPurchaseOrderLineItemForm(HelperForm): 'quantity', 'reference', 'received' - ] \ No newline at end of file + ] diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 30eb48849c..4593c6d9b1 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -29,6 +29,7 @@ InvenTree | {{ order }}
    +

    Purchase Order Details

    diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 569dfc488f..d7b47abf1a 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -11,7 +11,7 @@ from django.forms import HiddenInput from .models import PurchaseOrder, PurchaseOrderLineItem from .forms import EditPurchaseOrderLineItemForm -from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView +from InvenTree.views import AjaxCreateView, AjaxUpdateView from InvenTree.status_codes import OrderStatus @@ -88,7 +88,6 @@ class POLineItemCreate(AjaxCreateView): return form - def get_initial(self): """ Extract initial data for the line item. From bcc08f982b8764d988138d0edc7c5eeae55ba628 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 5 Jun 2019 22:08:22 +1000 Subject: [PATCH 25/41] Set default ajax form template --- InvenTree/InvenTree/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 71f87ffd19..282541d19f 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -92,6 +92,10 @@ class AjaxMixin(object): on the client side. """ + # By default, point to the modal_form template + # (this can be overridden by a child class) + ajax_template_name = 'modal_form.html' + ajax_form_action = '' ajax_form_title = '' @@ -165,10 +169,6 @@ class AjaxView(AjaxMixin, View): """ An 'AJAXified' View for displaying an object """ - # By default, point to the modal_form template - # (this can be overridden by a child class) - ajax_template_name = 'modal_form.html' - def post(self, request, *args, **kwargs): return JsonResponse('', safe=False) From 96eb4086cfc0b602119532f0afae2d39c7caef2d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 5 Jun 2019 22:24:18 +1000 Subject: [PATCH 26/41] Add form / view to edit purchase order details - Cannot edit the COMPANY if there are line items already --- InvenTree/order/forms.py | 27 +++++++++++++- .../order/templates/order/order_issue.html | 1 + .../order/templates/order/order_status.html | 10 +++--- .../order/purchase_order_detail.html | 15 ++++++++ InvenTree/order/urls.py | 3 ++ InvenTree/order/views.py | 36 ++++++++++++++++--- InvenTree/static/css/inventree.css | 1 - 7 files changed, 82 insertions(+), 11 deletions(-) create mode 100644 InvenTree/order/templates/order/order_issue.html diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index f80a4436a5..bdd77e15bc 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -7,10 +7,35 @@ from __future__ import unicode_literals from InvenTree.forms import HelperForm -from .models import PurchaseOrderLineItem +from .models import PurchaseOrder, PurchaseOrderLineItem + + +class IssuePurchaseOrderForm(HelperForm): + + class Meta: + model = PurchaseOrder + fields = [ + 'status', + ] + + +class EditPurchaseOrderForm(HelperForm): + """ Form for editing a PurchaseOrder object """ + + class Meta: + model = PurchaseOrder + fields = [ + 'reference', + 'supplier', + 'description', + 'URL', + 'status', + 'notes' + ] class EditPurchaseOrderLineItemForm(HelperForm): + """ Form for editing a PurchaseOrderLineItem object """ class Meta: model = PurchaseOrderLineItem diff --git a/InvenTree/order/templates/order/order_issue.html b/InvenTree/order/templates/order/order_issue.html new file mode 100644 index 0000000000..c7de8c74b2 --- /dev/null +++ b/InvenTree/order/templates/order/order_issue.html @@ -0,0 +1 @@ +{% extends "modal_form.html" %} \ No newline at end of file diff --git a/InvenTree/order/templates/order/order_status.html b/InvenTree/order/templates/order/order_status.html index 3e3d2a1e08..c9e13cac24 100644 --- a/InvenTree/order/templates/order/order_status.html +++ b/InvenTree/order/templates/order/order_status.html @@ -1,13 +1,13 @@ {% if order.status == OrderStatus.PENDING %} - + {% elif order.status == OrderStatus.PLACED %} - + {% elif order.status == OrderStatus.COMPLETE %} - + {% elif order.status == OrderStatus.CANCELLED or order.status == OrderStatus.RETURNED %} - + {% else %} - + {% endif %} {{ order.get_status_display }} \ No newline at end of file diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 4593c6d9b1..dbabb2f940 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -55,6 +55,13 @@ InvenTree | {{ order }}
    +
    + + {% if order.status == OrderStatus.PENDING %} + + {% endif %} +
    +

    Order Items

    @@ -96,6 +103,14 @@ InvenTree | {{ order }} {% block js_ready %} +$("#edit-order").click(function() { + launchModalForm("{% url 'purchase-order-edit' order.id %}", + { + reload: true, + } + ); +}); + $('#new-po-line').click(function() { launchModalForm("{% url 'po-line-item-create' %}", { diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 9aa350fc54..16ca858e39 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -11,6 +11,9 @@ from . import views 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'^.*$', views.PurchaseOrderDetail.as_view(), name='purchase-order-detail'), ] diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index d7b47abf1a..6bacdda93e 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -9,7 +9,8 @@ from django.views.generic import DetailView, ListView from django.forms import HiddenInput from .models import PurchaseOrder, PurchaseOrderLineItem -from .forms import EditPurchaseOrderLineItemForm + +from . import forms as order_forms from InvenTree.views import AjaxCreateView, AjaxUpdateView @@ -46,14 +47,41 @@ class PurchaseOrderDetail(DetailView): return ctx +class PurchaseOrderEdit(AjaxUpdateView): + """ View for editing a PurchaseOrder using a modal form """ + + model = PurchaseOrder + ajax_form_title = 'Edit Purchase Order' + form_class = order_forms.EditPurchaseOrderForm + + def get_form(self): + + form = super(AjaxUpdateView, self).get_form() + + order = self.get_object() + + if order.lines.count() > 0: + form.fields['supplier'].widget = HiddenInput() + + return form + + +class PurchaseOrderIssue(AjaxUpdateView): + """ View for changing a purchase order from 'PENDING' to 'ISSUED' """ + + model = PurchaseOrder + ajax_form_title = 'Issue Order' + ajax_template_name = "order/order_issue.html" + form_class = order_forms.IssuePurchaseOrderForm + + class POLineItemCreate(AjaxCreateView): """ AJAX view for creating a new PurchaseOrderLineItem object """ model = PurchaseOrderLineItem context_object_name = 'line' - form_class = EditPurchaseOrderLineItemForm - ajax_template_name = 'modal_form.html' + form_class = order_forms.EditPurchaseOrderLineItemForm ajax_form_title = 'Add Line Item' def get_form(self): @@ -113,6 +141,6 @@ class POLineItemCreate(AjaxCreateView): class POLineItemEdit(AjaxUpdateView): model = PurchaseOrderLineItem - form_class = EditPurchaseOrderLineItemForm + form_class = order_forms.EditPurchaseOrderLineItemForm ajax_template_name = 'modal_form.html' ajax_form_action = 'Edit Line Item' diff --git a/InvenTree/static/css/inventree.css b/InvenTree/static/css/inventree.css index 794676ca68..3bef992d34 100644 --- a/InvenTree/static/css/inventree.css +++ b/InvenTree/static/css/inventree.css @@ -38,7 +38,6 @@ /* Extra label styles */ .label-large { - padding: 5px; margin: 3px; font-size: 100%; } From ad5c6630bdda1a739682121b92ecae9b92e97490 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 6 Jun 2019 10:43:34 +1000 Subject: [PATCH 27/41] Bug fix in Build.completeBuild --- 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 4f5f1630aa..28c7ecfcd3 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -242,7 +242,7 @@ class Build(models.Model): item.save() # Finally, mark the build as complete - self.status = self.COMPLETE + self.status = BuildStatus.COMPLETE self.save() def getRequiredQuantity(self, part): From c1f3bddf4505103eeba6b1878dabebbe825164ba Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 6 Jun 2019 19:28:52 +1000 Subject: [PATCH 28/41] Secondary modal for creating a new stock location when moving stock --- InvenTree/static/script/inventree/stock.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index facbaa65f5..ae3ab55a7e 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -195,6 +195,18 @@ function loadStockTable(table, options) { stock.push(item.pk); }); + // Buttons for launching secondary modals + var secondary = []; + + if (action == 'move') { + secondary.push({ + field: 'destination', + label: 'New Location', + title: 'Create new location', + url: "/stock/location/new/", + }); + } + launchModalForm("/stock/adjust/", { data: { @@ -204,6 +216,7 @@ function loadStockTable(table, options) { success: function() { $("#stock-table").bootstrapTable('refresh'); }, + secondary: secondary, } ); } From 7b139a7f0523d083a9330d9eb765b5be8749221d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 6 Jun 2019 21:39:04 +1000 Subject: [PATCH 29/41] Form / view for creating a new purchase order --- InvenTree/order/forms.py | 1 - .../migrations/0009_auto_20190606_2133.py | 18 ++++++++++++++ InvenTree/order/models.py | 2 +- .../templates/order/purchase_orders.html | 24 ++++++++++++++++++- InvenTree/order/urls.py | 2 ++ InvenTree/order/views.py | 24 +++++++++++++++++++ 6 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 InvenTree/order/migrations/0009_auto_20190606_2133.py diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index bdd77e15bc..259b64f087 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -29,7 +29,6 @@ class EditPurchaseOrderForm(HelperForm): 'supplier', 'description', 'URL', - 'status', 'notes' ] diff --git a/InvenTree/order/migrations/0009_auto_20190606_2133.py b/InvenTree/order/migrations/0009_auto_20190606_2133.py new file mode 100644 index 0000000000..0cadfe87fe --- /dev/null +++ b/InvenTree/order/migrations/0009_auto_20190606_2133.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-06-06 11:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0008_auto_20190605_2140'), + ] + + operations = [ + migrations.AlterField( + model_name='purchaseorder', + name='description', + field=models.CharField(help_text='Order description', max_length=250), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index dc72522e67..a239458d33 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -43,7 +43,7 @@ class Order(models.Model): reference = models.CharField(unique=True, max_length=64, blank=False, help_text=_('Order reference')) - description = models.CharField(max_length=250, blank=True, help_text=_('Order description')) + description = models.CharField(max_length=250, help_text=_('Order description')) URL = models.URLField(blank=True, help_text=_('Link to external page')) diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html index c599bfcd20..0c7ac717cd 100644 --- a/InvenTree/order/templates/order/purchase_orders.html +++ b/InvenTree/order/templates/order/purchase_orders.html @@ -8,8 +8,30 @@ InvenTree | Purchase Orders {% block content %} -

    Purchase Orders

    +
    +
    +

    Purchase Orders

    +
    +
    +
    + +
    +
    +
    {% include "order/po_table.html" %} +{% endblock %} + +{% block js_ready %} +{{ block.super }} + +$("#po-create").click(function() { + launchModalForm("{% url 'purchase-order-create' %}", + { + reload: true, + } + ); +}); + {% endblock %} \ No newline at end of file diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 16ca858e39..20379edb76 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -24,6 +24,8 @@ po_line_urls = [ purchase_order_urls = [ + url(r'^new/', views.PurchaseOrderCreate.as_view(), name='purchase-order-create'), + # Display detail view for a single purchase order url(r'^(?P\d+)/', include(purchase_order_detail_urls)), diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 6bacdda93e..56b5077d22 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -24,6 +24,14 @@ class PurchaseOrderIndex(ListView): template_name = 'order/purchase_orders.html' context_object_name = 'orders' + def get_queryset(self): + """ Retrieve the list of purchase orders, + ensure that the most recent ones are returned first. """ + + queryset = PurchaseOrder.objects.all().order_by('-creation_date') + + return queryset + def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) @@ -47,6 +55,21 @@ class PurchaseOrderDetail(DetailView): return ctx +class PurchaseOrderCreate(AjaxCreateView): + """ View for creating a new PurchaseOrder object using a modal form """ + + model = PurchaseOrder + ajax_form_title = "Create Purchase Order" + form_class = order_forms.EditPurchaseOrderForm + + def get_initial(self): + initials = super().get_initial().copy() + + initials['status'] = OrderStatus.PENDING + + return initials + + class PurchaseOrderEdit(AjaxUpdateView): """ View for editing a PurchaseOrder using a modal form """ @@ -60,6 +83,7 @@ class PurchaseOrderEdit(AjaxUpdateView): order = self.get_object() + # Prevent user from editing supplier if there are already lines in the order if order.lines.count() > 0: form.fields['supplier'].widget = HiddenInput() From 4048091c2b2422e5c32f6a0ca301717de3d35506 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 6 Jun 2019 21:55:02 +1000 Subject: [PATCH 30/41] Prevent user from inputting a 'blank' supplier part into a line item --- InvenTree/order/views.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 56b5077d22..c4bcc05f69 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -5,10 +5,12 @@ Django views for interacting with Order app # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.utils.translation import ugettext as _ from django.views.generic import DetailView, ListView from django.forms import HiddenInput from .models import PurchaseOrder, PurchaseOrderLineItem +from company.models import SupplierPart from . import forms as order_forms @@ -108,6 +110,36 @@ class POLineItemCreate(AjaxCreateView): form_class = order_forms.EditPurchaseOrderLineItemForm ajax_form_title = 'Add Line Item' + def post(self, request, *arg, **kwargs): + + self.request = request + + form = self.get_form() + + valid = form.is_valid() + + part_id = form['part'].value() + + try: + SupplierPart.objects.get(id=part_id) + except (SupplierPart.DoesNotExist, ValueError): + valid = False + form.errors['part'] = [_('This field is required')] + + data = { + 'form_valid': valid, + } + + if valid: + self.object = form.save() + + data['pk'] = self.object.pk + data['text'] = str(self.object) + else: + self.object = None + + return self.renderJsonResponse(request, form, data,) + def get_form(self): """ Limit choice options based on the selected order, etc """ From 4af1f6ca9f5b50c237ced5e52b41f7a7c581ef01 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 6 Jun 2019 21:56:20 +1000 Subject: [PATCH 31/41] Update a TODO comment --- InvenTree/order/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index a239458d33..ed102e928a 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -119,7 +119,7 @@ class PurchaseOrderLineItem(OrderLineItem): help_text=_('Purchase Order') ) - # TODO - foreign key references to part and stockitem objects + # TODO - Function callback for when the SupplierPart is deleted? part = models.ForeignKey( SupplierPart, on_delete=models.SET_NULL, From 31ad31365a57ecf28ee3ed32a92ec83da54d2947 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 7 Jun 2019 08:37:25 +1000 Subject: [PATCH 32/41] Calculate parts on order for a Part / SupplierPart --- InvenTree/InvenTree/status_codes.py | 13 ++++++++ InvenTree/company/models.py | 33 +++++++++++++------- InvenTree/part/models.py | 5 +++ InvenTree/part/templates/part/part_base.html | 6 ++++ 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index 9207e28c7b..930242f396 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -32,6 +32,19 @@ class OrderStatus(StatusCode): RETURNED: _("Returned"), } + # Open orders + OPEN = [ + PENDING, + PLACED, + ] + + # Failed orders + FAILED = [ + CANCELLED, + LOST, + RETURNED + ] + class StockStatus(StatusCode): diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index d054aff040..324e5dcf59 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -10,9 +10,10 @@ import os import math from django.core.validators import MinValueValidator +from django.db import models +from django.db.models import Sum from django.apps import apps -from django.db import models from django.urls import reverse from django.conf import settings from django.contrib.staticfiles.templatetags.staticfiles import static @@ -132,10 +133,7 @@ class Company(models.Model): def outstanding_purchase_orders(self): """ Return purchase orders which are 'outstanding' """ - return self.purchase_orders.filter(status__in=[ - OrderStatus.PENDING, - OrderStatus.PLACED - ]) + return self.purchase_orders.filter(status__in=OrderStatus.OPEN) def complete_purchase_orders(self): return self.purchase_orders.filter(status=OrderStatus.COMPLETE) @@ -143,12 +141,7 @@ class Company(models.Model): def failed_purchase_orders(self): """ Return any purchase orders which were not successful """ - return self.purchase_orders.filter(status__in=[ - OrderStatus.CANCELLED, - OrderStatus.LOST, - OrderStatus.RETURNED - ]) - + return self.purchase_orders.filter(status__in=OrderStatus.FAILED) class Contact(models.Model): """ A Contact represents a person who works at a particular company. @@ -307,6 +300,24 @@ class SupplierPart(models.Model): else: return None + def open_orders(self): + """ Return a database query for PO line items for this SupplierPart, + limited to purchase orders that are open / outstanding. + """ + + return self.purchase_order_line_items.prefetch_related('order').filter(order__status__in=OrderStatus.OPEN) + + def on_order(self): + """ Return the total quantity of items currently on order. + + Subtract partially received stock as appropriate + """ + + totals = self.open_orders().aggregate(Sum('quantity'), Sum('received')) + + return totals['quantity__sum'] - totals['received__sum'] + + def purchase_orders(self): """ Returns a list of purchase orders relating to this supplier part """ diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 9a2486798d..f1294a972d 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -806,6 +806,11 @@ class Part(models.Model): return orders + def on_order(self): + """ Return the total number of items on order for this part. """ + + return sum([part.on_order() for part in self.supplier_parts.all()]) + def attach_file(instance, filename): """ Function for storing a file for a PartAttachment diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 9c68cfe69f..2661497c74 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -96,6 +96,12 @@
    {% endif %} + {% if part.on_order > 0 %} + + + + + {% endif %}
    Status{{ part.allocation_count }}
    On Order{{ part.on_order }}
    From 351c5fb7d0b432ab7a559f85581d071590ab4f24 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 7 Jun 2019 09:54:36 +1000 Subject: [PATCH 33/41] Fix for 'on_order' calculation - Handle null results --- InvenTree/company/models.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 324e5dcf59..33841d59db 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -237,6 +237,9 @@ class SupplierPart(models.Model): @property def manufacturer_string(self): + """ Format a MPN string for this SupplierPart. + Concatenates manufacture name and part number + """ items = [] @@ -315,7 +318,16 @@ class SupplierPart(models.Model): totals = self.open_orders().aggregate(Sum('quantity'), Sum('received')) - return totals['quantity__sum'] - totals['received__sum'] + # Quantity on order + q = totals.get('quantity__sum', 0) + + # Quantity received + r = totals.get('received__sum', 0) + + if q is None or r is None: + return 0 + else: + return q - r def purchase_orders(self): From 9efdd836f4adbee6f1765235cb4b74a91b954149 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 10 Jun 2019 21:56:50 +1000 Subject: [PATCH 34/41] Tweak the 'on_order' calculation --- InvenTree/company/models.py | 2 +- InvenTree/part/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 33841d59db..069ecb0ad6 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -327,7 +327,7 @@ class SupplierPart(models.Model): if q is None or r is None: return 0 else: - return q - r + return max(q-r, 0) def purchase_orders(self): diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index f1294a972d..6749fa0ad6 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -809,7 +809,7 @@ class Part(models.Model): def on_order(self): """ Return the total number of items on order for this part. """ - return sum([part.on_order() for part in self.supplier_parts.all()]) + return sum([part.on_order() for part in self.supplier_parts.all().prefetch_related('purchase_order_line_items')]) def attach_file(instance, filename): From ec669dd67085d0fb327c805932717bd5c5d6657e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 10 Jun 2019 22:14:23 +1000 Subject: [PATCH 35/41] Ability to 'issue' a purchase order --- InvenTree/order/forms.py | 6 ++++- InvenTree/order/models.py | 16 ++++++++++++ .../order/templates/order/order_issue.html | 8 +++++- .../order/purchase_order_detail.html | 9 ++++++- InvenTree/order/views.py | 25 +++++++++++++++++++ 5 files changed, 61 insertions(+), 3 deletions(-) diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 259b64f087..08e9a6f740 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -5,6 +5,8 @@ Django Forms for interacting with Order objects # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django import forms + from InvenTree.forms import HelperForm from .models import PurchaseOrder, PurchaseOrderLineItem @@ -12,10 +14,12 @@ from .models import PurchaseOrder, PurchaseOrderLineItem class IssuePurchaseOrderForm(HelperForm): + confirm = forms.BooleanField(required=False, help_text='Place order') + class Meta: model = PurchaseOrder fields = [ - 'status', + 'confirm', ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index ed102e928a..e71e50f743 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -1,9 +1,17 @@ +""" +Order model definitions +""" + +# -*- coding: utf-8 -*- + from django.db import models from django.core.validators import MinValueValidator from django.contrib.auth.models import User from django.utils.translation import ugettext as _ +from datetime import datetime + from company.models import Company, SupplierPart from InvenTree.status_codes import OrderStatus @@ -62,6 +70,14 @@ class Order(models.Model): notes = models.TextField(blank=True, help_text=_('Order notes')) + def place_order(self): + """ Marks the order as PLACED. Order must be currently PENDING. """ + + if self.status == OrderStatus.PENDING: + self.status = OrderStatus.PLACED + self.issue_date = datetime.now().date() + self.save() + class PurchaseOrder(Order): """ A PurchaseOrder represents goods shipped inwards from an external supplier. diff --git a/InvenTree/order/templates/order/order_issue.html b/InvenTree/order/templates/order/order_issue.html index c7de8c74b2..ad80fc5a0d 100644 --- a/InvenTree/order/templates/order/order_issue.html +++ b/InvenTree/order/templates/order/order_issue.html @@ -1 +1,7 @@ -{% extends "modal_form.html" %} \ No newline at end of file +{% extends "modal_form.html" %} + +{% block pre_form_content %} + +After placing this purchase order, line items will no longer be editable. + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index dbabb2f940..5fb9a76ac9 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -58,7 +58,7 @@ InvenTree | {{ order }}
    {% if order.status == OrderStatus.PENDING %} - + {% endif %}
    @@ -103,6 +103,13 @@ InvenTree | {{ order }} {% block js_ready %} +$("#place-order").click(function() { + launchModalForm("{% url 'purchase-order-issue' order.id %}", + { + reload: true, + }); +}); + $("#edit-order").click(function() { launchModalForm("{% url 'purchase-order-edit' order.id %}", { diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index c4bcc05f69..5e85e0ae82 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -15,6 +15,7 @@ from company.models import SupplierPart from . import forms as order_forms from InvenTree.views import AjaxCreateView, AjaxUpdateView +from InvenTree.helpers import str2bool from InvenTree.status_codes import OrderStatus @@ -100,6 +101,30 @@ class PurchaseOrderIssue(AjaxUpdateView): ajax_template_name = "order/order_issue.html" form_class = order_forms.IssuePurchaseOrderForm + def post(self, request, *args, **kwargs): + """ Mark the purchase order as 'PLACED' """ + + order = self.get_object() + form = self.get_form() + + confirm = str2bool(request.POST.get('confirm', False)) + + valid = False + + if not confirm: + form.errors['confirm'] = [_('Confirm order placement')] + else: + valid = True + + data = { + 'form_valid': valid, + } + + if valid: + order.issue_order() + + return self.renderJsonResponse(request, form, data) + class POLineItemCreate(AjaxCreateView): """ AJAX view for creating a new PurchaseOrderLineItem object From 228bf4e1daf848491ca70d775aa06c1680975d13 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 10 Jun 2019 22:17:19 +1000 Subject: [PATCH 36/41] Business logic --- InvenTree/order/templates/order/purchase_order_detail.html | 4 ++++ InvenTree/order/views.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 5fb9a76ac9..404b947bd1 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -64,7 +64,9 @@ InvenTree | {{ order }}

    Order Items

    +{% if order.status == OrderStatus.PENDING %} +{% endif %} @@ -118,6 +120,7 @@ $("#edit-order").click(function() { ); }); +{% if order.status == OrderStatus.PENDING %} $('#new-po-line').click(function() { launchModalForm("{% url 'po-line-item-create' %}", { @@ -139,6 +142,7 @@ $('#new-po-line').click(function() { } ); }); +{% endif %} $("#po-lines-table").bootstrapTable({ }); diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 5e85e0ae82..7666d007d9 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -87,7 +87,7 @@ class PurchaseOrderEdit(AjaxUpdateView): order = self.get_object() # Prevent user from editing supplier if there are already lines in the order - if order.lines.count() > 0: + if order.lines.count() > 0 or not order.status == OrderStatus.PENDING: form.fields['supplier'].widget = HiddenInput() return form @@ -121,7 +121,7 @@ class PurchaseOrderIssue(AjaxUpdateView): } if valid: - order.issue_order() + order.place_order() return self.renderJsonResponse(request, form, data) From b8bcc5ce0ccdbe87ec888bf883d1b9a25ee278ba Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 10 Jun 2019 22:31:19 +1000 Subject: [PATCH 37/41] Separate display of open and closed purchase orders (per part) --- InvenTree/part/models.py | 12 +++++++++++- InvenTree/part/templates/part/orders.html | 9 +++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 6749fa0ad6..c2c1ba03c2 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -33,7 +33,7 @@ from InvenTree import helpers from InvenTree import validators from InvenTree.models import InvenTreeTree -from InvenTree.status_codes import BuildStatus, StockStatus +from InvenTree.status_codes import BuildStatus, StockStatus, OrderStatus from company.models import SupplierPart @@ -806,6 +806,16 @@ class Part(models.Model): return orders + def open_purchase_orders(self): + """ Return a list of open purchase orders against this part """ + + return [order for order in self.purchase_orders() if order.status in OrderStatus.OPEN] + + def closed_purchase_orders(self): + """ Return a list of closed purchase orders against this part """ + + return [order for order in self.purchase_orders() if order.status not in OrderStatus.OPEN] + def on_order(self): """ Return the total number of items on order for this part. """ diff --git a/InvenTree/part/templates/part/orders.html b/InvenTree/part/templates/part/orders.html index ed8beb9b42..8c9f0c3cee 100644 --- a/InvenTree/part/templates/part/orders.html +++ b/InvenTree/part/templates/part/orders.html @@ -7,12 +7,17 @@
    -

    Part Orders

    +

    Open Part Orders

    -{% include "order/po_table.html" with orders=part.purchase_orders %} +{% include "order/po_table.html" with orders=part.open_purchase_orders %} + +{% if part.closed_purchase_orders|length > 0 %} +

    Closed Orders

    +{% include "order/po_table.html" with orders=part.closed_purchase_orders %} +{% endif %} {% endblock %} \ No newline at end of file From c132f275f5a403da3cdc7fa4099c57bc8e5237db Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 10 Jun 2019 22:43:09 +1000 Subject: [PATCH 38/41] Split display of purchase orders by company view --- InvenTree/company/models.py | 10 ++++++ .../company/detail_purchase_orders.html | 18 +++++------ .../company/templates/company/orders.html | 31 ------------------- .../templates/order/po_table_collapse.html | 11 +++++++ 4 files changed, 28 insertions(+), 42 deletions(-) delete mode 100644 InvenTree/company/templates/company/orders.html create mode 100644 InvenTree/order/templates/order/po_table_collapse.html diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 069ecb0ad6..4f0e45c2aa 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -135,6 +135,16 @@ class Company(models.Model): """ Return purchase orders which are 'outstanding' """ return self.purchase_orders.filter(status__in=OrderStatus.OPEN) + def closed_purchase_orders(self): + """ Return purchase orders which are not 'outstanding' + + - Complete + - Failed / lost + - Returned + """ + + return self.purchase_orders.exclude(status__in=OrderStatus.OPEN) + def complete_purchase_orders(self): return self.purchase_orders.filter(status=OrderStatus.COMPLETE) diff --git a/InvenTree/company/templates/company/detail_purchase_orders.html b/InvenTree/company/templates/company/detail_purchase_orders.html index 28d7becd1a..0b9882a041 100644 --- a/InvenTree/company/templates/company/detail_purchase_orders.html +++ b/InvenTree/company/templates/company/detail_purchase_orders.html @@ -4,17 +4,13 @@ {% include 'company/tabs.html' with tab='po' %} -

    Purchase Orders

    +

    Open Purchase Orders

    + +{% include "order/po_table.html" with orders=company.outstanding_purchase_orders.all %} + +{% if company.closed_purchase_orders.count > 0 %} +{% include "order/po_table_collapse.html" with title="Closed Orders" orders=company.closed_purchase_orders.all %} +{% endif %} -
    - - - - - - {% include "company/po_list.html" with orders=company.outstanding_purchase_orders %} - {% include "company/po_list.html" with orders=company.complete_purchase_orders %} - {% include "company/po_list.html" with orders=company.failed_purchase_orders %} -
    ReferenceDescriptionStatus
    {% endblock %} \ No newline at end of file diff --git a/InvenTree/company/templates/company/orders.html b/InvenTree/company/templates/company/orders.html deleted file mode 100644 index 519035f5c6..0000000000 --- a/InvenTree/company/templates/company/orders.html +++ /dev/null @@ -1,31 +0,0 @@ -{% extends "supplier/supplier_base.html" %} - -{% block details %} - -{% include "supplier/tabs.html" with tab='order' %} - -

    Supplier Orders

    - - - - - - - - -{% for order in supplier.orders.all %} - - - - - - -{% endfor %} -
    ReferenceIssuedDeliveryStatus
    {{ order.internal_ref }}{% if order.issued_date %}{{ order.issued_date }}{% endif %}{% if order.delivery_date %}{{ order.delivery_date }}{% endif %}{% include "supplier/order_status.html" with order=order %}
    - -
    - - - - -{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/templates/order/po_table_collapse.html b/InvenTree/order/templates/order/po_table_collapse.html new file mode 100644 index 0000000000..886e14ce97 --- /dev/null +++ b/InvenTree/order/templates/order/po_table_collapse.html @@ -0,0 +1,11 @@ +{% extends "collapse.html" %} + +{% load static %} + +{% block collapse_title %} +

    {{ title }}

    +{% endblock %} + +{% block collapse_content %} +{% include "order/po_table.html" %} +{% endblock %} \ No newline at end of file From 04a9b1a980bfb9b486cd1cd849f6ddce99f0d882 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 10 Jun 2019 22:56:34 +1000 Subject: [PATCH 39/41] Create a new purchase order from a company page --- .../company/detail_purchase_orders.html | 21 +++++++++++++++++++ InvenTree/order/models.py | 5 ++++- InvenTree/order/templates/order/po_table.html | 2 +- .../order/purchase_order_detail.html | 6 +++++- InvenTree/order/views.py | 11 +++++++++- 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/InvenTree/company/templates/company/detail_purchase_orders.html b/InvenTree/company/templates/company/detail_purchase_orders.html index 0b9882a041..f2f3b8ceb3 100644 --- a/InvenTree/company/templates/company/detail_purchase_orders.html +++ b/InvenTree/company/templates/company/detail_purchase_orders.html @@ -6,11 +6,32 @@

    Open Purchase Orders

    +
    +
    + +
    +
    + {% include "order/po_table.html" with orders=company.outstanding_purchase_orders.all %} {% if company.closed_purchase_orders.count > 0 %} {% include "order/po_table_collapse.html" with title="Closed Orders" orders=company.closed_purchase_orders.all %} {% endif %} +{% endblock %} + +{% block js_ready %} +{{ block.super }} + +$("#po-create").click(function() { + launchModalForm("{% url 'purchase-order-create' %}", + { + data: { + supplier: {{ company.id }}, + }, + follow: true, + } + ); +}); {% endblock %} \ No newline at end of file diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index e71e50f743..7ca2e942fd 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -7,7 +7,7 @@ Order model definitions from django.db import models from django.core.validators import MinValueValidator from django.contrib.auth.models import User - +from django.urls import reverse from django.utils.translation import ugettext as _ from datetime import datetime @@ -98,6 +98,9 @@ class PurchaseOrder(Order): help_text=_('Company') ) + def get_absolute_url(self): + return reverse('purchase-order-detail', kwargs={'pk': self.id}) + class OrderLineItem(models.Model): """ Abstract model for an order line item diff --git a/InvenTree/order/templates/order/po_table.html b/InvenTree/order/templates/order/po_table.html index 5d5295b25e..0c1149470f 100644 --- a/InvenTree/order/templates/order/po_table.html +++ b/InvenTree/order/templates/order/po_table.html @@ -7,7 +7,7 @@ {% for order in orders %} - {% include "hover_image.html" with image=order.supplier.image hover=True %}{{ order.supplier.name }} + {% include "hover_image.html" with image=order.supplier.image hover=True %}{{ order.supplier.name }} {{ order }} {{ order.description }} {% include "order/order_status.html" %} diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 404b947bd1..b6751e044d 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -31,6 +31,10 @@ InvenTree | {{ order }}

    Purchase Order Details

    + + + + @@ -57,7 +61,7 @@ InvenTree | {{ order }}
    - {% if order.status == OrderStatus.PENDING %} + {% if order.status == OrderStatus.PENDING and order.lines.count > 0 %} {% endif %}
    diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 7666d007d9..951de8c6fd 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -10,7 +10,7 @@ from django.views.generic import DetailView, ListView from django.forms import HiddenInput from .models import PurchaseOrder, PurchaseOrderLineItem -from company.models import SupplierPart +from company.models import Company, SupplierPart from . import forms as order_forms @@ -70,6 +70,15 @@ class PurchaseOrderCreate(AjaxCreateView): initials['status'] = OrderStatus.PENDING + supplier_id = self.request.GET.get('supplier', None) + + if supplier_id: + try: + supplier = Company.objects.get(id=supplier_id) + initials['supplier'] = supplier + except Company.DoesNotExist: + pass + return initials From 3954b33fb7b09880146688aa8bb71472f23758e5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 10 Jun 2019 23:05:14 +1000 Subject: [PATCH 40/41] Use 'on_order' count in calculation for parts we need to order --- InvenTree/order/admin.py | 1 + InvenTree/part/models.py | 3 ++- InvenTree/templates/required_part_table.html | 6 ++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py index e6b8db5735..bbd95cd42a 100644 --- a/InvenTree/order/admin.py +++ b/InvenTree/order/admin.py @@ -21,6 +21,7 @@ class PurchaseOrderLineItemAdmin(admin.ModelAdmin): list_display = ( 'order', + 'part', 'quantity', 'reference' ) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index c2c1ba03c2..67a82a9504 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -427,7 +427,7 @@ class Part(models.Model): then we need to restock. """ - return (self.total_stock - self.allocation_count) < self.minimum_stock + return (self.total_stock + self.on_order - self.allocation_count) < self.minimum_stock @property def can_build(self): @@ -816,6 +816,7 @@ class Part(models.Model): return [order for order in self.purchase_orders() if order.status not in OrderStatus.OPEN] + @property def on_order(self): """ Return the total number of items on order for this part. """ diff --git a/InvenTree/templates/required_part_table.html b/InvenTree/templates/required_part_table.html index 5a8c7e5734..24eae0012f 100644 --- a/InvenTree/templates/required_part_table.html +++ b/InvenTree/templates/required_part_table.html @@ -2,16 +2,18 @@ + - + {% for part in parts %} - + + {% endfor %} From d8d41c6effc344ddcbfa1c348b711d3ea0ffd8bb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 10 Jun 2019 23:08:08 +1000 Subject: [PATCH 41/41] PEP fixes --- InvenTree/InvenTree/status_codes.py | 2 +- InvenTree/company/models.py | 14 +++++++------- InvenTree/order/views.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index 930242f396..b7b50a58f7 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -43,7 +43,7 @@ class OrderStatus(StatusCode): CANCELLED, LOST, RETURNED - ] + ] class StockStatus(StatusCode): diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 4f0e45c2aa..68e9cbaf9b 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -140,7 +140,7 @@ class Company(models.Model): - Complete - Failed / lost - - Returned + - Returned """ return self.purchase_orders.exclude(status__in=OrderStatus.OPEN) @@ -153,6 +153,7 @@ class Company(models.Model): return self.purchase_orders.filter(status__in=OrderStatus.FAILED) + class Contact(models.Model): """ A Contact represents a person who works at a particular company. A Company may have zero or more associated Contact objects. @@ -248,7 +249,7 @@ class SupplierPart(models.Model): @property def manufacturer_string(self): """ Format a MPN string for this SupplierPart. - Concatenates manufacture name and part number + Concatenates manufacture name and part number. """ items = [] @@ -314,14 +315,14 @@ class SupplierPart(models.Model): return None def open_orders(self): - """ Return a database query for PO line items for this SupplierPart, + """ Return a database query for PO line items for this SupplierPart, limited to purchase orders that are open / outstanding. """ return self.purchase_order_line_items.prefetch_related('order').filter(order__status__in=OrderStatus.OPEN) def on_order(self): - """ Return the total quantity of items currently on order. + """ Return the total quantity of items currently on order. Subtract partially received stock as appropriate """ @@ -332,13 +333,12 @@ class SupplierPart(models.Model): q = totals.get('quantity__sum', 0) # Quantity received - r = totals.get('received__sum', 0) + r = totals.get('received__sum', 0) if q is None or r is None: return 0 else: - return max(q-r, 0) - + return max(q - r, 0) def purchase_orders(self): """ Returns a list of purchase orders relating to this supplier part """ diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 951de8c6fd..718b26c98c 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -79,7 +79,7 @@ class PurchaseOrderCreate(AjaxCreateView): except Company.DoesNotExist: pass - return initials + return initials class PurchaseOrderEdit(AjaxUpdateView):
    Supplier{{ order.supplier }}
    Status {% include "order/order_status.html" %}
    Part DescriptionRequired In StockAllocatedOn Order Net Stock
    {{ part.full_name }} {{ part.description }}{{ part.total_stock }} {{ part.allocation_count }}{{ part.total_stock }}{{ part.on_order }} {{ part.available_stock }}