Merge pull request #852 from SchrodingersGat/send-to-customer

Send to customer
This commit is contained in:
Oliver 2020-06-04 19:55:57 +10:00 committed by GitHub
commit 96b79397c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 190 additions and 36 deletions

View File

@ -346,6 +346,8 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
# Include context data about the updated object # Include context data about the updated object
data['pk'] = obj.id data['pk'] = obj.id
self.post_save(obj)
try: try:
data['url'] = obj.get_absolute_url() data['url'] = obj.get_absolute_url()
except AttributeError: except AttributeError:
@ -353,6 +355,13 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
return self.renderJsonResponse(request, form, data) return self.renderJsonResponse(request, form, data)
def post_save(self, obj, *args, **kwargs):
"""
Hook called after the form data is saved.
(Optional)
"""
pass
class AjaxDeleteView(AjaxMixin, UpdateView): class AjaxDeleteView(AjaxMixin, UpdateView):

View File

@ -632,24 +632,14 @@ class SalesOrderAllocation(models.Model):
order = self.line.order order = self.line.order
item = self.item item = self.item.allocateToCustomer(
order.customer,
quantity=self.quantity,
order=order,
user=user
)
# If the allocated quantity is less than the amount available, # Update our own reference to the StockItem
# then split the stock item into two lots # (It may have changed if the stock was split)
if item.quantity > self.quantity: self.item = item
self.save()
# Grab a copy of the new stock item (which will keep track of its "parent")
item = item.splitStock(self.quantity, None, user)
# Update our own reference to the new item
self.item = item
self.save()
# Assign the StockItem to the SalesOrder customer
item.sales_order = order
# Clear the location
item.location = None
item.status = StockStatus.SHIPPED
item.save()

View File

@ -34,6 +34,18 @@ class EditStockItemAttachmentForm(HelperForm):
] ]
class AssignStockItemToCustomerForm(HelperForm):
"""
Form for manually assigning a StockItem to a Customer
"""
class Meta:
model = StockItem
fields = [
'customer',
]
class EditStockItemTestResultForm(HelperForm): class EditStockItemTestResultForm(HelperForm):
""" """
Form for creating / editing a StockItemTestResult object. Form for creating / editing a StockItemTestResult object.

View File

