diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 44c5dd3c59..7177703afa 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -8,12 +8,16 @@ import re import common.models -INVENTREE_SW_VERSION = "0.3.0" +INVENTREE_SW_VERSION = "0.3.1" -INVENTREE_API_VERSION = 7 +INVENTREE_API_VERSION = 8 """ -Increment thi API version number whenever there is a significant change to the API that any clients need to know about +Increment this API version number whenever there is a significant change to the API that any clients need to know about + +v8 -> 2021-07-19 + - Refactors the API interface for SupplierPart and ManufacturerPart models + - ManufacturerPart objects can no longer be created via the SupplierPart API endpoint v7 -> 2021-07-03 - Introduced the concept of "API forms" in https://github.com/inventree/InvenTree/pull/1716 diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index c8a5839f4e..3d18d56880 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -233,6 +233,13 @@ class InvenTreeSetting(models.Model): 'validator': bool, }, + 'PART_CREATE_INITIAL': { + 'name': _('Create initial stock'), + 'description': _('Create initial stock on part creation'), + 'default': False, + 'validator': bool, + }, + 'PART_INTERNAL_PRICE': { 'name': _('Internal Prices'), 'description': _('Enable internal prices for parts'), diff --git a/InvenTree/company/forms.py b/InvenTree/company/forms.py index 079e871b84..506133df00 100644 --- a/InvenTree/company/forms.py +++ b/InvenTree/company/forms.py @@ -6,13 +6,12 @@ Django Forms for interacting with Company app from __future__ import unicode_literals from InvenTree.forms import HelperForm -from InvenTree.fields import InvenTreeMoneyField, RoundingDecimalFormField +from InvenTree.fields import RoundingDecimalFormField from django.utils.translation import ugettext_lazy as _ import django.forms from .models import Company -from .models import SupplierPart from .models import SupplierPriceBreak @@ -34,67 +33,6 @@ class CompanyImageDownloadForm(HelperForm): ] -class EditSupplierPartForm(HelperForm): - """ Form for editing a SupplierPart object """ - - field_prefix = { - 'link': 'fa-link', - 'SKU': 'fa-hashtag', - 'note': 'fa-pencil-alt', - } - - single_pricing = InvenTreeMoneyField( - label=_('Single Price'), - help_text=_('Single quantity price'), - decimal_places=4, - max_digits=19, - required=False, - ) - - manufacturer = django.forms.ChoiceField( - required=False, - help_text=_('Select manufacturer'), - choices=[], - ) - - MPN = django.forms.CharField( - required=False, - help_text=_('Manufacturer Part Number'), - max_length=100, - label=_('MPN'), - ) - - class Meta: - model = SupplierPart - fields = [ - 'part', - 'supplier', - 'SKU', - 'manufacturer', - 'MPN', - 'description', - 'link', - 'note', - 'single_pricing', - # 'base_cost', - # 'multiple', - 'packaging', - ] - - def get_manufacturer_choices(self): - """ Returns tuples for all manufacturers """ - empty_choice = [('', '----------')] - - manufacturers = [(manufacturer.id, manufacturer.name) for manufacturer in Company.objects.filter(is_manufacturer=True)] - - return empty_choice + manufacturers - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.fields['manufacturer'].choices = self.get_manufacturer_choices() - - class EditPriceBreakForm(HelperForm): """ Form for creating / editing a supplier price break """ diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 778e00cde1..3b731381b8 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -9,9 +9,7 @@ import os from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator -from django.core.exceptions import ValidationError from django.db import models -from django.db.utils import IntegrityError from django.db.models import Sum, Q, UniqueConstraint from django.apps import apps @@ -475,57 +473,6 @@ class SupplierPart(models.Model): def get_absolute_url(self): return reverse('supplier-part-detail', kwargs={'pk': self.id}) - def save(self, *args, **kwargs): - """ Overriding save method to process the linked ManufacturerPart - """ - - if 'manufacturer' in kwargs: - manufacturer_id = kwargs.pop('manufacturer') - - try: - manufacturer = Company.objects.get(pk=int(manufacturer_id)) - except (ValueError, Company.DoesNotExist): - manufacturer = None - else: - manufacturer = None - if 'MPN' in kwargs: - MPN = kwargs.pop('MPN') - else: - MPN = None - - if manufacturer or MPN: - if not self.manufacturer_part: - # Create ManufacturerPart - manufacturer_part = ManufacturerPart.create(part=self.part, - manufacturer=manufacturer, - mpn=MPN, - description=self.description) - self.manufacturer_part = manufacturer_part - else: - # Update ManufacturerPart (if ID exists) - try: - manufacturer_part_id = self.manufacturer_part.id - except AttributeError: - manufacturer_part_id = None - - if manufacturer_part_id: - try: - (manufacturer_part, created) = ManufacturerPart.objects.update_or_create(part=self.part, - manufacturer=manufacturer, - MPN=MPN) - except IntegrityError: - manufacturer_part = None - raise ValidationError(f'ManufacturerPart linked to {self.part} from manufacturer {manufacturer.name}' - f'with part number {MPN} already exists!') - - if manufacturer_part: - self.manufacturer_part = manufacturer_part - - self.clean() - self.validate_unique() - - super().save(*args, **kwargs) - class Meta: unique_together = ('part', 'supplier', 'SKU') diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index 6e5ef08d6e..e9f13d021d 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -96,7 +96,9 @@ class CompanySerializer(InvenTreeModelSerializer): class ManufacturerPartSerializer(InvenTreeModelSerializer): - """ Serializer for ManufacturerPart object """ + """ + Serializer for ManufacturerPart object + """ part_detail = PartBriefSerializer(source='part', many=False, read_only=True) @@ -106,8 +108,8 @@ class ManufacturerPartSerializer(InvenTreeModelSerializer): def __init__(self, *args, **kwargs): - part_detail = kwargs.pop('part_detail', False) - manufacturer_detail = kwargs.pop('manufacturer_detail', False) + part_detail = kwargs.pop('part_detail', True) + manufacturer_detail = kwargs.pop('manufacturer_detail', True) prettify = kwargs.pop('pretty', False) super(ManufacturerPartSerializer, self).__init__(*args, **kwargs) @@ -229,25 +231,6 @@ class SupplierPartSerializer(InvenTreeModelSerializer): 'supplier_detail', ] - def create(self, validated_data): - """ Extract manufacturer data and process ManufacturerPart """ - - # Create SupplierPart - supplier_part = super().create(validated_data) - - # Get ManufacturerPart raw data (unvalidated) - manufacturer_id = self.initial_data.get('manufacturer', None) - MPN = self.initial_data.get('MPN', None) - - if manufacturer_id and MPN: - kwargs = { - 'manufacturer': manufacturer_id, - 'MPN': MPN, - } - supplier_part.save(**kwargs) - - return supplier_part - class SupplierPriceBreakSerializer(InvenTreeModelSerializer): """ Serializer for SupplierPriceBreak object """ diff --git a/InvenTree/company/templates/company/detail.html b/InvenTree/company/templates/company/detail.html index 105e0f82ae..4a9ac43758 100644 --- a/InvenTree/company/templates/company/detail.html +++ b/InvenTree/company/templates/company/detail.html @@ -267,16 +267,8 @@ }); $("#stock-export").click(function() { - launchModalForm("{% url 'stock-export-options' %}", { - submit_text: '{% trans "Export" %}', - success: function(response) { - var url = "{% url 'stock-export' %}"; - - url += "?format=" + response.format; - url += "&supplier={{ company.id }}"; - - location.href = url; - }, + exportStock({ + supplier: {{ company.id }} }); }); @@ -284,23 +276,9 @@ $("#manufacturer-part-create").click(function () { - constructForm('{% url "api-manufacturer-part-list" %}', { - fields: { - part: {}, - manufacturer: { - value: {{ company.pk }}, - }, - MPN: { - icon: 'fa-hashtag', - }, - description: {}, - link: { - icon: 'fa-link', - }, - }, - method: 'POST', - title: '{% trans "Add Manufacturer Part" %}', - onSuccess: function() { + createManufacturerPart({ + manufacturer: {{ company.pk }}, + onSuccess: function() { $("#part-table").bootstrapTable("refresh"); } }); @@ -350,28 +328,16 @@ {% if company.is_supplier %} + function reloadSupplierPartTable() { + $('#supplier-part-table').bootstrapTable('refresh'); + } + $("#supplier-part-create").click(function () { - launchModalForm( - "{% url 'supplier-part-create' %}", - { - data: { - supplier: {{ company.id }}, - }, - reload: true, - secondary: [ - { - field: 'part', - label: '{% trans "New Part" %}', - title: '{% trans "Create new Part" %}', - url: "{% url 'part-create' %}" - }, - { - field: 'supplier', - label: "{% trans 'New Supplier' %}", - title: "{% trans 'Create new Supplier' %}", - }, - ] - }); + + createSupplierPart({ + supplier: {{ company.pk }}, + onSuccess: reloadSupplierPartTable, + }); }); loadSupplierPartTable( @@ -390,22 +356,27 @@ {% endif %} $("#multi-part-delete").click(function() { - var selections = $("#part-table").bootstrapTable("getSelections"); + var selections = $("#supplier-part-table").bootstrapTable("getSelections"); - var parts = []; + var requests = []; - selections.forEach(function(item) { - parts.push(item.pk); - }); - - var url = "{% url 'supplier-part-delete' %}" - - launchModalForm(url, { - data: { - parts: parts, - }, - reload: true, - }); + showQuestionDialog( + '{% trans "Delete Supplier Parts?" %}', + '{% trans "All selected supplier parts will be deleted" %}', + { + accept: function() { + selections.forEach(function(part) { + var url = `/api/company/part/${part.pk}/`; + + requests.push(inventreeDelete(url)); + }); + + $.when.apply($, requests).then(function() { + $('#supplier-part-table').bootstrapTable('refresh'); + }); + } + } + ); }); $("#multi-part-order").click(function() { diff --git a/InvenTree/company/templates/company/manufacturer_part.html b/InvenTree/company/templates/company/manufacturer_part.html index da5ea36173..94ff64440f 100644 --- a/InvenTree/company/templates/company/manufacturer_part.html +++ b/InvenTree/company/templates/company/manufacturer_part.html @@ -178,40 +178,41 @@ $('#parameter-create').click(function() { }); }); +function reloadSupplierPartTable() { + $('#supplier-table').bootstrapTable('refresh'); +} + $('#supplier-create').click(function () { - launchModalForm( - "{% url 'supplier-part-create' %}", - { - reload: true, - data: { - manufacturer_part: {{ part.id }} - }, - secondary: [ - { - field: 'supplier', - label: '{% trans "New Supplier" %}', - title: '{% trans "Create new supplier" %}', - }, - ] - }); + createSupplierPart({ + manufacturer_part: {{ part.pk }}, + part: {{ part.part.pk }}, + onSuccess: reloadSupplierPartTable, + }); }); $("#supplier-part-delete").click(function() { var selections = $("#supplier-table").bootstrapTable("getSelections"); - var parts = []; + var requests = []; - selections.forEach(function(item) { - parts.push(item.pk); - }); - - launchModalForm("{% url 'supplier-part-delete' %}", { - data: { - parts: parts, - }, - reload: true, - }); + showQuestionDialog( + '{% trans "Delete Supplier Parts?" %}', + '{% trans "All selected supplier parts will be deleted" %}', + { + accept: function() { + selections.forEach(function(part) { + var url = `/api/company/part/${part.pk}/`; + + requests.push(inventreeDelete(url)); + }); + + $.when.apply($, requests).then(function() { + reloadSupplierPartTable(); + }); + } + } + ); }); $("#multi-parameter-delete").click(function() { @@ -296,29 +297,19 @@ $('#order-part, #order-part2').click(function() { $('#edit-part').click(function () { - constructForm('{% url "api-manufacturer-part-detail" part.pk %}', { - fields: { - part: {}, - manufacturer: {}, - MPN: { - icon: 'fa-hashtag', - }, - description: {}, - link: { - icon: 'fa-link', - }, - }, - title: '{% trans "Edit Manufacturer Part" %}', - reload: true, + editManufacturerPart({{ part.pk }}, { + onSuccess: function() { + location.reload(); + } }); }); $('#delete-part').click(function() { - constructForm('{% url "api-manufacturer-part-detail" part.pk %}', { - method: 'DELETE', - title: '{% trans "Delete Manufacturer Part" %}', - redirect: "{% url 'company-detail' part.manufacturer.id %}", + deleteManufacturerPart({{ part.pk }}, { + onSuccess: function() { + window.location.href = "{% url 'company-detail' part.manufacturer.id %}"; + } }); }); diff --git a/InvenTree/company/templates/company/navbar.html b/InvenTree/company/templates/company/navbar.html index 025b1c6b4a..b652d6b603 100644 --- a/InvenTree/company/templates/company/navbar.html +++ b/InvenTree/company/templates/company/navbar.html @@ -18,7 +18,7 @@ {% endif %} - {% if company.is_supplier or company.is_manufacturer %} + {% if company.is_supplier %}
- {% include "hover_image.html" with image=part.part.image %} - {{ part.part.full_name }} - | -- {% include "hover_image.html" with image=part.supplier.image %} - {{ part.supplier.name }} - | -- {{ part.SKU }} - | -
+ + {{ sub_item.part.full_name }} + | ++ {% if sub_item.serialized %} + {% trans "Serial" %}: {{ sub_item.serial }} + {% else %} + {% trans "Quantity" %}: {% decimal sub_item.quantity %} + {% endif %} + | +
${data.pathstring}
`; - } - return html; } @@ -154,7 +152,9 @@ function renderOwner(name, data, parameters, options) { // Renderer for "PartCategory" model function renderPartCategory(name, data, parameters, options) { - var html = `${data.name}`; + var level = '- '.repeat(data.level); + + var html = `${level}${data.pathstring}`; if (data.description) { html += ` - ${data.description}`; @@ -162,10 +162,6 @@ function renderPartCategory(name, data, parameters, options) { html += `{% trans "Category ID" %}: ${data.pk}`; - if (data.pathstring) { - html += `${data.pathstring}
`; - } - return html; } @@ -178,7 +174,35 @@ function renderPartParameterTemplate(name, data, parameters, options) { } -// Rendered for "SupplierPart" model +// Renderer for "ManufacturerPart" model +function renderManufacturerPart(name, data, parameters, options) { + + var manufacturer_image = null; + var part_image = null; + + if (data.manufacturer_detail) { + manufacturer_image = data.manufacturer_detail.image; + } + + if (data.part_detail) { + part_image = data.part_detail.thumbnail || data.part_detail.image; + } + + var html = ''; + + html += select2Thumbnail(manufacturer_image); + html += select2Thumbnail(part_image); + + html += ` ${data.manufacturer_detail.name} - ${data.MPN}`; + html += ` - ${data.part_detail.full_name}`; + + html += `{% trans "Manufacturer Part ID" %}: ${data.pk}`; + + return html; +} + + +// Renderer for "SupplierPart" model function renderSupplierPart(name, data, parameters, options) { var supplier_image = null; diff --git a/InvenTree/templates/js/order.js b/InvenTree/templates/js/order.js index 5cb286e970..7091eb0577 100644 --- a/InvenTree/templates/js/order.js +++ b/InvenTree/templates/js/order.js @@ -14,6 +14,7 @@ function createSalesOrder(options={}) { customer: { value: options.customer, }, + customer_reference: {}, description: {}, target_date: { icon: 'fa-calendar-alt', @@ -44,6 +45,7 @@ function createPurchaseOrder(options={}) { supplier: { value: options.supplier, }, + supplier_reference: {}, description: {}, target_date: { icon: 'fa-calendar-alt', diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index 754d3d5b59..947e7fb3e9 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -20,6 +20,55 @@ function stockStatusCodes() { } +/* + * Export stock table + */ +function exportStock(params={}) { + + constructFormBody({}, { + title: '{% trans "Export Stock" %}', + fields: { + format: { + label: '{% trans "Format" %}', + help_text: '{% trans "Select file format" %}', + required: true, + type: 'choice', + value: 'csv', + choices: [ + { value: 'csv', display_name: 'CSV' }, + { value: 'tsv', display_name: 'TSV' }, + { value: 'xls', display_name: 'XLS' }, + { value: 'xlsx', display_name: 'XLSX' }, + ] + }, + sublocations: { + label: '{% trans "Include Sublocations" %}', + help_text: '{% trans "Include stock items in sublocations" %}', + type: 'boolean', + value: 'true', + } + }, + onSubmit: function(fields, form_options) { + + var format = getFormFieldValue('format', fields['format'], form_options); + var cascade = getFormFieldValue('sublocations', fields['sublocations'], form_options); + + // Hide the modal + $(form_options.modal).modal('hide'); + + var url = `{% url "stock-export" %}?format=${format}&cascade=${cascade}`; + + for (var key in params) { + url += `&${key}=${params[key]}`; + } + + console.log(url); + location.href = url; + } + }); +} + + /** * Perform stock adjustments */ @@ -1616,27 +1665,6 @@ function createNewStockItem(options) { }, ]; - options.secondary = [ - { - field: 'part', - label: '{% trans "New Part" %}', - title: '{% trans "Create New Part" %}', - url: "{% url 'part-create' %}", - }, - { - field: 'supplier_part', - label: '{% trans "New Supplier Part" %}', - title: '{% trans "Create new Supplier Part" %}', - url: "{% url 'supplier-part-create' %}" - }, - { - field: 'location', - label: '{% trans "New Location" %}', - title: '{% trans "Create New Location" %}', - url: "{% url 'stock-location-create' %}", - }, - ]; - launchModalForm("{% url 'stock-item-create' %}", options); } diff --git a/InvenTree/templates/js/tables.js b/InvenTree/templates/js/tables.js index 8fedeb8f9e..afe1fefbc9 100644 --- a/InvenTree/templates/js/tables.js +++ b/InvenTree/templates/js/tables.js @@ -1,5 +1,11 @@ {% load i18n %} + +function reloadtable(table) { + $(table).bootstrapTable('refresh'); +} + + function editButton(url, text='Edit') { return ""; }