Adds form to assign stock item by serial numbers

This commit is contained in:
Oliver Walters 2021-03-29 23:10:36 +11:00
parent cffe2ba84b
commit bd87f4c733
5 changed files with 133 additions and 11 deletions

View File

@ -14,6 +14,8 @@ from InvenTree.forms import HelperForm
from InvenTree.fields import RoundingDecimalFormField
from InvenTree.fields import DatePickerFormField
import part.models
from stock.models import StockLocation
from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderAttachment
from .models import SalesOrder, SalesOrderLineItem, SalesOrderAttachment
@ -211,22 +213,43 @@ class EditSalesOrderLineItemForm(HelperForm):
]
class CreateSalesOrderAllocationForm(HelperForm):
class AllocateSerialsToSalesOrderForm(HelperForm):
"""
Form for creating a SalesOrderAllocation item.
This can be allocated by selecting a specific stock item,
or by providing a sequence of serial numbers
Form for assigning stock to a sales order,
by serial number lookup
"""
quantity = RoundingDecimalFormField(max_digits = 10, decimal_places=5)
line = forms.ModelChoiceField(
queryset = SalesOrderLineItem.objects.all(),
)
part = forms.ModelChoiceField(
queryset = part.models.Part.objects.all(),
)
serials = forms.CharField(
label=_("Serial Numbers"),
required=False,
help_text=_('Enter stock serial numbers'),
help_text=_('Enter stock item serial numbers'),
)
class Meta:
model = SalesOrderAllocation
fields = [
'line',
'part',
'serials',
]
class CreateSalesOrderAllocationForm(HelperForm):
"""
Form for creating a SalesOrderAllocation item.
"""
quantity = RoundingDecimalFormField(max_digits = 10, decimal_places=5)
class Meta:
model = SalesOrderAllocation

View File

@ -732,6 +732,12 @@ class SalesOrderAllocation(models.Model):
errors = {}
try:
if not self.item:
raise ValidationError({'item': _('Stock item has not been assigned')})
except stock_models.StockItem.DoesNotExist:
raise ValidationError({'item': _('Stock item has not been assigned')})
try:
if not self.line.part == self.item.part:
errors['item'] = _('Cannot allocate stock item to a line with a different part')

View File

@ -275,15 +275,20 @@ $("#so-lines-table").inventreeTable({
if (row.part) {
var part = row.part_detail;
if (part.trackable) {
html += makeIconButton('fa-hashtag icon-green', 'button-add-by-sn', pk, '{% trans "Allocate serial numbers" %}');
}
html += makeIconButton('fa-sign-in-alt icon-green', 'button-add', pk, '{% trans "Allocate stock" %}');
if (part.purchaseable) {
html += makeIconButton('fa-shopping-cart', 'button-buy', row.part, '{% trans "Buy parts" %}');
html += makeIconButton('fa-shopping-cart', 'button-buy', row.part, '{% trans "Purchase stock" %}');
}
if (part.assembly) {
html += makeIconButton('fa-tools', 'button-build', row.part, '{% trans "Build parts" %}');
html += makeIconButton('fa-tools', 'button-build', row.part, '{% trans "Build stock" %}');
}
html += makeIconButton('fa-plus icon-green', 'button-add', pk, '{% trans "Allocate parts" %}');
}
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}');
@ -316,10 +321,28 @@ function setupCallbacks() {
var pk = $(this).attr('pk');
launchModalForm(`/order/sales-order/line/${pk}/delete/`, {
reload: true,
success: reloadTable,
});
});
table.find(".button-add-by-sn").click(function() {
var pk = $(this).attr('pk');
inventreeGet(`/api/order/so-line/${pk}/`, {},
{
success: function(response) {
launchModalForm('{% url "so-assign-serials" %}', {
success: reloadTable,
data: {
line: pk,
part: response.part,
}
});
}
}
);
});
table.find(".button-add").click(function() {
var pk = $(this).attr('pk');

View File

@ -81,6 +81,7 @@ sales_order_urls = [
# URLs for sales order allocations
url(r'^allocation/', include([
url(r'^new/', views.SalesOrderAllocationCreate.as_view(), name='so-allocation-create'),
url(r'^assign-serials/', views.SalesOrderAssignSerials.as_view(), name='so-assign-serials'),
url(r'(?P<pk>\d+)/', include([
url(r'^edit/', views.SalesOrderAllocationEdit.as_view(), name='so-allocation-edit'),
url(r'^delete/', views.SalesOrderAllocationDelete.as_view(), name='so-allocation-delete'),

View File

@ -1291,6 +1291,75 @@ class SOLineItemDelete(AjaxDeleteView):
}
class SalesOrderAssignSerials(AjaxCreateView):
"""
View for assigning stock items to a sales order,
by serial number lookup.
"""
model = SalesOrderAllocation
role_required = 'sales_order.change'
ajax_form_title = _('Allocate Serial Numbers')
form_class = order_forms.AllocateSerialsToSalesOrderForm
# Keep track of SalesOrderLineItem and Part references
line = None
part = None
def get_initial(self):
"""
Initial values are passed as query params
"""
initials = super().get_initial()
try:
self.line = SalesOrderLineItem.objects.get(pk=self.request.GET.get('line', None))
initials['line'] = self.line
except (ValueError, SalesOrderLineItem.DoesNotExist):
pass
try:
self.part = Part.objects.get(pk=self.request.GET.get('part', None))
initials['part'] = self.part
except (ValueError, Part.DoesNotExist):
pass
return initials
def get_form(self):
form = super().get_form()
if self.line is not None:
form.fields['line'].widget = HiddenInput()
# Hide the 'part' field if value provided
try:
print(form['part'])
# self.part = Part.objects.get(form['part'].value())
except (ValueError, Part.DoesNotExist):
self.part = None
if self.part is not None:
form.fields['part'].widget = HiddenInput()
return form
def get_context_data(self):
return {
'line': self.line,
'part': self.part,
}
def get(self, request, *args, **kwargs):
return self.renderJsonResponse(
request,
self.get_form(),
context=self.get_context_data(),
)
class SalesOrderAllocationCreate(AjaxCreateView):
""" View for creating a new SalesOrderAllocation """