@ -0,0 +1,20 @@
# Generated by Django 3.0.5 on 2020-06-04 03:20
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('company', '0021_remove_supplierpart_manufacturer_name'),
('stock', '0044_auto_20200528_1036'),
]
operations = [
migrations.AddField(
model_name='stockitem',
name='customer',
field=models.ForeignKey(help_text='Customer', limit_choices_to={'is_customer': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_stock', to='company.Company', verbose_name='Customer'),
),
]

View File

@ -32,6 +32,7 @@ from InvenTree.status_codes import StockStatus
from InvenTree.models import InvenTreeTree, InvenTreeAttachment from InvenTree.models import InvenTreeTree, InvenTreeAttachment
from InvenTree.fields import InvenTreeURLField from InvenTree.fields import InvenTreeURLField
from company import models as CompanyModels
from part import models as PartModels from part import models as PartModels
@ -217,12 +218,6 @@ class StockItem(MPTTModel):
super().clean() super().clean()
if self.status == StockStatus.SHIPPED and self.sales_order is None:
raise ValidationError({
'sales_order': "SalesOrder must be specified as status is marked as SHIPPED",
'status': "Status cannot be marked as SHIPPED if the Customer is not set",
})
if self.status == StockStatus.ASSIGNED_TO_OTHER_ITEM and self.belongs_to is None: if self.status == StockStatus.ASSIGNED_TO_OTHER_ITEM and self.belongs_to is None:
raise ValidationError({ raise ValidationError({
'belongs_to': "Belongs_to field must be specified as statis is marked as ASSIGNED_TO_OTHER_ITEM", 'belongs_to': "Belongs_to field must be specified as statis is marked as ASSIGNED_TO_OTHER_ITEM",
@ -352,6 +347,16 @@ class StockItem(MPTTModel):
help_text=_('Is this item installed in another item?') help_text=_('Is this item installed in another item?')
) )
customer = models.ForeignKey(
CompanyModels.Company,
on_delete=models.SET_NULL,
null=True,
limit_choices_to={'is_customer': True},
related_name='assigned_stock',
help_text=_("Customer"),
verbose_name=_("Customer"),
)
serial = models.PositiveIntegerField( serial = models.PositiveIntegerField(
verbose_name=_('Serial Number'), verbose_name=_('Serial Number'),
blank=True, null=True, blank=True, null=True,
@ -431,6 +436,64 @@ class StockItem(MPTTModel):
help_text=_('Stock Item Notes') help_text=_('Stock Item Notes')
) )
def clearAllocations(self):
"""
Clear all order allocations for this StockItem:
- SalesOrder allocations
- Build allocations
"""
# Delete outstanding SalesOrder allocations
self.sales_order_allocations.all().delete()
# Delete outstanding BuildOrder allocations
self.allocations.all().delete()
def allocateToCustomer(self, customer, quantity=None, order=None, user=None, notes=None):
"""
Allocate a StockItem to a customer.
This action can be called by the following processes:
- Completion of a SalesOrder
- User manually assigns a StockItem to the customer
Args:
customer: The customer (Company) to assign the stock to
quantity: Quantity to assign (if not supplied, total quantity is used)
order: SalesOrder reference
user: User that performed the action
notes: Notes field
"""
if quantity is None:
quantity = self.quantity
if quantity >= self.quantity:
item = self
else:
item = self.splitStock(quantity, None, user)
# Update StockItem fields with new information
item.sales_order = order
item.status = StockStatus.SHIPPED
item.customer = customer
item.location = None
item.save()
# TODO - Remove any stock item allocations from this stock item
item.addTransactionNote(
_("Assigned to Customer"),
user,
notes=_("Manually assigned to customer") + " " + customer.name,
system=True
)
# Return the reference to the stock item
return item
# If stock item is incoming, an (optional) ETA field # If stock item is incoming, an (optional) ETA field
# expected_arrival = models.DateField(null=True, blank=True) # expected_arrival = models.DateField(null=True, blank=True)

View File

@ -71,43 +71,48 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% include "qr_button.html" %} {% include "qr_button.html" %}
{% if item.in_stock %} {% if item.in_stock %}
{% if not item.serialized %} {% if not item.serialized %}
<button type='button' class='btn btn-default' id='stock-add' title='Add to stock'> <button type='button' class='btn btn-default' id='stock-add' title='{% trans "Add to stock" %}'>
<span class='fas fa-plus-circle icon-green'/> <span class='fas fa-plus-circle icon-green'/>
</button> </button>
<button type='button' class='btn btn-default' id='stock-remove' title='Take from stock'> <button type='button' class='btn btn-default' id='stock-remove' title='{% trans "Take from stock" %}'>
<span class='fas fa-minus-circle icon-red''/> <span class='fas fa-minus-circle icon-red''/>
</button> </button>
<button type='button' class='btn btn-default' id='stock-count' title='Count stock'> <button type='button' class='btn btn-default' id='stock-count' title='{% trans "Count stock" %}'>
<span class='fas fa-clipboard-list'/> <span class='fas fa-clipboard-list'/>
</button> </button>
{% if item.part.trackable %} {% if item.part.trackable %}
<button type='button' class='btn btn-default' id='stock-serialize' title='Serialize stock'> <button type='button' class='btn btn-default' id='stock-serialize' title='{% trans "Serialize stock" %}'>
<span class='fas fa-hashtag'/> <span class='fas fa-hashtag'/>
</button> </button>
{% endif %} {% endif %}
{% if item.part.salable %}
<button type='button' class='btn btn-default' id='stock-assign-to-customer' title='{% trans "Assign to Customer" %}'>
<span class='fas fa-user-tie'/>
</button>
{% endif %} {% endif %}
<button type='button' class='btn btn-default' id='stock-move' title='Transfer stock'> {% endif %}
<button type='button' class='btn btn-default' id='stock-move' title='{% trans "Transfer stock" %}'>
<span class='fas fa-exchange-alt icon-blue'/> <span class='fas fa-exchange-alt icon-blue'/>
</button> </button>
<button type='button' class='btn btn-default' id='stock-duplicate' title='Duplicate stock item'> <button type='button' class='btn btn-default' id='stock-duplicate' title='{% trans "Duplicate stock item" %}'>
<span class='fas fa-copy'/> <span class='fas fa-copy'/>
</button> </button>
{% endif %} {% endif %}
{% if item.part.has_variants %} {% if item.part.has_variants %}
<button type='button' class='btn btn-default' id='stock-convert' title="Convert stock to variant"> <button type='button' class='btn btn-default' id='stock-convert' title='{% trans "Convert stock to variant" %}'>
<span class='fas fa-screwdriver'/> <span class='fas fa-screwdriver'/>
</button> </button>
{% endif %} {% endif %}
{% if item.part.has_test_report_templates %} {% if item.part.has_test_report_templates %}
<button type='button' class='btn btn-default' id='stock-test-report' title='Generate test report'> <button type='button' class='btn btn-default' id='stock-test-report' title='{% trans "Generate test report" %}'>
<span class='fas fa-tasks'/> <span class='fas fa-tasks'/>
</button> </button>
{% endif %} {% endif %}
<button type='button' class='btn btn-default' id='stock-edit' title='Edit stock item'> <button type='button' class='btn btn-default' id='stock-edit' title='{% trans "Edit stock item" %}'>
<span class='fas fa-edit icon-blue'/> <span class='fas fa-edit icon-blue'/>
</button> </button>
{% if item.can_delete %} {% if item.can_delete %}
<button type='button' class='btn btn-default' id='stock-delete' title='Edit stock item'> <button type='button' class='btn btn-default' id='stock-delete' title='{% trans "Edit stock item" %}'>
<span class='fas fa-trash-alt icon-red'/> <span class='fas fa-trash-alt icon-red'/>
</button> </button>
{% endif %} {% endif %}
@ -126,6 +131,13 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
<a href="{% url 'part-stock' item.part.id %}">{{ item.part.full_name }} <a href="{% url 'part-stock' item.part.id %}">{{ item.part.full_name }}
</td> </td>
</tr> </tr>
{% if item.customer %}
<tr>
<td><span class='fas fa-user-tie'></span></td>
<td>{% trans "Customer" %}</td>
<td><a href="{% url 'company-detail' item.customer.id %}">{{ item.customer.name }}</a></td>
</tr>
{% endif %}
{% if item.belongs_to %} {% if item.belongs_to %}
<tr> <tr>
<td><span class='fas fa-box'></span></td> <td><span class='fas fa-box'></span></td>
@ -320,6 +332,16 @@ $("#show-qr-code").click(function() {
{% if item.in_stock %} {% if item.in_stock %}
{% if item.part.salable %}
$("#stock-assign-to-customer").click(function() {
launchModalForm("{% url 'stock-item-assign' item.id %}",
{
reload: true,
}
);
});
{% endif %}
function itemAdjust(action) { function itemAdjust(action) {
launchModalForm("/stock/adjust/", launchModalForm("/stock/adjust/",
{ {

View File

@ -23,6 +23,7 @@ stock_item_detail_urls = [
url(r'^delete/', views.StockItemDelete.as_view(), name='stock-item-delete'), url(r'^delete/', views.StockItemDelete.as_view(), name='stock-item-delete'),
url(r'^qr_code/', views.StockItemQRCode.as_view(), name='stock-item-qr'), url(r'^qr_code/', views.StockItemQRCode.as_view(), name='stock-item-qr'),
url(r'^delete_test_data/', views.StockItemDeleteTestData.as_view(), name='stock-item-delete-test-data'), url(r'^delete_test_data/', views.StockItemDeleteTestData.as_view(), name='stock-item-delete-test-data'),
url(r'^assign/', views.StockItemAssignToCustomer.as_view(), name='stock-item-assign'),
url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'), url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),

View File

@ -223,6 +223,43 @@ class StockItemAttachmentDelete(AjaxDeleteView):
} }
class StockItemAssignToCustomer(AjaxUpdateView):
"""
View for manually assigning a StockItem to a Customer
"""
model = StockItem
ajax_form_title = _("Assign to Customer")
context_object_name = "item"
form_class = StockForms.AssignStockItemToCustomerForm
def post(self, request, *args, **kwargs):
customer = request.POST.get('customer', None)
if customer:
try:
customer = Company.objects.get(pk=customer)
except (ValueError, Company.DoesNotExist):
customer = None
if customer is not None:
stock_item = self.get_object()
item = stock_item.allocateToCustomer(
customer,
user=request.user
)
item.clearAllocations()
data = {
'form_valid': True,
}
return self.renderJsonResponse(request, self.get_form(), data)
class StockItemDeleteTestData(AjaxUpdateView): class StockItemDeleteTestData(AjaxUpdateView):
""" """
View for deleting all test data View for deleting all test data