Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2019-09-13 23:38:07 +10:00
commit 4b33b15dd2
14 changed files with 454 additions and 17 deletions

View File

@ -227,6 +227,10 @@ USE_L10N = True
USE_TZ = True USE_TZ = True
DATE_INPUT_FORMATS = [
"%Y-%m-%d",
]
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/ # https://docs.djangoproject.com/en/1.10/howto/static-files/

View File

@ -137,6 +137,10 @@ function loadPartTable(table, url, options={}) {
var display = imageHoverIcon(row.image) + renderLink(name, '/part/' + row.pk + '/'); var display = imageHoverIcon(row.image) + renderLink(name, '/part/' + row.pk + '/');
if (row.is_template) {
display = display + "<span class='label label-info' style='float: right;'>TEMPLATE</span>";
}
if (!row.active) { if (!row.active) {
display = display + "<span class='label label-warning' style='float: right;'>INACTIVE</span>"; display = display + "<span class='label label-warning' style='float: right;'>INACTIVE</span>";
} }

View File

@ -1,9 +1,14 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from .models import Currency from .models import Currency
class CurrencyAdmin(admin.ModelAdmin): class CurrencyAdmin(ImportExportModelAdmin):
list_display = ('symbol', 'suffix', 'description', 'value', 'base') list_display = ('symbol', 'suffix', 'description', 'value', 'base')

View File

@ -1,20 +1,74 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from import_export.admin import ImportExportModelAdmin 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 Company
from .models import SupplierPart from .models import SupplierPart
from .models import SupplierPriceBreak 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): class CompanyAdmin(ImportExportModelAdmin):
resource_class = CompanyResource
list_display = ('name', 'website', 'contact') 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): class SupplierPartAdmin(ImportExportModelAdmin):
resource_class = SupplierPartResource
list_display = ('part', 'supplier', 'SKU') 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): class SupplierPriceBreakAdmin(ImportExportModelAdmin):
resource_class = SupplierPriceBreakResource
list_display = ('part', 'quantity', 'cost') list_display = ('part', 'quantity', 'cost')

View File

@ -15,6 +15,13 @@
supplier: 1 supplier: 1
SKU: 'ACME0002' SKU: 'ACME0002'
- model: company.supplierpart
pk: 3
fields:
part: 1
supplier: 1
SKU: 'ACME0003'
# Widget purchaseable from ACME # Widget purchaseable from ACME
- model: company.supplierpart - model: company.supplierpart
pk: 100 pk: 100
@ -33,7 +40,7 @@
# M2x4 LPHS from Zerg Corp # M2x4 LPHS from Zerg Corp
- model: company.supplierpart - model: company.supplierpart
pk: 3 pk: 7
fields: fields:
part: 1 part: 1
supplier: 3 supplier: 3

View File

@ -56,7 +56,7 @@ class CompanySimpleTest(TestCase):
zerg = Company.objects.get(pk=3) zerg = Company.objects.get(pk=3)
self.assertTrue(acme.has_parts) self.assertTrue(acme.has_parts)
self.assertEqual(acme.part_count, 3) self.assertEqual(acme.part_count, 4)
self.assertTrue(appel.has_parts) self.assertTrue(appel.has_parts)
self.assertEqual(appel.part_count, 2) self.assertEqual(appel.part_count, 2)

View File

