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/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";
}
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')
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/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/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)
diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py
index bbd95cd42a..44d26ea3f2 100644
--- a/InvenTree/order/admin.py
+++ b/InvenTree/order/admin.py
@@ -2,11 +2,12 @@
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 +18,7 @@ class PurchaseOrderAdmin(admin.ModelAdmin):
)
-class PurchaseOrderLineItemAdmin(admin.ModelAdmin):
+class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
list_display = (
'order',
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/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):
diff --git a/InvenTree/order/test_views.py b/InvenTree/order/test_views.py
index a197455a8a..9ef3c80276 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, PurchaseOrderLineItem
+
+import json
+
class OrderViewTestCase(TestCase):
@@ -75,3 +81,106 @@ 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)
+
+ 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'] = 7
+ 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)
+
+ 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)
+
+
+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..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
@@ -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):
@@ -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):
diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py
index 25d0dddb52..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.
@@ -611,13 +617,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 +662,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 +688,7 @@ class POLineItemCreate(AjaxCreateView):
form.fields['part'].queryset = query
form.fields['order'].widget = HiddenInput()
- except PurchaseOrder.DoesNotExist:
+ except (ValueError, PurchaseOrder.DoesNotExist):
pass
return form
diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py
index e7489c9810..7a9def473f 100644
--- a/InvenTree/part/admin.py
+++ b/InvenTree/part/admin.py
@@ -1,21 +1,116 @@
+# -*- 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 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 data import/export """
+
+ # ForeignKey fields
+ category = Field(attribute='category', widget=widgets.ForeignKeyWidget(PartCategory))
+
+ default_location = Field(attribute='default_location', widget=widgets.ForeignKeyWidget(StockLocation))
+
+ 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
+ report_skipped = False
+ exclude = [
+ '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):
+ resource_class = PartResource
+
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 """
+
+ parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(PartCategory))
+
+ 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
+ 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 PartCategory tree(s)
+ PartCategory.objects.rebuild()
+
class PartCategoryAdmin(ImportExportModelAdmin):
+ resource_class = PartCategoryResource
+
list_display = ('name', 'pathstring', 'description')
+ search_fields = ('name', 'description')
+
class PartAttachmentAdmin(admin.ModelAdmin):
@@ -27,15 +122,53 @@ 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')
+ search_fields = ('part__name', 'part__description', 'sub_part__name', 'sub_part__description')
+
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')
diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py
index 8f4e18461e..ef84b0890e 100644
--- a/InvenTree/stock/admin.py
+++ b/InvenTree/stock/admin.py
@@ -1,15 +1,93 @@
+# -*- 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
+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 """
+
+ 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')
+ search_fields = ('name', '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')