Merge branch 'master' of github.com:inventree/InvenTree into part_main_details

This commit is contained in:
eeintech 2021-07-19 09:20:06 -04:00
commit 2703ae520e
42 changed files with 604 additions and 929 deletions

View File

@ -8,12 +8,16 @@ import re
import common.models 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 v7 -> 2021-07-03
- Introduced the concept of "API forms" in https://github.com/inventree/InvenTree/pull/1716 - Introduced the concept of "API forms" in https://github.com/inventree/InvenTree/pull/1716

View File

@ -233,6 +233,13 @@ class InvenTreeSetting(models.Model):
'validator': bool, 'validator': bool,
}, },
'PART_CREATE_INITIAL': {
'name': _('Create initial stock'),
'description': _('Create initial stock on part creation'),
'default': False,
'validator': bool,
},
'PART_INTERNAL_PRICE': { 'PART_INTERNAL_PRICE': {
'name': _('Internal Prices'), 'name': _('Internal Prices'),
'description': _('Enable internal prices for parts'), 'description': _('Enable internal prices for parts'),

View File

@ -6,13 +6,12 @@ Django Forms for interacting with Company app
from __future__ import unicode_literals from __future__ import unicode_literals
from InvenTree.forms import HelperForm from InvenTree.forms import HelperForm
from InvenTree.fields import InvenTreeMoneyField, RoundingDecimalFormField from InvenTree.fields import RoundingDecimalFormField
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import django.forms import django.forms
from .models import Company from .models import Company
from .models import SupplierPart
from .models import SupplierPriceBreak 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): class EditPriceBreakForm(HelperForm):
""" Form for creating / editing a supplier price break """ """ Form for creating / editing a supplier price break """

View File

@ -9,9 +9,7 @@ import os
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.utils import IntegrityError
from django.db.models import Sum, Q, UniqueConstraint from django.db.models import Sum, Q, UniqueConstraint
from django.apps import apps from django.apps import apps
@ -475,57 +473,6 @@ class SupplierPart(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('supplier-part-detail', kwargs={'pk': self.id}) 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: class Meta:
unique_together = ('part', 'supplier', 'SKU') unique_together = ('part', 'supplier', 'SKU')

View File

@ -96,7 +96,9 @@ class CompanySerializer(InvenTreeModelSerializer):
class ManufacturerPartSerializer(InvenTreeModelSerializer): class ManufacturerPartSerializer(InvenTreeModelSerializer):
""" Serializer for ManufacturerPart object """ """
Serializer for ManufacturerPart object
"""
part_detail = PartBriefSerializer(source='part', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
@ -106,8 +108,8 @@ class ManufacturerPartSerializer(InvenTreeModelSerializer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
part_detail = kwargs.pop('part_detail', False) part_detail = kwargs.pop('part_detail', True)
manufacturer_detail = kwargs.pop('manufacturer_detail', False) manufacturer_detail = kwargs.pop('manufacturer_detail', True)
prettify = kwargs.pop('pretty', False) prettify = kwargs.pop('pretty', False)
super(ManufacturerPartSerializer, self).__init__(*args, **kwargs) super(ManufacturerPartSerializer, self).__init__(*args, **kwargs)
@ -229,25 +231,6 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
'supplier_detail', '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): class SupplierPriceBreakSerializer(InvenTreeModelSerializer):
""" Serializer for SupplierPriceBreak object """ """ Serializer for SupplierPriceBreak object """

View File

@ -267,16 +267,8 @@
}); });
$("#stock-export").click(function() { $("#stock-export").click(function() {
launchModalForm("{% url 'stock-export-options' %}", { exportStock({
submit_text: '{% trans "Export" %}', supplier: {{ company.id }}
success: function(response) {
var url = "{% url 'stock-export' %}";
url += "?format=" + response.format;
url += "&supplier={{ company.id }}";
location.href = url;
},
}); });
}); });
@ -284,22 +276,8 @@
$("#manufacturer-part-create").click(function () { $("#manufacturer-part-create").click(function () {
constructForm('{% url "api-manufacturer-part-list" %}', { createManufacturerPart({
fields: { manufacturer: {{ company.pk }},
part: {},
manufacturer: {
value: {{ company.pk }},
},
MPN: {
icon: 'fa-hashtag',
},
description: {},
link: {
icon: 'fa-link',
},
},
method: 'POST',
title: '{% trans "Add Manufacturer Part" %}',
onSuccess: function() { onSuccess: function() {
$("#part-table").bootstrapTable("refresh"); $("#part-table").bootstrapTable("refresh");
} }
@ -350,27 +328,15 @@
{% if company.is_supplier %} {% if company.is_supplier %}
function reloadSupplierPartTable() {
$('#supplier-part-table').bootstrapTable('refresh');
}
$("#supplier-part-create").click(function () { $("#supplier-part-create").click(function () {
launchModalForm(
"{% url 'supplier-part-create' %}", createSupplierPart({
{ supplier: {{ company.pk }},
data: { onSuccess: reloadSupplierPartTable,
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' %}",
},
]
}); });
}); });
@ -390,22 +356,27 @@
{% endif %} {% endif %}
$("#multi-part-delete").click(function() { $("#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) { showQuestionDialog(
parts.push(item.pk); '{% 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));
}); });
var url = "{% url 'supplier-part-delete' %}" $.when.apply($, requests).then(function() {
$('#supplier-part-table').bootstrapTable('refresh');
launchModalForm(url, {
data: {
parts: parts,
},
reload: true,
}); });
}
}
);
}); });
$("#multi-part-order").click(function() { $("#multi-part-order").click(function() {

View File

@ -178,21 +178,15 @@ $('#parameter-create').click(function() {
}); });
}); });
function reloadSupplierPartTable() {
$('#supplier-table').bootstrapTable('refresh');
}
$('#supplier-create').click(function () { $('#supplier-create').click(function () {
launchModalForm( createSupplierPart({
"{% url 'supplier-part-create' %}", manufacturer_part: {{ part.pk }},
{ part: {{ part.part.pk }},
reload: true, onSuccess: reloadSupplierPartTable,
data: {
manufacturer_part: {{ part.id }}
},
secondary: [
{
field: 'supplier',
label: '{% trans "New Supplier" %}',
title: '{% trans "Create new supplier" %}',
},
]
}); });
}); });
@ -200,18 +194,25 @@ $("#supplier-part-delete").click(function() {
var selections = $("#supplier-table").bootstrapTable("getSelections"); var selections = $("#supplier-table").bootstrapTable("getSelections");
var parts = []; var requests = [];
selections.forEach(function(item) { showQuestionDialog(
parts.push(item.pk); '{% 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));
}); });
launchModalForm("{% url 'supplier-part-delete' %}", { $.when.apply($, requests).then(function() {
data: { reloadSupplierPartTable();
parts: parts,
},
reload: true,
}); });
}
}
);
}); });
$("#multi-parameter-delete").click(function() { $("#multi-parameter-delete").click(function() {
@ -296,29 +297,19 @@ $('#order-part, #order-part2').click(function() {
$('#edit-part').click(function () { $('#edit-part').click(function () {
constructForm('{% url "api-manufacturer-part-detail" part.pk %}', { editManufacturerPart({{ part.pk }}, {
fields: { onSuccess: function() {
part: {}, location.reload();
manufacturer: {}, }
MPN: {
icon: 'fa-hashtag',
},
description: {},
link: {
icon: 'fa-link',
},
},
title: '{% trans "Edit Manufacturer Part" %}',
reload: true,
}); });
}); });
$('#delete-part').click(function() { $('#delete-part').click(function() {
constructForm('{% url "api-manufacturer-part-detail" part.pk %}', { deleteManufacturerPart({{ part.pk }}, {
method: 'DELETE', onSuccess: function() {
title: '{% trans "Delete Manufacturer Part" %}', window.location.href = "{% url 'company-detail' part.manufacturer.id %}";
redirect: "{% url 'company-detail' part.manufacturer.id %}", }
}); });
}); });

View File

@ -18,7 +18,7 @@
</li> </li>
{% endif %} {% endif %}
{% if company.is_supplier or company.is_manufacturer %} {% if company.is_supplier %}
<li class='list-group-item' title='{% trans "Supplied Parts" %}'> <li class='list-group-item' title='{% trans "Supplied Parts" %}'>
<a href='#' id='select-supplier-parts' class='nav-toggle'> <a href='#' id='select-supplier-parts' class='nav-toggle'>
<span class='fas fa-building sidebar-icon'></span> <span class='fas fa-building sidebar-icon'></span>

View File

@ -284,18 +284,11 @@ loadStockTable($("#stock-table"), {
}); });
$("#stock-export").click(function() { $("#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; exportStock({
url += "&cascade=" + response.cascade; supplier_part: {{ part.pk }},
url += "&supplier_part={{ part.id }}";
location.href = url;
},
}); });
}); });
$("#item-create").click(function() { $("#item-create").click(function() {
@ -327,21 +320,21 @@ $('#order-part, #order-part2').click(function() {
}); });
$('#edit-part').click(function () { $('#edit-part').click(function () {
launchModalForm(
"{% url 'supplier-part-edit' part.id %}", editSupplierPart({{ part.pk }}, {
{ onSuccess: function() {
reload: true location.reload();
} }
); });
}); });
$('#delete-part').click(function() { $('#delete-part').click(function() {
launchModalForm(
"{% url 'supplier-part-delete' %}?part={{ part.id }}", deleteSupplierPart({{ part.pk }}, {
{ onSuccess: function() {
redirect: "{% url 'company-detail' part.supplier.id %}" window.location.href = "{% url 'company-detail' part.supplier.id %}";
} }
); });
}); });
attachNavCallbacks({ attachNavCallbacks({

View File

@ -1,17 +0,0 @@
{% extends "modal_form.html" %}
{% load i18n %}
{% block pre_form_content %}
{{ block.super }}
{% if part %}
<div class='alert alert-block alert-info'>
{% include "hover_image.html" with image=part.image %}
{{ part.full_name}}
<br>
<i>{{ part.description }}</i>
</div>
{% endif %}
{% endblock %}

View File

@ -1,31 +0,0 @@
{% extends "modal_delete_form.html" %}
{% load i18n %}
{% block pre_form_content %}
{% trans "Are you sure you want to delete the following Supplier Parts?" %}
<hr>
{% endblock %}
{% block form_data %}
<table class='table table-striped table-condensed'>
{% for part in parts %}
<tr>
<input type='hidden' name='supplier-part-{{ part.id}}' value='supplier-part-{{ part.id }}'/>
<td>
{% include "hover_image.html" with image=part.part.image %}
{{ part.part.full_name }}
</td>
<td>
{% include "hover_image.html" with image=part.supplier.image %}
{{ part.supplier.name }}
</td>
<td>
{{ part.SKU }}
</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -218,14 +218,27 @@ class ManufacturerTest(InvenTreeAPITestCase):
def test_supplier_part_create(self): def test_supplier_part_create(self):
url = reverse('api-supplier-part-list') url = reverse('api-supplier-part-list')
# Create supplier part # Create a manufacturer part
response = self.post(
reverse('api-manufacturer-part-list'),
{
'part': 1,
'manufacturer': 7,
'MPN': 'PART_NUMBER',
},
expected_code=201
)
pk = response.data['pk']
# Create a supplier part (associated with the new manufacturer part)
data = { data = {
'part': 1, 'part': 1,
'supplier': 1, 'supplier': 1,
'SKU': 'SKU_TEST', 'SKU': 'SKU_TEST',
'manufacturer': 7, 'manufacturer_part': pk,
'MPN': 'PART_NUMBER',
} }
response = self.client.post(url, data, format='json') response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)

View File

@ -10,9 +10,6 @@ from django.urls import reverse
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from .models import ManufacturerPart
from .models import SupplierPart
class CompanyViewTestBase(TestCase): class CompanyViewTestBase(TestCase):
@ -75,108 +72,6 @@ class CompanyViewTestBase(TestCase):
return json_data, form_errors return json_data, form_errors
class SupplierPartViewTests(CompanyViewTestBase):
"""
Tests for the SupplierPart views.
"""
def test_supplier_part_create(self):
"""
Test the SupplierPartCreate view.
This view allows some additional functionality,
specifically it allows the user to create a single-quantity price break
automatically, when saving the new SupplierPart model.
"""
url = reverse('supplier-part-create')
# First check that we can GET the form
response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# How many supplier parts are already in the database?
n = SupplierPart.objects.all().count()
data = {
'part': 1,
'supplier': 1,
}
# SKU is required! (form should fail)
(response, errors) = self.post(url, data, valid=False)
self.assertIsNotNone(errors.get('SKU', None))
data['SKU'] = 'TEST-ME-123'
(response, errors) = self.post(url, data, valid=True)
# Check that the SupplierPart was created!
self.assertEqual(n + 1, SupplierPart.objects.all().count())
# Check that it was created *without* a price-break
supplier_part = SupplierPart.objects.get(pk=response['pk'])
self.assertEqual(supplier_part.price_breaks.count(), 0)
# Duplicate SKU is prohibited
(response, errors) = self.post(url, data, valid=False)
self.assertIsNotNone(errors.get('__all__', None))
# Add with a different SKU, *and* a single-quantity price
data['SKU'] = 'TEST-ME-1234'
data['single_pricing_0'] = '123.4'
data['single_pricing_1'] = 'CAD'
(response, errors) = self.post(url, data, valid=True)
pk = response.get('pk')
# Check that *another* SupplierPart was created
self.assertEqual(n + 2, SupplierPart.objects.all().count())
supplier_part = SupplierPart.objects.get(pk=pk)
# Check that a price-break has been created!
self.assertEqual(supplier_part.price_breaks.count(), 1)
price_break = supplier_part.price_breaks.first()
self.assertEqual(price_break.quantity, 1)
def test_supplier_part_delete(self):
"""
Test the SupplierPartDelete view
"""
url = reverse('supplier-part-delete')
# Get form using 'part' argument
response = self.client.get(url, {'part': '1'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# Get form using 'parts' argument
response = self.client.get(url + '?parts[]=1&parts[]=2', HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# POST to delete two parts
n = SupplierPart.objects.count()
response = self.client.post(
url,
{
'supplier-part-2': 'supplier-part-2',
'supplier-part-3': 'supplier-part-3',
'confirm_delete': True
},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
self.assertEqual(n - 2, SupplierPart.objects.count())
class CompanyViewTest(CompanyViewTestBase): class CompanyViewTest(CompanyViewTestBase):
""" """
Tests for various 'Company' views Tests for various 'Company' views
@ -187,36 +82,3 @@ class CompanyViewTest(CompanyViewTestBase):
response = self.client.get(reverse('company-index')) response = self.client.get(reverse('company-index'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
class ManufacturerPartViewTests(CompanyViewTestBase):
"""
Tests for the ManufacturerPart views.
"""
def test_supplier_part_create(self):
"""
Test that the SupplierPartCreate view creates Manufacturer Part.
"""
url = reverse('supplier-part-create')
# First check that we can GET the form
response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# How many manufacturer parts are already in the database?
n = ManufacturerPart.objects.all().count()
data = {
'part': 1,
'supplier': 1,
'SKU': 'SKU_TEST',
'manufacturer': 6,
'MPN': 'MPN_TEST',
}
(response, errors) = self.post(url, data, valid=True)
# Check that the ManufacturerPart was created!
self.assertEqual(n + 1, ManufacturerPart.objects.all().count())

View File

@ -192,18 +192,14 @@ class ManufacturerPartSimpleTest(TestCase):
SKU='SKU_TEST', SKU='SKU_TEST',
) )
kwargs = { supplier_part.save()
'manufacturer': manufacturer.id,
'MPN': 'MPN_TEST',
}
supplier_part.save(**kwargs)
def test_exists(self): def test_exists(self):
self.assertEqual(ManufacturerPart.objects.count(), 5) self.assertEqual(ManufacturerPart.objects.count(), 4)
# Check that manufacturer part was created from supplier part creation # Check that manufacturer part was created from supplier part creation
manufacturer_parts = ManufacturerPart.objects.filter(manufacturer=1) manufacturer_parts = ManufacturerPart.objects.filter(manufacturer=1)
self.assertEqual(manufacturer_parts.count(), 2) self.assertEqual(manufacturer_parts.count(), 1)
def test_delete(self): def test_delete(self):
# Remove a part # Remove a part

View File

@ -35,16 +35,6 @@ manufacturer_part_urls = [
])), ])),
] ]
supplier_part_detail_urls = [ supplier_part_urls = [
url(r'^edit/?', views.SupplierPartEdit.as_view(), name='supplier-part-edit'),
url('^.*$', views.SupplierPartDetail.as_view(template_name='company/supplier_part.html'), name='supplier-part-detail'), url('^.*$', views.SupplierPartDetail.as_view(template_name='company/supplier_part.html'), name='supplier-part-detail'),
] ]
supplier_part_urls = [
url(r'^new/?', views.SupplierPartCreate.as_view(), name='supplier-part-create'),
url(r'delete/', views.SupplierPartDelete.as_view(), name='supplier-part-delete'),
url(r'^(?P<pk>\d+)/', include(supplier_part_detail_urls)),
]

View File

@ -10,31 +10,22 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
from django.urls import reverse from django.urls import reverse
from django.forms import HiddenInput
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from moneyed import CURRENCIES
from PIL import Image from PIL import Image
import requests import requests
import io import io
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import AjaxUpdateView
from InvenTree.helpers import str2bool
from InvenTree.views import InvenTreeRoleMixin from InvenTree.views import InvenTreeRoleMixin
from .models import Company from .models import Company
from .models import ManufacturerPart from .models import ManufacturerPart
from .models import SupplierPart from .models import SupplierPart
from part.models import Part
from .forms import EditSupplierPartForm
from .forms import CompanyImageDownloadForm from .forms import CompanyImageDownloadForm
import common.models
import common.settings
class CompanyIndex(InvenTreeRoleMixin, ListView): class CompanyIndex(InvenTreeRoleMixin, ListView):
""" View for displaying list of companies """ View for displaying list of companies
@ -231,272 +222,3 @@ class SupplierPartDetail(DetailView):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
return ctx return ctx
class SupplierPartEdit(AjaxUpdateView):
""" Update view for editing SupplierPart """
model = SupplierPart
context_object_name = 'part'
form_class = EditSupplierPartForm
ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit Supplier Part')
def save(self, supplier_part, form, **kwargs):
""" Process ManufacturerPart data """
manufacturer = form.cleaned_data.get('manufacturer', None)
MPN = form.cleaned_data.get('MPN', None)
kwargs = {'manufacturer': manufacturer,
'MPN': MPN,
}
supplier_part.save(**kwargs)
def get_form(self):
form = super().get_form()
supplier_part = self.get_object()
# Hide Manufacturer fields
form.fields['manufacturer'].widget = HiddenInput()
form.fields['MPN'].widget = HiddenInput()
# It appears that hiding a MoneyField fails validation
# Therefore the idea to set the value before hiding
if form.is_valid():
form.cleaned_data['single_pricing'] = supplier_part.unit_pricing
# Hide the single-pricing field (only for creating a new SupplierPart!)
form.fields['single_pricing'].widget = HiddenInput()
return form
def get_initial(self):
""" Fetch data from ManufacturerPart """
initials = super(SupplierPartEdit, self).get_initial().copy()
supplier_part = self.get_object()
if supplier_part.manufacturer_part:
if supplier_part.manufacturer_part.manufacturer:
initials['manufacturer'] = supplier_part.manufacturer_part.manufacturer.id
initials['MPN'] = supplier_part.manufacturer_part.MPN
return initials
class SupplierPartCreate(AjaxCreateView):
""" Create view for making new SupplierPart """
model = SupplierPart
form_class = EditSupplierPartForm
ajax_template_name = 'company/supplier_part_create.html'
ajax_form_title = _('Create new Supplier Part')
context_object_name = 'part'
def validate(self, part, form):
single_pricing = form.cleaned_data.get('single_pricing', None)
if single_pricing:
# TODO - What validation steps can be performed on the single_pricing field?
pass
def get_context_data(self):
"""
Supply context data to the form
"""
ctx = super().get_context_data()
# Add 'part' object
form = self.get_form()
part = form['part'].value()
try:
part = Part.objects.get(pk=part)
except (ValueError, Part.DoesNotExist):
part = None
ctx['part'] = part
return ctx
def save(self, form):
"""
If single_pricing is defined, add a price break for quantity=1
"""
# Save the supplier part object
supplier_part = super().save(form)
# Process manufacturer data
manufacturer = form.cleaned_data.get('manufacturer', None)
MPN = form.cleaned_data.get('MPN', None)
kwargs = {'manufacturer': manufacturer,
'MPN': MPN,
}
supplier_part.save(**kwargs)
single_pricing = form.cleaned_data.get('single_pricing', None)
if single_pricing:
supplier_part.add_price_break(1, single_pricing)
return supplier_part
def get_form(self):
""" Create Form instance to create a new SupplierPart object.
Hide some fields if they are not appropriate in context
"""
form = super(AjaxCreateView, self).get_form()
if form.initial.get('part', None):
# Hide the part field
form.fields['part'].widget = HiddenInput()
if form.initial.get('manufacturer', None):
# Hide the manufacturer field
form.fields['manufacturer'].widget = HiddenInput()
# Hide the MPN field
form.fields['MPN'].widget = HiddenInput()
return form
def get_initial(self):
""" Provide initial data for new SupplierPart:
- If 'supplier_id' provided, pre-fill supplier field
- If 'part_id' provided, pre-fill part field
"""
initials = super(SupplierPartCreate, self).get_initial().copy()
manufacturer_id = self.get_param('manufacturer')
supplier_id = self.get_param('supplier')
part_id = self.get_param('part')
manufacturer_part_id = self.get_param('manufacturer_part')
supplier = None
if supplier_id:
try:
supplier = Company.objects.get(pk=supplier_id)
initials['supplier'] = supplier
except (ValueError, Company.DoesNotExist):
supplier = None
if manufacturer_id:
try:
initials['manufacturer'] = Company.objects.get(pk=manufacturer_id)
except (ValueError, Company.DoesNotExist):
pass
if manufacturer_part_id:
try:
# Get ManufacturerPart instance information
manufacturer_part_obj = ManufacturerPart.objects.get(pk=manufacturer_part_id)
initials['part'] = Part.objects.get(pk=manufacturer_part_obj.part.id)
initials['manufacturer'] = manufacturer_part_obj.manufacturer.id
initials['MPN'] = manufacturer_part_obj.MPN
except (ValueError, ManufacturerPart.DoesNotExist, Part.DoesNotExist, Company.DoesNotExist):
pass
if part_id:
try:
initials['part'] = Part.objects.get(pk=part_id)
except (ValueError, Part.DoesNotExist):
pass
# Initial value for single pricing
if supplier:
currency_code = supplier.currency_code
else:
currency_code = common.settings.currency_code_default()
currency = CURRENCIES.get(currency_code, None)
if currency_code:
initials['single_pricing'] = ('', currency)
return initials
class SupplierPartDelete(AjaxDeleteView):
""" Delete view for removing a SupplierPart.
SupplierParts can be deleted using a variety of 'selectors'.
- ?part=<pk> -> Delete a single SupplierPart object
- ?parts=[] -> Delete a list of SupplierPart objects
"""
success_url = '/supplier/'
ajax_template_name = 'company/supplier_part_delete.html'
ajax_form_title = _('Delete Supplier Part')
role_required = 'purchase_order.delete'
parts = []
def get_context_data(self):
ctx = {}
ctx['parts'] = self.parts
return ctx
def get_parts(self):
""" Determine which SupplierPart object(s) the user wishes to delete.
"""
self.parts = []
# User passes a single SupplierPart ID
if 'part' in self.request.GET:
try:
self.parts.append(SupplierPart.objects.get(pk=self.request.GET.get('part')))
except (ValueError, SupplierPart.DoesNotExist):
pass
elif 'parts[]' in self.request.GET:
part_id_list = self.request.GET.getlist('parts[]')
self.parts = SupplierPart.objects.filter(id__in=part_id_list)
def get(self, request, *args, **kwargs):
self.request = request
self.get_parts()
return self.renderJsonResponse(request, form=self.get_form())
def post(self, request, *args, **kwargs):
""" Handle the POST action for deleting supplier parts.
"""
self.request = request
self.parts = []
for item in self.request.POST:
if item.startswith('supplier-part-'):
pk = item.replace('supplier-part-', '')
try:
self.parts.append(SupplierPart.objects.get(pk=pk))
except (ValueError, SupplierPart.DoesNotExist):
pass
confirm = str2bool(self.request.POST.get('confirm_delete', False))
data = {
'form_valid': confirm,
}
if confirm:
for part in self.parts:
part.delete()
return self.renderJsonResponse(self.request, data=data, form=self.get_form())

View File

@ -132,7 +132,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
purchase_price_string = serializers.CharField(source='purchase_price', read_only=True) purchase_price_string = serializers.CharField(source='purchase_price', read_only=True)
destination = LocationBriefSerializer(source='get_destination', read_only=True) destination_detail = LocationBriefSerializer(source='get_destination', read_only=True)
purchase_price_currency = serializers.ChoiceField( purchase_price_currency = serializers.ChoiceField(
choices=currency_code_mappings(), choices=currency_code_mappings(),
@ -156,6 +156,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
'purchase_price_currency', 'purchase_price_currency',
'purchase_price_string', 'purchase_price_string',
'destination', 'destination',
'destination_detail',
] ]

View File

@ -170,6 +170,7 @@ $("#edit-order").click(function() {
supplier: { supplier: {
}, },
{% endif %} {% endif %}
supplier_reference: {},
description: {}, description: {},
target_date: { target_date: {
icon: 'fa-calendar-alt', icon: 'fa-calendar-alt',

View File

@ -401,8 +401,15 @@ $("#po-table").inventreeTable({
} }
}, },
{ {
field: 'destination.pathstring', field: 'destination',
title: '{% trans "Destination" %}', title: '{% trans "Destination" %}',
formatter: function(value, row) {
if (value) {
return renderLink(row.destination_detail.pathstring, `/stock/location/${value}/`);
} else {
return '-';
}
}
}, },
{ {
field: 'notes', field: 'notes',

View File

@ -163,6 +163,7 @@ $("#edit-order").click(function() {
customer: { customer: {
}, },
{% endif %} {% endif %}
customer_reference: {},
description: {}, description: {},
target_date: { target_date: {
icon: 'fa-calendar-alt', icon: 'fa-calendar-alt',

View File

@ -118,9 +118,17 @@ class CategoryList(generics.ListCreateAPIView):
ordering_fields = [ ordering_fields = [
'name', 'name',
'level',
'tree_id',
'lft',
] ]
ordering = 'name' # Use hierarchical ordering by default
ordering = [
'tree_id',
'lft',
'name'
]
search_fields = [ search_fields = [
'name', 'name',

View File

@ -217,6 +217,11 @@ class EditPartForm(HelperForm):
label=_('Include parent categories parameter templates'), label=_('Include parent categories parameter templates'),
widget=forms.HiddenInput()) widget=forms.HiddenInput())
initial_stock = forms.IntegerField(required=False,
initial=0,
label=_('Initial stock amount'),
help_text=_('Create stock for this part'))
class Meta: class Meta:
model = Part model = Part
fields = [ fields = [
@ -238,6 +243,7 @@ class EditPartForm(HelperForm):
'default_expiry', 'default_expiry',
'units', 'units',
'minimum_stock', 'minimum_stock',
'initial_stock',
'component', 'component',
'assembly', 'assembly',
'is_template', 'is_template',

View File

@ -32,6 +32,8 @@ class CategorySerializer(InvenTreeModelSerializer):
parts = serializers.IntegerField(source='item_count', read_only=True) parts = serializers.IntegerField(source='item_count', read_only=True)
level = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = PartCategory model = PartCategory
fields = [ fields = [
@ -40,10 +42,11 @@ class CategorySerializer(InvenTreeModelSerializer):
'description', 'description',
'default_location', 'default_location',
'default_keywords', 'default_keywords',
'pathstring', 'level',
'url',
'parent', 'parent',
'parts', 'parts',
'pathstring',
'url',
] ]

View File

@ -370,6 +370,16 @@
sub_part_detail: true, sub_part_detail: true,
}); });
// Load the BOM table data in the pricing view
loadBomTable($("#bom-pricing-table"), {
editable: {{ editing_enabled }},
bom_url: "{% url 'api-bom-list' %}",
part_url: "{% url 'api-part-list' %}",
parent_id: {{ part.id }} ,
sub_part_detail: true,
});
linkButtonsToSelection($("#bom-table"), linkButtonsToSelection($("#bom-table"),
[ [
"#bom-item-delete", "#bom-item-delete",
@ -634,17 +644,9 @@
}); });
$("#stock-export").click(function() { $("#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; exportStock({
url += "&cascade=" + response.cascade; part: {{ part.pk }}
url += "&part={{ part.id }}";
location.href = url;
},
}); });
}); });
@ -801,26 +803,15 @@
) )
}); });
$('#supplier-create').click(function () { function reloadSupplierPartTable() {
launchModalForm( $('#supplier-part-table').bootstrapTable('refresh');
"{% url 'supplier-part-create' %}",
{
reload: true,
data: {
part: {{ part.id }}
},
secondary: [
{
field: 'supplier',
label: '{% trans "New Supplier" %}',
title: '{% trans "Create new supplier" %}',
},
{
field: 'manufacturer',
label: '{% trans "New Manufacturer" %}',
title: '{% trans "Create new manufacturer" %}',
} }
]
$('#supplier-create').click(function () {
createSupplierPart({
part: {{ part.pk }},
onSuccess: reloadSupplierPartTable,
}); });
}); });
@ -828,18 +819,25 @@
var selections = $("#supplier-part-table").bootstrapTable("getSelections"); var selections = $("#supplier-part-table").bootstrapTable("getSelections");
var parts = []; var requests = [];
selections.forEach(function(item) { showQuestionDialog(
parts.push(item.pk); '{% 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));
}); });
launchModalForm("{% url 'supplier-part-delete' %}", { $.when.apply($, requests).then(function() {
data: { reloadSupplierPartTable();
parts: parts,
},
reload: true,
}); });
}
}
);
}); });
loadSupplierPartTable( loadSupplierPartTable(
@ -884,19 +882,8 @@
$('#manufacturer-create').click(function () { $('#manufacturer-create').click(function () {
constructForm('{% url "api-manufacturer-part-list" %}', { createManufacturerPart({
fields: { part: {{ part.pk }},
part: {
value: {{ part.pk }},
hidden: true,
},
manufacturer: {},
MPN: {},
description: {},
link: {},
},
method: 'POST',
title: '{% trans "Add Manufacturer Part" %}',
onSuccess: function() { onSuccess: function() {
$("#manufacturer-part-table").bootstrapTable("refresh"); $("#manufacturer-part-table").bootstrapTable("refresh");
} }

View File

@ -217,7 +217,7 @@
<div class='panel-content'> <div class='panel-content'>
<div class="row"> <div class="row">
<div class="col col-md-6"> <div class="col col-md-6">
<table class='table table-bom table-condensed' data-toolbar="#button-toolbar" id='bom-table'></table> <table class='table table-bom table-condensed' data-toolbar="#button-toolbar" id='bom-pricing-table'></table>
</div> </div>
{% if part.bom_count > 0 %} {% if part.bom_count > 0 %}

View File

@ -44,7 +44,7 @@ from common.files import FileManager
from common.views import FileManagementFormView, FileManagementAjaxView from common.views import FileManagementFormView, FileManagementAjaxView
from common.forms import UploadFileForm, MatchFieldForm from common.forms import UploadFileForm, MatchFieldForm
from stock.models import StockLocation from stock.models import StockItem, StockLocation
import common.settings as inventree_settings import common.settings as inventree_settings
@ -487,6 +487,10 @@ class PartCreate(AjaxCreateView):
if not inventree_settings.stock_expiry_enabled(): if not inventree_settings.stock_expiry_enabled():
form.fields['default_expiry'].widget = HiddenInput() form.fields['default_expiry'].widget = HiddenInput()
# Hide the "initial stock amount" field if the feature is not enabled
if not InvenTreeSetting.get_setting('PART_CREATE_INITIAL'):
form.fields['initial_stock'].widget = HiddenInput()
# Hide the default_supplier field (there are no matching supplier parts yet!) # Hide the default_supplier field (there are no matching supplier parts yet!)
form.fields['default_supplier'].widget = HiddenInput() form.fields['default_supplier'].widget = HiddenInput()
@ -547,6 +551,14 @@ class PartCreate(AjaxCreateView):
# Save part and pass category template settings # Save part and pass category template settings
part.save(**{'add_category_templates': add_category_templates}) part.save(**{'add_category_templates': add_category_templates})
# Add stock if set
init_stock = int(request.POST.get('initial_stock', 0))
if init_stock:
stock = StockItem(part=part,
quantity=init_stock,
location=part.default_location)
stock.save()
data['pk'] = part.pk data['pk'] = part.pk
data['text'] = str(part) data['text'] = str(part)

View File

@ -357,7 +357,8 @@ class TestReport(ReportTemplateBase):
'serial': stock_item.serial, 'serial': stock_item.serial,
'part': stock_item.part, 'part': stock_item.part,
'results': stock_item.testResultMap(include_installed=self.include_installed), 'results': stock_item.testResultMap(include_installed=self.include_installed),
'result_list': stock_item.testResultList(include_installed=self.include_installed) 'result_list': stock_item.testResultList(include_installed=self.include_installed),
'installed_items': stock_item.get_installed_items(cascade=True),
} }

View File

@ -56,6 +56,10 @@ content: "{% trans 'Stock Item Test Report' %}";
{% endblock %} {% endblock %}
{% block pre_page_content %}
{% endblock %}
{% block page_content %} {% block page_content %}
<div class='container'> <div class='container'>
@ -80,6 +84,7 @@ content: "{% trans 'Stock Item Test Report' %}";
</div> </div>
</div> </div>
{% if resul_list|length > 0 %}
<h3>{% trans "Test Results" %}</h3> <h3>{% trans "Test Results" %}</h3>
<table class='table test-table'> <table class='table test-table'>
@ -112,5 +117,37 @@ content: "{% trans 'Stock Item Test Report' %}";
</tbody> </tbody>
</table> </table>
{% endif %}
{% if installed_items|length > 0 %}
<h3>{% trans "Installed Items" %}</h3>
<table class='table test-table'>
<thead>
</thead>
<tbody>
{% for sub_item in installed_items %}
<tr>
<td>
<img src='{% part_image sub_item.part %}' class='part-img' style='max-width: 24px; max-height: 24px;'>
{{ sub_item.part.full_name }}
</td>
<td>
{% if sub_item.serialized %}
{% trans "Serial" %}: {{ sub_item.serial }}
{% else %}
{% trans "Quantity" %}: {% decimal sub_item.quantity %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}
{% block post_page_content %}
{% endblock %} {% endblock %}

View File

@ -363,6 +363,15 @@ class StockLocationList(generics.ListCreateAPIView):
ordering_fields = [ ordering_fields = [
'name', 'name',
'items', 'items',
'level',
'tree_id',
'lft',
]
ordering = [
'tree_id',
'lft',
'name',
] ]

View File

@ -13,7 +13,6 @@ from django.core.exceptions import ValidationError
from mptt.fields import TreeNodeChoiceField from mptt.fields import TreeNodeChoiceField
from InvenTree.helpers import GetExportFormats
from InvenTree.forms import HelperForm from InvenTree.forms import HelperForm
from InvenTree.fields import RoundingDecimalFormField from InvenTree.fields import RoundingDecimalFormField
from InvenTree.fields import DatePickerFormField from InvenTree.fields import DatePickerFormField
@ -226,33 +225,6 @@ class TestReportFormatForm(HelperForm):
template = forms.ChoiceField(label=_('Template'), help_text=_('Select test report template')) template = forms.ChoiceField(label=_('Template'), help_text=_('Select test report template'))
class ExportOptionsForm(HelperForm):
""" Form for selecting stock export options """
file_format = forms.ChoiceField(label=_('File Format'), help_text=_('Select output file format'))
include_sublocations = forms.BooleanField(required=False, initial=True, label=_('Include sublocations'), help_text=_("Include stock items in sub locations"))
class Meta:
model = StockLocation
fields = [
'file_format',
'include_sublocations',
]
def get_format_choices(self):
""" File format choices """
choices = [(x, x.upper()) for x in GetExportFormats()]
return choices
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['file_format'].choices = self.get_format_choices()
class InstallStockForm(HelperForm): class InstallStockForm(HelperForm):
""" """
Form for manually installing a stock item into another stock item Form for manually installing a stock item into another stock item

View File

@ -260,12 +260,15 @@ class LocationSerializer(InvenTreeModelSerializer):
items = serializers.IntegerField(source='item_count', read_only=True) items = serializers.IntegerField(source='item_count', read_only=True)
level = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = StockLocation model = StockLocation
fields = [ fields = [
'pk', 'pk',
'url', 'url',
'name', 'name',
'level',
'description', 'description',
'parent', 'parent',
'pathstring', 'pathstring',

View File

@ -227,20 +227,11 @@
{% endif %} {% endif %}
$("#stock-export").click(function() { $("#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 += "&cascade=" + response.cascade;
exportStock({
{% if location %} {% if location %}
url += "&location={{ location.id }}"; location: {{ location.pk }}
{% endif %} {% endif %}
location.href = url;
}
}); });
}); });

View File

@ -56,7 +56,6 @@ stock_urls = [
url(r'^track/', include(stock_tracking_urls)), url(r'^track/', include(stock_tracking_urls)),
url(r'^export-options/?', views.StockExportOptions.as_view(), name='stock-export-options'),
url(r'^export/?', views.StockExport.as_view(), name='stock-export'), url(r'^export/?', views.StockExport.as_view(), name='stock-export'),
# Individual stock items # Individual stock items

View File

@ -378,38 +378,6 @@ class StockItemDeleteTestData(AjaxUpdateView):
return self.renderJsonResponse(request, form, data) return self.renderJsonResponse(request, form, data)
class StockExportOptions(AjaxView):
""" Form for selecting StockExport options """
model = StockLocation
ajax_form_title = _('Stock Export Options')
form_class = StockForms.ExportOptionsForm
def post(self, request, *args, **kwargs):
self.request = request
fmt = request.POST.get('file_format', 'csv').lower()
cascade = str2bool(request.POST.get('include_sublocations', False))
# Format a URL to redirect to
url = reverse('stock-export')
url += '?format=' + fmt
url += '&cascade=' + str(cascade)
data = {
'form_valid': True,
'format': fmt,
'cascade': cascade
}
return self.renderJsonResponse(self.request, self.form_class(), data=data)
def get(self, request, *args, **kwargs):
return self.renderJsonResponse(request, self.form_class())
class StockExport(AjaxView): class StockExport(AjaxView):
""" Export stock data from a particular location. """ Export stock data from a particular location.
Returns a file containing stock information for that location. Returns a file containing stock information for that location.
@ -471,11 +439,10 @@ class StockExport(AjaxView):
) )
if location: if location:
# CHeck if locations should be cascading # Check if locations should be cascading
cascade = str2bool(request.GET.get('cascade', True)) cascade = str2bool(request.GET.get('cascade', True))
stock_items = location.get_stock_items(cascade) stock_items = location.get_stock_items(cascade)
else: else:
cascade = True
stock_items = StockItem.objects.all() stock_items = StockItem.objects.all()
if part: if part:

View File

@ -23,7 +23,8 @@
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_FORMS" icon="fa-dollar-sign" %} {% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_FORMS" icon="fa-dollar-sign" %}
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_RELATED" icon="fa-random" %} {% include "InvenTree/settings/setting.html" with key="PART_SHOW_RELATED" icon="fa-random" %}
{% include "InvenTree/settings/setting.html" with key="PART_RECENT_COUNT" icon="fa-clock" %} {% include "InvenTree/settings/setting.html" with key="PART_RECENT_COUNT" icon="fa-clock" %}
<tr><td colspan='5 '></td></tr> {% include "InvenTree/settings/setting.html" with key="PART_CREATE_INITIAL" icon="fa-boxes" %}
<tr><td colspan='5'></td></tr>
{% include "InvenTree/settings/setting.html" with key="PART_TEMPLATE" icon="fa-clone" %} {% include "InvenTree/settings/setting.html" with key="PART_TEMPLATE" icon="fa-clone" %}
{% include "InvenTree/settings/setting.html" with key="PART_ASSEMBLY" icon="fa-tools" %} {% include "InvenTree/settings/setting.html" with key="PART_ASSEMBLY" icon="fa-tools" %}
{% include "InvenTree/settings/setting.html" with key="PART_COMPONENT" icon="fa-th"%} {% include "InvenTree/settings/setting.html" with key="PART_COMPONENT" icon="fa-th"%}

View File

@ -1,6 +1,142 @@
{% load i18n %} {% load i18n %}
function manufacturerPartFields() {
return {
part: {},
manufacturer: {},
MPN: {
icon: 'fa-hashtag',
},
description: {},
link: {
icon: 'fa-link',
}
};
}
function createManufacturerPart(options={}) {
var fields = manufacturerPartFields();
if (options.part) {
fields.part.value = options.part;
fields.part.hidden = true;
}
if (options.manufacturer) {
fields.manufacturer.value = options.manufacturer;
}
constructForm('{% url "api-manufacturer-part-list" %}', {
fields: fields,
method: 'POST',
title: '{% trans "Add Manufacturer Part" %}',
onSuccess: options.onSuccess
});
}
function editManufacturerPart(part, options={}) {
var url = `/api/company/part/manufacturer/${part}/`;
constructForm(url, {
fields: manufacturerPartFields(),
title: '{% trans "Edit Manufacturer Part" %}',
onSuccess: options.onSuccess
});
}
function deleteManufacturerPart(part, options={}) {
constructForm(`/api/company/part/manufacturer/${part}/`, {
method: 'DELETE',
title: '{% trans "Delete Manufacturer Part" %}',
onSuccess: options.onSuccess,
});
}
function supplierPartFields() {
return {
part: {},
supplier: {},
SKU: {
icon: 'fa-hashtag',
},
manufacturer_part: {
filters: {
part_detail: true,
manufacturer_detail: true,
}
},
description: {},
link: {
icon: 'fa-link',
},
note: {
icon: 'fa-pencil-alt',
},
packaging: {
icon: 'fa-box',
}
};
}
/*
* Launch a form to create a new ManufacturerPart
*/
function createSupplierPart(options={}) {
var fields = supplierPartFields();
if (options.part) {
fields.manufacturer_part.filters.part = options.part;
fields.part.hidden = true;
fields.part.value = options.part;
}
if (options.supplier) {
fields.supplier.value = options.supplier;
}
if (options.manufacturer_part) {
fields.manufacturer_part.value = options.manufacturer_part;
}
constructForm('{% url "api-supplier-part-list" %}', {
fields: fields,
method: 'POST',
title: '{% trans "Add Supplier Part" %}',
onSuccess: options.onSuccess,
});
}
function editSupplierPart(part, options={}) {
constructForm(`/api/company/part/${part}/`, {
fields: supplierPartFields(),
title: '{% trans "Edit Supplier Part" %}',
onSuccess: options.onSuccess
});
}
function deleteSupplierPart(part, options={}) {
constructForm(`/api/company/part/${part}/`, {
method: 'DELETE',
title: '{% trans "Delete Supplier Part" %}',
onSuccess: options.onSuccess,
});
}
// Returns a default form-set for creating / editing a Company object // Returns a default form-set for creating / editing a Company object
function companyFormFields(options={}) { function companyFormFields(options={}) {
@ -323,8 +459,52 @@ function loadManufacturerPartTable(table, url, options) {
title: '{% trans "Description" %}', title: '{% trans "Description" %}',
sortable: false, sortable: false,
switchable: true, switchable: true,
},
{
field: 'actions',
title: '',
sortable: false,
switchable: false,
formatter: function(value, row) {
var pk = row.pk;
var html = `<div class='btn-group float-right' role='group'>`;
html += makeIconButton('fa-edit icon-blue', 'button-manufacturer-part-edit', pk, '{% trans "Edit manufacturer part" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-manufacturer-part-delete', pk, '{% trans "Delete manufacturer part" %}');
html += '</div>';
return html;
}
} }
], ],
onPostBody: function() {
// Callbacks
$(table).find('.button-manufacturer-part-edit').click(function() {
var pk = $(this).attr('pk');
editManufacturerPart(
pk,
{
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
});
$(table).find('.button-manufacturer-part-delete').click(function() {
var pk = $(this).attr('pk');
deleteManufacturerPart(
pk,
{
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
})
}
}); });
} }
@ -570,7 +750,51 @@ function loadSupplierPartTable(table, url, options) {
field: 'packaging', field: 'packaging',
title: '{% trans "Packaging" %}', title: '{% trans "Packaging" %}',
sortable: false, sortable: false,
},
{
field: 'actions',
title: '',
sortable: false,
switchable: false,
formatter: function(value, row) {
var pk = row.pk;
var html = `<div class='btn-group float-right' role='group'>`;
html += makeIconButton('fa-edit icon-blue', 'button-supplier-part-edit', pk, '{% trans "Edit supplier part" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-supplier-part-delete', pk, '{% trans "Delete supplier part" %}');
html += '</div>';
return html;
}
} }
], ],
onPostBody: function() {
// Callbacks
$(table).find('.button-supplier-part-edit').click(function() {
var pk = $(this).attr('pk');
editSupplierPart(
pk,
{
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
});
$(table).find('.button-supplier-part-delete').click(function() {
var pk = $(this).attr('pk');
deleteSupplierPart(
pk,
{
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
})
}
}); });
} }