@ -2,11 +2,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from .models import PurchaseOrder, PurchaseOrderLineItem from .models import PurchaseOrder, PurchaseOrderLineItem
class PurchaseOrderAdmin(admin.ModelAdmin): class PurchaseOrderAdmin(ImportExportModelAdmin):
list_display = ( list_display = (
'reference', 'reference',
@ -17,7 +18,7 @@ class PurchaseOrderAdmin(admin.ModelAdmin):
) )
class PurchaseOrderLineItemAdmin(admin.ModelAdmin): class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
list_display = ( list_display = (
'order', 'order',

View File

@ -35,8 +35,16 @@
quantity: 250 quantity: 250
received: 50 received: 50
# 1000 x ACME0003
- model: order.purchaseorderlineitem
fields:
order: 1
part: 3
quantity: 1000
# 100 x ZERGLPHS (M2x4 LPHS) # 100 x ZERGLPHS (M2x4 LPHS)
- model: order.purchaseorderlineitem - model: order.purchaseorderlineitem
pk: 22
fields: fields:
order: 2 order: 2
part: 3 part: 3

View File

@ -5,6 +5,7 @@ Order model definitions
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.db import models, transaction from django.db import models, transaction
from django.db.models import F
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -226,7 +227,7 @@ class PurchaseOrder(Order):
Any line item where 'received' < 'quantity' will be returned. 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 @transaction.atomic
def receive_line_item(self, line, location, quantity, user): def receive_line_item(self, line, location, quantity, user):

View File

@ -4,6 +4,12 @@ from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from InvenTree.status_codes import OrderStatus
from .models import PurchaseOrder, PurchaseOrderLineItem
import json
class OrderViewTestCase(TestCase): class OrderViewTestCase(TestCase):
@ -75,3 +81,106 @@ class POTests(OrderViewTestCase):
# Response should be streaming-content (file download) # Response should be streaming-content (file download)
self.assertIn('streaming_content', dir(response)) 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

View File

@ -47,10 +47,10 @@ class OrderTest(TestCase):
for supplier in part.supplier_parts.all(): for supplier in part.supplier_parts.all():
open_orders += supplier.open_orders() open_orders += supplier.open_orders()
self.assertEqual(len(open_orders), 3) self.assertEqual(len(open_orders), 4)
# Test the total on-order quantity # Test the total on-order quantity
self.assertEqual(part.on_order, 400) self.assertEqual(part.on_order, 1400)
def test_add_items(self): def test_add_items(self):
""" Test functions for adding line items to an order """ """ Test functions for adding line items to an order """
@ -58,7 +58,7 @@ class OrderTest(TestCase):
order = PurchaseOrder.objects.get(pk=1) order = PurchaseOrder.objects.get(pk=1)
self.assertEqual(order.status, OrderStatus.PENDING) 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') sku = SupplierPart.objects.get(SKU='ACME-WIDGET')
part = sku.part part = sku.part
@ -76,11 +76,11 @@ class OrderTest(TestCase):
order.add_line_item(sku, 100) order.add_line_item(sku, 100)
self.assertEqual(part.on_order, 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 the same part again (it should be merged)
order.add_line_item(sku, 50) order.add_line_item(sku, 50)
self.assertEqual(order.lines.count(), 3) self.assertEqual(order.lines.count(), 4)
self.assertEqual(part.on_order, 150) self.assertEqual(part.on_order, 150)
# Try to order a supplier part from the wrong supplier # Try to order a supplier part from the wrong supplier
@ -101,7 +101,7 @@ class OrderTest(TestCase):
loc = StockLocation.objects.get(id=1) loc = StockLocation.objects.get(id=1)
# There should be two lines against this order # 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" # Should fail, as order is 'PENDING' not 'PLACED"
self.assertEqual(order.status, OrderStatus.PENDING) self.assertEqual(order.status, OrderStatus.PENDING)
@ -117,7 +117,7 @@ class OrderTest(TestCase):
self.assertEqual(line.remaining(), 50) self.assertEqual(line.remaining(), 50)
self.assertEqual(part.on_order, 350) self.assertEqual(part.on_order, 1350)
# Try to order some invalid things # Try to order some invalid things
with self.assertRaises(django_exceptions.ValidationError): with self.assertRaises(django_exceptions.ValidationError):
@ -132,7 +132,12 @@ class OrderTest(TestCase):
line = PurchaseOrderLineItem.objects.get(id=2) line = PurchaseOrderLineItem.objects.get(id=2)
order.receive_line_item(line, loc, 2 * line.quantity, user=None) 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) self.assertEqual(order.status, OrderStatus.COMPLETE)
def test_export(self): def test_export(self):

View File

@ -243,6 +243,11 @@ class PurchaseOrderReceive(AjaxView):
except (PurchaseOrderLineItem.DoesNotExist, ValueError): except (PurchaseOrderLineItem.DoesNotExist, ValueError):
continue 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 # Ignore a part that doesn't map to a SupplierPart
try: try:
if line.part is None: if line.part is None:
@ -277,6 +282,7 @@ class PurchaseOrderReceive(AjaxView):
return self.renderJsonResponse(request, data=data) return self.renderJsonResponse(request, data=data)
@transaction.atomic
def receive_parts(self): def receive_parts(self):
""" Called once the form has been validated. """ Called once the form has been validated.
Create new stockitems against received parts. Create new stockitems against received parts.
@ -611,13 +617,30 @@ class POLineItemCreate(AjaxCreateView):
valid = form.is_valid() valid = form.is_valid()
# Extract the SupplierPart ID from the form
part_id = form['part'].value() part_id = form['part'].value()
# Extract the Order ID from the form
order_id = form['order'].value()
try: 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): except (SupplierPart.DoesNotExist, ValueError):
valid = False valid = False
form.errors['part'] = [_('This field is required')] form.errors['part'] = [_('Invalid SupplierPart selection')]
data = { data = {
'form_valid': valid, 'form_valid': valid,
@ -639,6 +662,11 @@ class POLineItemCreate(AjaxCreateView):
form = super().get_form() 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() order_id = form['order'].value()
try: try:
@ -660,7 +688,7 @@ class POLineItemCreate(AjaxCreateView):
form.fields['part'].queryset = query form.fields['part'].queryset = query
form.fields['order'].widget = HiddenInput() form.fields['order'].widget = HiddenInput()
except PurchaseOrder.DoesNotExist: except (ValueError, PurchaseOrder.DoesNotExist):
pass pass
return form return form

View File

@ -1,21 +1,116 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from import_export.admin import ImportExportModelAdmin 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 PartCategory, Part
from .models import PartAttachment, PartStar from .models import PartAttachment, PartStar
from .models import BomItem from .models import BomItem
from .models import PartParameterTemplate, PartParameter 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): class PartAdmin(ImportExportModelAdmin):
resource_class = PartResource
list_display = ('full_name', 'description', 'total_stock', 'category') 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): class PartCategoryAdmin(ImportExportModelAdmin):
resource_class = PartCategoryResource
list_display = ('name', 'pathstring', 'description') list_display = ('name', 'pathstring', 'description')
search_fields = ('name', 'description')
class PartAttachmentAdmin(admin.ModelAdmin): class PartAttachmentAdmin(admin.ModelAdmin):
@ -27,15 +122,53 @@ class PartStarAdmin(admin.ModelAdmin):
list_display = ('part', 'user') 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): class BomItemAdmin(ImportExportModelAdmin):
resource_class = BomItemResource
list_display = ('part', 'sub_part', 'quantity') list_display = ('part', 'sub_part', 'quantity')
search_fields = ('part__name', 'part__description', 'sub_part__name', 'sub_part__description')
class ParameterTemplateAdmin(ImportExportModelAdmin): class ParameterTemplateAdmin(ImportExportModelAdmin):
list_display = ('name', 'units') 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): class ParameterAdmin(ImportExportModelAdmin):
resource_class = ParameterResource
list_display = ('part', 'template', 'data') list_display = ('part', 'template', 'data')

View File

@ -1,15 +1,93 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from import_export.admin import ImportExportModelAdmin 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 StockLocation, StockItem
from .models import StockItemTracking 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): class LocationAdmin(ImportExportModelAdmin):
resource_class = LocationResource
list_display = ('name', 'pathstring', 'description') 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): class StockItemAdmin(ImportExportModelAdmin):
resource_class = StockItemResource
list_display = ('part', 'quantity', 'location', 'status', 'updated') list_display = ('part', 'quantity', 'location', 'status', 'updated')