From 6fc275ca300613116bea1631bef3872fddd6397b Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 20 Aug 2020 13:53:27 -0500 Subject: [PATCH 01/18] BoM export: added option to export part paremeters (#126) and stocks (#793) --- InvenTree/part/bom.py | 78 +++++++++++++++++++++++++++++++++++++---- InvenTree/part/forms.py | 6 +++- InvenTree/part/views.py | 16 ++++++++- 3 files changed, 91 insertions(+), 9 deletions(-) diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py index 86b33bc02f..9b3e6997d6 100644 --- a/InvenTree/part/bom.py +++ b/InvenTree/part/bom.py @@ -7,6 +7,8 @@ from rapidfuzz import fuzz import tablib import os +from collections import OrderedDict + from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError @@ -47,7 +49,7 @@ def MakeBomTemplate(fmt): return DownloadFile(data, filename) -def ExportBom(part, fmt='csv', cascade=False, max_levels=None, supplier_data=False): +def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=False, stock_data=False, supplier_data=False): """ Export a BOM (Bill of Materials) for a given part. Args: @@ -92,9 +94,75 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, supplier_data=Fal dataset = BomItemResource().export(queryset=bom_items, cascade=cascade) + def add_columns_to_dataset(columns, column_size): + try: + for header, column_dict in columns.items(): + # Construct column tuple + col = tuple(column_dict.get(c_idx, '') for c_idx in range(column_size)) + # Add column to dataset + dataset.append_col(col, header=header) + except AttributeError: + pass + + if parameter_data: + """ + If requested, add extra columns for each PartParameter associated with each line item + """ + + parameter_cols = {} + + for b_idx, bom_item in enumerate(bom_items): + # Get part parameters + parameters = bom_item.sub_part.get_parameters() + # Add parameters to columns + if parameters: + for parameter in parameters: + name = parameter.template.name + value = parameter.data + + try: + parameter_cols[name].update({b_idx: value}) + except KeyError: + parameter_cols[name] = {b_idx: value} + + # Add parameter columns to dataset + parameter_cols_ordered = OrderedDict(sorted(parameter_cols.items(), key=lambda x: x[0])) + add_columns_to_dataset(parameter_cols_ordered, len(bom_items)) + + if stock_data: + """ + If requested, add extra columns for stock data associated with each line item + """ + + stock_headers = [ + _('Location'), + _('Available Stock'), + ] + + stock_cols = {} + + for b_idx, bom_item in enumerate(bom_items): + stock_data = [] + # Get part default location + try: + stock_data.append(bom_item.sub_part.get_default_location().name) + except AttributeError: + stock_data.append('') + # Get part current stock + stock_data.append(bom_item.sub_part.available_stock) + + for s_idx, header in enumerate(stock_headers): + try: + stock_cols[header].update({b_idx: stock_data[s_idx]}) + except KeyError: + stock_cols[header] = {b_idx: stock_data[s_idx]} + + # Add stock columns to dataset + add_columns_to_dataset(stock_cols, len(bom_items)) + if supplier_data: """ - If requested, add extra columns for each SupplierPart associated with the each line item + If requested, add extra columns for each SupplierPart associated with each line item """ # Expand dataset with manufacturer parts @@ -150,11 +218,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, supplier_data=Fal manufacturer_cols[k_mpn] = {b_idx: manufacturer_mpn} # Add manufacturer columns to dataset - for header, col_dict in manufacturer_cols.items(): - # Construct column tuple - col = tuple(col_dict.get(c_idx, '') for c_idx in range(len(bom_items))) - # Add column to dataset - dataset.append_col(col, header=header) + add_columns_to_dataset(manufacturer_cols, len(bom_items)) data = dataset.export(fmt) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 7b252067e8..8dccf7f28b 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -58,7 +58,11 @@ class BomExportForm(forms.Form): levels = forms.IntegerField(label=_("Levels"), required=True, initial=0, help_text=_("Select maximum number of BOM levels to export (0 = all levels)")) - supplier_data = forms.BooleanField(label=_("Include Supplier Data"), required=False, initial=True, help_text=_("Include supplier part data in exported BOM")) + parameter_data = forms.BooleanField(label=_("Include Parameter Data"), required=False, initial=False, help_text=_("Include part parameters data in exported BOM")) + + stock_data = forms.BooleanField(label=_("Include Stock Data"), required=False, initial=False, help_text=_("Include part stock data in exported BOM")) + + supplier_data = forms.BooleanField(label=_("Include Supplier Data"), required=False, initial=True, help_text=_("Include part supplier data in exported BOM")) def get_choices(self): """ BOM export format choices """ diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 3afb863791..b8e157bfe3 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1499,6 +1499,10 @@ class BomDownload(AjaxView): cascade = str2bool(request.GET.get('cascade', False)) + parameter_data = str2bool(request.GET.get('parameter_data', False)) + + stock_data = str2bool(request.GET.get('stock_data', False)) + supplier_data = str2bool(request.GET.get('supplier_data', False)) levels = request.GET.get('levels', None) @@ -1516,7 +1520,13 @@ class BomDownload(AjaxView): if not IsValidBOMFormat(export_format): export_format = 'csv' - return ExportBom(part, fmt=export_format, cascade=cascade, max_levels=levels, supplier_data=supplier_data) + return ExportBom( part, + fmt=export_format, + cascade=cascade, + max_levels=levels, + parameter_data=parameter_data, + stock_data=stock_data, + supplier_data=supplier_data) def get_data(self): return { @@ -1541,6 +1551,8 @@ class BomExport(AjaxView): fmt = request.POST.get('file_format', 'csv').lower() cascade = str2bool(request.POST.get('cascading', False)) levels = request.POST.get('levels', None) + parameter_data = str2bool(request.POST.get('parameter_data', False)) + stock_data = str2bool(request.POST.get('stock_data', False)) supplier_data = str2bool(request.POST.get('supplier_data', False)) try: @@ -1556,6 +1568,8 @@ class BomExport(AjaxView): url += '?file_format=' + fmt url += '&cascade=' + str(cascade) + url += '¶meter_data=' + str(parameter_data) + url += '&stock_data=' + str(stock_data) url += '&supplier_data=' + str(supplier_data) if levels: From 89e63df1fb2a3c1306b66d50925333aefafd1593 Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 20 Aug 2020 14:53:03 -0500 Subject: [PATCH 02/18] Corrected style --- InvenTree/part/views.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index b8e157bfe3..35d6d5cf51 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1520,13 +1520,13 @@ class BomDownload(AjaxView): if not IsValidBOMFormat(export_format): export_format = 'csv' - return ExportBom( part, - fmt=export_format, - cascade=cascade, - max_levels=levels, - parameter_data=parameter_data, - stock_data=stock_data, - supplier_data=supplier_data) + return ExportBom(part, + fmt=export_format, + cascade=cascade, + max_levels=levels, + parameter_data=parameter_data, + stock_data=stock_data, + supplier_data=supplier_data) def get_data(self): return { From 9fa13aeae36e960b3add6dbde674c53bbe28e1f1 Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 20 Aug 2020 15:38:41 -0500 Subject: [PATCH 03/18] Show 'available_stock' in Part string representation --- InvenTree/part/models.py | 2 +- InvenTree/part/templates/part/bom_upload/select_parts.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index dd126d5730..4001ad4558 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -269,7 +269,7 @@ class Part(MPTTModel): super().save(*args, **kwargs) def __str__(self): - return "{n} - {d}".format(n=self.full_name, d=self.description) + return f"{self.full_name} - {self.description} - {self.available_stock}" def checkAddToBOM(self, parent): """ diff --git a/InvenTree/part/templates/part/bom_upload/select_parts.html b/InvenTree/part/templates/part/bom_upload/select_parts.html index d84cb0262f..1ee5e8821b 100644 --- a/InvenTree/part/templates/part/bom_upload/select_parts.html +++ b/InvenTree/part/templates/part/bom_upload/select_parts.html @@ -63,7 +63,7 @@ {% for part in row.part_options %} {% endfor %} From 946d8249957aaa28a0d2088332282a5ae90ad864 Mon Sep 17 00:00:00 2001 From: eeintech Date: Mon, 24 Aug 2020 11:41:14 -0500 Subject: [PATCH 04/18] Switched to ModelChoiceField --- InvenTree/part/forms.py | 8 ++++++++ InvenTree/part/models.py | 2 +- .../part/templates/part/bom_upload/select_parts.html | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 7b252067e8..a02a8da82c 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -196,11 +196,19 @@ class EditCategoryForm(HelperForm): ] +class PartModelChoiceField(forms.ModelChoiceField): + """ Extending string representation of Part instance with available stock """ + def label_from_instance(self, part): + return f'{part} - {part.available_stock}' + + class EditBomItemForm(HelperForm): """ Form for editing a BomItem object """ quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5) + sub_part = PartModelChoiceField(queryset=Part.objects.all()) + class Meta: model = BomItem fields = [ diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index cbc9f21572..e5b035f856 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -268,7 +268,7 @@ class Part(MPTTModel): super().save(*args, **kwargs) def __str__(self): - return f"{self.full_name} - {self.description} - {self.available_stock}" + return f"{self.full_name} - {self.description}" def checkAddToBOM(self, parent): """ diff --git a/InvenTree/part/templates/part/bom_upload/select_parts.html b/InvenTree/part/templates/part/bom_upload/select_parts.html index 1ee5e8821b..ede92c6c30 100644 --- a/InvenTree/part/templates/part/bom_upload/select_parts.html +++ b/InvenTree/part/templates/part/bom_upload/select_parts.html @@ -63,7 +63,7 @@ {% for part in row.part_options %} {% endfor %} From 5d6def75cccadc1543c2633005753869250e627b Mon Sep 17 00:00:00 2001 From: eeintech Date: Tue, 25 Aug 2020 16:02:46 -0500 Subject: [PATCH 05/18] BoM export, Part stock: changed 'Location' header to 'Default Location' --- InvenTree/part/bom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py index 9b3e6997d6..092b3e3183 100644 --- a/InvenTree/part/bom.py +++ b/InvenTree/part/bom.py @@ -135,7 +135,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa """ stock_headers = [ - _('Location'), + _('Default Location'), _('Available Stock'), ] From 54d0c4e8a8c465ad748ba729618db8b344425928 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 26 Aug 2020 14:29:49 +1000 Subject: [PATCH 06/18] Bugfix: Select test report template - Actually, two bugs! --- InvenTree/report/models.py | 2 +- InvenTree/stock/forms.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 05507099a7..bba8e50d3d 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -180,7 +180,7 @@ class TestReport(ReportTemplateBase): Test if this report template matches a given StockItem objects """ - filters = validateFilterString(self.part_filters) + filters = validateFilterString(self.filters) items = StockItem.objects.filter(**filters) diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 97d028ec2b..e0b1623a54 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -237,7 +237,7 @@ class TestReportFormatForm(HelperForm): for template in templates: if template.matches_stock_item(self.stock_item): - choices.append(template) + choices.append((template.pk, template)) return choices From d44ad541ebf26a3d728bdeafb3d2425a727e4cd5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 26 Aug 2020 22:35:36 +1000 Subject: [PATCH 07/18] Add "callback" functionality for modal forms when a given field is changed - Attach callback function - Add a function to retrieve a field by name --- .../static/script/inventree/modals.js | 45 +++++++++++++++++++ InvenTree/stock/templates/stock/location.html | 8 ++++ 2 files changed, 53 insertions(+) diff --git a/InvenTree/InvenTree/static/script/inventree/modals.js b/InvenTree/InvenTree/static/script/inventree/modals.js index 5b76203927..83fdbd1bb1 100644 --- a/InvenTree/InvenTree/static/script/inventree/modals.js +++ b/InvenTree/InvenTree/static/script/inventree/modals.js @@ -397,6 +397,13 @@ function injectModalForm(modal, form_html) { } +function getFieldByName(modal, name) { + /* Find the field (with the given name) within the modal */ + + return $(modal).find(`#id_${name}`); +} + + function insertNewItemButton(modal, options) { /* Insert a button into a modal form, after a field label. * Looks for a