Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2019-09-15 22:28:55 +10:00
commit d0ad3f0e37
15 changed files with 101 additions and 336 deletions

View File

@ -103,6 +103,7 @@ def GetExportFormats():
'xls', 'xls',
'xlsx', 'xlsx',
'json', 'json',
'yaml',
] ]

View File

@ -23,6 +23,7 @@ class CompanyResource(ModelResource):
model = Company model = Company
skip_unchanged = True skip_unchanged = True
report_skipped = False report_skipped = False
clean_model_instances = True
class CompanyAdmin(ImportExportModelAdmin): class CompanyAdmin(ImportExportModelAdmin):
@ -43,6 +44,7 @@ class SupplierPartResource(ModelResource):
model = SupplierPart model = SupplierPart
skip_unchanged = True skip_unchanged = True
report_skipped = False report_skipped = False
clean_model_instances = True
class SupplierPartAdmin(ImportExportModelAdmin): class SupplierPartAdmin(ImportExportModelAdmin):
@ -63,6 +65,7 @@ class SupplierPriceBreakResource(ModelResource):
model = SupplierPriceBreak model = SupplierPriceBreak
skip_unchanged = True skip_unchanged = True
report_skipped = False report_skipped = False
clean_model_instances = True
class SupplierPriceBreakAdmin(ImportExportModelAdmin): class SupplierPriceBreakAdmin(ImportExportModelAdmin):

View File

@ -9,7 +9,7 @@
<div id='button-bar'> <div id='button-bar'>
<div class='btn-group'> <div class='btn-group'>
<button class='btn btn-primary' type='button' id='company-order-2' title='Create new purchase order'>New Purchase Order</button> <button class='btn btn-primary' type='button' id='company-order2' title='Create new purchase order'>New Purchase Order</button>
</div> </div>
</div> </div>
@ -38,7 +38,7 @@
newOrder(); newOrder();
}); });
$("#company-order-2").click(function() { $("#company-order2").click(function() {
newOrder(); newOrder();
}); });
@ -47,4 +47,4 @@
sortable: true, sortable: true,
}); });
{% endblock %} {% endblock %}

View File

@ -2,8 +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 import_export.admin import ImportExportModelAdmin
from import_export.resources import ModelResource
from import_export.fields import Field
from .models import PurchaseOrder, PurchaseOrderLineItem from .models import PurchaseOrder, PurchaseOrderLineItem
@ -18,8 +22,28 @@ class PurchaseOrderAdmin(ImportExportModelAdmin):
) )
class POLineItemResource(ModelResource):
""" Class for managing import / export of POLineItem data """
part_name = Field(attribute='part__part__name', readonly=True)
manufacturer = Field(attribute='part__manufacturer', readonly=True)
MPN = Field(attribute='part__MPN', readonly=True)
SKU = Field(attribute='part__SKU', readonly=True)
class Meta:
model = PurchaseOrderLineItem
skip_unchanged = True
report_skipped = False
clean_model_instances = True
class PurchaseOrderLineItemAdmin(ImportExportModelAdmin): class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
resource_class = POLineItemResource
list_display = ( list_display = (
'order', 'order',
'part', 'part',

View File

@ -12,7 +12,6 @@ from django.contrib.auth.models import User
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
import tablib
from datetime import datetime from datetime import datetime
from stock.models import StockItem from stock.models import StockItem
@ -126,54 +125,6 @@ class PurchaseOrder(Order):
related_name='+' related_name='+'
) )
def export_to_file(self, **kwargs):
""" Export order information to external file """
file_format = kwargs.get('format', 'csv').lower()
data = tablib.Dataset(headers=[
'Line',
'Part',
'Description',
'Manufacturer',
'MPN',
'Order Code',
'Quantity',
'Received',
'Reference',
'Notes',
])
idx = 0
for item in self.lines.all():
line = []
line.append(idx)
if item.part:
line.append(item.part.part.name)
line.append(item.part.part.description)
line.append(item.part.manufacturer)
line.append(item.part.MPN)
line.append(item.part.SKU)
else:
line += [[] * 5]
line.append(item.quantity)
line.append(item.received)
line.append(item.reference)
line.append(item.notes)
idx += 1
data.append(line)
return data.export(file_format)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('purchase-order-detail', kwargs={'pk': self.id}) return reverse('purchase-order-detail', kwargs={'pk': self.id})

