mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #852 from SchrodingersGat/send-to-customer
Send to customer
This commit is contained in:
commit
96b79397c2
@ -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):
|
||||||
|
|
||||||
|
@ -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:
|
|
||||||
|
|
||||||
# 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.item = item
|
||||||
self.save()
|
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()
|
|
||||||
|
@ -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.
|
||||||
|
20
InvenTree/stock/migrations/0045_stockitem_customer.py
Normal file
20
InvenTree/stock/migrations/0045_stockitem_customer.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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)
|
||||||
|
|
||||||
|
@ -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/",
|
||||||
{
|
{
|
||||||
|
@ -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'),
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user