mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
60fe54ac07
@ -10,6 +10,9 @@
|
|||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
{% trans "Build Notes" %}
|
{% trans "Build Notes" %}
|
||||||
|
{% if roles.build.change and not editing %}
|
||||||
|
<button title='{% trans "Edit notes" %}' class='btn btn-default' id='edit-notes'><span class='fas fa-edit'></span></button>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block details %}
|
{% block details %}
|
||||||
@ -20,14 +23,13 @@
|
|||||||
|
|
||||||
{{ form }}
|
{{ form }}
|
||||||
<hr>
|
<hr>
|
||||||
<input type="submit" value='{% trans "Save" %}'/>
|
<button type="submit" class='btn btn-default'>{% trans "Save" %}</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{{ form.media }}
|
{{ form.media }}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<button title='{% trans "Edit notes" %}' class='btn btn-default action-button' id='edit-notes'><span class='fas fa-edit'></span></button>
|
|
||||||
|
|
||||||
{{ build.notes | markdownify }}
|
{{ build.notes | markdownify }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -9,6 +9,9 @@
|
|||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
{% trans "Company Notes" %}
|
{% trans "Company Notes" %}
|
||||||
|
{% if not editing %}
|
||||||
|
<button title='{% trans "Edit notes" %}' class='btn btn-default' id='edit-notes'><span class='fas fa-edit'></span></button>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block details %}
|
{% block details %}
|
||||||
@ -18,7 +21,7 @@
|
|||||||
|
|
||||||
{{ form }}
|
{{ form }}
|
||||||
<hr>
|
<hr>
|
||||||
<input type="submit" value='{% trans "Save" %}'/>
|
<button type="submit" class='btn btn-default'>{% trans "Save" %}</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -26,7 +29,6 @@
|
|||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
<button title='{% trans "Edit notes" %}' class='btn btn-default action-button' id='edit-notes'><span class='fas fa-edit'></span></button>
|
|
||||||
{{ company.notes | markdownify }}
|
{{ company.notes | markdownify }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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,7 +213,65 @@ class EditSalesOrderLineItemForm(HelperForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class AllocateSerialsToSalesOrderForm(forms.Form):
|
||||||
|
"""
|
||||||
|
Form for assigning stock to a sales order,
|
||||||
|
by serial number lookup
|
||||||
|
"""
|
||||||
|
|
||||||
|
line = forms.ModelChoiceField(
|
||||||
|
queryset=SalesOrderLineItem.objects.all(),
|
||||||
|
)
|
||||||
|
|
||||||
|
part = forms.ModelChoiceField(
|
||||||
|
queryset=part.models.Part.objects.all(),
|
||||||
|
)
|
||||||
|
|
||||||
|
serials = forms.CharField(
|
||||||
|
label=_("Serial Numbers"),
|
||||||
|
required=True,
|
||||||
|
help_text=_('Enter stock item serial numbers'),
|
||||||
|
)
|
||||||
|
|
||||||
|
quantity = forms.IntegerField(
|
||||||
|
label=_('Quantity'),
|
||||||
|
required=True,
|
||||||
|
help_text=_('Enter quantity of stock items'),
|
||||||
|
initial=1,
|
||||||
|
min_value=1
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
'line',
|
||||||
|
'part',
|
||||||
|
'serials',
|
||||||
|
'quantity',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CreateSalesOrderAllocationForm(HelperForm):
|
||||||
|
"""
|
||||||
|
Form for creating a SalesOrderAllocation item.
|
||||||
|
"""
|
||||||
|
|
||||||
|
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SalesOrderAllocation
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
'line',
|
||||||
|
'item',
|
||||||
|
'quantity',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class EditSalesOrderAllocationForm(HelperForm):
|
class EditSalesOrderAllocationForm(HelperForm):
|
||||||
|
"""
|
||||||
|
Form for editing a SalesOrderAllocation item
|
||||||
|
"""
|
||||||
|
|
||||||
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5)
|
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5)
|
||||||
|
|
||||||
|
17
InvenTree/order/migrations/0043_auto_20210330_0013.py
Normal file
17
InvenTree/order/migrations/0043_auto_20210330_0013.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2021-03-29 13:13
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('order', '0042_auto_20210310_1619'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='salesorderlineitem',
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
]
|
@ -663,7 +663,6 @@ class SalesOrderLineItem(OrderLineItem):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [
|
unique_together = [
|
||||||
('order', 'part'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def fulfilled_quantity(self):
|
def fulfilled_quantity(self):
|
||||||
@ -732,6 +731,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')
|
||||||
|
@ -11,6 +11,9 @@
|
|||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
{% trans "Order Notes" %}
|
{% trans "Order Notes" %}
|
||||||
|
{% if roles.purchase_order.change and not editing %}
|
||||||
|
<button title='{% trans "Edit notes" %}' class='btn btn-default' id='edit-notes'><span class='fas fa-edit'></span></button>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block details %}
|
{% block details %}
|
||||||
@ -21,21 +24,19 @@
|
|||||||
|
|
||||||
{{ form }}
|
{{ form }}
|
||||||
<hr>
|
<hr>
|
||||||
<input type='submit' value='{% trans "Save" %}'/>
|
<button type="submit" class='btn btn-default'>{% trans "Save" %}</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{{ form.media }}
|
{{ form.media }}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if roles.purchase_order.change %}
|
|
||||||
<button title='{% trans "Edit notes" %}' class='btn btn-default action-button' id='edit-notes'><span class='fas fa-edit'></span></button>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
<div class='panel panel-default'>
|
<div class='panel panel-default'>
|
||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
{{ order.notes | markdownify }}
|
{{ order.notes | markdownify }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -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');
|
||||||
|
|
||||||
|
@ -12,6 +12,9 @@
|
|||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
{% trans "Sales Order Notes" %}
|
{% trans "Sales Order Notes" %}
|
||||||
|
{% if roles.sales_order.change and not editing %}
|
||||||
|
<button title='{% trans "Edit notes" %}' class='btn btn-default' id='edit-notes'><span class='fas fa-edit'></span></button>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block details %}
|
{% block details %}
|
||||||
@ -23,13 +26,12 @@
|
|||||||
|
|
||||||
{{ form }}
|
{{ form }}
|
||||||
<hr>
|
<hr>
|
||||||
<input type='submit' value='{% trans "Save" %}'/>
|
<button type="submit" class='btn btn-default'>{% trans "Save" %}</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{{ form.media }}
|
{{ form.media }}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<button title='{% trans "Edit notes" %}' class='btn btn-default action-button float-right' id='edit-notes'><span class='fas fa-edit'></span></button>
|
|
||||||
<div class='panel panel-default'>
|
<div class='panel panel-default'>
|
||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
{{ order.notes | markdownify }}
|
{{ order.notes | markdownify }}
|
||||||
|
12
InvenTree/order/templates/order/so_allocate_by_serial.html
Normal file
12
InvenTree/order/templates/order/so_allocate_by_serial.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% extends "modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block pre_form_content %}
|
||||||
|
|
||||||
|
<div class='alert alert-block alert-info'>
|
||||||
|
{% include "hover_image.html" with image=part.image hover=true %}{{ part }}
|
||||||
|
<hr>
|
||||||
|
{% trans "Allocate stock items by serial number" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -3,7 +3,6 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.utils import IntegrityError
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
@ -73,10 +72,10 @@ class SalesOrderTest(TestCase):
|
|||||||
self.assertFalse(self.order.is_fully_allocated())
|
self.assertFalse(self.order.is_fully_allocated())
|
||||||
|
|
||||||
def test_add_duplicate_line_item(self):
|
def test_add_duplicate_line_item(self):
|
||||||
# Adding a duplicate line item to a SalesOrder must throw an error
|
# Adding a duplicate line item to a SalesOrder is accepted
|
||||||
|
|
||||||
with self.assertRaises(IntegrityError):
|
for ii in range(1, 5):
|
||||||
SalesOrderLineItem.objects.create(order=self.order, part=self.part)
|
SalesOrderLineItem.objects.create(order=self.order, part=self.part, quantity=ii)
|
||||||
|
|
||||||
def allocate_stock(self, full=True):
|
def allocate_stock(self, full=True):
|
||||||
|
|
||||||
|
@ -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'),
|
||||||
|
@ -7,9 +7,11 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.views.generic import DetailView, ListView, UpdateView
|
from django.views.generic import DetailView, ListView, UpdateView
|
||||||
|
from django.views.generic.edit import FormMixin
|
||||||
from django.forms import HiddenInput
|
from django.forms import HiddenInput
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@ -30,6 +32,7 @@ from . import forms as order_forms
|
|||||||
|
|
||||||
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
||||||
from InvenTree.helpers import DownloadFile, str2bool
|
from InvenTree.helpers import DownloadFile, str2bool
|
||||||
|
from InvenTree.helpers import extract_serial_numbers
|
||||||
from InvenTree.views import InvenTreeRoleMixin
|
from InvenTree.views import InvenTreeRoleMixin
|
||||||
|
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus
|
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus
|
||||||
@ -1291,11 +1294,179 @@ class SOLineItemDelete(AjaxDeleteView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SalesOrderAssignSerials(AjaxView, FormMixin):
|
||||||
|
"""
|
||||||
|
View for assigning stock items to a sales order,
|
||||||
|
by serial number lookup.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = SalesOrderAllocation
|
||||||
|
role_required = 'sales_order.change'
|
||||||
|
ajax_template_name = 'order/so_allocate_by_serial.html'
|
||||||
|
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 post(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
self.form = self.get_form()
|
||||||
|
|
||||||
|
# Validate the form
|
||||||
|
self.form.is_valid()
|
||||||
|
self.validate()
|
||||||
|
|
||||||
|
valid = self.form.is_valid()
|
||||||
|
|
||||||
|
if valid:
|
||||||
|
self.allocate_items()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'form_valid': valid,
|
||||||
|
'form_errors': self.form.errors.as_json(),
|
||||||
|
'non_field_errors': self.form.non_field_errors().as_json(),
|
||||||
|
'success': _("Allocated") + f" {len(self.stock_items)} " + _("items")
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.renderJsonResponse(request, self.form, data)
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
|
||||||
|
data = self.form.cleaned_data
|
||||||
|
|
||||||
|
# Extract hidden fields from posted data
|
||||||
|
self.line = data.get('line', None)
|
||||||
|
self.part = data.get('part', None)
|
||||||
|
|
||||||
|
if self.line:
|
||||||
|
self.form.fields['line'].widget = HiddenInput()
|
||||||
|
else:
|
||||||
|
self.form.add_error('line', _('Select line item'))
|
||||||
|
|
||||||
|
if self.part:
|
||||||
|
self.form.fields['part'].widget = HiddenInput()
|
||||||
|
else:
|
||||||
|
self.form.add_error('part', _('Select part'))
|
||||||
|
|
||||||
|
if not self.form.is_valid():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Form is otherwise valid - check serial numbers
|
||||||
|
serials = data.get('serials', '')
|
||||||
|
quantity = data.get('quantity', 1)
|
||||||
|
|
||||||
|
# Save a list of serial_numbers
|
||||||
|
self.serial_numbers = None
|
||||||
|
self.stock_items = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.serial_numbers = extract_serial_numbers(serials, quantity)
|
||||||
|
|
||||||
|
for serial in self.serial_numbers:
|
||||||
|
try:
|
||||||
|
# Find matching stock item
|
||||||
|
stock_item = StockItem.objects.get(
|
||||||
|
part=self.part,
|
||||||
|
serial=serial
|
||||||
|
)
|
||||||
|
except StockItem.DoesNotExist:
|
||||||
|
self.form.add_error(
|
||||||
|
'serials',
|
||||||
|
_('No matching item for serial') + f" '{serial}'"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Now we have a valid stock item - but can it be added to the sales order?
|
||||||
|
|
||||||
|
# If not in stock, cannot be added to the order
|
||||||
|
if not stock_item.in_stock:
|
||||||
|
self.form.add_error(
|
||||||
|
'serials',
|
||||||
|
f"'{serial}' " + _("is not in stock")
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Already allocated to an order
|
||||||
|
if stock_item.is_allocated():
|
||||||
|
self.form.add_error(
|
||||||
|
'serials',
|
||||||
|
f"'{serial}' " + _("already allocated to an order")
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add it to the list!
|
||||||
|
self.stock_items.append(stock_item)
|
||||||
|
|
||||||
|
except ValidationError as e:
|
||||||
|
self.form.add_error('serials', e.messages)
|
||||||
|
|
||||||
|
def allocate_items(self):
|
||||||
|
"""
|
||||||
|
Create stock item allocations for each selected serial number
|
||||||
|
"""
|
||||||
|
|
||||||
|
for stock_item in self.stock_items:
|
||||||
|
SalesOrderAllocation.objects.create(
|
||||||
|
item=stock_item,
|
||||||
|
line=self.line,
|
||||||
|
quantity=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_form(self):
|
||||||
|
|
||||||
|
form = super().get_form()
|
||||||
|
|
||||||
|
if self.line:
|
||||||
|
form.fields['line'].widget = HiddenInput()
|
||||||
|
|
||||||
|
if self.part:
|
||||||
|
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 """
|
||||||
|
|
||||||
model = SalesOrderAllocation
|
model = SalesOrderAllocation
|
||||||
form_class = order_forms.EditSalesOrderAllocationForm
|
form_class = order_forms.CreateSalesOrderAllocationForm
|
||||||
ajax_form_title = _('Allocate Stock to Order')
|
ajax_form_title = _('Allocate Stock to Order')
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
|
@ -10,35 +10,34 @@
|
|||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
{% trans "Part Notes" %}
|
{% trans "Part Notes" %}
|
||||||
|
{% if roles.part.change and not editing %}
|
||||||
|
<button title='{% trans "Edit notes" %}' class='btn btn-default' id='edit-notes'><span class='fas fa-edit'></span></button>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block details %}
|
{% block details %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% if editing %}
|
{% if editing %}
|
||||||
<form method='POST'>
|
<form method='POST'>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{{ form }}
|
{{ form }}
|
||||||
<hr>
|
<hr>
|
||||||
<input type="submit" value='{% trans "Save" %}'/>
|
|
||||||
|
<button type="submit" class='btn btn-default'>{% trans "Save" %}</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{{ form.media }}
|
{{ form.media }}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if roles.part.change %}
|
|
||||||
<button title='{% trans "Edit notes" %}' class='btn btn-default action-button' id='edit-notes'><span class='fas fa-edit'></span></button>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class='panel panel-default'>
|
<div class='panel panel-default'>
|
||||||
|
{% if part.notes %}
|
||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
{% if part.notes %}
|
|
||||||
{{ part.notes | markdownify }}
|
{{ part.notes | markdownify }}
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -155,18 +155,24 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
|||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<button id='stock-actions' title='{% trans "Stock adjustment actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button>
|
<button id='stock-actions' title='{% trans "Stock adjustment actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button>
|
||||||
<ul class='dropdown-menu' role='menu'>
|
<ul class='dropdown-menu' role='menu'>
|
||||||
{% if item.can_adjust_location %}
|
|
||||||
{% if not item.serialized %}
|
{% if not item.serialized %}
|
||||||
|
{% if item.in_stock %}
|
||||||
<li><a href='#' id='stock-count' title='{% trans "Count stock" %}'><span class='fas fa-clipboard-list'></span> {% trans "Count stock" %}</a></li>
|
<li><a href='#' id='stock-count' title='{% trans "Count stock" %}'><span class='fas fa-clipboard-list'></span> {% trans "Count stock" %}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if not item.customer %}
|
||||||
<li><a href='#' id='stock-add' title='{% trans "Add stock" %}'><span class='fas fa-plus-circle icon-green'></span> {% trans "Add stock" %}</a></li>
|
<li><a href='#' id='stock-add' title='{% trans "Add stock" %}'><span class='fas fa-plus-circle icon-green'></span> {% trans "Add stock" %}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if item.in_stock %}
|
||||||
<li><a href='#' id='stock-remove' title='{% trans "Remove stock" %}'><span class='fas fa-minus-circle icon-red'></span> {% trans "Remove stock" %}</a></li>
|
<li><a href='#' id='stock-remove' title='{% trans "Remove stock" %}'><span class='fas fa-minus-circle icon-red'></span> {% trans "Remove stock" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if item.in_stock and item.can_adjust_location %}
|
||||||
<li><a href='#' id='stock-move' title='{% trans "Transfer stock" %}'><span class='fas fa-exchange-alt icon-blue'></span> {% trans "Transfer stock" %}</a></li>
|
<li><a href='#' id='stock-move' title='{% trans "Transfer stock" %}'><span class='fas fa-exchange-alt icon-blue'></span> {% trans "Transfer stock" %}</a></li>
|
||||||
{% if item.part.trackable and not item.serialized %}
|
{% endif %}
|
||||||
|
{% if item.in_stock and item.part.trackable %}
|
||||||
<li><a href='#' id='stock-serialize' title='{% trans "Serialize stock" %}'><span class='fas fa-hashtag'></span> {% trans "Serialize stock" %}</a> </li>
|
<li><a href='#' id='stock-serialize' title='{% trans "Serialize stock" %}'><span class='fas fa-hashtag'></span> {% trans "Serialize stock" %}</a> </li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if item.part.salable and not item.customer %}
|
{% if item.in_stock and item.can_adjust_location and item.part.salable and not item.customer %}
|
||||||
<li><a href='#' id='stock-assign-to-customer' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
|
<li><a href='#' id='stock-assign-to-customer' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if item.customer %}
|
{% if item.customer %}
|
||||||
@ -469,16 +475,6 @@ $("#barcode-scan-into-location").click(function() {
|
|||||||
scanItemsIntoLocation([{{ item.id }}]);
|
scanItemsIntoLocation([{{ item.id }}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
{% if item.in_stock %}
|
|
||||||
|
|
||||||
$("#stock-assign-to-customer").click(function() {
|
|
||||||
launchModalForm("{% url 'stock-item-assign' item.id %}",
|
|
||||||
{
|
|
||||||
reload: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
function itemAdjust(action) {
|
function itemAdjust(action) {
|
||||||
launchModalForm("/stock/adjust/",
|
launchModalForm("/stock/adjust/",
|
||||||
{
|
{
|
||||||
@ -492,6 +488,29 @@ function itemAdjust(action) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$('#stock-add').click(function() {
|
||||||
|
itemAdjust('add');
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#stock-delete").click(function () {
|
||||||
|
launchModalForm(
|
||||||
|
"{% url 'stock-item-delete' item.id %}",
|
||||||
|
{
|
||||||
|
redirect: "{% url 'part-stock' item.part.id %}"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
{% if item.in_stock %}
|
||||||
|
|
||||||
|
$("#stock-assign-to-customer").click(function() {
|
||||||
|
launchModalForm("{% url 'stock-item-assign' item.id %}",
|
||||||
|
{
|
||||||
|
reload: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
{% if item.part.has_variants %}
|
{% if item.part.has_variants %}
|
||||||
$("#stock-convert").click(function() {
|
$("#stock-convert").click(function() {
|
||||||
launchModalForm("{% url 'stock-item-convert' item.id %}",
|
launchModalForm("{% url 'stock-item-convert' item.id %}",
|
||||||
@ -514,10 +533,6 @@ $('#stock-remove').click(function() {
|
|||||||
itemAdjust('take');
|
itemAdjust('take');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#stock-add').click(function() {
|
|
||||||
itemAdjust('add');
|
|
||||||
});
|
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
$("#stock-return-from-customer").click(function() {
|
$("#stock-return-from-customer").click(function() {
|
||||||
@ -530,13 +545,4 @@ $("#stock-return-from-customer").click(function() {
|
|||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
$("#stock-delete").click(function () {
|
|
||||||
launchModalForm(
|
|
||||||
"{% url 'stock-item-delete' item.id %}",
|
|
||||||
{
|
|
||||||
redirect: "{% url 'part-stock' item.part.id %}"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -11,6 +11,9 @@
|
|||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
{% trans "Stock Item Notes" %}
|
{% trans "Stock Item Notes" %}
|
||||||
|
{% if roles.stock.change and not editing %}
|
||||||
|
<button title='{% trans "Edit notes" %}' class='btn btn-default' id='edit-notes'><span class='fas fa-edit'></span></button>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block details %}
|
{% block details %}
|
||||||
@ -20,13 +23,12 @@
|
|||||||
|
|
||||||
{{ form }}
|
{{ form }}
|
||||||
<hr>
|
<hr>
|
||||||
<input type='submit' value='{% trans "Save" %}'/>
|
<button type="submit" class='btn btn-default'>{% trans "Save" %}</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{{ form.media }}
|
{{ form.media }}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<button title='{% trans "Edit notes" %}' class='btn btn-default action-button' id='edit-notes'><span class='fas fa-edit'></span></button>
|
|
||||||
{% if item.notes %}
|
{% if item.notes %}
|
||||||
{{ item.notes | markdownify }}
|
{{ item.notes | markdownify }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -12,6 +12,11 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.db.models.signals import post_save, post_delete
|
from django.db.models.signals import post_save, post_delete
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RuleSet(models.Model):
|
class RuleSet(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -345,7 +350,7 @@ def update_group_roles(group, debug=False):
|
|||||||
content_type = ContentType.objects.get(app_label=app, model=model)
|
content_type = ContentType.objects.get(app_label=app, model=model)
|
||||||
permission = Permission.objects.get(content_type=content_type, codename=perm)
|
permission = Permission.objects.get(content_type=content_type, codename=perm)
|
||||||
except ContentType.DoesNotExist:
|
except ContentType.DoesNotExist:
|
||||||
raise ValueError(f"Error: Could not find permission matching '{permission_string}'")
|
logger.warning(f"Error: Could not find permission matching '{permission_string}'")
|
||||||
permission = None
|
permission = None
|
||||||
|
|
||||||
return permission
|
return permission
|
||||||
|
38
InvenTree/users/test_migrations.py
Normal file
38
InvenTree/users/test_migrations.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for the user model database migrations
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django_test_migrations.contrib.unittest_case import MigratorTestCase
|
||||||
|
|
||||||
|
from InvenTree import helpers
|
||||||
|
|
||||||
|
|
||||||
|
class TestForwardMigrations(MigratorTestCase):
|
||||||
|
"""
|
||||||
|
Test entire schema migration sequence for the users app
|
||||||
|
"""
|
||||||
|
|
||||||
|
migrate_from = ('users', helpers.getOldestMigrationFile('users'))
|
||||||
|
migrate_to = ('users', helpers.getNewestMigrationFile('users'))
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
|
||||||
|
User = self.old_state.apps.get_model('auth', 'user')
|
||||||
|
|
||||||
|
User.objects.create(
|
||||||
|
username='fred',
|
||||||
|
email='fred@fred.com',
|
||||||
|
password='password'
|
||||||
|
)
|
||||||
|
|
||||||
|
User.objects.create(
|
||||||
|
username='brad',
|
||||||
|
email='brad@fred.com',
|
||||||
|
password='password'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_users_exist(self):
|
||||||
|
|
||||||
|
User = self.new_state.apps.get_model('auth', 'user')
|
||||||
|
|
||||||
|
self.assertEqual(User.objects.count(), 2)
|
@ -1,7 +1,7 @@
|
|||||||
wheel>=0.34.2 # Wheel
|
wheel>=0.34.2 # Wheel
|
||||||
Django==3.0.7 # Django package
|
Django==3.0.7 # Django package
|
||||||
pillow==8.1.1 # Image manipulation
|
pillow==8.1.1 # Image manipulation
|
||||||
djangorestframework==3.10.3 # DRF framework
|
djangorestframework==3.11.2 # DRF framework
|
||||||
django-dbbackup==3.3.0 # Database backup / restore functionality
|
django-dbbackup==3.3.0 # Database backup / restore functionality
|
||||||
django-cors-headers==3.2.0 # CORS headers extension for DRF
|
django-cors-headers==3.2.0 # CORS headers extension for DRF
|
||||||
django_filter==2.2.0 # Extended filtering options
|
django_filter==2.2.0 # Extended filtering options
|
||||||
@ -10,7 +10,7 @@ django-sql-utils==0.5.0 # Advanced query annotation / aggregation
|
|||||||
django-markdownx==3.0.1 # Markdown form fields
|
django-markdownx==3.0.1 # Markdown form fields
|
||||||
django-markdownify==0.8.0 # Markdown rendering
|
django-markdownify==0.8.0 # Markdown rendering
|
||||||
coreapi==2.3.0 # API documentation
|
coreapi==2.3.0 # API documentation
|
||||||
pygments==2.2.0 # Syntax highlighting
|
pygments==2.7.4 # Syntax highlighting
|
||||||
tablib==0.13.0 # Import / export data files
|
tablib==0.13.0 # Import / export data files
|
||||||
django-crispy-forms==1.8.1 # Form helpers
|
django-crispy-forms==1.8.1 # Form helpers
|
||||||
django-import-export==2.0.0 # Data import / export for admin interface
|
django-import-export==2.0.0 # Data import / export for admin interface
|
||||||
|
Loading…
Reference in New Issue
Block a user