View File

@ -139,13 +139,3 @@ class OrderTest(TestCase):
order.receive_line_item(line, loc, line.quantity, user=None) 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):
""" Test order exporting """
order = PurchaseOrder.objects.get(pk=1)
output = order.export_to_file(format='csv')
self.assertIn('M2x4 LPHS', output)
self.assertIn('Line,Part,Description', output)

View File

@ -14,6 +14,7 @@ from django.forms import HiddenInput
import logging import logging
from .models import PurchaseOrder, PurchaseOrderLineItem from .models import PurchaseOrder, PurchaseOrderLineItem
from .admin import POLineItemResource
from build.models import Build from build.models import Build
from company.models import Company, SupplierPart from company.models import Company, SupplierPart
from stock.models import StockItem, StockLocation from stock.models import StockItem, StockLocation
@ -165,7 +166,9 @@ class PurchaseOrderExport(AjaxView):
fmt=export_format fmt=export_format
) )
filedata = order.export_to_file(format=export_format) dataset = POLineItemResource().export(queryset=order.lines.all())
filedata = dataset.export(format=export_format)
return DownloadFile(filedata, filename) return DownloadFile(filedata, filename)

View File

@ -31,6 +31,8 @@ class PartResource(ModelResource):
variant_of = Field(attribute='variant_of', widget=widgets.ForeignKeyWidget(Part)) variant_of = Field(attribute='variant_of', widget=widgets.ForeignKeyWidget(Part))
suppliers = Field(attribute='supplier_count', readonly=True)
# Extra calculated meta-data (readonly) # Extra calculated meta-data (readonly)
in_stock = Field(attribute='total_stock', readonly=True, widget=widgets.IntegerWidget()) in_stock = Field(attribute='total_stock', readonly=True, widget=widgets.IntegerWidget())
@ -46,6 +48,7 @@ class PartResource(ModelResource):
model = Part model = Part
skip_unchanged = True skip_unchanged = True
report_skipped = False report_skipped = False
clean_model_instances = True
exclude = [ exclude = [
'bom_checksum', 'bom_checked_by', 'bom_checked_date' 'bom_checksum', 'bom_checked_by', 'bom_checked_date'
] ]
@ -89,6 +92,7 @@ class PartCategoryResource(ModelResource):
model = PartCategory model = PartCategory
skip_unchanged = True skip_unchanged = True
report_skipped = False report_skipped = False
clean_model_instances = True
exclude = [ exclude = [
# Exclude MPTT internal model fields # Exclude MPTT internal model fields
@ -127,12 +131,21 @@ class BomItemResource(ModelResource):
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part)) part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
sub_part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part)) part_name = Field(attribute='part__full_name', readonly=True)
sub_part = Field(attribute='sub_part', widget=widgets.ForeignKeyWidget(Part))
sub_part_name = Field(attribute='sub_part__full_name', readonly=True)
stock = Field(attribute='sub_part__total_stock', readonly=True)
class Meta: class Meta:
model = BomItem model = BomItem
skip_unchanged = True skip_unchanged = True
report_skipped = False report_skipped = False
clean_model_instances = True
exclude = ('checksum')
class BomItemAdmin(ImportExportModelAdmin): class BomItemAdmin(ImportExportModelAdmin):
@ -163,6 +176,7 @@ class ParameterResource(ModelResource):
model = PartParameter model = PartParameter
skip_unchanged = True skip_unchanged = True
report_skipped = False report_skipped = False
clean_model_instance = True
class ParameterAdmin(ImportExportModelAdmin): class ParameterAdmin(ImportExportModelAdmin):

View File

