diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 84409226c2..231423be8f 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -103,6 +103,7 @@ def GetExportFormats(): 'xls', 'xlsx', 'json', + 'yaml', ] diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index ada60c7975..ce023aedc0 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -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): diff --git a/InvenTree/company/templates/company/detail_purchase_orders.html b/InvenTree/company/templates/company/detail_purchase_orders.html index aad4f7bf99..077a107848 100644 --- a/InvenTree/company/templates/company/detail_purchase_orders.html +++ b/InvenTree/company/templates/company/detail_purchase_orders.html @@ -9,7 +9,7 @@
@@ -38,7 +38,7 @@ newOrder(); }); - $("#company-order-2").click(function() { + $("#company-order2").click(function() { newOrder(); }); @@ -47,4 +47,4 @@ sortable: true, }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py index 44d26ea3f2..05598928a7 100644 --- a/InvenTree/order/admin.py +++ b/InvenTree/order/admin.py @@ -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', diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index ef79a4d39b..9eff4d11c3 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -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}) diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py index c271e0be43..351c152472 100644 --- a/InvenTree/order/tests.py +++ b/InvenTree/order/tests.py @@ -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) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index fafd313022..86a231075e 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -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) diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 7a9def473f..fcef36a772 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -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): diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py index baf46e8618..394314d461 100644 --- a/InvenTree/part/bom.py +++ b/InvenTree/part/bom.py @@ -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 """ diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 76d9f606d0..5040bdc2f0 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -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. diff --git a/InvenTree/part/test_bom_item.py b/InvenTree/part/test_bom_item.py index ec29dbaab3..e5d94744c0 100644 --- a/InvenTree/part/test_bom_item.py +++ b/InvenTree/part/test_bom_item.py @@ -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) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 7f66cd7718..7d1a3ab633 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -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 { diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index ef84b0890e..c33010bf3b 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -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): diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 272d212cef..7b5d01c58b 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -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 """ diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 26a957a644..1bb9a27a83 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -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)