2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-03 04:00:57 +00:00

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 RoundingDecimalFormField
from InvenTree.fields import DatePickerFormField from InvenTree.fields import DatePickerFormField
import part.models
from stock.models import StockLocation from stock.models import StockLocation
from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderAttachment from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderAttachment
from .models import SalesOrder, SalesOrderLineItem, SalesOrderAttachment 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. Form for assigning stock to a sales order,
by serial number lookup
This can be allocated by selecting a specific stock item,
or by providing a sequence of serial numbers
""" """
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( serials = forms.CharField(
label=_("Serial Numbers"), label=_("Serial Numbers"),
required=False, 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: class Meta:
model = SalesOrderAllocation model = SalesOrderAllocation

View File

@ -732,6 +732,12 @@ class SalesOrderAllocation(models.Model):
errors = {} 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: try:
if not self.line.part == self.item.part: if not self.line.part == self.item.part:
errors['item'] = _('Cannot allocate stock item to a line with a different 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) { if (row.part) {
var part = row.part_detail; 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) { 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) { 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" %}'); html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}');
@ -316,10 +321,28 @@ function setupCallbacks() {
var pk = $(this).attr('pk'); var pk = $(this).attr('pk');
launchModalForm(`/order/sales-order/line/${pk}/delete/`, { 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() { table.find(".button-add").click(function() {
var pk = $(this).attr('pk'); var pk = $(this).attr('pk');

View File

@ -81,6 +81,7 @@ sales_order_urls = [
# URLs for sales order allocations # URLs for sales order allocations
url(r'^allocation/', include([ url(r'^allocation/', include([
url(r'^new/', views.SalesOrderAllocationCreate.as_view(), name='so-allocation-create'), 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'(?P<pk>\d+)/', include([
url(r'^edit/', views.SalesOrderAllocationEdit.as_view(), name='so-allocation-edit'), url(r'^edit/', views.SalesOrderAllocationEdit.as_view(), name='so-allocation-edit'),
url(r'^delete/', views.SalesOrderAllocationDelete.as_view(), name='so-allocation-delete'), 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): class SalesOrderAllocationCreate(AjaxCreateView):
""" View for creating a new SalesOrderAllocation """ """ View for creating a new SalesOrderAllocation """