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',
'xlsx',
'json',
'yaml',
]

View File

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

View File

@ -9,7 +9,7 @@
<div id='button-bar'>
<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>
@ -38,7 +38,7 @@
newOrder();
});
$("#company-order-2").click(function() {
$("#company-order2").click(function() {
newOrder();
});
@ -47,4 +47,4 @@
sortable: true,
});
{% endblock %}
{% endblock %}

View File

@ -2,8 +2,12 @@
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 .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):
resource_class = POLineItemResource
list_display = (
'order',
'part',

View File

@ -12,7 +12,6 @@ from django.contrib.auth.models import User
from django.urls import reverse
from django.utils.translation import ugettext as _
import tablib
from datetime import datetime
from stock.models import StockItem
@ -126,54 +125,6 @@ class PurchaseOrder(Order):
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):
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)
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
from .models import PurchaseOrder, PurchaseOrderLineItem
from .admin import POLineItemResource
from build.models import Build
from company.models import Company, SupplierPart
from stock.models import StockItem, StockLocation
@ -165,7 +166,9 @@ class PurchaseOrderExport(AjaxView):
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)

View File

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

View File

@ -10,13 +10,16 @@ import os
from django.utils.translation import gettext_lazy as _
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):
""" 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):
@ -27,21 +30,33 @@ def MakeBomTemplate(fmt):
if not IsValidBOMFormat(fmt):
fmt = 'csv'
fields = [
'Part',
'Quantity',
'Overage',
'Reference',
'Notes'
]
query = BomItem.objects.filter(pk=None)
dataset = BomItemResource().export(queryset=query)
data = tablib.Dataset(headers=fields).export(fmt)
data = dataset.export(fmt)
filename = 'InvenTree_BOM_Template.' + fmt
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 for managing an uploaded BOM file """

View File

@ -7,8 +7,6 @@ from __future__ import unicode_literals
import os
import tablib
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from django.urls import reverse
@ -857,76 +855,6 @@ class Part(models.Model):
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
def attachment_count(self):
""" Count the number of attachments for this part.

View File

@ -32,15 +32,6 @@ class BomItemTest(TestCase):
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):
self.assertEqual(self.bob.used_in_count, 0)
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 import HiddenInput, CheckboxInput
import tablib
from fuzzywuzzy import fuzz
from decimal import Decimal
@ -28,7 +26,9 @@ from common.models import Currency
from company.models import SupplierPart
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 QRCodeView
@ -1216,108 +1216,9 @@ class PartExport(AjaxView):
parts = self.get_parts(request)
headers = [
'ID',
'Name',
'Description',
'Category',
'Category ID',
'IPN',
'Revision',
'URL',
'Keywords',
'Notes',
'Assembly',
'Component',
'Template',
'Trackable',
'Purchaseable',
'Salable',
'Active',
'Virtual',
dataset = PartResource().export(queryset=parts)
# Part meta-data
'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')
csv = dataset.export('csv')
return DownloadFile(csv, 'InvenTree_Parts.csv')
@ -1348,12 +1249,10 @@ class BomDownload(AjaxView):
export_format = request.GET.get('format', 'csv')
# Placeholder to test file export
filename = '"' + part.name + '_BOM.' + export_format + '"'
if not IsValidBOMFormat(export_format):
export_format = 'csv'
filedata = part.export_bom(format=export_format)
return DownloadFile(filedata, filename)
return ExportBom(part, fmt=export_format)
def get_data(self):
return {

View File

@ -28,6 +28,7 @@ class LocationResource(ModelResource):
model = StockLocation
skip_unchanged = True
report_skipped = False
clean_model_instances = True
exclude = [
# Exclude MPTT internal model fields
@ -57,10 +58,16 @@ class StockItemResource(ModelResource):
# Custom manaegrs for ForeignKey fields
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 = 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_name = Field(attribute='location__name', readonly=True)
@ -82,6 +89,7 @@ class StockItemResource(ModelResource):
model = StockItem
skip_unchanged = True
report_skipped = False
clean_model_instance = True
class StockItemAdmin(ImportExportModelAdmin):

View File

@ -140,6 +140,11 @@ class StockItem(models.Model):
system=True
)
@property
def status_label(self):
return StockStatus.label(self.status)
@property
def serialized(self):
""" 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 QRCodeView
from InvenTree.status_codes import StockStatus
from InvenTree.helpers import str2bool, DownloadFile, GetExportFormats
from InvenTree.helpers import ExtractSerialNumbers
from datetime import datetime
import tablib
from company.models import Company
from part.models import Part
from .models import StockItem, StockLocation, StockItemTracking
from .admin import StockItemResource
from .forms import EditStockLocationForm
from .forms import CreateStockItemForm
from .forms import EditStockItemForm
@ -226,75 +225,9 @@ class StockExport(AjaxView):
# Pre-fetch related fields to reduce DB queries
stock_items = stock_items.prefetch_related('part', 'supplier_part__supplier', 'location', 'purchase_order', 'build')
# Column headers
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'),
]
dataset = StockItemResource().export(queryset=stock_items)
data = tablib.Dataset(headers=headers)
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)
filedata = dataset.export(export_format)
return DownloadFile(filedata, filename)