@ -10,13 +10,16 @@ import os
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from InvenTree.helpers import DownloadFile from InvenTree.helpers import DownloadFile, GetExportFormats
from .admin import BomItemResource
from .models import BomItem
def IsValidBOMFormat(fmt): def IsValidBOMFormat(fmt):
""" Test if a file format specifier is in the valid list of BOM file formats """ """ Test if a file format specifier is in the valid list of BOM file formats """
return fmt.strip().lower() in ['csv', 'xls', 'xlsx', 'tsv'] return fmt.strip().lower() in GetExportFormats()
def MakeBomTemplate(fmt): def MakeBomTemplate(fmt):
@ -27,21 +30,33 @@ def MakeBomTemplate(fmt):
if not IsValidBOMFormat(fmt): if not IsValidBOMFormat(fmt):
fmt = 'csv' fmt = 'csv'
fields = [ query = BomItem.objects.filter(pk=None)
'Part', dataset = BomItemResource().export(queryset=query)
'Quantity',
'Overage',
'Reference',
'Notes'
]
data = tablib.Dataset(headers=fields).export(fmt) data = dataset.export(fmt)
filename = 'InvenTree_BOM_Template.' + fmt filename = 'InvenTree_BOM_Template.' + fmt
return DownloadFile(data, filename) return DownloadFile(data, filename)
def ExportBom(part, fmt='csv'):
""" Export a BOM (Bill of Materials) for a given part.
"""
if not IsValidBOMFormat(fmt):
fmt = 'csv'
bom_items = part.bom_items.all().order_by('id')
dataset = BomItemResource().export(queryset=bom_items)
data = dataset.export(fmt)
filename = '{n}_BOM.{fmt}'.format(n=part.full_name, fmt=fmt)
return DownloadFile(data, filename)
class BomUploadManager: class BomUploadManager:
""" Class for managing an uploaded BOM file """ """ Class for managing an uploaded BOM file """

View File

@ -7,8 +7,6 @@ from __future__ import unicode_literals
import os import os
import tablib
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.urls import reverse from django.urls import reverse
@ -857,76 +855,6 @@ class Part(models.Model):
self.save() self.save()
def export_bom(self, **kwargs):
""" Export Bill of Materials to a spreadsheet file.
Includes a row for each item in the BOM.
Also includes extra information such as supplier data.
"""
items = self.bom_items.all().order_by('id')
supplier_names = set()
headers = [
'Part',
'Description',
'Quantity',
'Overage',
'Reference',
'Note',
'',
'In Stock',
]
# Contstruct list of suppliers for each part
for item in items:
part = item.sub_part
supplier_parts = part.supplier_parts.all()
item.suppliers = {}
for sp in supplier_parts:
name = sp.supplier.name
supplier_names.add(name)
item.suppliers[name] = sp
if len(supplier_names) > 0:
headers.append('')
for name in supplier_names:
headers.append(name)
data = tablib.Dataset(headers=headers)
for it in items:
line = []
# Information about each BOM item
line.append(it.sub_part.full_name)
line.append(it.sub_part.description)
line.append(it.quantity)
line.append(it.overage)
line.append(it.reference)
line.append(it.note)
# Extra information about the part
line.append('')
line.append(it.sub_part.available_stock)
if len(supplier_names) > 0:
line.append('') # Blank column separates supplier info
for name in supplier_names:
sp = it.suppliers.get(name, None)
if sp:
line.append(sp.SKU)
else:
line.append('')
data.append(line)
file_format = kwargs.get('format', 'csv').lower()
return data.export(file_format)
@property @property
def attachment_count(self): def attachment_count(self):
""" Count the number of attachments for this part. """ Count the number of attachments for this part.

View File

@ -32,15 +32,6 @@ class BomItemTest(TestCase):
self.assertIn(self.orphan, parts) self.assertIn(self.orphan, parts)
def test_bom_export(self):
parts = self.bob.required_parts()
data = self.bob.export_bom(format='csv')
for p in parts:
self.assertIn(p.name, data)
self.assertIn(p.description, data)
def test_used_in(self): def test_used_in(self):
self.assertEqual(self.bob.used_in_count, 0) self.assertEqual(self.bob.used_in_count, 0)
self.assertEqual(self.orphan.used_in_count, 1) self.assertEqual(self.orphan.used_in_count, 1)

View File

