2019-06-04 13:59:15 +00:00
|
|
|
"""
|
|
|
|
Django views for interacting with Order app
|
|
|
|
"""
|
2019-06-04 12:19:04 +00:00
|
|
|
|
2019-06-04 13:59:15 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
|
2019-09-13 05:32:52 +00:00
|
|
|
from django.db import transaction
|
2019-06-13 12:38:10 +00:00
|
|
|
from django.shortcuts import get_object_or_404
|
2021-03-29 13:10:28 +00:00
|
|
|
from django.core.exceptions import ValidationError
|
2020-02-02 00:44:44 +00:00
|
|
|
from django.urls import reverse
|
2021-05-03 21:30:33 +00:00
|
|
|
from django.http import HttpResponseRedirect
|
2021-04-04 20:47:01 +00:00
|
|
|
from django.utils.translation import ugettext_lazy as _
|
2020-02-02 00:44:44 +00:00
|
|
|
from django.views.generic import DetailView, ListView, UpdateView
|
2021-03-29 12:32:41 +00:00
|
|
|
from django.views.generic.edit import FormMixin
|
2019-06-05 11:19:41 +00:00
|
|
|
from django.forms import HiddenInput
|
2019-06-04 13:59:15 +00:00
|
|
|
|
2019-06-13 11:17:06 +00:00
|
|
|
import logging
|
2020-01-05 22:23:13 +00:00
|
|
|
from decimal import Decimal, InvalidOperation
|
2019-06-13 11:17:06 +00:00
|
|
|
|
2020-03-22 07:41:41 +00:00
|
|
|
from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderAttachment
|
2020-04-20 10:40:45 +00:00
|
|
|
from .models import SalesOrder, SalesOrderLineItem, SalesOrderAttachment
|
2020-04-22 11:26:38 +00:00
|
|
|
from .models import SalesOrderAllocation
|
2019-09-15 12:04:52 +00:00
|
|
|
from .admin import POLineItemResource
|
2019-06-11 14:21:18 +00:00
|
|
|
from build.models import Build
|
2021-05-05 19:28:04 +00:00
|
|
|
from company.models import Company, SupplierPart # ManufacturerPart
|
2019-06-15 09:39:57 +00:00
|
|
|
from stock.models import StockItem, StockLocation
|
2019-06-11 13:37:32 +00:00
|
|
|
from part.models import Part
|
2019-06-05 12:24:18 +00:00
|
|
|
|
2021-01-06 09:22:56 +00:00
|
|
|
from common.models import InvenTreeSetting
|
2021-05-04 14:28:00 +00:00
|
|
|
from common.views import MultiStepFormView
|
2021-05-05 16:40:48 +00:00
|
|
|
from common.files import FileManager
|
2021-01-06 09:22:56 +00:00
|
|
|
|
2019-06-05 12:24:18 +00:00
|
|
|
from . import forms as order_forms
|
2019-06-05 10:59:30 +00:00
|
|
|
|
2019-06-13 12:13:22 +00:00
|
|
|
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
2019-06-13 12:38:10 +00:00
|
|
|
from InvenTree.helpers import DownloadFile, str2bool
|
2021-03-29 13:10:28 +00:00
|
|
|
from InvenTree.helpers import extract_serial_numbers
|
2020-10-06 08:46:53 +00:00
|
|
|
from InvenTree.views import InvenTreeRoleMixin
|
2019-06-04 13:59:15 +00:00
|
|
|
|
2020-05-14 05:05:55 +00:00
|
|
|
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus
|
2019-06-04 13:59:15 +00:00
|
|
|
|
2021-04-10 10:08:13 +00:00
|
|
|
logger = logging.getLogger("inventree")
|
2019-06-13 11:17:06 +00:00
|
|
|
|
2019-06-04 13:59:15 +00:00
|
|
|
|
2020-10-06 08:46:53 +00:00
|
|
|
class PurchaseOrderIndex(InvenTreeRoleMixin, ListView):
|
2019-06-04 14:17:25 +00:00
|
|
|
""" List view for all purchase orders """
|
2019-06-04 13:59:15 +00:00
|
|
|
|
|
|
|
model = PurchaseOrder
|
|
|
|
template_name = 'order/purchase_orders.html'
|
|
|
|
context_object_name = 'orders'
|
|
|
|
|
2019-06-06 11:39:04 +00:00
|
|
|
def get_queryset(self):
|
|
|
|
""" Retrieve the list of purchase orders,
|
|
|
|
ensure that the most recent ones are returned first. """
|
|
|
|
|
|
|
|
queryset = PurchaseOrder.objects.all().order_by('-creation_date')
|
|
|
|
|
|
|
|
return queryset
|
|
|
|
|
2019-06-05 10:59:30 +00:00
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
ctx = super().get_context_data(**kwargs)
|
2019-06-04 13:59:15 +00:00
|
|
|
|
|
|
|
return ctx
|
2019-06-04 14:17:25 +00:00
|
|
|
|
|
|
|
|
2020-10-06 08:46:53 +00:00
|
|
|
class SalesOrderIndex(InvenTreeRoleMixin, ListView):
|
2020-04-20 10:40:45 +00:00
|
|
|
|
|
|
|
model = SalesOrder
|
|
|
|
template_name = 'order/sales_orders.html'
|
|
|
|
context_object_name = 'orders'
|
|
|
|
|
|
|
|
|
2020-10-06 08:46:53 +00:00
|
|
|
class PurchaseOrderDetail(InvenTreeRoleMixin, DetailView):
|
2019-06-04 14:17:25 +00:00
|
|
|
""" Detail view for a PurchaseOrder object """
|
|
|
|
|
|
|
|
context_object_name = 'order'
|
|
|
|
queryset = PurchaseOrder.objects.all().prefetch_related('lines')
|
|
|
|
template_name = 'order/purchase_order_detail.html'
|
2019-06-05 10:59:30 +00:00
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
ctx = super().get_context_data(**kwargs)
|
|
|
|
|
|
|
|
return ctx
|
|
|
|
|
|
|
|
|
2020-10-06 08:46:53 +00:00
|
|
|
class SalesOrderDetail(InvenTreeRoleMixin, DetailView):
|
2020-04-20 10:59:14 +00:00
|
|
|
""" Detail view for a SalesOrder object """
|
|
|
|
|
|
|
|
context_object_name = 'order'
|
|
|
|
queryset = SalesOrder.objects.all().prefetch_related('lines')
|
|
|
|
template_name = 'order/sales_order_detail.html'
|
|
|
|
|
|
|
|
|
2020-03-22 07:41:41 +00:00
|
|
|
class PurchaseOrderAttachmentCreate(AjaxCreateView):
|
|
|
|
"""
|
2020-10-26 07:21:45 +00:00
|
|
|
View for creating a new PurchaseOrderAttachment
|
2020-03-22 07:41:41 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
model = PurchaseOrderAttachment
|
|
|
|
form_class = order_forms.EditPurchaseOrderAttachmentForm
|
|
|
|
ajax_form_title = _("Add Purchase Order Attachment")
|
|
|
|
ajax_template_name = "modal_form.html"
|
|
|
|
|
2020-11-02 11:56:26 +00:00
|
|
|
def save(self, form, **kwargs):
|
|
|
|
|
|
|
|
attachment = form.save(commit=False)
|
2020-10-29 23:12:42 +00:00
|
|
|
attachment.user = self.request.user
|
|
|
|
attachment.save()
|
2020-05-12 11:40:42 +00:00
|
|
|
|
2020-03-22 07:41:41 +00:00
|
|
|
def get_data(self):
|
|
|
|
return {
|
|
|
|
"success": _("Added attachment")
|
|
|
|
}
|
|
|
|
|
|
|
|
def get_initial(self):
|
|
|
|
"""
|
|
|
|
Get initial data for creating a new PurchaseOrderAttachment object.
|
|
|
|
|
|
|
|
- Client must request this form with a parent PurchaseOrder in midn.
|
|
|
|
- e.g. ?order=<pk>
|
|
|
|
"""
|
|
|
|
|
|
|
|
initials = super(AjaxCreateView, self).get_initial()
|
|
|
|
|
2020-05-13 01:28:45 +00:00
|
|
|
try:
|
|
|
|
initials["order"] = PurchaseOrder.objects.get(id=self.request.GET.get('order', -1))
|
|
|
|
except (ValueError, PurchaseOrder.DoesNotExist):
|
|
|
|
pass
|
2020-03-22 07:41:41 +00:00
|
|
|
|
|
|
|
return initials
|
|
|
|
|
|
|
|
def get_form(self):
|
|
|
|
"""
|
|
|
|
Create a form to upload a new PurchaseOrderAttachment
|
|
|
|
|
|
|
|
- Hide the 'order' field
|
|
|
|
"""
|
|
|
|
|
|
|
|
form = super(AjaxCreateView, self).get_form()
|
|
|
|
|
|
|
|
form.fields['order'].widget = HiddenInput()
|
|
|
|
|
|
|
|
return form
|
|
|
|
|
|
|
|
|
2020-04-20 23:42:11 +00:00
|
|
|
class SalesOrderAttachmentCreate(AjaxCreateView):
|
|
|
|
""" View for creating a new SalesOrderAttachment """
|
|
|
|
|
|
|
|
model = SalesOrderAttachment
|
|
|
|
form_class = order_forms.EditSalesOrderAttachmentForm
|
|
|
|
ajax_form_title = _('Add Sales Order Attachment')
|
|
|
|
|
2020-11-02 11:56:26 +00:00
|
|
|
def save(self, form, **kwargs):
|
|
|
|
"""
|
|
|
|
Save the user that uploaded the attachment
|
|
|
|
"""
|
|
|
|
|
|
|
|
attachment = form.save(commit=False)
|
|
|
|
attachment.user = self.request.user
|
|
|
|
attachment.save()
|
2020-05-12 11:40:42 +00:00
|
|
|
|
2020-04-20 23:42:11 +00:00
|
|
|
def get_data(self):
|
|
|
|
return {
|
|
|
|
'success': _('Added attachment')
|
|
|
|
}
|
|
|
|
|
|
|
|
def get_initial(self):
|
|
|
|
initials = super().get_initial().copy()
|
|
|
|
|
2020-05-13 01:28:45 +00:00
|
|
|
try:
|
|
|
|
initials['order'] = SalesOrder.objects.get(id=self.request.GET.get('order', None))
|
|
|
|
except (ValueError, SalesOrder.DoesNotExist):
|
|
|
|
pass
|
2020-04-20 23:42:11 +00:00
|
|
|
|
|
|
|
return initials
|
|
|
|
|
|
|
|
def get_form(self):
|
|
|
|
""" Hide the 'order' field """
|
|
|
|
|
|
|
|
form = super().get_form()
|
|
|
|
form.fields['order'].widget = HiddenInput()
|
|
|
|
|
|
|
|
return form
|
|
|
|
|
|
|
|
|
2020-03-22 08:55:46 +00:00
|
|
|
class PurchaseOrderAttachmentEdit(AjaxUpdateView):
|
|
|
|
""" View for editing a PurchaseOrderAttachment object """
|
|
|
|
|
|
|
|
model = PurchaseOrderAttachment
|
|
|
|
form_class = order_forms.EditPurchaseOrderAttachmentForm
|
|
|
|
ajax_form_title = _("Edit Attachment")
|
|
|
|
|
|
|
|
def get_data(self):
|
|
|
|
return {
|
|
|
|
'success': _('Attachment updated')
|
|
|
|
}
|
|
|
|
|
|
|
|
def get_form(self):
|
|
|
|
form = super(AjaxUpdateView, self).get_form()
|
|
|
|
|
|
|
|
# Hide the 'order' field
|
|
|
|
form.fields['order'].widget = HiddenInput()
|
|
|
|
|
|
|
|
return form
|
|
|
|
|
|
|
|
|
2020-04-20 23:42:11 +00:00
|
|
|
class SalesOrderAttachmentEdit(AjaxUpdateView):
|
|
|
|
""" View for editing a SalesOrderAttachment object """
|
|
|
|
|
|
|
|
model = SalesOrderAttachment
|
|
|
|
form_class = order_forms.EditSalesOrderAttachmentForm
|
|
|
|
ajax_form_title = _("Edit Attachment")
|
|
|
|
|
|
|
|
def get_data(self):
|
|
|
|
return {
|
|
|
|
'success': _('Attachment updated')
|
|
|
|
}
|
|
|
|
|
|
|
|
def get_form(self):
|
|
|
|
form = super().get_form()
|
|
|
|
|
|
|
|
form.fields['order'].widget = HiddenInput()
|
|
|
|
|
|
|
|
return form
|
|
|
|
|
|
|
|
|
2020-03-22 08:55:46 +00:00
|
|
|
class PurchaseOrderAttachmentDelete(AjaxDeleteView):
|
|
|
|
""" View for deleting a PurchaseOrderAttachment """
|
|
|
|
|
|
|
|
model = PurchaseOrderAttachment
|
|
|
|
ajax_form_title = _("Delete Attachment")
|
2020-04-20 23:42:11 +00:00
|
|
|
ajax_template_name = "order/delete_attachment.html"
|
|
|
|
context_object_name = "attachment"
|
|
|
|
|
|
|
|
def get_data(self):
|
|
|
|
return {
|
|
|
|
"danger": _("Deleted attachment")
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class SalesOrderAttachmentDelete(AjaxDeleteView):
|
|
|
|
""" View for deleting a SalesOrderAttachment """
|
|
|
|
|
|
|
|
model = SalesOrderAttachment
|
|
|
|
ajax_form_title = _("Delete Attachment")
|
|
|
|
ajax_template_name = "order/delete_attachment.html"
|
2020-03-22 08:55:46 +00:00
|
|
|
context_object_name = "attachment"
|
|
|
|
|
|
|
|
def get_data(self):
|
|
|
|
return {
|
|
|
|
"danger": _("Deleted attachment")
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-10-06 08:46:53 +00:00
|
|
|
class PurchaseOrderNotes(InvenTreeRoleMixin, UpdateView):
|
2020-02-02 00:44:44 +00:00
|
|
|
""" View for updating the 'notes' field of a PurchaseOrder """
|
|
|
|
|
|
|
|
context_object_name = 'order'
|
|
|
|
template_name = 'order/order_notes.html'
|
|
|
|
model = PurchaseOrder
|
2021-03-02 10:32:32 +00:00
|
|
|
|
|
|
|
# Override the default permission roles
|
2020-10-06 09:10:14 +00:00
|
|
|
role_required = 'purchase_order.view'
|
2020-02-02 00:44:44 +00:00
|
|
|
|
|
|
|
fields = ['notes']
|
|
|
|
|
|
|
|
def get_success_url(self):
|
|
|
|
|
2020-04-27 23:35:24 +00:00
|
|
|
return reverse('po-notes', kwargs={'pk': self.get_object().id})
|
2020-02-02 00:44:44 +00:00
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
|
|
|
|
ctx = super().get_context_data(**kwargs)
|
|
|
|
|
2020-04-20 12:33:49 +00:00
|
|
|
ctx['editing'] = str2bool(self.request.GET.get('edit', False))
|
|
|
|
|
|
|
|
return ctx
|
|
|
|
|
|
|
|
|
2020-10-06 08:46:53 +00:00
|
|
|
class SalesOrderNotes(InvenTreeRoleMixin, UpdateView):
|
2020-04-20 12:33:49 +00:00
|
|
|
""" View for editing the 'notes' field of a SalesORder """
|
|
|
|
|
|
|
|
context_object_name = 'order'
|
|
|
|
template_name = 'order/sales_order_notes.html'
|
|
|
|
model = SalesOrder
|
2020-10-06 08:46:53 +00:00
|
|
|
role_required = 'sales_order.view'
|
2020-04-20 12:33:49 +00:00
|
|
|
|
|
|
|
fields = ['notes']
|
|
|
|
|
|
|
|
def get_success_url(self):
|
|
|
|
return reverse('so-notes', kwargs={'pk': self.get_object().pk})
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
|
|
|
|
ctx = super().get_context_data(**kwargs)
|
|
|
|
|
|
|
|
ctx['editing'] = str2bool(self.request.GET.get('edit', False))
|
2020-02-02 00:44:44 +00:00
|
|
|
|
|
|
|
return ctx
|
|
|
|
|
|
|
|
|
2019-06-06 11:39:04 +00:00
|
|
|
class PurchaseOrderCreate(AjaxCreateView):
|
2020-11-02 11:56:26 +00:00
|
|
|
"""
|
|
|
|
View for creating a new PurchaseOrder object using a modal form
|
|
|
|
"""
|
2019-06-06 11:39:04 +00:00
|
|
|
|
|
|
|
model = PurchaseOrder
|
2020-02-11 23:25:46 +00:00
|
|
|
ajax_form_title = _("Create Purchase Order")
|
2019-06-06 11:39:04 +00:00
|
|
|
form_class = order_forms.EditPurchaseOrderForm
|
|
|
|
|
|
|
|
def get_initial(self):
|
|
|
|
initials = super().get_initial().copy()
|
|
|
|
|
2020-05-14 05:02:46 +00:00
|
|
|
initials['reference'] = PurchaseOrder.getNextOrderNumber()
|
2020-04-23 10:38:09 +00:00
|
|
|
initials['status'] = PurchaseOrderStatus.PENDING
|
2019-06-06 11:39:04 +00:00
|
|
|
|
2019-06-10 12:56:34 +00:00
|
|
|
supplier_id = self.request.GET.get('supplier', None)
|
|
|
|
|
|
|
|
if supplier_id:
|
|
|
|
try:
|
|
|
|
supplier = Company.objects.get(id=supplier_id)
|
|
|
|
initials['supplier'] = supplier
|
2019-08-08 13:49:35 +00:00
|
|
|
except (Company.DoesNotExist, ValueError):
|
2019-06-10 12:56:34 +00:00
|
|
|
pass
|
|
|
|
|
2019-06-10 13:08:08 +00:00
|
|
|
return initials
|
2019-06-06 11:39:04 +00:00
|
|
|
|
2020-11-02 11:56:26 +00:00
|
|
|
def save(self, form, **kwargs):
|
|
|
|
"""
|
|
|
|
Record the user who created this PurchaseOrder
|
|
|
|
"""
|
2019-09-23 21:43:14 +00:00
|
|
|
|
2020-11-02 11:56:26 +00:00
|
|
|
order = form.save(commit=False)
|
2020-10-29 23:12:42 +00:00
|
|
|
order.created_by = self.request.user
|
2020-11-05 19:38:54 +00:00
|
|
|
|
|
|
|
return super().save(form)
|
2019-09-23 21:43:14 +00:00
|
|
|
|
2019-06-06 11:39:04 +00:00
|
|
|
|
2020-04-20 12:13:07 +00:00
|
|
|
class SalesOrderCreate(AjaxCreateView):
|
|
|
|
""" View for creating a new SalesOrder object """
|
|
|
|
|
|
|
|
model = SalesOrder
|
|
|
|
ajax_form_title = _("Create Sales Order")
|
|
|
|
form_class = order_forms.EditSalesOrderForm
|
|
|
|
|
|
|
|
def get_initial(self):
|
|
|
|
initials = super().get_initial().copy()
|
|
|
|
|
2020-05-14 05:05:55 +00:00
|
|
|
initials['reference'] = SalesOrder.getNextOrderNumber()
|
|
|
|
initials['status'] = SalesOrderStatus.PENDING
|
2020-04-20 12:13:07 +00:00
|
|
|
|
|
|
|
customer_id = self.request.GET.get('customer', None)
|
|
|
|
|
|
|
|
if customer_id is not None:
|
|
|
|
try:
|
|
|
|
customer = Company.objects.get(id=customer_id)
|
|
|
|
initials['customer'] = customer
|
|
|
|
except (Company.DoesNotExist, ValueError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
return initials
|
|
|
|
|
2020-11-02 11:56:26 +00:00
|
|
|
def save(self, form, **kwargs):
|
|
|
|
"""
|
|
|
|
Record the user who created this SalesOrder
|
|
|
|
"""
|
|
|
|
|
|
|
|
order = form.save(commit=False)
|
2020-10-29 23:12:42 +00:00
|
|
|
order.created_by = self.request.user
|
2020-11-05 19:38:54 +00:00
|
|
|
|
|
|
|
return super().save(form)
|
2020-04-20 12:13:07 +00:00
|
|
|
|
|
|
|
|
2019-06-05 12:24:18 +00:00
|
|
|
class PurchaseOrderEdit(AjaxUpdateView):
|
|
|
|
""" View for editing a PurchaseOrder using a modal form """
|
|
|
|
|
|
|
|
model = PurchaseOrder
|
2020-02-11 23:25:46 +00:00
|
|
|
ajax_form_title = _('Edit Purchase Order')
|
2019-06-05 12:24:18 +00:00
|
|
|
form_class = order_forms.EditPurchaseOrderForm
|
|
|
|
|
|
|
|
def get_form(self):
|
|
|
|
|
|
|
|
form = super(AjaxUpdateView, self).get_form()
|
|
|
|
|
|
|
|
order = self.get_object()
|
|
|
|
|
2019-06-06 11:39:04 +00:00
|
|
|
# Prevent user from editing supplier if there are already lines in the order
|
2020-04-23 10:38:09 +00:00
|
|
|
if order.lines.count() > 0 or not order.status == PurchaseOrderStatus.PENDING:
|
2019-06-05 12:24:18 +00:00
|
|
|
form.fields['supplier'].widget = HiddenInput()
|
|
|
|
|
|
|
|
return form
|
|
|
|
|
|
|
|
|
2020-04-20 12:20:03 +00:00
|
|
|
class SalesOrderEdit(AjaxUpdateView):
|
|
|
|
""" View for editing a SalesOrder """
|
|
|
|
|
|
|
|
model = SalesOrder
|
|
|
|
ajax_form_title = _('Edit Sales Order')
|
|
|
|
form_class = order_forms.EditSalesOrderForm
|
|
|
|
|
|
|
|
def get_form(self):
|
|
|
|
form = super().get_form()
|
|
|
|
|
|
|
|
# Prevent user from editing customer
|
|
|
|
form.fields['customer'].widget = HiddenInput()
|
|
|
|
|
|
|
|
return form
|
|
|
|
|
|
|
|
|
2019-09-20 01:52:38 +00:00
|
|
|
class PurchaseOrderCancel(AjaxUpdateView):
|
|
|
|
""" View for cancelling a purchase order """
|
|
|
|
|
|
|
|
model = PurchaseOrder
|
2020-02-11 23:25:46 +00:00
|
|
|
ajax_form_title = _('Cancel Order')
|
2019-09-20 01:52:38 +00:00
|
|
|
ajax_template_name = 'order/order_cancel.html'
|
|
|
|
form_class = order_forms.CancelPurchaseOrderForm
|
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
def validate(self, order, form, **kwargs):
|
|
|
|
|
|
|
|
confirm = str2bool(form.cleaned_data.get('confirm', False))
|
2019-09-20 01:52:38 +00:00
|
|
|
|
|
|
|
if not confirm:
|
2020-10-28 12:33:33 +00:00
|
|
|
form.add_error('confirm', _('Confirm order cancellation'))
|
2019-09-20 01:52:38 +00:00
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
if not order.can_cancel():
|
2021-01-14 03:37:49 +00:00
|
|
|
form.add_error(None, _('Order cannot be cancelled'))
|
2019-09-20 01:52:38 +00:00
|
|
|
|
2020-11-02 11:56:26 +00:00
|
|
|
def save(self, order, form, **kwargs):
|
|
|
|
"""
|
|
|
|
Cancel the PurchaseOrder
|
|
|
|
"""
|
2019-09-20 01:52:38 +00:00
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
order.cancel_order()
|
2019-09-20 01:52:38 +00:00
|
|
|
|
|
|
|
|
2020-04-23 11:38:40 +00:00
|
|
|
class SalesOrderCancel(AjaxUpdateView):
|
|
|
|
""" View for cancelling a sales order """
|
|
|
|
|
|
|
|
model = SalesOrder
|
|
|
|
ajax_form_title = _("Cancel sales order")
|
|
|
|
ajax_template_name = "order/sales_order_cancel.html"
|
|
|
|
form_class = order_forms.CancelSalesOrderForm
|
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
def validate(self, order, form, **kwargs):
|
2020-04-23 11:38:40 +00:00
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
confirm = str2bool(form.cleaned_data.get('confirm', False))
|
2020-04-23 11:38:40 +00:00
|
|
|
|
|
|
|
if not confirm:
|
2020-10-28 12:33:33 +00:00
|
|
|
form.add_error('confirm', _('Confirm order cancellation'))
|
2020-04-23 11:38:40 +00:00
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
if not order.can_cancel():
|
|
|
|
form.add_error(None, _('Order cannot be cancelled'))
|
2020-04-24 00:20:56 +00:00
|
|
|
|
2020-11-02 11:56:26 +00:00
|
|
|
def save(self, order, form, **kwargs):
|
|
|
|
"""
|
|
|
|
Once the form has been validated, cancel the SalesOrder
|
|
|
|
"""
|
2020-04-23 11:38:40 +00:00
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
order.cancel_order()
|
2020-04-23 11:38:40 +00:00
|
|
|
|
|
|
|
|
2019-06-05 12:24:18 +00:00
|
|
|
class PurchaseOrderIssue(AjaxUpdateView):
|
|
|
|
""" View for changing a purchase order from 'PENDING' to 'ISSUED' """
|
|
|
|
|
|
|
|
model = PurchaseOrder
|
2020-02-11 23:25:46 +00:00
|
|
|
ajax_form_title = _('Issue Order')
|
2019-06-05 12:24:18 +00:00
|
|
|
ajax_template_name = "order/order_issue.html"
|
|
|
|
form_class = order_forms.IssuePurchaseOrderForm
|
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
def validate(self, order, form, **kwargs):
|
2019-06-10 12:14:23 +00:00
|
|
|
|
2020-10-30 10:34:56 +00:00
|
|
|
confirm = str2bool(self.request.POST.get('confirm', False))
|
2019-06-10 12:14:23 +00:00
|
|
|
|
|
|
|
if not confirm:
|
2020-10-28 12:33:33 +00:00
|
|
|
form.add_error('confirm', _('Confirm order placement'))
|
2019-06-10 12:14:23 +00:00
|
|
|
|
2020-11-02 11:56:26 +00:00
|
|
|
def save(self, order, form, **kwargs):
|
|
|
|
"""
|
|
|
|
Once the form has been validated, place the order.
|
|
|
|
"""
|
2020-10-30 05:54:05 +00:00
|
|
|
order.place_order()
|
2019-06-10 12:14:23 +00:00
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
def get_data(self):
|
|
|
|
return {
|
|
|
|
'success': _('Purchase order issued')
|
|
|
|
}
|
2019-06-10 12:14:23 +00:00
|
|
|
|
2019-06-05 12:24:18 +00:00
|
|
|
|
2019-12-04 23:29:16 +00:00
|
|
|
class PurchaseOrderComplete(AjaxUpdateView):
|
|
|
|
""" View for marking a PurchaseOrder as complete.
|
|
|
|
"""
|
|
|
|
|
|
|
|
form_class = order_forms.CompletePurchaseOrderForm
|
|
|
|
model = PurchaseOrder
|
|
|
|
ajax_template_name = "order/order_complete.html"
|
2020-02-11 23:25:46 +00:00
|
|
|
ajax_form_title = _("Complete Order")
|
2019-12-04 23:29:16 +00:00
|
|
|
context_object_name = 'order'
|
|
|
|
|
|
|
|
def get_context_data(self):
|
|
|
|
|
|
|
|
ctx = {
|
|
|
|
'order': self.get_object(),
|
|
|
|
}
|
|
|
|
|
|
|
|
return ctx
|
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
def validate(self, order, form, **kwargs):
|
2019-12-04 23:29:16 +00:00
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
confirm = str2bool(form.cleaned_data.get('confirm', False))
|
2019-12-04 23:29:16 +00:00
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
if not confirm:
|
|
|
|
form.add_error('confirm', _('Confirm order completion'))
|
2019-12-04 23:29:16 +00:00
|
|
|
|
2020-11-02 11:56:26 +00:00
|
|
|
def save(self, order, form, **kwargs):
|
|
|
|
"""
|
|
|
|
Complete the PurchaseOrder
|
|
|
|
"""
|
2019-12-04 23:29:16 +00:00
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
order.complete_order()
|
2019-12-04 23:29:16 +00:00
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
def get_data(self):
|
|
|
|
return {
|
|
|
|
'success': _('Purchase order completed')
|
|
|
|
}
|
2019-12-04 23:29:16 +00:00
|
|
|
|
|
|
|
|
2020-04-24 00:20:56 +00:00
|
|
|
class SalesOrderShip(AjaxUpdateView):
|
|
|
|
""" View for 'shipping' a SalesOrder """
|
|
|
|
form_class = order_forms.ShipSalesOrderForm
|
|
|
|
model = SalesOrder
|
|
|
|
context_object_name = 'order'
|
|
|
|
ajax_template_name = 'order/sales_order_ship.html'
|
|
|
|
ajax_form_title = _('Ship Order')
|
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
|
2020-04-24 22:46:28 +00:00
|
|
|
self.request = request
|
|
|
|
|
2020-04-24 00:20:56 +00:00
|
|
|
order = self.get_object()
|
2020-04-24 22:46:28 +00:00
|
|
|
self.object = order
|
|
|
|
|
2020-04-24 00:20:56 +00:00
|
|
|
form = self.get_form()
|
|
|
|
|
|
|
|
confirm = str2bool(request.POST.get('confirm', False))
|
|
|
|
|
|
|
|
valid = False
|
|
|
|
|
|
|
|
if not confirm:
|
2020-10-28 12:33:33 +00:00
|
|
|
form.add_error('confirm', _('Confirm order shipment'))
|
2020-04-24 00:20:56 +00:00
|
|
|
else:
|
|
|
|
valid = True
|
|
|
|
|
|
|
|
if valid:
|
|
|
|
if not order.ship_order(request.user):
|
2020-10-28 12:33:33 +00:00
|
|
|
form.add_error(None, _('Could not ship order'))
|
2020-04-24 00:20:56 +00:00
|
|
|
valid = False
|
|
|
|
|
|
|
|
data = {
|
|
|
|
'form_valid': valid,
|
|
|
|
}
|
|
|
|
|
2020-04-24 22:46:28 +00:00
|
|
|
context = self.get_context_data()
|
|
|
|
|
|
|
|
context['order'] = order
|
|
|
|
|
|
|
|
return self.renderJsonResponse(request, form, data, context)
|
2020-04-24 00:20:56 +00:00
|
|
|
|
|
|
|
|
2021-05-04 14:28:00 +00:00
|
|
|
class PurchaseOrderUpload(MultiStepFormView):
|
2021-05-04 18:45:52 +00:00
|
|
|
''' PurchaseOrder: Upload file, match to fields and parts (using multi-Step form) '''
|
2021-05-03 21:30:33 +00:00
|
|
|
|
2021-05-04 14:28:00 +00:00
|
|
|
form_list = [
|
2021-05-04 21:35:27 +00:00
|
|
|
('upload', order_forms.UploadFile),
|
|
|
|
('fields', order_forms.MatchField),
|
|
|
|
('parts', order_forms.MatchPart),
|
2021-05-04 14:28:00 +00:00
|
|
|
]
|
2021-05-04 18:45:52 +00:00
|
|
|
form_steps_template = [
|
|
|
|
'order/order_wizard/po_upload.html',
|
|
|
|
'order/order_wizard/match_fields.html',
|
|
|
|
'order/order_wizard/match_parts.html',
|
|
|
|
]
|
2021-05-04 14:28:00 +00:00
|
|
|
form_steps_description = [
|
|
|
|
_("Upload File"),
|
2021-05-04 18:45:52 +00:00
|
|
|
_("Match Fields"),
|
|
|
|
_("Match Parts"),
|
2021-05-04 14:28:00 +00:00
|
|
|
]
|
2021-05-04 16:20:57 +00:00
|
|
|
media_folder = 'order_uploads/'
|
2021-05-03 21:30:33 +00:00
|
|
|
|
2021-05-04 18:45:52 +00:00
|
|
|
# Used for data table
|
|
|
|
headers = None
|
|
|
|
rows = None
|
|
|
|
columns = None
|
2021-05-05 16:40:48 +00:00
|
|
|
missing_columns = None
|
2021-05-05 19:28:04 +00:00
|
|
|
allowed_parts = None
|
2021-05-04 18:45:52 +00:00
|
|
|
|
2021-05-03 21:30:33 +00:00
|
|
|
def get_context_data(self, form, **kwargs):
|
|
|
|
context = super().get_context_data(form=form, **kwargs)
|
|
|
|
|
|
|
|
order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk'])
|
|
|
|
|
|
|
|
context.update({'order': order})
|
|
|
|
|
2021-05-04 18:45:52 +00:00
|
|
|
if self.headers:
|
|
|
|
context.update({'headers': self.headers})
|
|
|
|
# print(f'{self.headers}')
|
|
|
|
if self.columns:
|
|
|
|
context.update({'columns': self.columns})
|
2021-05-05 19:28:04 +00:00
|
|
|
# print(f'{self.columns}')
|
2021-05-04 18:45:52 +00:00
|
|
|
if self.rows:
|
2021-05-05 19:28:04 +00:00
|
|
|
for row in self.rows:
|
|
|
|
row_data = row['data']
|
|
|
|
|
|
|
|
data = []
|
|
|
|
|
|
|
|
for idx, item in enumerate(row_data):
|
|
|
|
data.append({
|
|
|
|
'cell': item,
|
|
|
|
'idx': idx,
|
|
|
|
'column': self.columns[idx]
|
|
|
|
})
|
|
|
|
|
|
|
|
row['data'] = data
|
|
|
|
|
2021-05-04 18:45:52 +00:00
|
|
|
context.update({'rows': self.rows})
|
|
|
|
# print(f'{self.rows}')
|
2021-05-05 16:40:48 +00:00
|
|
|
if self.missing_columns:
|
|
|
|
context.update({'missing_columns': self.missing_columns})
|
2021-05-04 18:45:52 +00:00
|
|
|
|
2021-05-03 21:30:33 +00:00
|
|
|
return context
|
|
|
|
|
2021-05-04 21:35:27 +00:00
|
|
|
def getTableDataFromForm(self, form_data):
|
|
|
|
""" Extract table cell data from form data.
|
|
|
|
These data are used to maintain state between sessions.
|
2021-05-04 18:45:52 +00:00
|
|
|
|
2021-05-04 21:35:27 +00:00
|
|
|
Table data keys are as follows:
|
2021-05-04 18:45:52 +00:00
|
|
|
|
2021-05-04 21:35:27 +00:00
|
|
|
col_name_<idx> - Column name at idx as provided in the uploaded file
|
|
|
|
col_guess_<idx> - Column guess at idx as selected
|
|
|
|
row_<x>_col<y> - Cell data as provided in the uploaded file
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Map the columns
|
|
|
|
self.column_names = {}
|
|
|
|
self.column_selections = {}
|
|
|
|
|
|
|
|
self.row_data = {}
|
|
|
|
|
|
|
|
for item in form_data:
|
|
|
|
value = form_data[item]
|
|
|
|
|
|
|
|
# Column names as passed as col_name_<idx> where idx is an integer
|
|
|
|
|
|
|
|
# Extract the column names
|
|
|
|
if item.startswith('col_name_'):
|
|
|
|
try:
|
|
|
|
col_id = int(item.replace('col_name_', ''))
|
|
|
|
except ValueError:
|
|
|
|
continue
|
|
|
|
col_name = value
|
|
|
|
|
|
|
|
self.column_names[col_id] = col_name
|
|
|
|
|
|
|
|
# Extract the column selections (in the 'select fields' view)
|
|
|
|
if item.startswith('col_guess_'):
|
|
|
|
|
|
|
|
try:
|
|
|
|
col_id = int(item.replace('col_guess_', ''))
|
|
|
|
except ValueError:
|
|
|
|
continue
|
|
|
|
|
|
|
|
col_name = value
|
|
|
|
|
|
|
|
self.column_selections[col_id] = value
|
|
|
|
|
|
|
|
# Extract the row data
|
|
|
|
if item.startswith('row_'):
|
|
|
|
# Item should be of the format row_<r>_col_<c>
|
|
|
|
s = item.split('_')
|
|
|
|
|
|
|
|
if len(s) < 4:
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Ignore row/col IDs which are not correct numeric values
|
|
|
|
try:
|
|
|
|
row_id = int(s[1])
|
|
|
|
col_id = int(s[3])
|
|
|
|
except ValueError:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if row_id not in self.row_data:
|
|
|
|
self.row_data[row_id] = {}
|
|
|
|
|
|
|
|
self.row_data[row_id][col_id] = value
|
|
|
|
|
|
|
|
self.col_ids = sorted(self.column_names.keys())
|
|
|
|
|
|
|
|
# Re-construct the data table
|
|
|
|
self.rows = []
|
|
|
|
|
|
|
|
for row_idx in sorted(self.row_data.keys()):
|
|
|
|
row = self.row_data[row_idx]
|
|
|
|
items = []
|
|
|
|
|
|
|
|
for col_idx in sorted(row.keys()):
|
|
|
|
|
|
|
|
value = row[col_idx]
|
|
|
|
items.append(value)
|
|
|
|
|
|
|
|
self.rows.append({
|
|
|
|
'index': row_idx,
|
|
|
|
'data': items,
|
|
|
|
'errors': {},
|
|
|
|
})
|
|
|
|
|
|
|
|
# Construct the column data
|
|
|
|
self.columns = []
|
|
|
|
|
|
|
|
# Track any duplicate column selections
|
|
|
|
self.duplicates = False
|
|
|
|
|
|
|
|
for col in self.col_ids:
|
|
|
|
|
|
|
|
if col in self.column_selections:
|
|
|
|
guess = self.column_selections[col]
|
|
|
|
else:
|
|
|
|
guess = None
|
|
|
|
|
|
|
|
header = ({
|
|
|
|
'name': self.column_names[col],
|
|
|
|
'guess': guess
|
|
|
|
})
|
|
|
|
|
|
|
|
if guess:
|
|
|
|
n = list(self.column_selections.values()).count(self.column_selections[col])
|
|
|
|
if n > 1:
|
|
|
|
header['duplicate'] = True
|
|
|
|
self.duplicates = True
|
|
|
|
|
|
|
|
self.columns.append(header)
|
|
|
|
|
|
|
|
# Are there any missing columns?
|
|
|
|
self.missing_columns = []
|
|
|
|
|
|
|
|
# Check that all required fields are present
|
|
|
|
for col in self.file_manager.REQUIRED_HEADERS:
|
|
|
|
if col not in self.column_selections.values():
|
|
|
|
self.missing_columns.append(col)
|
|
|
|
|
|
|
|
# Check that at least one of the part match field is present
|
|
|
|
part_match_found = False
|
|
|
|
for col in self.file_manager.PART_MATCH_HEADERS:
|
|
|
|
if col in self.column_selections.values():
|
|
|
|
part_match_found = True
|
|
|
|
break
|
|
|
|
|
|
|
|
# If not, notify user
|
|
|
|
if not part_match_found:
|
|
|
|
for col in self.file_manager.PART_MATCH_HEADERS:
|
|
|
|
self.missing_columns.append(col)
|
|
|
|
|
2021-05-05 17:16:40 +00:00
|
|
|
def getColumnIndex(self, name):
|
|
|
|
""" Return the index of the column with the given name.
|
|
|
|
It named column is not found, return -1
|
|
|
|
"""
|
|
|
|
|
|
|
|
try:
|
|
|
|
idx = list(self.column_selections.values()).index(name)
|
|
|
|
except ValueError:
|
|
|
|
idx = -1
|
|
|
|
|
|
|
|
return idx
|
|
|
|
|
|
|
|
def preFillSelections(self):
|
|
|
|
""" Once data columns have been selected, attempt to pre-select the proper data from the database.
|
|
|
|
This function is called once the field selection has been validated.
|
|
|
|
The pre-fill data are then passed through to the part selection form.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Fields prefixed with "Part_" can be used to do "smart matching" against Part objects in the database
|
|
|
|
q_idx = self.getColumnIndex('Quantity')
|
2021-05-05 19:28:04 +00:00
|
|
|
s_idx = self.getColumnIndex('Supplier_SKU')
|
|
|
|
# m_idx = self.getColumnIndex('Manufacturer_MPN')
|
|
|
|
# p_idx = self.getColumnIndex('Unit_Price')
|
|
|
|
# e_idx = self.getColumnIndex('Extended_Price')
|
2021-05-05 17:16:40 +00:00
|
|
|
|
|
|
|
for row in self.rows:
|
|
|
|
|
|
|
|
# Initially use a quantity of zero
|
|
|
|
quantity = Decimal(0)
|
|
|
|
|
|
|
|
# Initially we do not have a part to reference
|
|
|
|
exact_match_part = None
|
|
|
|
|
|
|
|
# A list of potential Part matches
|
2021-05-05 19:28:04 +00:00
|
|
|
part_options = self.allowed_parts
|
2021-05-05 17:16:40 +00:00
|
|
|
|
|
|
|
# Check if there is a column corresponding to "quantity"
|
|
|
|
if q_idx >= 0:
|
|
|
|
q_val = row['data'][q_idx]
|
|
|
|
|
|
|
|
if q_val:
|
2021-05-06 16:35:52 +00:00
|
|
|
# Delete commas
|
|
|
|
q_val = q_val.replace(',','')
|
|
|
|
|
2021-05-05 17:16:40 +00:00
|
|
|
try:
|
|
|
|
# Attempt to extract a valid quantity from the field
|
|
|
|
quantity = Decimal(q_val)
|
|
|
|
except (ValueError, InvalidOperation):
|
|
|
|
pass
|
|
|
|
|
|
|
|
# Store the 'quantity' value
|
|
|
|
row['quantity'] = quantity
|
|
|
|
|
|
|
|
# Check if there is a column corresponding to "Supplier SKU"
|
|
|
|
if s_idx >= 0:
|
2021-05-05 19:28:04 +00:00
|
|
|
sku = row['data'][s_idx]
|
2021-05-05 17:16:40 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
# Attempt SupplierPart lookup based on SKU value
|
2021-05-05 19:28:04 +00:00
|
|
|
exact_match_part = SupplierPart.objects.get(SKU__contains=sku)
|
|
|
|
except (ValueError, SupplierPart.DoesNotExist, SupplierPart.MultipleObjectsReturned):
|
2021-05-05 17:16:40 +00:00
|
|
|
exact_match_part = None
|
|
|
|
|
|
|
|
# Check if there is a column corresponding to "Manufacturer MPN"
|
2021-05-05 19:28:04 +00:00
|
|
|
# if m_idx >= 0:
|
|
|
|
# row['part_mpn'] = row['data'][m_idx]
|
|
|
|
|
|
|
|
# try:
|
|
|
|
# # Attempt ManufacturerPart lookup based on MPN value
|
|
|
|
# exact_match_part = ManufacturerPart.objects.get(MPN=row['part_mpn'])
|
|
|
|
# except (ValueError, ManufacturerPart.DoesNotExist):
|
|
|
|
# exact_match_part = None
|
2021-05-05 17:16:40 +00:00
|
|
|
|
|
|
|
# Supply list of part options for each row, sorted by how closely they match the part name
|
|
|
|
row['part_options'] = part_options
|
|
|
|
|
|
|
|
# Unless found, the 'part_match' is blank
|
|
|
|
row['part_match'] = None
|
|
|
|
|
|
|
|
if exact_match_part:
|
|
|
|
# If there is an exact match based on SKU or MPN, use that
|
|
|
|
row['part_match'] = exact_match_part
|
|
|
|
|
2021-05-05 19:28:04 +00:00
|
|
|
def updatePartSelectionColumns(self, form):
|
|
|
|
# for idx, row in enumerate(self.rows):
|
|
|
|
# print(f'{idx} | {row}\n\n')
|
|
|
|
pass
|
2021-05-05 17:16:40 +00:00
|
|
|
|
2021-05-05 16:40:48 +00:00
|
|
|
def getFileManager(self, form=None):
|
|
|
|
""" Create FileManager instance from upload file """
|
2021-05-04 16:20:57 +00:00
|
|
|
|
2021-05-05 16:40:48 +00:00
|
|
|
if self.file_manager:
|
|
|
|
return
|
2021-05-04 16:20:57 +00:00
|
|
|
|
2021-05-05 16:40:48 +00:00
|
|
|
if self.steps.current == 'upload':
|
|
|
|
# Get file from form data
|
|
|
|
order_file = form.cleaned_data['file']
|
|
|
|
self.file_manager = FileManager(file=order_file, name='order')
|
|
|
|
else:
|
|
|
|
# Retrieve stored files from upload step
|
|
|
|
upload_files = self.storage.get_step_files('upload')
|
|
|
|
# Get file
|
|
|
|
order_file = upload_files.get('upload-file', None)
|
|
|
|
if order_file:
|
|
|
|
self.file_manager = FileManager(file=order_file, name='order')
|
|
|
|
|
|
|
|
def setupFieldSelection(self, form):
|
|
|
|
""" Setup fields form """
|
|
|
|
|
|
|
|
# Get FileManager
|
|
|
|
self.getFileManager(form)
|
|
|
|
# Setup headers
|
|
|
|
self.file_manager.setup()
|
|
|
|
# Set headers
|
|
|
|
self.headers = self.file_manager.HEADERS
|
|
|
|
# Set columns and rows
|
|
|
|
self.columns = self.file_manager.columns()
|
|
|
|
self.rows = self.file_manager.rows()
|
|
|
|
|
|
|
|
def handleFieldSelection(self, form):
|
2021-05-04 21:35:27 +00:00
|
|
|
""" Process field matching """
|
|
|
|
|
2021-05-05 19:28:04 +00:00
|
|
|
# Retrieve FileManager instance from uploaded file
|
|
|
|
self.getFileManager(form)
|
|
|
|
|
2021-05-05 16:40:48 +00:00
|
|
|
# Update headers
|
|
|
|
if self.file_manager:
|
|
|
|
self.file_manager.setup()
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
2021-05-04 21:35:27 +00:00
|
|
|
# Extract form data
|
2021-05-05 16:40:48 +00:00
|
|
|
self.getTableDataFromForm(form.data)
|
2021-05-04 21:35:27 +00:00
|
|
|
|
|
|
|
valid = len(self.missing_columns) == 0 and not self.duplicates
|
|
|
|
|
2021-05-05 16:40:48 +00:00
|
|
|
return valid
|
2021-05-04 21:35:27 +00:00
|
|
|
|
2021-05-06 16:35:52 +00:00
|
|
|
def getRowByIndex(self, idx):
|
|
|
|
|
|
|
|
for row in self.rows:
|
|
|
|
if row['index'] == idx:
|
|
|
|
return row
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
2021-05-05 16:40:48 +00:00
|
|
|
def handlePartSelection(self, form):
|
2021-05-05 17:16:40 +00:00
|
|
|
|
2021-05-05 19:28:04 +00:00
|
|
|
# Retrieve FileManager instance from uploaded file
|
|
|
|
self.getFileManager(form)
|
|
|
|
|
2021-05-05 17:16:40 +00:00
|
|
|
# Extract form data
|
|
|
|
self.getTableDataFromForm(form.data)
|
|
|
|
|
2021-05-06 16:35:52 +00:00
|
|
|
# Keep track of the parts that have been selected
|
|
|
|
parts = {}
|
|
|
|
|
|
|
|
# Extract other data (part selections, etc)
|
|
|
|
for key, value in form.data.items():
|
|
|
|
|
|
|
|
# Extract quantity from each row
|
|
|
|
if key.startswith('quantity_'):
|
|
|
|
try:
|
|
|
|
row_id = int(key.replace('quantity_', ''))
|
|
|
|
|
|
|
|
row = self.getRowByIndex(row_id)
|
|
|
|
|
|
|
|
if row is None:
|
|
|
|
continue
|
|
|
|
|
|
|
|
q = Decimal(1)
|
|
|
|
|
|
|
|
try:
|
|
|
|
q = Decimal(value)
|
|
|
|
if q < 0:
|
|
|
|
row['errors']['quantity'] = _('Quantity must be greater than zero')
|
|
|
|
|
|
|
|
if 'part' in row.keys():
|
|
|
|
if row['part'].trackable:
|
|
|
|
# Trackable parts must use integer quantities
|
|
|
|
if not q == int(q):
|
|
|
|
row['errors']['quantity'] = _('Quantity must be integer value for trackable parts')
|
|
|
|
|
|
|
|
except (ValueError, InvalidOperation):
|
|
|
|
row['errors']['quantity'] = _('Enter a valid quantity')
|
|
|
|
|
|
|
|
row['quantity'] = q
|
|
|
|
|
|
|
|
except ValueError:
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Extract part from each row
|
|
|
|
if key.startswith('part_'):
|
|
|
|
|
|
|
|
try:
|
|
|
|
row_id = int(key.replace('part_', ''))
|
|
|
|
|
|
|
|
row = self.getRowByIndex(row_id)
|
|
|
|
|
|
|
|
if row is None:
|
|
|
|
continue
|
|
|
|
except ValueError:
|
|
|
|
# Row ID non integer value
|
|
|
|
continue
|
|
|
|
|
|
|
|
try:
|
|
|
|
part_id = int(value)
|
|
|
|
part = Part.objects.get(id=part_id)
|
|
|
|
except ValueError:
|
|
|
|
row['errors']['part'] = _('Select valid part')
|
|
|
|
continue
|
|
|
|
except Part.DoesNotExist:
|
|
|
|
row['errors']['part'] = _('Select valid part')
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Keep track of how many of each part we have seen
|
|
|
|
if part_id in parts:
|
|
|
|
parts[part_id]['quantity'] += 1
|
|
|
|
row['errors']['part'] = _('Duplicate part selected')
|
|
|
|
else:
|
|
|
|
parts[part_id] = {
|
|
|
|
'part': part,
|
|
|
|
'quantity': 1,
|
|
|
|
}
|
|
|
|
|
|
|
|
row['part'] = part
|
|
|
|
|
|
|
|
if part.trackable:
|
|
|
|
# For trackable parts, ensure the quantity is an integer value!
|
|
|
|
if 'quantity' in row.keys():
|
|
|
|
q = row['quantity']
|
|
|
|
|
|
|
|
if not q == int(q):
|
|
|
|
row['errors']['quantity'] = _('Quantity must be integer value for trackable parts')
|
|
|
|
|
|
|
|
# Extract other fields which do not require further validation
|
|
|
|
for field in ['reference', 'notes']:
|
|
|
|
if key.startswith(field + '_'):
|
|
|
|
try:
|
|
|
|
row_id = int(key.replace(field + '_', ''))
|
|
|
|
|
|
|
|
row = self.getRowByIndex(row_id)
|
|
|
|
|
|
|
|
if row:
|
|
|
|
row[field] = value
|
|
|
|
except:
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Are there any errors after form handling?
|
|
|
|
valid = True
|
|
|
|
|
|
|
|
for row in self.rows:
|
|
|
|
# Has a part been selected for the given row?
|
|
|
|
part = row.get('part', None)
|
|
|
|
|
|
|
|
if part is None:
|
|
|
|
row['errors']['part'] = _('Select a part')
|
|
|
|
|
|
|
|
# Has a quantity been specified?
|
|
|
|
if row.get('quantity', None) is None:
|
|
|
|
row['errors']['quantity'] = _('Specify quantity')
|
|
|
|
|
|
|
|
errors = row.get('errors', [])
|
|
|
|
|
|
|
|
if len(errors) > 0:
|
|
|
|
valid = False
|
|
|
|
|
|
|
|
return valid
|
2021-05-04 21:35:27 +00:00
|
|
|
|
|
|
|
def get_form_step_data(self, form):
|
2021-05-05 16:40:48 +00:00
|
|
|
""" Process form data after it has been posted """
|
|
|
|
|
|
|
|
# print(f'{self.steps.current=}\n{form.data=}')
|
|
|
|
|
|
|
|
# Retrieve FileManager instance from uploaded file
|
|
|
|
self.getFileManager(form)
|
|
|
|
# print(f'{self.file_manager=}')
|
|
|
|
|
2021-05-04 21:35:27 +00:00
|
|
|
# Process steps
|
|
|
|
if self.steps.current == 'upload':
|
2021-05-05 16:40:48 +00:00
|
|
|
self.setupFieldSelection(form)
|
2021-05-05 17:16:40 +00:00
|
|
|
elif self.steps.current == 'fields':
|
2021-05-05 19:28:04 +00:00
|
|
|
self.allowed_parts = SupplierPart.objects.all()
|
|
|
|
self.rows = self.file_manager.rows()
|
2021-05-05 17:16:40 +00:00
|
|
|
self.preFillSelections()
|
2021-05-06 16:35:52 +00:00
|
|
|
# self.updatePartSelectionColumns(form)
|
2021-05-05 16:40:48 +00:00
|
|
|
# elif self.steps.current == 'parts':
|
|
|
|
# self.handlePartSelection(form)
|
2021-05-04 21:35:27 +00:00
|
|
|
|
|
|
|
return form.data
|
|
|
|
|
2021-05-05 16:40:48 +00:00
|
|
|
def validate(self, step, form):
|
|
|
|
""" Validate forms """
|
|
|
|
|
|
|
|
valid = False
|
|
|
|
|
|
|
|
# Process steps
|
|
|
|
if step == 'upload':
|
|
|
|
# Validation is done during POST
|
|
|
|
valid = True
|
|
|
|
elif step == 'fields':
|
|
|
|
# Validate user form data
|
|
|
|
valid = self.handleFieldSelection(form)
|
|
|
|
|
|
|
|
if not valid:
|
|
|
|
form.add_error(None, 'Fields matching failed')
|
2021-05-05 17:16:40 +00:00
|
|
|
# Reload headers
|
2021-05-05 16:40:48 +00:00
|
|
|
self.headers = self.file_manager.HEADERS
|
|
|
|
|
|
|
|
elif step == 'parts':
|
|
|
|
valid = self.handlePartSelection(form)
|
|
|
|
|
2021-05-06 16:35:52 +00:00
|
|
|
# if not valid:
|
|
|
|
# pass
|
|
|
|
|
2021-05-05 16:40:48 +00:00
|
|
|
return valid
|
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
""" Perform validations before posting data """
|
|
|
|
|
|
|
|
wizard_goto_step = self.request.POST.get('wizard_goto_step', None)
|
|
|
|
|
|
|
|
form = self.get_form(data=self.request.POST, files=self.request.FILES)
|
|
|
|
|
|
|
|
print(f'\nCurrent step = {self.steps.current}')
|
|
|
|
form_valid = self.validate(self.steps.current, form)
|
|
|
|
|
|
|
|
if not form_valid and not wizard_goto_step:
|
|
|
|
# Re-render same step
|
|
|
|
return self.render(form)
|
|
|
|
|
|
|
|
print('\nPosting... ')
|
|
|
|
return super().post(*args, **kwargs)
|
2021-05-04 16:20:57 +00:00
|
|
|
|
2021-05-03 21:30:33 +00:00
|
|
|
def done(self, form_list, **kwargs):
|
|
|
|
return HttpResponseRedirect(reverse('po-detail', kwargs={'pk': self.kwargs['pk']}))
|
|
|
|
|
|
|
|
|
2019-06-13 12:38:10 +00:00
|
|
|
class PurchaseOrderExport(AjaxView):
|
|
|
|
""" File download for a purchase order
|
|
|
|
|
|
|
|
- File format can be optionally passed as a query param e.g. ?format=CSV
|
|
|
|
- Default file format is CSV
|
|
|
|
"""
|
|
|
|
|
|
|
|
model = PurchaseOrder
|
2021-03-02 10:32:32 +00:00
|
|
|
|
|
|
|
# Specify role as we cannot introspect from "AjaxView"
|
2020-10-06 08:46:53 +00:00
|
|
|
role_required = 'purchase_order.view'
|
2019-06-13 12:38:10 +00:00
|
|
|
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
|
|
|
|
|
order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk'])
|
|
|
|
|
|
|
|
export_format = request.GET.get('format', 'csv')
|
|
|
|
|
2019-06-18 13:18:48 +00:00
|
|
|
filename = '{order} - {company}.{fmt}'.format(
|
|
|
|
order=str(order),
|
|
|
|
company=order.supplier.name,
|
|
|
|
fmt=export_format
|
|
|
|
)
|
2019-06-13 12:38:10 +00:00
|
|
|
|
2019-09-15 12:04:52 +00:00
|
|
|
dataset = POLineItemResource().export(queryset=order.lines.all())
|
|
|
|
|
|
|
|
filedata = dataset.export(format=export_format)
|
2019-06-13 12:38:10 +00:00
|
|
|
|
|
|
|
return DownloadFile(filedata, filename)
|
|
|
|
|
|
|
|
|
2019-09-23 09:02:36 +00:00
|
|
|
class PurchaseOrderReceive(AjaxUpdateView):
|
2019-06-15 07:09:25 +00:00
|
|
|
""" View for receiving parts which are outstanding against a PurchaseOrder.
|
|
|
|
|
|
|
|
Any parts which are outstanding are listed.
|
|
|
|
If all parts are marked as received, the order is closed out.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2019-09-23 09:02:36 +00:00
|
|
|
form_class = order_forms.ReceivePurchaseOrderForm
|
2020-02-11 23:25:46 +00:00
|
|
|
ajax_form_title = _("Receive Parts")
|
2019-06-15 07:09:25 +00:00
|
|
|
ajax_template_name = "order/receive_parts.html"
|
2021-03-02 10:32:32 +00:00
|
|
|
|
|
|
|
# Specify role as we do not specify a Model against this view
|
2020-10-06 08:46:53 +00:00
|
|
|
role_required = 'purchase_order.change'
|
2019-06-15 07:09:25 +00:00
|
|
|
|
2019-06-15 09:39:57 +00:00
|
|
|
# Where the parts will be going (selected in POST request)
|
|
|
|
destination = None
|
|
|
|
|
2019-06-15 07:09:25 +00:00
|
|
|
def get_context_data(self):
|
|
|
|
|
|
|
|
ctx = {
|
|
|
|
'order': self.order,
|
2019-06-15 07:29:33 +00:00
|
|
|
'lines': self.lines,
|
2019-06-15 07:09:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return ctx
|
|
|
|
|
2019-09-23 09:31:18 +00:00
|
|
|
def get_lines(self):
|
|
|
|
"""
|
|
|
|
Extract particular line items from the request,
|
|
|
|
or default to *all* pending line items if none are provided
|
|
|
|
"""
|
|
|
|
|
|
|
|
lines = None
|
|
|
|
|
|
|
|
if 'line' in self.request.GET:
|
|
|
|
line_id = self.request.GET.get('line')
|
|
|
|
|
|
|
|
try:
|
|
|
|
lines = PurchaseOrderLineItem.objects.filter(pk=line_id)
|
|
|
|
except (PurchaseOrderLineItem.DoesNotExist, ValueError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
# TODO - Option to pass multiple lines?
|
|
|
|
|
|
|
|
# No lines specified - default selection
|
|
|
|
if lines is None:
|
|
|
|
lines = self.order.pending_line_items()
|
|
|
|
|
|
|
|
return lines
|
|
|
|
|
2019-06-15 07:09:25 +00:00
|
|
|
def get(self, request, *args, **kwargs):
|
2019-06-15 09:39:57 +00:00
|
|
|
""" Respond to a GET request. Determines which parts are outstanding,
|
|
|
|
and presents a list of these parts to the user.
|
|
|
|
"""
|
2019-06-15 07:09:25 +00:00
|
|
|
|
|
|
|
self.request = request
|
|
|
|
self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk'])
|
|
|
|
|
2019-09-23 09:31:18 +00:00
|
|
|
self.lines = self.get_lines()
|
2019-06-15 07:29:33 +00:00
|
|
|
|
2019-06-15 09:39:57 +00:00
|
|
|
for line in self.lines:
|
|
|
|
# Pre-fill the remaining quantity
|
|
|
|
line.receive_quantity = line.remaining()
|
|
|
|
|
2019-09-23 09:02:36 +00:00
|
|
|
return self.renderJsonResponse(request, form=self.get_form())
|
2019-06-15 07:09:25 +00:00
|
|
|
|
2019-06-15 09:39:57 +00:00
|
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
""" Respond to a POST request. Data checking and error handling.
|
|
|
|
If the request is valid, new StockItem objects will be made
|
|
|
|
for each received item.
|
|
|
|
"""
|
|
|
|
|
|
|
|
self.request = request
|
|
|
|
self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk'])
|
|
|
|
|
|
|
|
self.lines = []
|
|
|
|
self.destination = None
|
|
|
|
|
2020-01-05 22:23:13 +00:00
|
|
|
msg = _("Items received")
|
|
|
|
|
2019-06-15 09:39:57 +00:00
|
|
|
# Extract the destination for received parts
|
2019-09-23 09:02:36 +00:00
|
|
|
if 'location' in request.POST:
|
|
|
|
pk = request.POST['location']
|
2019-06-15 09:39:57 +00:00
|
|
|
try:
|
|
|
|
self.destination = StockLocation.objects.get(id=pk)
|
|
|
|
except (StockLocation.DoesNotExist, ValueError):
|
|
|
|
pass
|
|
|
|
|
2020-01-05 22:23:13 +00:00
|
|
|
errors = False
|
|
|
|
|
|
|
|
if self.destination is None:
|
|
|
|
errors = True
|
|
|
|
msg = _("No destination set")
|
2019-06-15 09:39:57 +00:00
|
|
|
|
|
|
|
# Extract information on all submitted line items
|
|
|
|
for item in request.POST:
|
|
|
|
if item.startswith('line-'):
|
|
|
|
pk = item.replace('line-', '')
|
|
|
|
|
|
|
|
try:
|
|
|
|
line = PurchaseOrderLineItem.objects.get(id=pk)
|
|
|
|
except (PurchaseOrderLineItem.DoesNotExist, ValueError):
|
|
|
|
continue
|
|
|
|
|
2020-05-02 23:50:00 +00:00
|
|
|
# Check that the StockStatus was set
|
|
|
|
status_key = 'status-{pk}'.format(pk=pk)
|
|
|
|
status = request.POST.get(status_key, StockStatus.OK)
|
|
|
|
|
|
|
|
try:
|
|
|
|
status = int(status)
|
|
|
|
except ValueError:
|
|
|
|
status = StockStatus.OK
|
|
|
|
|
|
|
|
if status in StockStatus.RECEIVING_CODES:
|
|
|
|
line.status_code = status
|
|
|
|
else:
|
|
|
|
line.status_code = StockStatus.OK
|
|
|
|
|
2019-09-13 10:52:25 +00:00
|
|
|
# Check that line matches the order
|
|
|
|
if not line.order == self.order:
|
|
|
|
# TODO - Display a non-field error?
|
|
|
|
continue
|
|
|
|
|
2019-06-15 09:39:57 +00:00
|
|
|
# Ignore a part that doesn't map to a SupplierPart
|
|
|
|
try:
|
|
|
|
if line.part is None:
|
|
|
|
continue
|
|
|
|
except SupplierPart.DoesNotExist:
|
|
|
|
continue
|
|
|
|
|
|
|
|
receive = self.request.POST[item]
|
|
|
|
|
|
|
|
try:
|
2020-01-05 22:23:13 +00:00
|
|
|
receive = Decimal(receive)
|
|
|
|
except InvalidOperation:
|
2019-06-15 09:39:57 +00:00
|
|
|
# In the case on an invalid input, reset to default
|
2019-06-15 09:42:09 +00:00
|
|
|
receive = line.remaining()
|
2020-01-05 22:23:13 +00:00
|
|
|
msg = _("Error converting quantity to number")
|
2019-06-15 09:39:57 +00:00
|
|
|
errors = True
|
|
|
|
|
|
|
|
if receive < 0:
|
|
|
|
receive = 0
|
|
|
|
errors = True
|
2020-01-05 22:23:13 +00:00
|
|
|
msg = _("Receive quantity less than zero")
|
2019-06-15 09:39:57 +00:00
|
|
|
|
|
|
|
line.receive_quantity = receive
|
|
|
|
self.lines.append(line)
|
|
|
|
|
2020-01-06 09:50:16 +00:00
|
|
|
if len(self.lines) == 0:
|
|
|
|
msg = _("No lines specified")
|
|
|
|
errors = True
|
|
|
|
|
2019-06-15 09:39:57 +00:00
|
|
|
# No errors? Receive the submitted parts!
|
|
|
|
if errors is False:
|
|
|
|
self.receive_parts()
|
|
|
|
|
|
|
|
data = {
|
|
|
|
'form_valid': errors is False,
|
2020-01-05 22:23:13 +00:00
|
|
|
'success': msg,
|
2019-06-15 09:39:57 +00:00
|
|
|
}
|
|
|
|
|
2019-09-23 09:02:36 +00:00
|
|
|
return self.renderJsonResponse(request, data=data, form=self.get_form())
|
2019-06-15 09:39:57 +00:00
|
|
|
|
2019-09-13 10:52:25 +00:00
|
|
|
@transaction.atomic
|
2019-06-15 09:39:57 +00:00
|
|
|
def receive_parts(self):
|
|
|
|
""" Called once the form has been validated.
|
|
|
|
Create new stockitems against received parts.
|
|
|
|
"""
|
|
|
|
|
|
|
|
for line in self.lines:
|
|
|
|
|
|
|
|
if not line.part:
|
|
|
|
continue
|
|
|
|
|
2020-05-02 23:50:00 +00:00
|
|
|
self.order.receive_line_item(
|
|
|
|
line,
|
|
|
|
self.destination,
|
|
|
|
line.receive_quantity,
|
|
|
|
self.request.user,
|
|
|
|
status=line.status_code,
|
2021-05-04 19:33:00 +00:00
|
|
|
purchase_price=line.purchase_price,
|
2020-05-02 23:50:00 +00:00
|
|
|
)
|
2019-06-15 07:09:25 +00:00
|
|
|
|
|
|
|
|
2019-06-11 13:37:32 +00:00
|
|
|
class OrderParts(AjaxView):
|
|
|
|
""" View for adding various SupplierPart items to a Purchase Order.
|
|
|
|
|
|
|
|
SupplierParts can be selected from a variety of 'sources':
|
|
|
|
|
|
|
|
- ?supplier_parts[]= -> Direct list of SupplierPart objects
|
|
|
|
- ?parts[]= -> List of base Part objects (user must then select supplier parts)
|
|
|
|
- ?stock[]= -> List of StockItem objects (user must select supplier parts)
|
|
|
|
- ?build= -> A Build object (user must select parts, then supplier parts)
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2020-02-11 23:25:46 +00:00
|
|
|
ajax_form_title = _("Order Parts")
|
2019-06-13 09:10:31 +00:00
|
|
|
ajax_template_name = 'order/order_wizard/select_parts.html'
|
2019-06-11 13:37:32 +00:00
|
|
|
|
2020-10-06 08:46:53 +00:00
|
|
|
role_required = [
|
|
|
|
'part.view',
|
|
|
|
'purchase_order.change',
|
|
|
|
]
|
|
|
|
|
2019-06-11 13:37:32 +00:00
|
|
|
# List of Parts we wish to order
|
|
|
|
parts = []
|
2019-06-13 09:30:18 +00:00
|
|
|
suppliers = []
|
2019-06-11 13:37:32 +00:00
|
|
|
|
|
|
|
def get_context_data(self):
|
|
|
|
|
|
|
|
ctx = {}
|
|
|
|
|
2019-06-13 09:12:34 +00:00
|
|
|
ctx['parts'] = sorted(self.parts, key=lambda part: int(part.order_quantity), reverse=True)
|
2019-06-13 09:30:18 +00:00
|
|
|
ctx['suppliers'] = self.suppliers
|
2019-06-11 13:37:32 +00:00
|
|
|
|
|
|
|
return ctx
|
|
|
|
|
2019-06-13 09:30:18 +00:00
|
|
|
def get_suppliers(self):
|
|
|
|
""" Calculates a list of suppliers which the user will need to create POs for.
|
|
|
|
This is calculated AFTER the user finishes selecting the parts to order.
|
|
|
|
Crucially, get_parts() must be called before get_suppliers()
|
|
|
|
"""
|
|
|
|
|
|
|
|
suppliers = {}
|
|
|
|
|
2019-06-13 10:17:36 +00:00
|
|
|
for supplier in self.suppliers:
|
|
|
|
supplier.order_items = []
|
2020-05-02 10:25:47 +00:00
|
|
|
|
2019-06-13 10:17:36 +00:00
|
|
|
suppliers[supplier.name] = supplier
|
|
|
|
|
2019-06-13 09:30:18 +00:00
|
|
|
for part in self.parts:
|
|
|
|
supplier_part_id = part.order_supplier
|
|
|
|
|
|
|
|
try:
|
|
|
|
supplier = SupplierPart.objects.get(pk=supplier_part_id).supplier
|
|
|
|
except SupplierPart.DoesNotExist:
|
|
|
|
continue
|
|
|
|
|
2019-06-13 12:16:27 +00:00
|
|
|
if supplier.name not in suppliers:
|
2019-06-13 09:30:18 +00:00
|
|
|
supplier.order_items = []
|
2020-05-02 10:25:47 +00:00
|
|
|
|
|
|
|
# Attempt to auto-select a purchase order
|
|
|
|
orders = PurchaseOrder.objects.filter(supplier=supplier, status__in=PurchaseOrderStatus.OPEN)
|
|
|
|
|
|
|
|
if orders.count() == 1:
|
|
|
|
supplier.selected_purchase_order = orders.first().id
|
|
|
|
else:
|
|
|
|
supplier.selected_purchase_order = None
|
|
|
|
|
2019-06-13 09:30:18 +00:00
|
|
|
suppliers[supplier.name] = supplier
|
|
|
|
|
|
|
|
suppliers[supplier.name].order_items.append(part)
|
|
|
|
|
|
|
|
self.suppliers = [suppliers[key] for key in suppliers.keys()]
|
|
|
|
|
2019-06-11 13:37:32 +00:00
|
|
|
def get_parts(self):
|
|
|
|
""" Determine which parts the user wishes to order.
|
|
|
|
This is performed on the initial GET request.
|
|
|
|
"""
|
|
|
|
|
|
|
|
self.parts = []
|
|
|
|
|
|
|
|
part_ids = set()
|
|
|
|
|
|
|
|
# User has passed a list of stock items
|
|
|
|
if 'stock[]' in self.request.GET:
|
|
|
|
|
|
|
|
stock_id_list = self.request.GET.getlist('stock[]')
|
|
|
|
|
|
|
|
""" Get a list of all the parts associated with the stock items.
|
|
|
|
- Base part must be purchaseable.
|
2019-06-11 14:39:33 +00:00
|
|
|
- Return a set of corresponding Part IDs
|
2019-06-11 13:37:32 +00:00
|
|
|
"""
|
|
|
|
stock_items = StockItem.objects.filter(
|
|
|
|
part__purchaseable=True,
|
|
|
|
id__in=stock_id_list)
|
|
|
|
|
|
|
|
for item in stock_items:
|
|
|
|
part_ids.add(item.part.id)
|
|
|
|
|
2019-06-11 14:04:20 +00:00
|
|
|
# User has passed a single Part ID
|
|
|
|
elif 'part' in self.request.GET:
|
|
|
|
try:
|
|
|
|
part_id = self.request.GET.get('part')
|
|
|
|
part = Part.objects.get(id=part_id)
|
|
|
|
|
|
|
|
part_ids.add(part.id)
|
|
|
|
|
|
|
|
except Part.DoesNotExist:
|
|
|
|
pass
|
|
|
|
|
2019-06-11 13:55:20 +00:00
|
|
|
# User has passed a list of part ID values
|
2019-06-11 14:04:20 +00:00
|
|
|
elif 'parts[]' in self.request.GET:
|
2019-06-11 13:55:20 +00:00
|
|
|
part_id_list = self.request.GET.getlist('parts[]')
|
|
|
|
|
|
|
|
parts = Part.objects.filter(
|
|
|
|
purchaseable=True,
|
|
|
|
id__in=part_id_list)
|
|
|
|
|
|
|
|
for part in parts:
|
|
|
|
part_ids.add(part.id)
|
|
|
|
|
2019-06-11 14:21:18 +00:00
|
|
|
# User has provided a Build ID
|
|
|
|
elif 'build' in self.request.GET:
|
|
|
|
build_id = self.request.GET.get('build')
|
|
|
|
try:
|
|
|
|
build = Build.objects.get(id=build_id)
|
|
|
|
|
2020-11-03 09:37:33 +00:00
|
|
|
parts = build.required_parts
|
2019-06-11 14:21:18 +00:00
|
|
|
|
|
|
|
for part in parts:
|
2020-11-03 09:37:33 +00:00
|
|
|
|
2019-06-13 11:28:01 +00:00
|
|
|
# If ordering from a Build page, ignore parts that we have enough of
|
|
|
|
if part.quantity_to_order <= 0:
|
|
|
|
continue
|
2019-06-11 14:21:18 +00:00
|
|
|
part_ids.add(part.id)
|
|
|
|
except Build.DoesNotExist:
|
|
|
|
pass
|
|
|
|
|
2019-06-11 13:37:32 +00:00
|
|
|
# Create the list of parts
|
|
|
|
for id in part_ids:
|
|
|
|
try:
|
|
|
|
part = Part.objects.get(id=id)
|
2019-06-13 09:10:31 +00:00
|
|
|
# Pre-fill the 'order quantity' value
|
|
|
|
part.order_quantity = part.quantity_to_order
|
|
|
|
|
|
|
|
default_supplier = part.get_default_supplier()
|
|
|
|
|
|
|
|
if default_supplier:
|
|
|
|
part.order_supplier = default_supplier.id
|
|
|
|
else:
|
|
|
|
part.order_supplier = None
|
2019-06-11 13:37:32 +00:00
|
|
|
except Part.DoesNotExist:
|
|
|
|
continue
|
|
|
|
|
|
|
|
self.parts.append(part)
|
|
|
|
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
|
|
|
|
|
self.request = request
|
|
|
|
|
|
|
|
self.get_parts()
|
|
|
|
|
|
|
|
return self.renderJsonResponse(request)
|
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
2019-06-13 09:10:31 +00:00
|
|
|
""" Handle the POST action for part selection.
|
|
|
|
|
|
|
|
- Validates each part / quantity / supplier / etc
|
|
|
|
|
|
|
|
Part selection form contains the following fields for each part:
|
|
|
|
|
|
|
|
- supplier-<pk> : The ID of the selected supplier
|
|
|
|
- quantity-<pk> : The quantity to add to the order
|
|
|
|
"""
|
|
|
|
|
2019-06-13 10:17:36 +00:00
|
|
|
self.request = request
|
|
|
|
|
2019-06-13 09:10:31 +00:00
|
|
|
self.parts = []
|
2019-06-13 10:17:36 +00:00
|
|
|
self.suppliers = []
|
2019-06-13 09:10:31 +00:00
|
|
|
|
2019-06-13 09:30:18 +00:00
|
|
|
# Any errors for the part selection form?
|
2019-06-13 10:17:36 +00:00
|
|
|
part_errors = False
|
|
|
|
supplier_errors = False
|
2019-06-13 09:30:18 +00:00
|
|
|
|
2019-06-13 10:17:36 +00:00
|
|
|
# Extract part information from the form
|
2019-06-13 09:10:31 +00:00
|
|
|
for item in self.request.POST:
|
|
|
|
|
2019-06-13 10:17:36 +00:00
|
|
|
if item.startswith('part-supplier-'):
|
2019-06-13 09:10:31 +00:00
|
|
|
|
2019-06-13 10:17:36 +00:00
|
|
|
pk = item.replace('part-supplier-', '')
|
2019-06-13 09:10:31 +00:00
|
|
|
|
|
|
|
# Check that the part actually exists
|
|
|
|
try:
|
|
|
|
part = Part.objects.get(id=pk)
|
|
|
|
except (Part.DoesNotExist, ValueError):
|
|
|
|
continue
|
|
|
|
|
|
|
|
supplier_part_id = self.request.POST[item]
|
|
|
|
|
2019-06-13 10:17:36 +00:00
|
|
|
quantity = self.request.POST.get('part-quantity-' + str(pk), 0)
|
2019-06-13 09:10:31 +00:00
|
|
|
|
|
|
|
# Ensure a valid supplier has been passed
|
|
|
|
try:
|
|
|
|
supplier_part = SupplierPart.objects.get(id=supplier_part_id)
|
|
|
|
except (SupplierPart.DoesNotExist, ValueError):
|
|
|
|
supplier_part = None
|
|
|
|
|
|
|
|
# Ensure a valid quantity is passed
|
|
|
|
try:
|
2019-06-13 10:17:36 +00:00
|
|
|
quantity = int(quantity)
|
|
|
|
|
|
|
|
# Eliminate lines where the quantity is zero
|
|
|
|
if quantity == 0:
|
2019-06-13 09:10:31 +00:00
|
|
|
continue
|
|
|
|
except ValueError:
|
|
|
|
quantity = part.quantity_to_order
|
|
|
|
|
|
|
|
part.order_supplier = supplier_part.id if supplier_part else None
|
|
|
|
part.order_quantity = quantity
|
|
|
|
|
2021-05-04 19:33:00 +00:00
|
|
|
# set supplier-price
|
|
|
|
if supplier_part:
|
|
|
|
supplier_price = supplier_part.get_price(quantity)
|
|
|
|
if supplier_price:
|
|
|
|
part.purchase_price = supplier_price / quantity
|
|
|
|
if not hasattr(part, 'purchase_price'):
|
|
|
|
part.purchase_price = None
|
|
|
|
|
2019-06-13 09:10:31 +00:00
|
|
|
self.parts.append(part)
|
|
|
|
|
2019-06-13 09:30:18 +00:00
|
|
|
if supplier_part is None:
|
2019-06-13 10:17:36 +00:00
|
|
|
part_errors = True
|
|
|
|
|
|
|
|
elif quantity < 0:
|
|
|
|
part_errors = True
|
|
|
|
|
|
|
|
elif item.startswith('purchase-order-'):
|
|
|
|
# Which purchase order is selected for a given supplier?
|
|
|
|
pk = item.replace('purchase-order-', '')
|
|
|
|
|
|
|
|
# Check that the Supplier actually exists
|
|
|
|
try:
|
|
|
|
supplier = Company.objects.get(id=pk)
|
|
|
|
except Company.DoesNotExist:
|
|
|
|
# Skip this item
|
|
|
|
continue
|
|
|
|
|
|
|
|
purchase_order_id = self.request.POST[item]
|
|
|
|
|
|
|
|
# Ensure that a valid purchase order has been passed
|
|
|
|
try:
|
|
|
|
purchase_order = PurchaseOrder.objects.get(pk=purchase_order_id)
|
|
|
|
except (PurchaseOrder.DoesNotExist, ValueError):
|
|
|
|
purchase_order = None
|
|
|
|
|
|
|
|
supplier.selected_purchase_order = purchase_order.id if purchase_order else None
|
|
|
|
|
|
|
|
self.suppliers.append(supplier)
|
|
|
|
|
|
|
|
if supplier.selected_purchase_order is None:
|
|
|
|
supplier_errors = True
|
|
|
|
|
|
|
|
form_step = request.POST.get('form_step')
|
|
|
|
|
|
|
|
# Map parts to suppliers
|
|
|
|
self.get_suppliers()
|
|
|
|
|
2019-06-13 11:17:06 +00:00
|
|
|
valid = False
|
|
|
|
|
2019-06-13 10:17:36 +00:00
|
|
|
if form_step == 'select_parts':
|
|
|
|
# No errors? Proceed to PO selection form
|
2019-06-13 12:16:27 +00:00
|
|
|
if part_errors is False:
|
2019-06-13 10:17:36 +00:00
|
|
|
self.ajax_template_name = 'order/order_wizard/select_pos.html'
|
|
|
|
|
|
|
|
else:
|
|
|
|
self.ajax_template_name = 'order/order_wizard/select_parts.html'
|
|
|
|
|
|
|
|
elif form_step == 'select_purchase_orders':
|
|
|
|
|
|
|
|
self.ajax_template_name = 'order/order_wizard/select_pos.html'
|
2019-06-13 09:10:31 +00:00
|
|
|
|
2019-06-13 11:17:06 +00:00
|
|
|
valid = part_errors is False and supplier_errors is False
|
|
|
|
|
|
|
|
# Form wizard is complete! Add items to purchase orders
|
|
|
|
if valid:
|
|
|
|
self.order_items()
|
|
|
|
|
2019-06-13 09:10:31 +00:00
|
|
|
data = {
|
2019-06-13 11:17:06 +00:00
|
|
|
'form_valid': valid,
|
2021-04-04 20:49:47 +00:00
|
|
|
'success': _('Ordered {n} parts').format(n=len(self.parts))
|
2019-06-13 09:10:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return self.renderJsonResponse(self.request, data=data)
|
|
|
|
|
2019-09-13 05:32:52 +00:00
|
|
|
@transaction.atomic
|
2019-06-13 11:17:06 +00:00
|
|
|
def order_items(self):
|
|
|
|
""" Add the selected items to the purchase orders. """
|
|
|
|
|
|
|
|
for supplier in self.suppliers:
|
|
|
|
|
|
|
|
# Check that the purchase order does actually exist
|
|
|
|
try:
|
|
|
|
order = PurchaseOrder.objects.get(pk=supplier.selected_purchase_order)
|
|
|
|
except PurchaseOrder.DoesNotExist:
|
|
|
|
logger.critical('Could not add items to purchase order {po} - Order does not exist'.format(po=supplier.selected_purchase_order))
|
|
|
|
continue
|
|
|
|
|
|
|
|
for item in supplier.order_items:
|
|
|
|
|
|
|
|
# Ensure that the quantity is valid
|
|
|
|
try:
|
|
|
|
quantity = int(item.order_quantity)
|
|
|
|
if quantity <= 0:
|
|
|
|
continue
|
|
|
|
except ValueError:
|
|
|
|
logger.warning("Did not add part to purchase order - incorrect quantity")
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Check that the supplier part does actually exist
|
|
|
|
try:
|
|
|
|
supplier_part = SupplierPart.objects.get(pk=item.order_supplier)
|
|
|
|
except SupplierPart.DoesNotExist:
|
|
|
|
logger.critical("Could not add part '{part}' to purchase order - selected supplier part '{sp}' does not exist.".format(
|
|
|
|
part=item,
|
|
|
|
sp=item.order_supplier))
|
|
|
|
continue
|
|
|
|
|
2021-05-04 19:33:00 +00:00
|
|
|
# get purchase price
|
|
|
|
purchase_price = item.purchase_price
|
|
|
|
|
|
|
|
order.add_line_item(supplier_part, quantity, purchase_price=purchase_price)
|
2019-06-13 11:17:06 +00:00
|
|
|
|
2019-06-11 13:37:32 +00:00
|
|
|
|
2019-06-05 10:59:30 +00:00
|
|
|
class POLineItemCreate(AjaxCreateView):
|
|
|
|
""" AJAX view for creating a new PurchaseOrderLineItem object
|
|
|
|
"""
|
|
|
|
|
|
|
|
model = PurchaseOrderLineItem
|
|
|
|
context_object_name = 'line'
|
2019-06-05 12:24:18 +00:00
|
|
|
form_class = order_forms.EditPurchaseOrderLineItemForm
|
2020-02-11 23:25:46 +00:00
|
|
|
ajax_form_title = _('Add Line Item')
|
2019-06-05 10:59:30 +00:00
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
def validate(self, item, form, **kwargs):
|
2019-06-06 11:55:02 +00:00
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
order = form.cleaned_data.get('order', None)
|
2019-06-06 11:55:02 +00:00
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
part = form.cleaned_data.get('part', None)
|
2019-06-06 11:55:02 +00:00
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
if not part:
|
|
|
|
form.add_error('part', _('Supplier part must be specified'))
|
2019-06-06 11:55:02 +00:00
|
|
|
|
2020-10-30 05:54:05 +00:00
|
|
|
if part and order:
|
|
|
|
if not part.supplier == order.supplier:
|
|
|
|
form.add_error(
|
|
|
|
'part',
|
|
|
|
_('Supplier must match for Part and Order')
|
|
|
|
)
|
2019-06-06 11:55:02 +00:00
|
|
|
|
2019-06-05 10:59:30 +00:00
|
|
|
def get_form(self):
|
|
|
|
""" Limit choice options based on the selected order, etc
|
|
|
|
"""
|
|
|
|
|
|
|
|
form = super().get_form()
|
|
|
|
|
2019-09-13 10:01:41 +00:00
|
|
|
# Limit the available to orders to ones that are PENDING
|
|
|
|
query = form.fields['order'].queryset
|
2020-04-23 10:38:09 +00:00
|
|
|
query = query.filter(status=PurchaseOrderStatus.PENDING)
|
2019-09-13 10:01:41 +00:00
|
|
|
form.fields['order'].queryset = query
|
|
|
|
|
2019-06-05 10:59:30 +00:00
|
|
|
order_id = form['order'].value()
|
|
|
|
|
|
|
|
try:
|
|
|
|
order = PurchaseOrder.objects.get(id=order_id)
|
|
|
|
|
|
|
|
query = form.fields['part'].queryset
|
|
|
|
|
|
|
|
# Only allow parts from the selected supplier
|
|
|
|
query = query.filter(supplier=order.supplier.id)
|
|
|
|
|
2019-06-05 11:19:41 +00:00
|
|
|
exclude = []
|
|
|
|
|
|
|
|
for line in order.lines.all():
|
|
|
|
if line.part and line.part.id not in exclude:
|
|
|
|
exclude.append(line.part.id)
|
|
|
|
|
2019-06-05 11:13:08 +00:00
|
|
|
# Remove parts that are already in the order
|
2019-06-05 11:19:41 +00:00
|
|
|
query = query.exclude(id__in=exclude)
|
2019-06-05 10:59:30 +00:00
|
|
|
|
|
|
|
form.fields['part'].queryset = query
|
2019-06-05 11:19:41 +00:00
|
|
|
form.fields['order'].widget = HiddenInput()
|
2019-09-13 10:01:41 +00:00
|
|
|
except (ValueError, PurchaseOrder.DoesNotExist):
|
2019-06-05 10:59:30 +00:00
|
|
|
pass
|
|
|
|
|
|
|
|
return form
|
|
|
|
|
|
|
|
def get_initial(self):
|
|
|
|
""" Extract initial data for the line item.
|
|
|
|
|
|
|
|
- The 'order' will be passed as a query parameter
|
|
|
|
- Use this to set the 'order' field and limit the options for 'part'
|
|
|
|
"""
|
|
|
|
|
|
|
|
initials = super().get_initial().copy()
|
|
|
|
|
|
|
|
order_id = self.request.GET.get('order', None)
|
|
|
|
|
|
|
|
if order_id:
|
|
|
|
try:
|
|
|
|
order = PurchaseOrder.objects.get(id=order_id)
|
|
|
|
initials['order'] = order
|
|
|
|
|
2020-04-20 23:02:10 +00:00
|
|
|
except (PurchaseOrder.DoesNotExist, ValueError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
return initials
|
|
|
|
|
|
|
|
|
|
|
|
class SOLineItemCreate(AjaxCreateView):
|
|
|
|
""" Ajax view for creating a new SalesOrderLineItem object """
|
|
|
|
|
|
|
|
model = SalesOrderLineItem
|
|
|
|
context_order_name = 'line'
|
|
|
|
form_class = order_forms.EditSalesOrderLineItemForm
|
|
|
|
ajax_form_title = _('Add Line Item')
|
|
|
|
|
2020-04-24 00:20:56 +00:00
|
|
|
def get_form(self, *args, **kwargs):
|
|
|
|
|
|
|
|
form = super().get_form(*args, **kwargs)
|
|
|
|
|
|
|
|
# If the order is specified, hide the widget
|
|
|
|
order_id = form['order'].value()
|
|
|
|
|
|
|
|
if SalesOrder.objects.filter(id=order_id).exists():
|
|
|
|
form.fields['order'].widget = HiddenInput()
|
|
|
|
|
|
|
|
return form
|
|
|
|
|
2020-04-20 23:02:10 +00:00
|
|
|
def get_initial(self):
|
|
|
|
"""
|
|
|
|
Extract initial data for this line item:
|
|
|
|
|
|
|
|
Options:
|
|
|
|
order: The SalesOrder object
|
|
|
|
part: The Part object
|
|
|
|
"""
|
|
|
|
|
|
|
|
initials = super().get_initial().copy()
|
|
|
|
|
|
|
|
order_id = self.request.GET.get('order', None)
|
|
|
|
part_id = self.request.GET.get('part', None)
|
|
|
|
|
|
|
|
if order_id:
|
|
|
|
try:
|
|
|
|
order = SalesOrder.objects.get(id=order_id)
|
|
|
|
initials['order'] = order
|
|
|
|
except (SalesOrder.DoesNotExist, ValueError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
if part_id:
|
|
|
|
try:
|
|
|
|
part = Part.objects.get(id=part_id)
|
|
|
|
if part.salable:
|
|
|
|
initials['part'] = part
|
|
|
|
except (Part.DoesNotExist, ValueError):
|
2019-06-05 10:59:30 +00:00
|
|
|
pass
|
|
|
|
|
|
|
|
return initials
|
|
|
|
|
|
|
|
|
2020-04-22 11:26:38 +00:00
|
|
|
class SOLineItemEdit(AjaxUpdateView):
|
|
|
|
""" View for editing a SalesOrderLineItem """
|
|
|
|
|
|
|
|
model = SalesOrderLineItem
|
|
|
|
form_class = order_forms.EditSalesOrderLineItemForm
|
|
|
|
ajax_form_title = _('Edit Line Item')
|
|
|
|
|
|
|
|
def get_form(self):
|
|
|
|
form = super().get_form()
|
|
|
|
|
|
|
|
form.fields.pop('order')
|
|
|
|
form.fields.pop('part')
|
|
|
|
|
|
|
|
return form
|
|
|
|
|
|
|
|
|
2019-06-05 10:59:30 +00:00
|
|
|
class POLineItemEdit(AjaxUpdateView):
|
2019-06-13 12:13:22 +00:00
|
|
|
""" View for editing a PurchaseOrderLineItem object in a modal form.
|
|
|
|
"""
|
2019-06-05 10:59:30 +00:00
|
|
|
|
|
|
|
model = PurchaseOrderLineItem
|
2019-06-05 12:24:18 +00:00
|
|
|
form_class = order_forms.EditPurchaseOrderLineItemForm
|
2019-06-05 10:59:30 +00:00
|
|
|
ajax_template_name = 'modal_form.html'
|
2020-02-11 23:25:46 +00:00
|
|
|
ajax_form_title = _('Edit Line Item')
|
2019-06-13 12:13:22 +00:00
|
|
|
|
|
|
|
def get_form(self):
|
|
|
|
form = super().get_form()
|
|
|
|
|
|
|
|
# Prevent user from editing order once line item is assigned
|
2020-09-28 12:08:38 +00:00
|
|
|
form.fields['order'].widget = HiddenInput()
|
2019-06-13 12:13:22 +00:00
|
|
|
|
|
|
|
return form
|
|
|
|
|
|
|
|
|
|
|
|
class POLineItemDelete(AjaxDeleteView):
|
|
|
|
""" View for deleting a PurchaseOrderLineItem object in a modal form
|
|
|
|
"""
|
|
|
|
|
|
|
|
model = PurchaseOrderLineItem
|
2020-02-11 23:25:46 +00:00
|
|
|
ajax_form_title = _('Delete Line Item')
|
2019-06-13 12:13:22 +00:00
|
|
|
ajax_template_name = 'order/po_lineitem_delete.html'
|
2020-10-06 08:46:53 +00:00
|
|
|
|
2019-06-13 12:13:22 +00:00
|
|
|
def get_data(self):
|
|
|
|
return {
|
2020-02-11 23:25:46 +00:00
|
|
|
'danger': _('Deleted line item'),
|
2019-06-13 12:16:27 +00:00
|
|
|
}
|
2020-04-22 11:26:38 +00:00
|
|
|
|
|
|
|
|
2020-04-22 12:36:55 +00:00
|
|
|
class SOLineItemDelete(AjaxDeleteView):
|
|
|
|
|
|
|
|
model = SalesOrderLineItem
|
|
|
|
ajax_form_title = _("Delete Line Item")
|
|
|
|
ajax_template_name = "order/so_lineitem_delete.html"
|
|
|
|
|
|
|
|
def get_data(self):
|
|
|
|
return {
|
|
|
|
'danger': _('Deleted line item'),
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-03-29 12:32:41 +00:00
|
|
|
class SalesOrderAssignSerials(AjaxView, FormMixin):
|
2021-03-29 12:10:36 +00:00
|
|
|
"""
|
|
|
|
View for assigning stock items to a sales order,
|
|
|
|
by serial number lookup.
|
|
|
|
"""
|
|
|
|
|
|
|
|
model = SalesOrderAllocation
|
|
|
|
role_required = 'sales_order.change'
|
2021-03-29 13:10:28 +00:00
|
|
|
ajax_template_name = 'order/so_allocate_by_serial.html'
|
2021-03-29 12:10:36 +00:00
|
|
|
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
|
|
|
|
|
2021-03-29 12:32:41 +00:00
|
|
|
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()
|
|
|
|
|
2021-03-29 13:10:28 +00:00
|
|
|
if valid:
|
|
|
|
self.allocate_items()
|
|
|
|
|
2021-03-29 12:32:41 +00:00
|
|
|
data = {
|
|
|
|
'form_valid': valid,
|
|
|
|
'form_errors': self.form.errors.as_json(),
|
|
|
|
'non_field_errors': self.form.non_field_errors().as_json(),
|
2021-04-04 20:49:47 +00:00
|
|
|
'success': _("Allocated {n} items").format(n=len(self.stock_items))
|
2021-03-29 12:32:41 +00:00
|
|
|
}
|
|
|
|
|
2021-03-29 12:38:38 +00:00
|
|
|
return self.renderJsonResponse(request, self.form, data)
|
2021-03-29 12:32:41 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2021-03-29 12:38:38 +00:00
|
|
|
if self.line:
|
|
|
|
self.form.fields['line'].widget = HiddenInput()
|
|
|
|
else:
|
2021-03-29 12:32:41 +00:00
|
|
|
self.form.add_error('line', _('Select line item'))
|
|
|
|
|
2021-03-29 12:38:38 +00:00
|
|
|
if self.part:
|
|
|
|
self.form.fields['part'].widget = HiddenInput()
|
|
|
|
else:
|
2021-03-29 12:32:41 +00:00
|
|
|
self.form.add_error('part', _('Select part'))
|
|
|
|
|
2021-03-29 13:10:28 +00:00
|
|
|
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',
|
2021-05-06 10:29:58 +00:00
|
|
|
_('No matching item for serial {serial}').format(serial=serial)
|
2021-03-29 13:10:28 +00:00
|
|
|
)
|
|
|
|
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',
|
2021-05-06 10:29:58 +00:00
|
|
|
_('{serial} is not in stock').format(serial=serial)
|
2021-03-29 13:10:28 +00:00
|
|
|
)
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Already allocated to an order
|
|
|
|
if stock_item.is_allocated():
|
|
|
|
self.form.add_error(
|
|
|
|
'serials',
|
2021-05-06 10:29:58 +00:00
|
|
|
_('{serial} already allocated to an order').format(serial=serial)
|
2021-03-29 13:10:28 +00:00
|
|
|
)
|
|
|
|
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,
|
|
|
|
)
|
2021-03-29 12:32:41 +00:00
|
|
|
|
2021-03-29 12:10:36 +00:00
|
|
|
def get_form(self):
|
|
|
|
|
|
|
|
form = super().get_form()
|
|
|
|
|
2021-03-29 12:38:38 +00:00
|
|
|
if self.line:
|
2021-03-29 12:10:36 +00:00
|
|
|
form.fields['line'].widget = HiddenInput()
|
|
|
|
|
2021-03-29 12:38:38 +00:00
|
|
|
if self.part:
|
2021-03-29 12:10:36 +00:00
|
|
|
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):
|
2021-03-29 12:38:38 +00:00
|
|
|
|
2021-03-29 12:10:36 +00:00
|
|
|
return self.renderJsonResponse(
|
|
|
|
request,
|
|
|
|
self.get_form(),
|
|
|
|
context=self.get_context_data(),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2020-04-22 11:26:38 +00:00
|
|
|
class SalesOrderAllocationCreate(AjaxCreateView):
|
|
|
|
""" View for creating a new SalesOrderAllocation """
|
|
|
|
|
|
|
|
model = SalesOrderAllocation
|
2021-03-29 05:43:26 +00:00
|
|
|
form_class = order_forms.CreateSalesOrderAllocationForm
|
2020-04-22 11:26:38 +00:00
|
|
|
ajax_form_title = _('Allocate Stock to Order')
|
2021-03-02 10:32:32 +00:00
|
|
|
|
2020-04-22 11:26:38 +00:00
|
|
|
def get_initial(self):
|
|
|
|
initials = super().get_initial().copy()
|
|
|
|
|
2020-04-22 12:22:22 +00:00
|
|
|
line_id = self.request.GET.get('line', None)
|
2020-04-22 11:26:38 +00:00
|
|
|
|
2020-04-22 12:22:22 +00:00
|
|
|
if line_id is not None:
|
|
|
|
line = SalesOrderLineItem.objects.get(pk=line_id)
|
|
|
|
|
|
|
|
initials['line'] = line
|
|
|
|
|
|
|
|
# Search for matching stock items, pre-fill if there is only one
|
|
|
|
items = StockItem.objects.filter(part=line.part)
|
|
|
|
|
|
|
|
quantity = line.quantity - line.allocated_quantity()
|
|
|
|
|
|
|
|
if quantity < 0:
|
|
|
|
quantity = 0
|
2020-04-22 11:26:38 +00:00
|
|
|
|
2020-04-22 12:22:22 +00:00
|
|
|
if items.count() == 1:
|
|
|
|
item = items.first()
|
|
|
|
initials['item'] = item
|
|
|
|
|
|
|
|
# Reduce the quantity IF there is not enough stock
|
|
|
|
qmax = item.quantity - item.allocation_count()
|
|
|
|
|
|
|
|
if qmax < quantity:
|
|
|
|
quantity = qmax
|
|
|
|
|
|
|
|
initials['quantity'] = quantity
|
|
|
|
|
2020-04-22 11:26:38 +00:00
|
|
|
return initials
|
|
|
|
|
|
|
|
def get_form(self):
|
|
|
|
|
|
|
|
form = super().get_form()
|
|
|
|
|
|
|
|
line_id = form['line'].value()
|
|
|
|
|
|
|
|
# If a line item has been specified, reduce the queryset for the stockitem accordingly
|
|
|
|
try:
|
|
|
|
line = SalesOrderLineItem.objects.get(pk=line_id)
|
|
|
|
|
2021-01-06 09:22:56 +00:00
|
|
|
# Construct a queryset for allowable stock items
|
|
|
|
queryset = StockItem.objects.filter(StockItem.IN_STOCK_FILTER)
|
2020-04-22 11:26:38 +00:00
|
|
|
|
|
|
|
# Ensure the part reference matches
|
|
|
|
queryset = queryset.filter(part=line.part)
|
|
|
|
|
|
|
|
# Exclude StockItem which are already allocated to this order
|
|
|
|
allocated = [allocation.item.pk for allocation in line.allocations.all()]
|
|
|
|
|
|
|
|
queryset = queryset.exclude(pk__in=allocated)
|
|
|
|
|
2021-01-06 09:22:56 +00:00
|
|
|
# Exclude stock items which have expired
|
|
|
|
if not InvenTreeSetting.get_setting('STOCK_ALLOW_EXPIRED_SALE'):
|
|
|
|
queryset = queryset.exclude(StockItem.EXPIRED_FILTER)
|
|
|
|
|
2020-04-22 11:26:38 +00:00
|
|
|
form.fields['item'].queryset = queryset
|
|
|
|
|
|
|
|
# Hide the 'line' field
|
|
|
|
form.fields['line'].widget = HiddenInput()
|
|
|
|
|
2020-04-22 12:24:06 +00:00
|
|
|
except (ValueError, SalesOrderLineItem.DoesNotExist):
|
2020-04-22 11:26:38 +00:00
|
|
|
pass
|
|
|
|
|
|
|
|
return form
|
2020-04-22 12:22:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
class SalesOrderAllocationEdit(AjaxUpdateView):
|
|
|
|
|
|
|
|
model = SalesOrderAllocation
|
2020-04-22 13:21:54 +00:00
|
|
|
form_class = order_forms.EditSalesOrderAllocationForm
|
2020-04-22 12:22:22 +00:00
|
|
|
ajax_form_title = _('Edit Allocation Quantity')
|
2021-03-02 10:32:32 +00:00
|
|
|
|
2020-04-22 12:22:22 +00:00
|
|
|
def get_form(self):
|
|
|
|
form = super().get_form()
|
|
|
|
|
|
|
|
# Prevent the user from editing particular fields
|
|
|
|
form.fields.pop('item')
|
|
|
|
form.fields.pop('line')
|
|
|
|
|
|
|
|
return form
|
2020-04-22 13:21:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
class SalesOrderAllocationDelete(AjaxDeleteView):
|
|
|
|
|
|
|
|
model = SalesOrderAllocation
|
|
|
|
ajax_form_title = _("Remove allocation")
|
2020-04-24 00:20:56 +00:00
|
|
|
context_object_name = 'allocation'
|
|
|
|
ajax_template_name = "order/so_allocation_delete.html"
|