View File

@ -252,6 +252,11 @@ function constructDeleteForm(fields, options) {
*/ */
function constructForm(url, options) { function constructForm(url, options) {
// An "empty" form will be defined locally
if (url == null) {
constructFormBody({}, options);
}
// Save the URL // Save the URL
options.url = url; options.url = url;
@ -378,6 +383,11 @@ function constructFormBody(fields, options) {
fields[field].placeholder = field_options.placeholder; fields[field].placeholder = field_options.placeholder;
} }
// Choices
if (field_options.choices) {
fields[field].choices = field_options.choices;
}
// Field prefix // Field prefix
if (field_options.prefix) { if (field_options.prefix) {
fields[field].prefix = field_options.prefix; fields[field].prefix = field_options.prefix;
@ -1113,7 +1123,7 @@ function initializeRelatedField(name, field, options) {
var pk = field.value; var pk = field.value;
var url = `${field.api_url}/${pk}/`.replace('//', '/'); var url = `${field.api_url}/${pk}/`.replace('//', '/');
inventreeGet(url, {}, { inventreeGet(url, field.filters || {}, {
success: function(data) { success: function(data) {
setRelatedFieldData(name, data, options); setRelatedFieldData(name, data, options);
} }
@ -1211,6 +1221,9 @@ function renderModelData(name, model, data, parameters, options) {
case 'partparametertemplate': case 'partparametertemplate':
renderer = renderPartParameterTemplate; renderer = renderPartParameterTemplate;
break; break;
case 'manufacturerpart':
renderer = renderManufacturerPart;
break;
case 'supplierpart': case 'supplierpart':
renderer = renderSupplierPart; renderer = renderSupplierPart;
break; break;

View File

@ -765,6 +765,9 @@ function attachSecondaryModal(modal, options) {
function attachSecondaries(modal, secondaries) { function attachSecondaries(modal, secondaries) {
/* Attach a provided list of secondary modals */ /* Attach a provided list of secondary modals */
// 2021-07-18 - Secondary modals will be disabled for now, until they are re-implemented in the "API forms" architecture
return;
for (var i = 0; i < secondaries.length; i++) { for (var i = 0; i < secondaries.length; i++) {
attachSecondaryModal(modal, secondaries[i]); attachSecondaryModal(modal, secondaries[i]);
} }

View File

@ -67,7 +67,9 @@ function renderStockItem(name, data, parameters, options) {
// Renderer for "StockLocation" model // Renderer for "StockLocation" model
function renderStockLocation(name, data, parameters, options) { function renderStockLocation(name, data, parameters, options) {
var html = `<span>${data.name}</span>`; var level = '- '.repeat(data.level);
var html = `<span>${level}${data.pathstring}</span>`;
if (data.description) { if (data.description) {
html += ` - <i>${data.description}</i>`; html += ` - <i>${data.description}</i>`;
@ -75,10 +77,6 @@ function renderStockLocation(name, data, parameters, options) {
html += `<span class='float-right'>{% trans "Location ID" %}: ${data.pk}</span>`; html += `<span class='float-right'>{% trans "Location ID" %}: ${data.pk}</span>`;
if (data.pathstring) {
html += `<p><small>${data.pathstring}</small></p>`;
}
return html; return html;
} }
@ -154,7 +152,9 @@ function renderOwner(name, data, parameters, options) {
// Renderer for "PartCategory" model // Renderer for "PartCategory" model
function renderPartCategory(name, data, parameters, options) { function renderPartCategory(name, data, parameters, options) {
var html = `<span><b>${data.name}</b></span>`; var level = '- '.repeat(data.level);
var html = `<span>${level}${data.pathstring}</span>`;
if (data.description) { if (data.description) {
html += ` - <i>${data.description}</i>`; html += ` - <i>${data.description}</i>`;
@ -162,10 +162,6 @@ function renderPartCategory(name, data, parameters, options) {
html += `<span class='float-right'>{% trans "Category ID" %}: ${data.pk}</span>`; html += `<span class='float-right'>{% trans "Category ID" %}: ${data.pk}</span>`;
if (data.pathstring) {
html += `<p><small>${data.pathstring}</small></p>`;
}
return html; 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 += ` <span><b>${data.manufacturer_detail.name}</b> - ${data.MPN}</span>`;
html += ` - <i>${data.part_detail.full_name}</i>`;
html += `<span class='float-right'>{% trans "Manufacturer Part ID" %}: ${data.pk}</span>`;
return html;
}
// Renderer for "SupplierPart" model
function renderSupplierPart(name, data, parameters, options) { function renderSupplierPart(name, data, parameters, options) {
var supplier_image = null; var supplier_image = null;

View File

@ -14,6 +14,7 @@ function createSalesOrder(options={}) {
customer: { customer: {
value: options.customer, value: options.customer,
}, },
customer_reference: {},
description: {}, description: {},
target_date: { target_date: {
icon: 'fa-calendar-alt', icon: 'fa-calendar-alt',
@ -44,6 +45,7 @@ function createPurchaseOrder(options={}) {
supplier: { supplier: {
value: options.supplier, value: options.supplier,
}, },
supplier_reference: {},
description: {}, description: {},
target_date: { target_date: {
icon: 'fa-calendar-alt', icon: 'fa-calendar-alt',

View File

@ -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 * 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); launchModalForm("{% url 'stock-item-create' %}", options);
} }

View File

@ -1,5 +1,11 @@
{% load i18n %} {% load i18n %}
function reloadtable(table) {
$(table).bootstrapTable('refresh');
}
function editButton(url, text='Edit') { function editButton(url, text='Edit') {
return "<button class='btn btn-success edit-button btn-sm' type='button' url='" + url + "'>" + text + "</button>"; return "<button class='btn btn-success edit-button btn-sm' type='button' url='" + url + "'>" + text + "</button>";
} }