@ -14,8 +14,6 @@ from django.views.generic import DetailView, ListView, FormView
from django.forms.models import model_to_dict from django.forms.models import model_to_dict
from django.forms import HiddenInput, CheckboxInput from django.forms import HiddenInput, CheckboxInput
import tablib
from fuzzywuzzy import fuzz from fuzzywuzzy import fuzz
from decimal import Decimal from decimal import Decimal
@ -28,7 +26,9 @@ from common.models import Currency
from company.models import SupplierPart from company.models import SupplierPart
from . import forms as part_forms from . import forms as part_forms
from .bom import MakeBomTemplate, BomUploadManager from .bom import MakeBomTemplate, BomUploadManager, ExportBom, IsValidBOMFormat
from .admin import PartResource
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from InvenTree.views import QRCodeView from InvenTree.views import QRCodeView
@ -1216,108 +1216,9 @@ class PartExport(AjaxView):
parts = self.get_parts(request) parts = self.get_parts(request)
headers = [ dataset = PartResource().export(queryset=parts)
'ID',
'Name',
'Description',
'Category',
'Category ID',
'IPN',
'Revision',
'URL',
'Keywords',
'Notes',
'Assembly',
'Component',
'Template',
'Trackable',
'Purchaseable',
'Salable',
'Active',
'Virtual',
# Part meta-data csv = dataset.export('csv')
'Used In',
# Stock Information
'Stock Info',
'In Stock',
'Allocated',
'Building',
'On Order',
]
# Construct list of suppliers for each part
supplier_names = set()
for part in parts:
supplier_parts = part.supplier_parts.all()
part.suppliers = {}
for sp in supplier_parts:
name = sp.supplier.name
supplier_names.add(name)
part.suppliers[name] = sp
if len(supplier_names) > 0:
headers.append('Suppliers')
for name in supplier_names:
headers.append(name)
data = tablib.Dataset(headers=headers)
for part in parts:
line = []
line.append(part.pk)
line.append(part.name)
line.append(part.description)
if part.category:
line.append(str(part.category))
line.append(part.category.pk)
else:
line.append('')
line.append('')
line.append(part.IPN)
line.append(part.revision)
line.append(part.URL)
line.append(part.keywords)
line.append(part.notes)
line.append(part.assembly)
line.append(part.component)
line.append(part.is_template)
line.append(part.trackable)
line.append(part.purchaseable)
line.append(part.salable)
line.append(part.active)
line.append(part.virtual)
# Part meta-data
line.append(part.used_in_count)
# Stock information
line.append('')
line.append(part.total_stock)
line.append(part.allocation_count)
line.append(part.quantity_being_built)
line.append(part.on_order)
# Supplier Information
if len(supplier_names) > 0:
line.append('')
for name in supplier_names:
sp = part.suppliers.get(name, None)
if sp:
line.append(sp.SKU)
else:
line.append('')
data.append(line)
csv = data.export('csv')
return DownloadFile(csv, 'InvenTree_Parts.csv') return DownloadFile(csv, 'InvenTree_Parts.csv')
@ -1348,12 +1249,10 @@ class BomDownload(AjaxView):
export_format = request.GET.get('format', 'csv') export_format = request.GET.get('format', 'csv')
# Placeholder to test file export if not IsValidBOMFormat(export_format):
filename = '"' + part.name + '_BOM.' + export_format + '"' export_format = 'csv'
filedata = part.export_bom(format=export_format) return ExportBom(part, fmt=export_format)
return DownloadFile(filedata, filename)
def get_data(self): def get_data(self):
return { return {

View File

@ -28,6 +28,7 @@ class LocationResource(ModelResource):
model = StockLocation model = StockLocation
skip_unchanged = True skip_unchanged = True
report_skipped = False report_skipped = False
clean_model_instances = True
exclude = [ exclude = [
# Exclude MPTT internal model fields # Exclude MPTT internal model fields
@ -57,10 +58,16 @@ class StockItemResource(ModelResource):
# Custom manaegrs for ForeignKey fields # Custom manaegrs for ForeignKey fields
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part)) part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
part_name = Field(attribute='part__name', readonly=True) part_name = Field(attribute='part__full_ame', readonly=True)
supplier_part = Field(attribute='supplier_part', widget=widgets.ForeignKeyWidget(SupplierPart)) supplier_part = Field(attribute='supplier_part', widget=widgets.ForeignKeyWidget(SupplierPart))
supplier = Field(attribute='supplier_part__supplier__id', readonly=True)
supplier_name = Field(attribute='supplier_part__supplier__name', readonly=True)
status_label = Field(attribute='status_label', readonly=True)
location = Field(attribute='location', widget=widgets.ForeignKeyWidget(StockLocation)) location = Field(attribute='location', widget=widgets.ForeignKeyWidget(StockLocation))
location_name = Field(attribute='location__name', readonly=True) location_name = Field(attribute='location__name', readonly=True)
@ -82,6 +89,7 @@ class StockItemResource(ModelResource):
model = StockItem model = StockItem
skip_unchanged = True skip_unchanged = True
report_skipped = False report_skipped = False
clean_model_instance = True
class StockItemAdmin(ImportExportModelAdmin): class StockItemAdmin(ImportExportModelAdmin):

View File

@ -140,6 +140,11 @@ class StockItem(models.Model):
system=True system=True
) )
@property
def status_label(self):
return StockStatus.label(self.status)
@property @property
def serialized(self): def serialized(self):
""" Return True if this StockItem is serialized """ """ Return True if this StockItem is serialized """

