From 7c6901f4453691b27936f2051bd951f44f5f658a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 18:15:05 +1000 Subject: [PATCH 01/24] Tests for purchas order issue form --- InvenTree/order/test_views.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/InvenTree/order/test_views.py b/InvenTree/order/test_views.py index a197455a8a..5496dec6df 100644 --- a/InvenTree/order/test_views.py +++ b/InvenTree/order/test_views.py @@ -4,6 +4,12 @@ from django.test import TestCase from django.urls import reverse from django.contrib.auth import get_user_model +from InvenTree.status_codes import OrderStatus + +from .models import PurchaseOrder + +import json + class OrderViewTestCase(TestCase): @@ -75,3 +81,29 @@ class POTests(OrderViewTestCase): # Response should be streaming-content (file download) self.assertIn('streaming_content', dir(response)) + + def test_po_issue(self): + """ Test PurchaseOrderIssue view """ + + url = reverse('purchase-order-issue', args=(1,)) + + order = PurchaseOrder.objects.get(pk=1) + self.assertEqual(order.status, OrderStatus.PENDING) + + # Test without confirmation + response = self.client.post(url, {'confirm': 0}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + data = json.loads(response.content) + self.assertFalse(data['form_valid']) + + # Test WITH confirmation + response = self.client.post(url, {'confirm': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + data = json.loads(response.content) + self.assertTrue(data['form_valid']) + + # Test that the order was actually placed + order = PurchaseOrder.objects.get(pk=1) + self.assertEqual(order.status, OrderStatus.PLACED) From d515e2d96837df614fee48062be459665e842b14 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 20:01:41 +1000 Subject: [PATCH 02/24] Tests for POLineItem creation form --- InvenTree/order/test_views.py | 56 ++++++++++++++++++++++++++++++++++- InvenTree/order/views.py | 28 ++++++++++++++++-- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/InvenTree/order/test_views.py b/InvenTree/order/test_views.py index 5496dec6df..841e4054b8 100644 --- a/InvenTree/order/test_views.py +++ b/InvenTree/order/test_views.py @@ -6,7 +6,7 @@ from django.contrib.auth import get_user_model from InvenTree.status_codes import OrderStatus -from .models import PurchaseOrder +from .models import PurchaseOrder, PurchaseOrderLineItem import json @@ -107,3 +107,57 @@ class POTests(OrderViewTestCase): # Test that the order was actually placed order = PurchaseOrder.objects.get(pk=1) self.assertEqual(order.status, OrderStatus.PLACED) + + def test_line_item_create(self): + """ Test the form for adding a new LineItem to a PurchaseOrder """ + + # Record the number of line items in the PurchaseOrder + po = PurchaseOrder.objects.get(pk=1) + n = po.lines.count() + self.assertEqual(po.status, OrderStatus.PENDING) + + url = reverse('po-line-item-create') + + # GET the form (pass the correct info) + response = self.client.get(url, {'order': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + + post_data = { + 'part': 100, + 'quantity': 45, + 'reference': 'Test reference field', + 'notes': 'Test notes field' + } + + # POST with an invalid purchase order + post_data['order'] = 99 + response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + data = json.loads(response.content) + self.assertFalse(data['form_valid']) + self.assertIn('Invalid Purchase Order', str(data['html_form'])) + + # POST with a part that does not match the purchase order + post_data['order'] = 1 + post_data['part'] = 3 + response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + data = json.loads(response.content) + self.assertFalse(data['form_valid']) + self.assertIn('must match for Part and Order', str(data['html_form'])) + + # POST with an invalid part + post_data['part'] = 12345 + response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + data = json.loads(response.content) + self.assertFalse(data['form_valid']) + self.assertIn('Invalid SupplierPart selection', str(data['html_form'])) + + # POST the form with valid data + post_data['part'] = 100 + response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertTrue(data['form_valid']) + + self.assertEqual(n+1, PurchaseOrder.objects.get(pk=1).lines.count()) + + line = PurchaseOrderLineItem.objects.get(order=1, part=100) + self.assertEqual(line.quantity, 45) \ No newline at end of file diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 25d0dddb52..0328093cdd 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -611,13 +611,30 @@ class POLineItemCreate(AjaxCreateView): valid = form.is_valid() + # Extract the SupplierPart ID from the form part_id = form['part'].value() + # Extract the Order ID from the form + order_id = form['order'].value() + try: - SupplierPart.objects.get(id=part_id) + order = PurchaseOrder.objects.get(id=order_id) + except (ValueError, PurchaseOrder.DoesNotExist): + order = None + form.errors['order'] = [_('Invalid Purchase Order')] + valid = False + + try: + sp = SupplierPart.objects.get(id=part_id) + + if order is not None: + if not sp.supplier == order.supplier: + form.errors['part'] = [_('Supplier must match for Part and Order')] + valid = False + except (SupplierPart.DoesNotExist, ValueError): valid = False - form.errors['part'] = [_('This field is required')] + form.errors['part'] = [_('Invalid SupplierPart selection')] data = { 'form_valid': valid, @@ -639,6 +656,11 @@ class POLineItemCreate(AjaxCreateView): form = super().get_form() + # Limit the available to orders to ones that are PENDING + query = form.fields['order'].queryset + query = query.filter(status=OrderStatus.PENDING) + form.fields['order'].queryset = query + order_id = form['order'].value() try: @@ -660,7 +682,7 @@ class POLineItemCreate(AjaxCreateView): form.fields['part'].queryset = query form.fields['order'].widget = HiddenInput() - except PurchaseOrder.DoesNotExist: + except (ValueError, PurchaseOrder.DoesNotExist): pass return form From 6854190ff9315201f9ea76b9b0db6f95f885a944 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 20:06:19 +1000 Subject: [PATCH 03/24] Simple test for POLineItemedit view --- InvenTree/order/test_views.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/InvenTree/order/test_views.py b/InvenTree/order/test_views.py index 841e4054b8..9b8eccae54 100644 --- a/InvenTree/order/test_views.py +++ b/InvenTree/order/test_views.py @@ -157,7 +157,15 @@ class POTests(OrderViewTestCase): data = json.loads(response.content) self.assertTrue(data['form_valid']) - self.assertEqual(n+1, PurchaseOrder.objects.get(pk=1).lines.count()) + self.assertEqual(n + 1, PurchaseOrder.objects.get(pk=1).lines.count()) line = PurchaseOrderLineItem.objects.get(order=1, part=100) - self.assertEqual(line.quantity, 45) \ No newline at end of file + self.assertEqual(line.quantity, 45) + + def test_line_item_edit(self): + """ Test editing form for PO line item """ + + url = reverse('po-line-item-edit', args=(22,)) + + response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) From 59f102af3c62b331aaf48c9a5fabd0119728f82c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 20:15:34 +1000 Subject: [PATCH 04/24] Database filtering beats list comprehension! --- InvenTree/order/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 306b748a79..ef79a4d39b 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -5,6 +5,7 @@ Order model definitions # -*- coding: utf-8 -*- from django.db import models, transaction +from django.db.models import F from django.core.validators import MinValueValidator from django.core.exceptions import ValidationError from django.contrib.auth.models import User @@ -226,7 +227,7 @@ class PurchaseOrder(Order): Any line item where 'received' < 'quantity' will be returned. """ - return [line for line in self.lines.all() if line.quantity > line.received] + return self.lines.filter(quantity__gt=F('received')) @transaction.atomic def receive_line_item(self, line, location, quantity, user): From 36ec5e41b0c4f2abd93a2f4d5503d63c1f076514 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 20:52:25 +1000 Subject: [PATCH 05/24] Cleanup --- InvenTree/company/fixtures/supplier_part.yaml | 9 ++++++++- InvenTree/order/fixtures/order.yaml | 8 ++++++++ InvenTree/order/test_views.py | 17 ++++++++++++++++- InvenTree/order/tests.py | 4 ++-- InvenTree/order/views.py | 6 ++++++ 5 files changed, 40 insertions(+), 4 deletions(-) diff --git a/InvenTree/company/fixtures/supplier_part.yaml b/InvenTree/company/fixtures/supplier_part.yaml index b9649704e1..446339d58b 100644 --- a/InvenTree/company/fixtures/supplier_part.yaml +++ b/InvenTree/company/fixtures/supplier_part.yaml @@ -15,6 +15,13 @@ supplier: 1 SKU: 'ACME0002' +- model: company.supplierpart + pk: 3 + fields: + part: 1 + supplier: 1 + SKU: 'ACME0003' + # Widget purchaseable from ACME - model: company.supplierpart pk: 100 @@ -33,7 +40,7 @@ # M2x4 LPHS from Zerg Corp - model: company.supplierpart - pk: 3 + pk: 7 fields: part: 1 supplier: 3 diff --git a/InvenTree/order/fixtures/order.yaml b/InvenTree/order/fixtures/order.yaml index 6fab8a705d..0ba4bdbeb5 100644 --- a/InvenTree/order/fixtures/order.yaml +++ b/InvenTree/order/fixtures/order.yaml @@ -35,8 +35,16 @@ quantity: 250 received: 50 +# 1000 x ACME0003 +- model: order.purchaseorderlineitem + fields: + order: 1 + part: 3 + quantity: 1000 + # 100 x ZERGLPHS (M2x4 LPHS) - model: order.purchaseorderlineitem + pk: 22 fields: order: 2 part: 3 diff --git a/InvenTree/order/test_views.py b/InvenTree/order/test_views.py index 9b8eccae54..9ef3c80276 100644 --- a/InvenTree/order/test_views.py +++ b/InvenTree/order/test_views.py @@ -137,7 +137,7 @@ class POTests(OrderViewTestCase): # POST with a part that does not match the purchase order post_data['order'] = 1 - post_data['part'] = 3 + post_data['part'] = 7 response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') data = json.loads(response.content) self.assertFalse(data['form_valid']) @@ -169,3 +169,18 @@ class POTests(OrderViewTestCase): response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) + + +class TestPOReceive(OrderViewTestCase): + """ Tests for receiving a purchase order """ + + def setUp(self): + super().setUp() + + self.po = PurchaseOrder.objects.get(pk=1) + self.url = reverse('purchase-order-receive', args=(1,)) + + def test_receive_lines(self): + + # TODO + pass diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py index 3a773ad0e9..814cea2be9 100644 --- a/InvenTree/order/tests.py +++ b/InvenTree/order/tests.py @@ -101,7 +101,7 @@ class OrderTest(TestCase): loc = StockLocation.objects.get(id=1) # There should be two lines against this order - self.assertEqual(len(order.pending_line_items()), 2) + self.assertEqual(len(order.pending_line_items()), 3) # Should fail, as order is 'PENDING' not 'PLACED" self.assertEqual(order.status, OrderStatus.PENDING) @@ -117,7 +117,7 @@ class OrderTest(TestCase): self.assertEqual(line.remaining(), 50) - self.assertEqual(part.on_order, 350) + self.assertEqual(part.on_order, 1350) # Try to order some invalid things with self.assertRaises(django_exceptions.ValidationError): diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 0328093cdd..fafd313022 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -243,6 +243,11 @@ class PurchaseOrderReceive(AjaxView): except (PurchaseOrderLineItem.DoesNotExist, ValueError): continue + # Check that line matches the order + if not line.order == self.order: + # TODO - Display a non-field error? + continue + # Ignore a part that doesn't map to a SupplierPart try: if line.part is None: @@ -277,6 +282,7 @@ class PurchaseOrderReceive(AjaxView): return self.renderJsonResponse(request, data=data) + @transaction.atomic def receive_parts(self): """ Called once the form has been validated. Create new stockitems against received parts. From c8be9cb90c6bcd042311fc56f0e863acc1e9202d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 20:58:17 +1000 Subject: [PATCH 06/24] Display template badge in part table --- InvenTree/InvenTree/static/script/inventree/part.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/InvenTree/static/script/inventree/part.js b/InvenTree/InvenTree/static/script/inventree/part.js index 18909fff0d..1056a7706b 100644 --- a/InvenTree/InvenTree/static/script/inventree/part.js +++ b/InvenTree/InvenTree/static/script/inventree/part.js @@ -137,6 +137,10 @@ function loadPartTable(table, url, options={}) { var display = imageHoverIcon(row.image) + renderLink(name, '/part/' + row.pk + '/'); + if (row.is_template) { + display = display + "TEMPLATE"; + } + if (!row.active) { display = display + "INACTIVE"; } From 588713467d66f55c2da82f942224da3deacdda42 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 21:07:32 +1000 Subject: [PATCH 07/24] Fixed unit tests --- InvenTree/order/tests.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py index 814cea2be9..c271e0be43 100644 --- a/InvenTree/order/tests.py +++ b/InvenTree/order/tests.py @@ -47,10 +47,10 @@ class OrderTest(TestCase): for supplier in part.supplier_parts.all(): open_orders += supplier.open_orders() - self.assertEqual(len(open_orders), 3) + self.assertEqual(len(open_orders), 4) # Test the total on-order quantity - self.assertEqual(part.on_order, 400) + self.assertEqual(part.on_order, 1400) def test_add_items(self): """ Test functions for adding line items to an order """ @@ -58,7 +58,7 @@ class OrderTest(TestCase): order = PurchaseOrder.objects.get(pk=1) self.assertEqual(order.status, OrderStatus.PENDING) - self.assertEqual(order.lines.count(), 2) + self.assertEqual(order.lines.count(), 3) sku = SupplierPart.objects.get(SKU='ACME-WIDGET') part = sku.part @@ -76,11 +76,11 @@ class OrderTest(TestCase): order.add_line_item(sku, 100) self.assertEqual(part.on_order, 100) - self.assertEqual(order.lines.count(), 3) + self.assertEqual(order.lines.count(), 4) # Order the same part again (it should be merged) order.add_line_item(sku, 50) - self.assertEqual(order.lines.count(), 3) + self.assertEqual(order.lines.count(), 4) self.assertEqual(part.on_order, 150) # Try to order a supplier part from the wrong supplier @@ -132,7 +132,12 @@ class OrderTest(TestCase): line = PurchaseOrderLineItem.objects.get(id=2) order.receive_line_item(line, loc, 2 * line.quantity, user=None) - self.assertEqual(part.on_order, 100) + self.assertEqual(part.on_order, 1100) + self.assertEqual(order.status, OrderStatus.PLACED) + + for line in order.pending_line_items(): + order.receive_line_item(line, loc, line.quantity, user=None) + self.assertEqual(order.status, OrderStatus.COMPLETE) def test_export(self): From 7e9c095edb9971a0e8ef6c4afcf19c667fbf7f05 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 21:14:00 +1000 Subject: [PATCH 08/24] Ok, fixed now --- InvenTree/company/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/company/tests.py b/InvenTree/company/tests.py index b4baec2976..7037170862 100644 --- a/InvenTree/company/tests.py +++ b/InvenTree/company/tests.py @@ -56,7 +56,7 @@ class CompanySimpleTest(TestCase): zerg = Company.objects.get(pk=3) self.assertTrue(acme.has_parts) - self.assertEqual(acme.part_count, 3) + self.assertEqual(acme.part_count, 4) self.assertTrue(appel.has_parts) self.assertEqual(appel.part_count, 2) From 8a68313e5e2a1bd530a7b69730be637818c73f44 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 21:39:37 +1000 Subject: [PATCH 09/24] Customize admin export of Part object --- InvenTree/part/admin.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index e7489c9810..d0496fee4c 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -1,5 +1,7 @@ from django.contrib import admin -from import_export.admin import ImportExportModelAdmin +from import_export.admin import ImportExportModelAdmin, ImportExportActionModelAdmin +from import_export.resources import ModelResource +from import_export.fields import Field from .models import PartCategory, Part from .models import PartAttachment, PartStar @@ -7,8 +9,35 @@ from .models import BomItem from .models import PartParameterTemplate, PartParameter +class PartResource(ModelResource): + """ Class for managing Part model export """ + + # Constuct some extra fields for export + category = Field(attribute='category__pk', column_name='Category') + + category_name = Field(attribute='category__name', column_name='Category Name') + + default_location = Field(attribute='default_location__pk', column_name='Default Location') + + default_supplier = Field(attribute='default_supplier__pk', column_name='Default Supplier') + + in_stock = Field(attribute='total_stock') + + on_order = Field(attribute='on_order') + + variant_of = Field(attribute='variant_of__pk') + + class Meta: + model = Part + exclude = [ + 'bom_checksum', 'bom_checked_by', 'bom_checked_date' + ] + + class PartAdmin(ImportExportModelAdmin): + resource_class = PartResource + list_display = ('full_name', 'description', 'total_stock', 'category') From ac36048230468535fdf5dc0376e1c82a7f722c30 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 22:08:31 +1000 Subject: [PATCH 10/24] Improve import/export of Part - Can now import part data - Either UPDATE existing rows, or CREATE new ones --- InvenTree/part/admin.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index d0496fee4c..ba59042874 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -2,35 +2,33 @@ from django.contrib import admin from import_export.admin import ImportExportModelAdmin, ImportExportActionModelAdmin from import_export.resources import ModelResource from import_export.fields import Field +import import_export.widgets as widgets from .models import PartCategory, Part from .models import PartAttachment, PartStar from .models import BomItem from .models import PartParameterTemplate, PartParameter +from stock.models import StockLocation +from company.models import SupplierPart + class PartResource(ModelResource): """ Class for managing Part model export """ # Constuct some extra fields for export - category = Field(attribute='category__pk', column_name='Category') + category = Field(attribute='category', widget=widgets.ForeignKeyWidget(PartCategory)) - category_name = Field(attribute='category__name', column_name='Category Name') + default_location = Field(attribute='default_location', widget=widgets.ForeignKeyWidget(StockLocation)) + + category_name = Field(attribute='category__name', readonly=True) - default_location = Field(attribute='default_location__pk', column_name='Default Location') - - default_supplier = Field(attribute='default_supplier__pk', column_name='Default Supplier') - - in_stock = Field(attribute='total_stock') - - on_order = Field(attribute='on_order') - - variant_of = Field(attribute='variant_of__pk') + variant_of = Field(attribute='variant_of', widget=widgets.ForeignKeyWidget(Part)) class Meta: model = Part exclude = [ - 'bom_checksum', 'bom_checked_by', 'bom_checked_date' + 'image', 'bom_checksum', 'bom_checked_by', 'bom_checked_date' ] From 89acc778f57a44335f4f32ed8a62612dfb17cac6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 22:11:31 +1000 Subject: [PATCH 11/24] Skip unchanged lines for matching ID values --- InvenTree/part/admin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index ba59042874..8b5e415019 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -14,7 +14,7 @@ from company.models import SupplierPart class PartResource(ModelResource): - """ Class for managing Part model export """ + """ Class for managing Part data import/export """ # Constuct some extra fields for export category = Field(attribute='category', widget=widgets.ForeignKeyWidget(PartCategory)) @@ -27,8 +27,10 @@ class PartResource(ModelResource): class Meta: model = Part + skip_unchanged = True + report_skipped = False exclude = [ - 'image', 'bom_checksum', 'bom_checked_by', 'bom_checked_date' + 'bom_checksum', 'bom_checked_by', 'bom_checked_date' ] From bacd70687d63c89d40547c9643410502377429ba Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 22:20:08 +1000 Subject: [PATCH 12/24] Management class for PartCategory import / export --- InvenTree/part/admin.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 8b5e415019..bed6779f8e 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -41,8 +41,38 @@ class PartAdmin(ImportExportModelAdmin): list_display = ('full_name', 'description', 'total_stock', 'category') +class PartCategoryResource(ModelResource): + """ Class for managing PartCategory data import/export """ + + parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(PartCategory)) + + default_location = Field(attribute='default_location', widget=widgets.ForeignKeyWidget(StockLocation)) + + parent_name = Field(attribute='parent__name', readonly=True) + + class Meta: + model = PartCategory + skip_unchanged = True + report_skipped = False + + exclude = [ + # Exclude MPTT internal model fields + 'lft', 'rght', 'tree_id', 'level', + ] + + def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): + + super().after_import(dataset, result, using_transactions, dry_run, **kwargs) + + print("Rebuilding PartCategory tree") + # Rebuild teh PartCategory tree + PartCategory.objects.rebuild() + print("Done!") + class PartCategoryAdmin(ImportExportModelAdmin): + resource_class = PartCategoryResource + list_display = ('name', 'pathstring', 'description') From c469e48f26b5840ba4f46005ce2b323e87ab17b7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 22:23:40 +1000 Subject: [PATCH 13/24] Data manager for BomItem --- InvenTree/part/admin.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index bed6779f8e..3f52abe628 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -69,6 +69,7 @@ class PartCategoryResource(ModelResource): PartCategory.objects.rebuild() print("Done!") + class PartCategoryAdmin(ImportExportModelAdmin): resource_class = PartCategoryResource @@ -86,7 +87,23 @@ class PartStarAdmin(admin.ModelAdmin): list_display = ('part', 'user') +class BomItemResource(ModelResource): + """ Class for managing BomItem data import/export """ + + part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part)) + + sub_part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part)) + + class Meta: + model = BomItem + skip_unchanged = True + report_skipped = False + + class BomItemAdmin(ImportExportModelAdmin): + + resource_class = BomItemResource + list_display = ('part', 'sub_part', 'quantity') From 2bc34853e25a27965e2ab5f379d2fb97b3485c7a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 22:27:32 +1000 Subject: [PATCH 14/24] import/export manager for PartParameter --- InvenTree/part/admin.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 3f52abe628..8b51cd0546 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -111,7 +111,27 @@ class ParameterTemplateAdmin(ImportExportModelAdmin): list_display = ('name', 'units') +class ParameterResource(ModelResource): + """ Class for managing PartParameter data import/export """ + + part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part)) + + part_name = Field(attribute='part__name', readonly=True) + + template = Field(attribute='template', widget=widgets.ForeignKeyWidget(PartParameterTemplate)) + + template_name = Field(attribute='template__name', readonly=True) + + class Meta: + model = PartParameter + skip_unchanged = True + report_skipped = False + + class ParameterAdmin(ImportExportModelAdmin): + + resource_class = ParameterResource + list_display = ('part', 'template', 'data') From c579854e89e5a4810d118a55ed0c87a3a3099aa3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 22:29:11 +1000 Subject: [PATCH 15/24] Export 'default_supplier' field --- InvenTree/part/admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 8b51cd0546..259e20c996 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from import_export.admin import ImportExportModelAdmin, ImportExportActionModelAdmin +from import_export.admin import ImportExportModelAdmin from import_export.resources import ModelResource from import_export.fields import Field import import_export.widgets as widgets @@ -21,6 +21,8 @@ class PartResource(ModelResource): default_location = Field(attribute='default_location', widget=widgets.ForeignKeyWidget(StockLocation)) + default_supplirt = Field(attribute='default_supplier', widget=widgets.ForeignKeyWidget(SupplierPart)) + category_name = Field(attribute='category__name', readonly=True) variant_of = Field(attribute='variant_of', widget=widgets.ForeignKeyWidget(Part)) From 37ab3d214d7fbc897ce6e52e1123b4a33345edb9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 22:39:15 +1000 Subject: [PATCH 16/24] Import/export management for the Company app - Company - SupplierPart - SupplierPriceBreak --- InvenTree/company/admin.py | 54 ++++++++++++++++++++++++++++++++++++++ InvenTree/part/admin.py | 4 +++ 2 files changed, 58 insertions(+) diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index 607f773198..ada60c7975 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -1,20 +1,74 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + from django.contrib import admin + from import_export.admin import ImportExportModelAdmin +from import_export.resources import ModelResource +from import_export.fields import Field +import import_export.widgets as widgets from .models import Company from .models import SupplierPart from .models import SupplierPriceBreak +from part.models import Part +from common.models import Currency + + +class CompanyResource(ModelResource): + """ Class for managing Company data import/export """ + + class Meta: + model = Company + skip_unchanged = True + report_skipped = False + class CompanyAdmin(ImportExportModelAdmin): + + resource_class = CompanyResource + list_display = ('name', 'website', 'contact') +class SupplierPartResource(ModelResource): + """ Class for managing SupplierPart data import/export """ + + part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part)) + + supplier = Field(attribute='supplier', widget=widgets.ForeignKeyWidget(Company)) + + class Meta: + model = SupplierPart + skip_unchanged = True + report_skipped = False + + class SupplierPartAdmin(ImportExportModelAdmin): + + resource_class = SupplierPartResource + list_display = ('part', 'supplier', 'SKU') +class SupplierPriceBreakResource(ModelResource): + """ Class for managing SupplierPriceBreak data import/export """ + + part = Field(attribute='part', widget=widgets.ForeignKeyWidget(SupplierPart)) + + currency = Field(attribute='currency', widget=widgets.ForeignKeyWidget(Currency)) + + class Meta: + model = SupplierPriceBreak + skip_unchanged = True + report_skipped = False + + class SupplierPriceBreakAdmin(ImportExportModelAdmin): + + resource_class = SupplierPriceBreakResource + list_display = ('part', 'quantity', 'cost') diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 259e20c996..04d00a7e66 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -1,4 +1,8 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + from django.contrib import admin + from import_export.admin import ImportExportModelAdmin from import_export.resources import ModelResource from import_export.fields import Field From 23b814569a0c94b57e79cf39560ef24e20c32ba1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 22:44:50 +1000 Subject: [PATCH 17/24] Manager for importing StockLocation data --- InvenTree/part/admin.py | 8 +++----- InvenTree/stock/admin.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 04d00a7e66..12ebe77ca6 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -52,10 +52,10 @@ class PartCategoryResource(ModelResource): parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(PartCategory)) - default_location = Field(attribute='default_location', widget=widgets.ForeignKeyWidget(StockLocation)) - parent_name = Field(attribute='parent__name', readonly=True) + default_location = Field(attribute='default_location', widget=widgets.ForeignKeyWidget(StockLocation)) + class Meta: model = PartCategory skip_unchanged = True @@ -70,10 +70,8 @@ class PartCategoryResource(ModelResource): super().after_import(dataset, result, using_transactions, dry_run, **kwargs) - print("Rebuilding PartCategory tree") - # Rebuild teh PartCategory tree + # Rebuild the PartCategory tree(s) PartCategory.objects.rebuild() - print("Done!") class PartCategoryAdmin(ImportExportModelAdmin): diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index 8f4e18461e..c2975dfd0c 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -1,11 +1,46 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + from django.contrib import admin + from import_export.admin import ImportExportModelAdmin +from import_export.resources import ModelResource +from import_export.fields import Field +import import_export.widgets as widgets from .models import StockLocation, StockItem from .models import StockItemTracking +class LocationResource(ModelResource): + """ Class for managing StockLocation data import/export """ + + parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(StockLocation)) + + parent_name = Field(attribute='parent__name', readonly=True) + + class Meta: + model = StockLocation + skip_unchanged = True + report_skipped = False + + exclude = [ + # Exclude MPTT internal model fields + 'lft', 'rght', 'tree_id', 'level', + ] + + def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): + + super().after_import(dataset, result, using_transactions, dry_run, **kwargs) + + # Rebuild the StockLocation tree(s) + StockLocation.objects.rebuild() + + class LocationAdmin(ImportExportModelAdmin): + + resource_class = LocationResource + list_display = ('name', 'pathstring', 'description') From cb5db332d340f9ae746167c3cf89fdf484119d4a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 23:00:21 +1000 Subject: [PATCH 18/24] Manager for import/export of StockItem data --- InvenTree/InvenTree/settings.py | 4 ++++ InvenTree/stock/admin.py | 41 +++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 2f5572a5f8..f73a5034ca 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -227,6 +227,10 @@ USE_L10N = True USE_TZ = True +DATE_INPUT_FORMATS = [ + "%Y-%m-%d", +] + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.10/howto/static-files/ diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index c2975dfd0c..8ea92e6242 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -11,6 +11,11 @@ import import_export.widgets as widgets from .models import StockLocation, StockItem from .models import StockItemTracking +from build.models import Build +from company.models import Company, SupplierPart +from order.models import PurchaseOrder +from part.models import Part + class LocationResource(ModelResource): """ Class for managing StockLocation data import/export """ @@ -44,7 +49,43 @@ class LocationAdmin(ImportExportModelAdmin): list_display = ('name', 'pathstring', 'description') +class StockItemResource(ModelResource): + """ Class for managing StockItem data import/export """ + + # Custom manaegrs for ForeignKey fields + part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part)) + + part_name = Field(attribute='part__name', readonly=True) + + supplier_part = Field(attribute='supplier_part', widget=widgets.ForeignKeyWidget(SupplierPart)) + + location = Field(attribute='location', widget=widgets.ForeignKeyWidget(StockLocation)) + + location_name = Field(attribute='location__name', readonly=True) + + belongs_to = Field(attribute='belongs_to', widget=widgets.ForeignKeyWidget(StockItem)) + + customer = Field(attribute='customer', widget=widgets.ForeignKeyWidget(Company)) + + build = Field(attribute='build', widget=widgets.ForeignKeyWidget(Build)) + + purchase_order = Field(attribute='purchase_order', widget=widgets.ForeignKeyWidget(PurchaseOrder)) + + # Date management + updated = Field(attribute='updated', widget=widgets.DateWidget()) + + stocktake_date = Field(attribute='stocktake_date', widget=widgets.DateWidget()) + + class Meta: + model = StockItem + skip_unchanged = True + report_skipped = False + + class StockItemAdmin(ImportExportModelAdmin): + + resource_class = StockItemResource + list_display = ('part', 'quantity', 'location', 'status', 'updated') From f707dd34305384351d58406e65315719a70dd582 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 23:02:54 +1000 Subject: [PATCH 19/24] Currency model admin now supports import / export --- InvenTree/common/admin.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/admin.py b/InvenTree/common/admin.py index 1628f811a7..e0db0a7136 100644 --- a/InvenTree/common/admin.py +++ b/InvenTree/common/admin.py @@ -1,9 +1,14 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + from django.contrib import admin +from import_export.admin import ImportExportModelAdmin + from .models import Currency -class CurrencyAdmin(admin.ModelAdmin): +class CurrencyAdmin(ImportExportModelAdmin): list_display = ('symbol', 'suffix', 'description', 'value', 'base') From 52eeffc2c4f35820d07f4045c507d25ef7f70b5f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 23:05:16 +1000 Subject: [PATCH 20/24] Change more models to use ImportExportModelAdmin --- InvenTree/order/admin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py index bbd95cd42a..5e338dfc00 100644 --- a/InvenTree/order/admin.py +++ b/InvenTree/order/admin.py @@ -2,11 +2,11 @@ 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): +class PurchaseOrderAdmin(ImportExportModelAdmin): list_display = ( 'reference', @@ -17,7 +17,7 @@ class PurchaseOrderAdmin(admin.ModelAdmin): ) -class PurchaseOrderLineItemAdmin(admin.ModelAdmin): +class PurchaseOrderLineItemAdmin(ImportExportModelAdmin): list_display = ( 'order', From 6a19e94feb37e142a6d045cfd3dd9761f1046f97 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 23:15:34 +1000 Subject: [PATCH 21/24] Include some extra calculated fields for Part export (readonly) --- InvenTree/part/admin.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 12ebe77ca6..478434b193 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -20,17 +20,28 @@ from company.models import SupplierPart class PartResource(ModelResource): """ Class for managing Part data import/export """ - # Constuct some extra fields for export + # ForeignKey fields category = Field(attribute='category', widget=widgets.ForeignKeyWidget(PartCategory)) default_location = Field(attribute='default_location', widget=widgets.ForeignKeyWidget(StockLocation)) - default_supplirt = Field(attribute='default_supplier', widget=widgets.ForeignKeyWidget(SupplierPart)) + default_supplier = Field(attribute='default_supplier', widget=widgets.ForeignKeyWidget(SupplierPart)) category_name = Field(attribute='category__name', readonly=True) variant_of = Field(attribute='variant_of', widget=widgets.ForeignKeyWidget(Part)) + # Extra calculated meta-data (readonly) + in_stock = Field(attribute='total_stock', readonly=True, widget=widgets.IntegerWidget()) + + on_order = Field(attribute='on_order', readonly=True, widget=widgets.IntegerWidget()) + + used_in = Field(attribute='used_in_count', readonly=True, widget=widgets.IntegerWidget()) + + allocated = Field(attribute='allocation_count', readonly=True, widget=widgets.IntegerWidget()) + + building = Field(attribute='quantity_being_built', readonly=True, widget=widgets.IntegerWidget()) + class Meta: model = Part skip_unchanged = True @@ -39,6 +50,20 @@ class PartResource(ModelResource): 'bom_checksum', 'bom_checked_by', 'bom_checked_date' ] + def get_queryset(self): + """ Prefetch related data for quicker access """ + + query = super().get_queryset() + query = query.prefetch_related( + 'category', + 'used_in', + 'builds', + 'supplier_parts__purchase_order_line_items', + 'stock_items__allocations' + ) + + return query + class PartAdmin(ImportExportModelAdmin): From 28d49bdd4730f92425144325bc410f30602ca5d6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 23:19:12 +1000 Subject: [PATCH 22/24] PEP --- InvenTree/order/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py index 5e338dfc00..44d26ea3f2 100644 --- a/InvenTree/order/admin.py +++ b/InvenTree/order/admin.py @@ -6,6 +6,7 @@ from import_export.admin import ImportExportModelAdmin from .models import PurchaseOrder, PurchaseOrderLineItem + class PurchaseOrderAdmin(ImportExportModelAdmin): list_display = ( From 9b1d0bee3b36b5458d99566c1708e0f4718842ea Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 23:27:22 +1000 Subject: [PATCH 23/24] Add filtering and searching to Part admin --- InvenTree/part/admin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 478434b193..1d17b82e5d 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -71,6 +71,10 @@ class PartAdmin(ImportExportModelAdmin): list_display = ('full_name', 'description', 'total_stock', 'category') + list_filter = ('active', 'assembly', 'is_template', 'virtual') + + search_fields = ('name', 'description', 'category__name', 'category__description', 'IPN') + class PartCategoryResource(ModelResource): """ Class for managing PartCategory data import/export """ From 8578a3b8d14cdc4e89db54f4cbccf75bed050176 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 13 Sep 2019 23:32:49 +1000 Subject: [PATCH 24/24] Add searching to other admin views --- InvenTree/part/admin.py | 4 ++++ InvenTree/stock/admin.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 1d17b82e5d..7a9def473f 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -109,6 +109,8 @@ class PartCategoryAdmin(ImportExportModelAdmin): list_display = ('name', 'pathstring', 'description') + search_fields = ('name', 'description') + class PartAttachmentAdmin(admin.ModelAdmin): @@ -139,6 +141,8 @@ class BomItemAdmin(ImportExportModelAdmin): list_display = ('part', 'sub_part', 'quantity') + search_fields = ('part__name', 'part__description', 'sub_part__name', 'sub_part__description') + class ParameterTemplateAdmin(ImportExportModelAdmin): list_display = ('name', 'units') diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index 8ea92e6242..ef84b0890e 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -48,6 +48,8 @@ class LocationAdmin(ImportExportModelAdmin): list_display = ('name', 'pathstring', 'description') + search_fields = ('name', 'description') + class StockItemResource(ModelResource): """ Class for managing StockItem data import/export """