View File

@ -18,17 +18,16 @@ from InvenTree.views import AjaxView
from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView
from InvenTree.views import QRCodeView from InvenTree.views import QRCodeView
from InvenTree.status_codes import StockStatus
from InvenTree.helpers import str2bool, DownloadFile, GetExportFormats from InvenTree.helpers import str2bool, DownloadFile, GetExportFormats
from InvenTree.helpers import ExtractSerialNumbers from InvenTree.helpers import ExtractSerialNumbers
from datetime import datetime from datetime import datetime
import tablib
from company.models import Company from company.models import Company
from part.models import Part from part.models import Part
from .models import StockItem, StockLocation, StockItemTracking from .models import StockItem, StockLocation, StockItemTracking
from .admin import StockItemResource
from .forms import EditStockLocationForm from .forms import EditStockLocationForm
from .forms import CreateStockItemForm from .forms import CreateStockItemForm
from .forms import EditStockItemForm from .forms import EditStockItemForm
@ -226,75 +225,9 @@ class StockExport(AjaxView):
# Pre-fetch related fields to reduce DB queries # Pre-fetch related fields to reduce DB queries
stock_items = stock_items.prefetch_related('part', 'supplier_part__supplier', 'location', 'purchase_order', 'build') stock_items = stock_items.prefetch_related('part', 'supplier_part__supplier', 'location', 'purchase_order', 'build')
# Column headers dataset = StockItemResource().export(queryset=stock_items)
headers = [
_('Stock ID'),
_('Part ID'),
_('Part'),
_('Supplier Part ID'),
_('Supplier ID'),
_('Supplier'),
_('Location ID'),
_('Location'),
_('Quantity'),
_('Batch'),
_('Serial'),
_('Status'),
_('Notes'),
_('Review Needed'),
_('Last Updated'),
_('Last Stocktake'),
_('Purchase Order ID'),
_('Build ID'),
]
data = tablib.Dataset(headers=headers) filedata = dataset.export(export_format)
for item in stock_items:
line = []
line.append(item.pk)
line.append(item.part.pk)
line.append(item.part.full_name)
if item.supplier_part:
line.append(item.supplier_part.pk)
line.append(item.supplier_part.supplier.pk)
line.append(item.supplier_part.supplier.name)
else:
line.append('')
line.append('')
line.append('')
if item.location:
line.append(item.location.pk)
line.append(item.location.name)
else:
line.append('')
line.append('')
line.append(item.quantity)
line.append(item.batch)
line.append(item.serial)
line.append(StockStatus.label(item.status))
line.append(item.notes)
line.append(item.review_needed)
line.append(item.updated)
line.append(item.stocktake_date)
if item.purchase_order:
line.append(item.purchase_order.pk)
else:
line.append('')
if item.build:
line.append(item.build.pk)
else:
line.append('')
data.append(line)
filedata = data.export(export_format)
return DownloadFile(filedata, filename) return DownloadFile(filedata, filename)