mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 20:45:44 +00:00
Adds "ON HOLD" status to order models (#7807)
* Add "ON_HOLD" status code for orders * Add placeholder buttons for purchase order status change * Adds hooks for introspecting status code enumerations * Refactor status codes for import session - Remove hard-coded values * Refactor into <PrimaryActionButton /> * Cleanup * more permission checks * Add placeholder actions for SalesOrder * Placeholder actions for ReturnOrder * Placeholder actions for build order * Actions for "return order" * Update actions for return order - Add "on hold" transition * Implement transitions for SalesOrder * Allow control over SalesOrderLineItemTable * Implement PurchaseOrder actions * Improve API query lookup efficiency * UI cleanup * CUI cleanup * Build Order Updates - Implement StateTransitionMixin for BuildOrder model - Add BuildIssue API endpoint - Add BuildHold API endpoint - API query improvements - PUI actions * Increase timeout * Bump API version * Fix API version * Fix sales order actions * Update src/backend/InvenTree/order/serializers.py Co-authored-by: Matthias Mair <code@mjmair.com> * Adjust build filters * PUI updates * CUI refactoring for purchase orders * Refactor CUI sales order page * Refactor for return order * Refactor CUI build page * Playwright tests for build order * Add playwright test for sales orders * Add playwright test for purchase orders * js linting * Refactor return order page * Add missing functions from previous commit * Fix for "on order" badge on PartDetail page * UI tweaks * Fix unit tests * Update version check script * Fix typo * Enforce integer conversion for BaseEnum class * Unit test updates - Includes improvement for equality comparison for enums * Update documentation --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
@ -1,13 +1,19 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 232
|
||||
INVENTREE_API_VERSION = 233
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v233 - 2024-08-04 : https://github.com/inventree/InvenTree/pull/7807
|
||||
- Adds new endpoints for managing state of build orders
|
||||
- Adds new endpoints for managing state of purchase orders
|
||||
- Adds new endpoints for managing state of sales orders
|
||||
- Adds new endpoints for managing state of return orders
|
||||
|
||||
v232 - 2024-08-03 : https://github.com/inventree/InvenTree/pull/7793
|
||||
- Allow ordering of SalesOrderShipment API by 'shipment_date' and 'delivery_date'
|
||||
|
||||
|
@ -470,9 +470,19 @@ class BuildFinish(BuildOrderContextMixin, CreateAPI):
|
||||
"""API endpoint for marking a build as finished (completed)."""
|
||||
|
||||
queryset = Build.objects.none()
|
||||
|
||||
serializer_class = build.serializers.BuildCompleteSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return the queryset for the BuildFinish API endpoint."""
|
||||
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.prefetch_related(
|
||||
'build_lines',
|
||||
'build_lines__allocations'
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class BuildAutoAllocate(BuildOrderContextMixin, CreateAPI):
|
||||
"""API endpoint for 'automatically' allocating stock against a build order.
|
||||
@ -484,7 +494,6 @@ class BuildAutoAllocate(BuildOrderContextMixin, CreateAPI):
|
||||
"""
|
||||
|
||||
queryset = Build.objects.none()
|
||||
|
||||
serializer_class = build.serializers.BuildAutoAllocationSerializer
|
||||
|
||||
|
||||
@ -500,10 +509,22 @@ class BuildAllocate(BuildOrderContextMixin, CreateAPI):
|
||||
"""
|
||||
|
||||
queryset = Build.objects.none()
|
||||
|
||||
serializer_class = build.serializers.BuildAllocationSerializer
|
||||
|
||||
|
||||
class BuildIssue(BuildOrderContextMixin, CreateAPI):
|
||||
"""API endpoint for issuing a BuildOrder."""
|
||||
|
||||
queryset = Build.objects.all()
|
||||
serializer_class = build.serializers.BuildIssueSerializer
|
||||
|
||||
|
||||
class BuildHold(BuildOrderContextMixin, CreateAPI):
|
||||
"""API endpoint for placing a BuildOrder on hold."""
|
||||
|
||||
queryset = Build.objects.all()
|
||||
serializer_class = build.serializers.BuildHoldSerializer
|
||||
|
||||
class BuildCancel(BuildOrderContextMixin, CreateAPI):
|
||||
"""API endpoint for cancelling a BuildOrder."""
|
||||
|
||||
@ -663,6 +684,8 @@ build_api_urls = [
|
||||
path('create-output/', BuildOutputCreate.as_view(), name='api-build-output-create'),
|
||||
path('delete-outputs/', BuildOutputDelete.as_view(), name='api-build-output-delete'),
|
||||
path('scrap-outputs/', BuildOutputScrap.as_view(), name='api-build-output-scrap'),
|
||||
path('issue/', BuildIssue.as_view(), name='api-build-issue'),
|
||||
path('hold/', BuildHold.as_view(), name='api-build-hold'),
|
||||
path('finish/', BuildFinish.as_view(), name='api-build-finish'),
|
||||
path('cancel/', BuildCancel.as_view(), name='api-build-cancel'),
|
||||
path('unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'),
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
import decimal
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from django.conf import settings
|
||||
|
||||
@ -26,6 +25,7 @@ from build.status_codes import BuildStatus, BuildStatusGroups
|
||||
from stock.status_codes import StockStatus, StockHistoryCode
|
||||
|
||||
from build.validators import generate_next_build_reference, validate_build_order_reference
|
||||
from generic.states import StateTransitionMixin
|
||||
|
||||
import InvenTree.fields
|
||||
import InvenTree.helpers
|
||||
@ -56,6 +56,7 @@ class Build(
|
||||
InvenTree.models.MetadataMixin,
|
||||
InvenTree.models.PluginValidationMixin,
|
||||
InvenTree.models.ReferenceIndexingMixin,
|
||||
StateTransitionMixin,
|
||||
MPTTModel):
|
||||
"""A Build object organises the creation of new StockItem objects from other existing StockItem objects.
|
||||
|
||||
@ -574,6 +575,10 @@ class Build(
|
||||
- Completed count must meet the required quantity
|
||||
- Untracked parts must be allocated
|
||||
"""
|
||||
|
||||
if self.status != BuildStatus.PRODUCTION.value:
|
||||
return False
|
||||
|
||||
if self.incomplete_count > 0:
|
||||
return False
|
||||
|
||||
@ -602,8 +607,18 @@ class Build(
|
||||
def complete_build(self, user, trim_allocated_stock=False):
|
||||
"""Mark this build as complete."""
|
||||
|
||||
return self.handle_transition(
|
||||
self.status, BuildStatus.COMPLETE.value, self, self._action_complete, user=user, trim_allocated_stock=trim_allocated_stock
|
||||
)
|
||||
|
||||
def _action_complete(self, *args, **kwargs):
|
||||
"""Action to be taken when a build is completed."""
|
||||
|
||||
import build.tasks
|
||||
|
||||
trim_allocated_stock = kwargs.pop('trim_allocated_stock', False)
|
||||
user = kwargs.pop('user', None)
|
||||
|
||||
if self.incomplete_count > 0:
|
||||
return
|
||||
|
||||
@ -665,6 +680,59 @@ class Build(
|
||||
target_exclude=[user],
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def issue_build(self):
|
||||
"""Mark the Build as IN PRODUCTION.
|
||||
|
||||
Args:
|
||||
user: The user who is issuing the build
|
||||
"""
|
||||
return self.handle_transition(
|
||||
self.status, BuildStatus.PENDING.value, self, self._action_issue
|
||||
)
|
||||
|
||||
@property
|
||||
def can_issue(self):
|
||||
"""Returns True if this BuildOrder can be issued."""
|
||||
return self.status in [
|
||||
BuildStatus.PENDING.value,
|
||||
BuildStatus.ON_HOLD.value,
|
||||
]
|
||||
|
||||
def _action_issue(self, *args, **kwargs):
|
||||
"""Perform the action to mark this order as PRODUCTION."""
|
||||
|
||||
if self.can_issue:
|
||||
self.status = BuildStatus.PRODUCTION.value
|
||||
self.save()
|
||||
|
||||
trigger_event('build.issued', id=self.pk)
|
||||
|
||||
@transaction.atomic
|
||||
def hold_build(self):
|
||||
"""Mark the Build as ON HOLD."""
|
||||
|
||||
return self.handle_transition(
|
||||
self.status, BuildStatus.ON_HOLD.value, self, self._action_hold
|
||||
)
|
||||
|
||||
@property
|
||||
def can_hold(self):
|
||||
"""Returns True if this BuildOrder can be placed on hold"""
|
||||
return self.status in [
|
||||
BuildStatus.PENDING.value,
|
||||
BuildStatus.PRODUCTION.value,
|
||||
]
|
||||
|
||||
def _action_hold(self, *args, **kwargs):
|
||||
"""Action to be taken when a build is placed on hold."""
|
||||
|
||||
if self.can_hold:
|
||||
self.status = BuildStatus.ON_HOLD.value
|
||||
self.save()
|
||||
|
||||
trigger_event('build.hold', id=self.pk)
|
||||
|
||||
@transaction.atomic
|
||||
def cancel_build(self, user, **kwargs):
|
||||
"""Mark the Build as CANCELLED.
|
||||
@ -674,8 +742,17 @@ class Build(
|
||||
- Save the Build object
|
||||
"""
|
||||
|
||||
return self.handle_transition(
|
||||
self.status, BuildStatus.CANCELLED.value, self, self._action_cancel, user=user, **kwargs
|
||||
)
|
||||
|
||||
def _action_cancel(self, *args, **kwargs):
|
||||
"""Action to be taken when a build is cancelled."""
|
||||
|
||||
import build.tasks
|
||||
|
||||
user = kwargs.pop('user', None)
|
||||
|
||||
remove_allocated_stock = kwargs.get('remove_allocated_stock', False)
|
||||
remove_incomplete_outputs = kwargs.get('remove_incomplete_outputs', False)
|
||||
|
||||
@ -1276,7 +1353,7 @@ class Build(
|
||||
@property
|
||||
def is_complete(self):
|
||||
"""Returns True if the build status is COMPLETE."""
|
||||
return self.status == BuildStatus.COMPLETE
|
||||
return self.status == BuildStatus.COMPLETE.value
|
||||
|
||||
@transaction.atomic
|
||||
def create_build_line_items(self, prevent_duplicates=True):
|
||||
|
@ -34,6 +34,7 @@ import part.serializers as part_serializers
|
||||
from users.serializers import OwnerSerializer
|
||||
|
||||
from .models import Build, BuildLine, BuildItem
|
||||
from .status_codes import BuildStatus
|
||||
|
||||
|
||||
class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTreeModelSerializer):
|
||||
@ -597,6 +598,33 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
|
||||
class BuildIssueSerializer(serializers.Serializer):
|
||||
"""DRF serializer for issuing a build order."""
|
||||
|
||||
class Meta:
|
||||
"""Serializer metaclass"""
|
||||
fields = []
|
||||
|
||||
def save(self):
|
||||
"""Issue the specified build order"""
|
||||
build = self.context['build']
|
||||
build.issue_build()
|
||||
|
||||
|
||||
class BuildHoldSerializer(serializers.Serializer):
|
||||
"""DRF serializer for placing a BuildOrder on hold."""
|
||||
|
||||
class Meta:
|
||||
"""Serializer metaclass."""
|
||||
fields = []
|
||||
|
||||
def save(self):
|
||||
"""Place the specified build on hold."""
|
||||
build = self.context['build']
|
||||
|
||||
build.hold_build()
|
||||
|
||||
|
||||
class BuildCancelSerializer(serializers.Serializer):
|
||||
"""DRF serializer class for cancelling an active BuildOrder"""
|
||||
|
||||
@ -737,6 +765,9 @@ class BuildCompleteSerializer(serializers.Serializer):
|
||||
"""Perform validation of this serializer prior to saving"""
|
||||
build = self.context['build']
|
||||
|
||||
if build.status != BuildStatus.PRODUCTION.value:
|
||||
raise ValidationError(_("Build order must be in production state"))
|
||||
|
||||
if build.incomplete_count > 0:
|
||||
raise ValidationError(_("Build order has incomplete outputs"))
|
||||
|
||||
|
@ -10,6 +10,7 @@ class BuildStatus(StatusCode):
|
||||
|
||||
PENDING = 10, _('Pending'), 'secondary' # Build is pending / active
|
||||
PRODUCTION = 20, _('Production'), 'primary' # Build is in production
|
||||
ON_HOLD = 25, _('On Hold'), 'warning' # Build is on hold
|
||||
CANCELLED = 30, _('Cancelled'), 'danger' # Build was cancelled
|
||||
COMPLETE = 40, _('Complete'), 'success' # Build is complete
|
||||
|
||||
@ -19,5 +20,6 @@ class BuildStatusGroups:
|
||||
|
||||
ACTIVE_CODES = [
|
||||
BuildStatus.PENDING.value,
|
||||
BuildStatus.ON_HOLD.value,
|
||||
BuildStatus.PRODUCTION.value,
|
||||
]
|
||||
|
@ -69,22 +69,30 @@ src="{% static 'img/blank_image.png' %}"
|
||||
</button>
|
||||
<ul class='dropdown-menu' role='menu'>
|
||||
<li><a class='dropdown-item' href='#' id='build-edit'><span class='fas fa-edit icon-green'></span> {% trans "Edit Build" %}</a></li>
|
||||
{% if build.is_active %}
|
||||
<li><a class='dropdown-item' href='#' id='build-cancel'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel Build" %}</a></li>
|
||||
{% endif %}
|
||||
{% if roles.build.add %}
|
||||
<li><a class='dropdown-item' href='#' id='build-duplicate'><span class='fas fa-clone'></span> {% trans "Duplicate Build" %}</a></li>
|
||||
{% endif %}
|
||||
{% if build.can_hold %}
|
||||
<li><a class='dropdown-item' href='#' id='build-hold'><span class='fas fa-hand-paper icon-yellow'></span> {% trans "Hold Build" %}</a></li>
|
||||
{% endif %}
|
||||
{% if build.is_active %}
|
||||
<li><a class='dropdown-item' href='#' id='build-cancel'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel Build" %}</a></li>
|
||||
{% endif %}
|
||||
{% if build.status == BuildStatus.CANCELLED and roles.build.delete %}
|
||||
<li><a class='dropdown-item' href='#' id='build-delete'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete Build" %}</a>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% if build.active %}
|
||||
{% if build.can_issue %}
|
||||
<button id='build-issue' title='{% trans "Isueue Build" %}' class='btn btn-primary'>
|
||||
<span class='fas fa-paper-plane'></span> {% trans "Issue Build" %}
|
||||
</button>
|
||||
{% elif build.active %}
|
||||
<button id='build-complete' title='{% trans "Complete Build" %}' class='btn btn-success'>
|
||||
<span class='fas fa-check-circle'></span> {% trans "Complete Build" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
{% endblock actions %}
|
||||
|
||||
@ -244,6 +252,31 @@ src="{% static 'img/blank_image.png' %}"
|
||||
);
|
||||
});
|
||||
|
||||
$('#build-hold').click(function() {
|
||||
holdOrder(
|
||||
'{% url "api-build-hold" build.pk %}',
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$('#build-issue').click(function() {
|
||||
constructForm('{% url "api-build-issue" build.pk %}', {
|
||||
method: 'POST',
|
||||
title: '{% trans "Issue Build Order" %}',
|
||||
confirm: true,
|
||||
preFormContent: `
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "Issue this Build Order?" %}
|
||||
</div>
|
||||
`,
|
||||
onSuccess: function(response) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#build-complete").on('click', function() {
|
||||
completeBuildOrder({{ build.pk }});
|
||||
});
|
||||
|
@ -15,6 +15,7 @@ import common.models
|
||||
from common.settings import set_global_setting
|
||||
import build.tasks
|
||||
from build.models import Build, BuildItem, BuildLine, generate_next_build_reference
|
||||
from build.status_codes import BuildStatus
|
||||
from part.models import Part, BomItem, BomItemSubstitute, PartTestTemplate
|
||||
from stock.models import StockItem, StockItemTestResult
|
||||
from users.models import Owner
|
||||
@ -175,6 +176,7 @@ class BuildTestBase(TestCase):
|
||||
part=cls.assembly,
|
||||
quantity=10,
|
||||
issued_by=get_user_model().objects.get(pk=1),
|
||||
status=BuildStatus.PENDING,
|
||||
)
|
||||
|
||||
# Create some BuildLine items we can use later on
|
||||
@ -321,6 +323,10 @@ class BuildTest(BuildTestBase):
|
||||
# Build is PENDING
|
||||
self.assertEqual(self.build.status, status.BuildStatus.PENDING)
|
||||
|
||||
self.assertTrue(self.build.is_active)
|
||||
self.assertTrue(self.build.can_hold)
|
||||
self.assertTrue(self.build.can_issue)
|
||||
|
||||
# Build has two build outputs
|
||||
self.assertEqual(self.build.output_count, 2)
|
||||
|
||||
@ -470,6 +476,11 @@ class BuildTest(BuildTestBase):
|
||||
|
||||
def test_overallocation_and_trim(self):
|
||||
"""Test overallocation of stock and trim function"""
|
||||
|
||||
self.assertEqual(self.build.status, status.BuildStatus.PENDING)
|
||||
self.build.issue_build()
|
||||
self.assertEqual(self.build.status, status.BuildStatus.PRODUCTION)
|
||||
|
||||
# Fully allocate tracked stock (not eligible for trimming)
|
||||
self.allocate_stock(
|
||||
self.output_1,
|
||||
@ -516,6 +527,7 @@ class BuildTest(BuildTestBase):
|
||||
|
||||
self.build.complete_build_output(self.output_1, None)
|
||||
self.build.complete_build_output(self.output_2, None)
|
||||
|
||||
self.assertTrue(self.build.can_complete)
|
||||
|
||||
n = StockItem.objects.filter(consumed_by=self.build).count()
|
||||
@ -583,6 +595,8 @@ class BuildTest(BuildTestBase):
|
||||
self.stock_2_1.quantity = 30
|
||||
self.stock_2_1.save()
|
||||
|
||||
self.build.issue_build()
|
||||
|
||||
# Allocate non-tracked parts
|
||||
self.allocate_stock(
|
||||
None,
|
||||
|
@ -16,11 +16,26 @@ class BaseEnum(enum.IntEnum):
|
||||
obj._value_ = args[0]
|
||||
return obj
|
||||
|
||||
def __int__(self):
|
||||
"""Return an integer representation of the value."""
|
||||
return self.value
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the value."""
|
||||
return str(self.value)
|
||||
|
||||
def __eq__(self, obj):
|
||||
"""Override equality operator to allow comparison with int."""
|
||||
if type(self) is type(obj):
|
||||
return super().__eq__(obj)
|
||||
return self.value == obj
|
||||
if type(obj) is int:
|
||||
return self.value == obj
|
||||
|
||||
if isinstance(obj, BaseEnum):
|
||||
return self.value == obj.value
|
||||
|
||||
if hasattr(obj, 'value'):
|
||||
return self.value == obj.value
|
||||
|
||||
return super().__eq__(obj)
|
||||
|
||||
def __ne__(self, obj):
|
||||
"""Override inequality operator to allow comparison with int."""
|
||||
|
@ -360,6 +360,12 @@ class PurchaseOrderContextMixin:
|
||||
return context
|
||||
|
||||
|
||||
class PurchaseOrderHold(PurchaseOrderContextMixin, CreateAPI):
|
||||
"""API endpoint to place a PurchaseOrder on hold."""
|
||||
|
||||
serializer_class = serializers.PurchaseOrderHoldSerializer
|
||||
|
||||
|
||||
class PurchaseOrderCancel(PurchaseOrderContextMixin, CreateAPI):
|
||||
"""API endpoint to 'cancel' a purchase order.
|
||||
|
||||
@ -893,6 +899,12 @@ class SalesOrderContextMixin:
|
||||
return ctx
|
||||
|
||||
|
||||
class SalesOrderHold(SalesOrderContextMixin, CreateAPI):
|
||||
"""API endpoint to place a SalesOrder on hold."""
|
||||
|
||||
serializer_class = serializers.SalesOrderHoldSerializer
|
||||
|
||||
|
||||
class SalesOrderCancel(SalesOrderContextMixin, CreateAPI):
|
||||
"""API endpoint to cancel a SalesOrder."""
|
||||
|
||||
@ -1198,6 +1210,12 @@ class ReturnOrderCancel(ReturnOrderContextMixin, CreateAPI):
|
||||
serializer_class = serializers.ReturnOrderCancelSerializer
|
||||
|
||||
|
||||
class ReturnOrderHold(ReturnOrderContextMixin, CreateAPI):
|
||||
"""API endpoint to hold a ReturnOrder."""
|
||||
|
||||
serializer_class = serializers.ReturnOrderHoldSerializer
|
||||
|
||||
|
||||
class ReturnOrderComplete(ReturnOrderContextMixin, CreateAPI):
|
||||
"""API endpoint to complete a ReturnOrder."""
|
||||
|
||||
@ -1481,6 +1499,7 @@ order_api_urls = [
|
||||
path(
|
||||
'cancel/', PurchaseOrderCancel.as_view(), name='api-po-cancel'
|
||||
),
|
||||
path('hold/', PurchaseOrderHold.as_view(), name='api-po-hold'),
|
||||
path(
|
||||
'complete/',
|
||||
PurchaseOrderComplete.as_view(),
|
||||
@ -1610,6 +1629,7 @@ order_api_urls = [
|
||||
SalesOrderAllocateSerials.as_view(),
|
||||
name='api-so-allocate-serials',
|
||||
),
|
||||
path('hold/', SalesOrderHold.as_view(), name='api-so-hold'),
|
||||
path('cancel/', SalesOrderCancel.as_view(), name='api-so-cancel'),
|
||||
path('issue/', SalesOrderIssue.as_view(), name='api-so-issue'),
|
||||
path(
|
||||
@ -1709,6 +1729,7 @@ order_api_urls = [
|
||||
ReturnOrderCancel.as_view(),
|
||||
name='api-return-order-cancel',
|
||||
),
|
||||
path('hold/', ReturnOrderHold.as_view(), name='api-ro-hold'),
|
||||
path(
|
||||
'complete/',
|
||||
ReturnOrderComplete.as_view(),
|
||||
|
@ -609,7 +609,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
|
||||
Order must be currently PENDING.
|
||||
"""
|
||||
if self.is_pending:
|
||||
if self.can_issue:
|
||||
self.status = PurchaseOrderStatus.PLACED.value
|
||||
self.issue_date = InvenTree.helpers.current_date()
|
||||
self.save()
|
||||
@ -642,6 +642,19 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
|
||||
trigger_event('purchaseorder.completed', id=self.pk)
|
||||
|
||||
@transaction.atomic
|
||||
def issue_order(self):
|
||||
"""Equivalent to 'place_order'."""
|
||||
self.place_order()
|
||||
|
||||
@property
|
||||
def can_issue(self):
|
||||
"""Return True if this order can be issued."""
|
||||
return self.status in [
|
||||
PurchaseOrderStatus.PENDING.value,
|
||||
PurchaseOrderStatus.ON_HOLD.value,
|
||||
]
|
||||
|
||||
@transaction.atomic
|
||||
def place_order(self):
|
||||
"""Attempt to transition to PLACED status."""
|
||||
@ -656,6 +669,13 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
self.status, PurchaseOrderStatus.COMPLETE.value, self, self._action_complete
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def hold_order(self):
|
||||
"""Attempt to transition to ON_HOLD status."""
|
||||
return self.handle_transition(
|
||||
self.status, PurchaseOrderStatus.ON_HOLD.value, self, self._action_hold
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def cancel_order(self):
|
||||
"""Attempt to transition to CANCELLED status."""
|
||||
@ -678,12 +698,9 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
"""A PurchaseOrder can only be cancelled under the following circumstances.
|
||||
|
||||
- Status is PLACED
|
||||
- Status is PENDING
|
||||
- Status is PENDING (or ON_HOLD)
|
||||
"""
|
||||
return self.status in [
|
||||
PurchaseOrderStatus.PLACED.value,
|
||||
PurchaseOrderStatus.PENDING.value,
|
||||
]
|
||||
return self.status in PurchaseOrderStatusGroups.OPEN
|
||||
|
||||
def _action_cancel(self, *args, **kwargs):
|
||||
"""Marks the PurchaseOrder as CANCELLED."""
|
||||
@ -701,6 +718,22 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
content=InvenTreeNotificationBodies.OrderCanceled,
|
||||
)
|
||||
|
||||
@property
|
||||
def can_hold(self):
|
||||
"""Return True if this order can be placed on hold."""
|
||||
return self.status in [
|
||||
PurchaseOrderStatus.PENDING.value,
|
||||
PurchaseOrderStatus.PLACED.value,
|
||||
]
|
||||
|
||||
def _action_hold(self, *args, **kwargs):
|
||||
"""Mark this purchase order as 'on hold'."""
|
||||
if self.can_hold:
|
||||
self.status = PurchaseOrderStatus.ON_HOLD.value
|
||||
self.save()
|
||||
|
||||
trigger_event('purchaseorder.hold', id=self.pk)
|
||||
|
||||
# endregion
|
||||
|
||||
def pending_line_items(self):
|
||||
@ -1074,15 +1107,39 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
"""Deprecated version of 'issue_order'."""
|
||||
self.issue_order()
|
||||
|
||||
@property
|
||||
def can_issue(self):
|
||||
"""Return True if this order can be issued."""
|
||||
return self.status in [
|
||||
SalesOrderStatus.PENDING.value,
|
||||
SalesOrderStatus.ON_HOLD.value,
|
||||
]
|
||||
|
||||
def _action_place(self, *args, **kwargs):
|
||||
"""Change this order from 'PENDING' to 'IN_PROGRESS'."""
|
||||
if self.status == SalesOrderStatus.PENDING:
|
||||
if self.can_issue:
|
||||
self.status = SalesOrderStatus.IN_PROGRESS.value
|
||||
self.issue_date = InvenTree.helpers.current_date()
|
||||
self.save()
|
||||
|
||||
trigger_event('salesorder.issued', id=self.pk)
|
||||
|
||||
@property
|
||||
def can_hold(self):
|
||||
"""Return True if this order can be placed on hold."""
|
||||
return self.status in [
|
||||
SalesOrderStatus.PENDING.value,
|
||||
SalesOrderStatus.IN_PROGRESS.value,
|
||||
]
|
||||
|
||||
def _action_hold(self, *args, **kwargs):
|
||||
"""Mark this sales order as 'on hold'."""
|
||||
if self.can_hold:
|
||||
self.status = SalesOrderStatus.ON_HOLD.value
|
||||
self.save()
|
||||
|
||||
trigger_event('salesorder.onhold', id=self.pk)
|
||||
|
||||
def _action_complete(self, *args, **kwargs):
|
||||
"""Mark this order as "complete."""
|
||||
user = kwargs.pop('user', None)
|
||||
@ -1176,6 +1233,13 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def hold_order(self):
|
||||
"""Attempt to transition to ON_HOLD status."""
|
||||
return self.handle_transition(
|
||||
self.status, SalesOrderStatus.ON_HOLD.value, self, self._action_hold
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def cancel_order(self):
|
||||
"""Attempt to transition to CANCELLED status."""
|
||||
@ -2133,9 +2197,30 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
"""Return True if this order is fully received."""
|
||||
return not self.lines.filter(received_date=None).exists()
|
||||
|
||||
@property
|
||||
def can_hold(self):
|
||||
"""Return True if this order can be placed on hold."""
|
||||
return self.status in [
|
||||
ReturnOrderStatus.PENDING.value,
|
||||
ReturnOrderStatus.IN_PROGRESS.value,
|
||||
]
|
||||
|
||||
def _action_hold(self, *args, **kwargs):
|
||||
"""Mark this order as 'on hold' (if allowed)."""
|
||||
if self.can_hold:
|
||||
self.status = ReturnOrderStatus.ON_HOLD.value
|
||||
self.save()
|
||||
|
||||
trigger_event('returnorder.hold', id=self.pk)
|
||||
|
||||
@property
|
||||
def can_cancel(self):
|
||||
"""Return True if this order can be cancelled."""
|
||||
return self.status in ReturnOrderStatusGroups.OPEN
|
||||
|
||||
def _action_cancel(self, *args, **kwargs):
|
||||
"""Cancel this ReturnOrder (if not already cancelled)."""
|
||||
if self.status != ReturnOrderStatus.CANCELLED:
|
||||
if self.can_cancel:
|
||||
self.status = ReturnOrderStatus.CANCELLED.value
|
||||
self.save()
|
||||
|
||||
@ -2151,7 +2236,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
|
||||
def _action_complete(self, *args, **kwargs):
|
||||
"""Complete this ReturnOrder (if not already completed)."""
|
||||
if self.status == ReturnOrderStatus.IN_PROGRESS:
|
||||
if self.status == ReturnOrderStatus.IN_PROGRESS.value:
|
||||
self.status = ReturnOrderStatus.COMPLETE.value
|
||||
self.complete_date = InvenTree.helpers.current_date()
|
||||
self.save()
|
||||
@ -2162,15 +2247,30 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
"""Deprecated version of 'issue_order."""
|
||||
self.issue_order()
|
||||
|
||||
@property
|
||||
def can_issue(self):
|
||||
"""Return True if this order can be issued."""
|
||||
return self.status in [
|
||||
ReturnOrderStatus.PENDING.value,
|
||||
ReturnOrderStatus.ON_HOLD.value,
|
||||
]
|
||||
|
||||
def _action_place(self, *args, **kwargs):
|
||||
"""Issue this ReturnOrder (if currently pending)."""
|
||||
if self.status == ReturnOrderStatus.PENDING:
|
||||
if self.can_issue:
|
||||
self.status = ReturnOrderStatus.IN_PROGRESS.value
|
||||
self.issue_date = InvenTree.helpers.current_date()
|
||||
self.save()
|
||||
|
||||
trigger_event('returnorder.issued', id=self.pk)
|
||||
|
||||
@transaction.atomic
|
||||
def hold_order(self):
|
||||
"""Attempt to tranasition to ON_HOLD status."""
|
||||
return self.handle_transition(
|
||||
self.status, ReturnOrderStatus.ON_HOLD.value, self, self._action_hold
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def issue_order(self):
|
||||
"""Attempt to transition to IN_PROGRESS status."""
|
||||
|
@ -284,14 +284,37 @@ class PurchaseOrderSerializer(
|
||||
)
|
||||
|
||||
|
||||
class PurchaseOrderCancelSerializer(serializers.Serializer):
|
||||
"""Serializer for cancelling a PurchaseOrder."""
|
||||
class OrderAdjustSerializer(serializers.Serializer):
|
||||
"""Generic serializer class for adjusting the status of an order."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
"""Metaclass options.
|
||||
|
||||
By default, there are no fields required for this serializer type.
|
||||
"""
|
||||
|
||||
fields = []
|
||||
|
||||
@property
|
||||
def order(self):
|
||||
"""Return the order object associated with this serializer.
|
||||
|
||||
Note: It is passed in via the serializer context data.
|
||||
"""
|
||||
return self.context['order']
|
||||
|
||||
|
||||
class PurchaseOrderHoldSerializer(OrderAdjustSerializer):
|
||||
"""Serializer for placing a PurchaseOrder on hold."""
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to 'hold' the order."""
|
||||
self.order.hold_order()
|
||||
|
||||
|
||||
class PurchaseOrderCancelSerializer(OrderAdjustSerializer):
|
||||
"""Serializer for cancelling a PurchaseOrder."""
|
||||
|
||||
def get_context_data(self):
|
||||
"""Return custom context information about the order."""
|
||||
self.order = self.context['order']
|
||||
@ -300,21 +323,19 @@ class PurchaseOrderCancelSerializer(serializers.Serializer):
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to 'cancel' the order."""
|
||||
order = self.context['order']
|
||||
|
||||
if not order.can_cancel:
|
||||
if not self.order.can_cancel:
|
||||
raise ValidationError(_('Order cannot be cancelled'))
|
||||
|
||||
order.cancel_order()
|
||||
self.order.cancel_order()
|
||||
|
||||
|
||||
class PurchaseOrderCompleteSerializer(serializers.Serializer):
|
||||
class PurchaseOrderCompleteSerializer(OrderAdjustSerializer):
|
||||
"""Serializer for completing a purchase order."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = []
|
||||
fields = ['accept_incomplete']
|
||||
|
||||
accept_incomplete = serializers.BooleanField(
|
||||
label=_('Accept Incomplete'),
|
||||
@ -340,22 +361,15 @@ class PurchaseOrderCompleteSerializer(serializers.Serializer):
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to 'complete' the order."""
|
||||
order = self.context['order']
|
||||
order.complete_order()
|
||||
self.order.complete_order()
|
||||
|
||||
|
||||
class PurchaseOrderIssueSerializer(serializers.Serializer):
|
||||
class PurchaseOrderIssueSerializer(OrderAdjustSerializer):
|
||||
"""Serializer for issuing (sending) a purchase order."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = []
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to 'place' the order."""
|
||||
order = self.context['order']
|
||||
order.place_order()
|
||||
self.order.place_order()
|
||||
|
||||
|
||||
@register_importer()
|
||||
@ -402,7 +416,6 @@ class PurchaseOrderLineItemSerializer(
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer."""
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
|
||||
order_detail = kwargs.pop('order_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -436,6 +449,18 @@ class PurchaseOrderLineItemSerializer(
|
||||
)
|
||||
)
|
||||
|
||||
queryset = queryset.prefetch_related(
|
||||
'order',
|
||||
'order__responsible',
|
||||
'order__stock_items',
|
||||
'part__tags',
|
||||
'part__supplier',
|
||||
'part__manufacturer_part',
|
||||
'part__manufacturer_part__manufacturer',
|
||||
'part__part__pricing_data',
|
||||
'part__part__tags',
|
||||
)
|
||||
|
||||
queryset = queryset.annotate(
|
||||
total_price=ExpressionWrapper(
|
||||
F('purchase_price') * F('quantity'), output_field=models.DecimalField()
|
||||
@ -489,7 +514,7 @@ class PurchaseOrderLineItemSerializer(
|
||||
)
|
||||
|
||||
supplier_part_detail = SupplierPartSerializer(
|
||||
source='part', many=False, read_only=True
|
||||
source='part', brief=True, many=False, read_only=True
|
||||
)
|
||||
|
||||
purchase_price = InvenTreeMoneySerializer(allow_null=True)
|
||||
@ -898,18 +923,12 @@ class SalesOrderSerializer(
|
||||
)
|
||||
|
||||
|
||||
class SalesOrderIssueSerializer(serializers.Serializer):
|
||||
class SalesOrderIssueSerializer(OrderAdjustSerializer):
|
||||
"""Serializer for issuing a SalesOrder."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = []
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to 'issue' the order."""
|
||||
order = self.context['order']
|
||||
order.issue_order()
|
||||
self.order.issue_order()
|
||||
|
||||
|
||||
class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
||||
@ -1313,9 +1332,14 @@ class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
|
||||
class SalesOrderCompleteSerializer(serializers.Serializer):
|
||||
class SalesOrderCompleteSerializer(OrderAdjustSerializer):
|
||||
"""DRF serializer for manually marking a sales order as complete."""
|
||||
|
||||
class Meta:
|
||||
"""Serializer metaclass options."""
|
||||
|
||||
fields = ['accept_incomplete']
|
||||
|
||||
accept_incomplete = serializers.BooleanField(
|
||||
label=_('Accept Incomplete'),
|
||||
help_text=_('Allow order to be closed with incomplete line items'),
|
||||
@ -1344,10 +1368,7 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
|
||||
def validate(self, data):
|
||||
"""Custom validation for the serializer."""
|
||||
data = super().validate(data)
|
||||
|
||||
order = self.context['order']
|
||||
|
||||
order.can_complete(
|
||||
self.order.can_complete(
|
||||
raise_error=True,
|
||||
allow_incomplete_lines=str2bool(data.get('accept_incomplete', False)),
|
||||
)
|
||||
@ -1357,17 +1378,24 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
|
||||
def save(self):
|
||||
"""Save the serializer to complete the SalesOrder."""
|
||||
request = self.context['request']
|
||||
order = self.context['order']
|
||||
data = self.validated_data
|
||||
|
||||
user = getattr(request, 'user', None)
|
||||
|
||||
order.ship_order(
|
||||
self.order.ship_order(
|
||||
user, allow_incomplete_lines=str2bool(data.get('accept_incomplete', False))
|
||||
)
|
||||
|
||||
|
||||
class SalesOrderCancelSerializer(serializers.Serializer):
|
||||
class SalesOrderHoldSerializer(OrderAdjustSerializer):
|
||||
"""Serializer for placing a SalesOrder on hold."""
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to place the SalesOrder on hold."""
|
||||
self.order.hold_order()
|
||||
|
||||
|
||||
class SalesOrderCancelSerializer(OrderAdjustSerializer):
|
||||
"""Serializer for marking a SalesOrder as cancelled."""
|
||||
|
||||
def get_context_data(self):
|
||||
@ -1378,9 +1406,7 @@ class SalesOrderCancelSerializer(serializers.Serializer):
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to cancel the order."""
|
||||
order = self.context['order']
|
||||
|
||||
order.cancel_order()
|
||||
self.order.cancel_order()
|
||||
|
||||
|
||||
class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
@ -1657,46 +1683,36 @@ class ReturnOrderSerializer(
|
||||
)
|
||||
|
||||
|
||||
class ReturnOrderIssueSerializer(serializers.Serializer):
|
||||
class ReturnOrderHoldSerializer(OrderAdjustSerializer):
|
||||
"""Serializers for holding a ReturnOrder."""
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to 'hold' the order."""
|
||||
self.order.hold_order()
|
||||
|
||||
|
||||
class ReturnOrderIssueSerializer(OrderAdjustSerializer):
|
||||
"""Serializer for issuing a ReturnOrder."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = []
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to 'issue' the order."""
|
||||
order = self.context['order']
|
||||
order.issue_order()
|
||||
self.order.issue_order()
|
||||
|
||||
|
||||
class ReturnOrderCancelSerializer(serializers.Serializer):
|
||||
class ReturnOrderCancelSerializer(OrderAdjustSerializer):
|
||||
"""Serializer for cancelling a ReturnOrder."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = []
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to 'cancel' the order."""
|
||||
order = self.context['order']
|
||||
order.cancel_order()
|
||||
self.order.cancel_order()
|
||||
|
||||
|
||||
class ReturnOrderCompleteSerializer(serializers.Serializer):
|
||||
class ReturnOrderCompleteSerializer(OrderAdjustSerializer):
|
||||
"""Serializer for completing a ReturnOrder."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = []
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to 'complete' the order."""
|
||||
order = self.context['order']
|
||||
order.complete_order()
|
||||
self.order.complete_order()
|
||||
|
||||
|
||||
class ReturnOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
|
@ -11,6 +11,7 @@ class PurchaseOrderStatus(StatusCode):
|
||||
# Order status codes
|
||||
PENDING = 10, _('Pending'), 'secondary' # Order is pending (not yet placed)
|
||||
PLACED = 20, _('Placed'), 'primary' # Order has been placed with supplier
|
||||
ON_HOLD = 25, _('On Hold'), 'warning' # Order is on hold
|
||||
COMPLETE = 30, _('Complete'), 'success' # Order has been completed
|
||||
CANCELLED = 40, _('Cancelled'), 'danger' # Order was cancelled
|
||||
LOST = 50, _('Lost'), 'warning' # Order was lost
|
||||
@ -21,7 +22,11 @@ class PurchaseOrderStatusGroups:
|
||||
"""Groups for PurchaseOrderStatus codes."""
|
||||
|
||||
# Open orders
|
||||
OPEN = [PurchaseOrderStatus.PENDING.value, PurchaseOrderStatus.PLACED.value]
|
||||
OPEN = [
|
||||
PurchaseOrderStatus.PENDING.value,
|
||||
PurchaseOrderStatus.ON_HOLD.value,
|
||||
PurchaseOrderStatus.PLACED.value,
|
||||
]
|
||||
|
||||
# Failed orders
|
||||
FAILED = [
|
||||
@ -41,6 +46,7 @@ class SalesOrderStatus(StatusCode):
|
||||
'primary',
|
||||
) # Order has been issued, and is in progress
|
||||
SHIPPED = 20, _('Shipped'), 'success' # Order has been shipped to customer
|
||||
ON_HOLD = 25, _('On Hold'), 'warning' # Order is on hold
|
||||
COMPLETE = 30, _('Complete'), 'success' # Order is complete
|
||||
CANCELLED = 40, _('Cancelled'), 'danger' # Order has been cancelled
|
||||
LOST = 50, _('Lost'), 'warning' # Order was lost
|
||||
@ -51,7 +57,11 @@ class SalesOrderStatusGroups:
|
||||
"""Groups for SalesOrderStatus codes."""
|
||||
|
||||
# Open orders
|
||||
OPEN = [SalesOrderStatus.PENDING.value, SalesOrderStatus.IN_PROGRESS.value]
|
||||
OPEN = [
|
||||
SalesOrderStatus.PENDING.value,
|
||||
SalesOrderStatus.ON_HOLD.value,
|
||||
SalesOrderStatus.IN_PROGRESS.value,
|
||||
]
|
||||
|
||||
# Completed orders
|
||||
COMPLETE = [SalesOrderStatus.SHIPPED.value, SalesOrderStatus.COMPLETE.value]
|
||||
@ -66,6 +76,8 @@ class ReturnOrderStatus(StatusCode):
|
||||
# Items have been received, and are being inspected
|
||||
IN_PROGRESS = 20, _('In Progress'), 'primary'
|
||||
|
||||
ON_HOLD = 25, _('On Hold'), 'warning'
|
||||
|
||||
COMPLETE = 30, _('Complete'), 'success'
|
||||
CANCELLED = 40, _('Cancelled'), 'danger'
|
||||
|
||||
@ -73,7 +85,11 @@ class ReturnOrderStatus(StatusCode):
|
||||
class ReturnOrderStatusGroups:
|
||||
"""Groups for ReturnOrderStatus codes."""
|
||||
|
||||
OPEN = [ReturnOrderStatus.PENDING.value, ReturnOrderStatus.IN_PROGRESS.value]
|
||||
OPEN = [
|
||||
ReturnOrderStatus.PENDING.value,
|
||||
ReturnOrderStatus.ON_HOLD.value,
|
||||
ReturnOrderStatus.IN_PROGRESS.value,
|
||||
]
|
||||
|
||||
|
||||
class ReturnOrderLineStatus(StatusCode):
|
||||
|
@ -63,23 +63,28 @@
|
||||
<li><a class='dropdown-item' href='#' id='edit-order'>
|
||||
<span class='fas fa-edit icon-green'></span> {% trans "Edit order" %}
|
||||
</a></li>
|
||||
{% if order.can_cancel %}
|
||||
<li><a class='dropdown-item' href='#' id='cancel-order'>
|
||||
<span class='fas fa-times-circle icon-red'></span> {% trans "Cancel order" %}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
{% if roles.purchase_order.add %}
|
||||
<li><a class='dropdown-item' href='#' id='duplicate-order'>
|
||||
<span class='fas fa-clone'></span> {% trans "Duplicate order" %}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
{% if order.can_hold %}
|
||||
<li><a class='dropdown-item' href='#' id='hold-order'>
|
||||
<span class='fas fa-hand-paper icon-yellow'></span> {% trans "Hold order" %}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
{% if order.can_cancel %}
|
||||
<li><a class='dropdown-item' href='#' id='cancel-order'>
|
||||
<span class='fas fa-times-circle icon-red'></span> {% trans "Cancel order" %}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% if order.is_pending %}
|
||||
{% if order.can_issue %}
|
||||
<button type='button' class='btn btn-primary' id='place-order' title='{% trans "Issue Order" %}'>
|
||||
<span class='fas fa-paper-plane'></span> {% trans "Issue Order" %}
|
||||
</button>
|
||||
{% elif order.is_open %}
|
||||
{% elif order.status == PurchaseOrderStatus.PLACED %}
|
||||
<button type='button' class='btn btn-success' id='complete-order' title='{% trans "Mark order as complete" %}'>
|
||||
<span class='fas fa-check-circle'></span> {% trans "Complete Order" %}
|
||||
</button>
|
||||
@ -238,7 +243,7 @@ src="{% static 'img/blank_image.png' %}"
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
{% if order.status == PurchaseOrderStatus.PENDING %}
|
||||
{% if order.status == PurchaseOrderStatus.PENDING or order.status == PurchaseOrderStatus.ON_HOLD %}
|
||||
$("#place-order").click(function() {
|
||||
|
||||
issuePurchaseOrder(
|
||||
@ -281,6 +286,7 @@ $("#complete-order").click(function() {
|
||||
);
|
||||
});
|
||||
|
||||
{% if order.can_cancel %}
|
||||
$("#cancel-order").click(function() {
|
||||
|
||||
cancelPurchaseOrder(
|
||||
@ -292,6 +298,21 @@ $("#cancel-order").click(function() {
|
||||
},
|
||||
);
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if order.can_hold %}
|
||||
$("#hold-order").click(function() {
|
||||
|
||||
holdOrder(
|
||||
'{% url "api-po-hold" order.pk %}',
|
||||
{
|
||||
onSuccess: function() {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
@ -74,11 +74,14 @@ src="{% static 'img/blank_image.png' %}"
|
||||
</button>
|
||||
<ul class='dropdown-menu' role='menu'>
|
||||
<li><a class='dropdown-item' href='#' id='edit-order'><span class='fas fa-edit icon-green'></span> {% trans "Edit order" %}</a></li>
|
||||
{% if order.is_open %}
|
||||
{% if order.can_hold %}
|
||||
<li><a class='dropdown-item' href='#' id='hold-order'><span class='fas fa-hand-paper icon-yellow'></span> {% trans "Hold order" %}</a></li>
|
||||
{% endif %}
|
||||
{% if order.can_cancel %}
|
||||
<li><a class='dropdown-item' href='#' id='cancel-order'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel order" %}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% if order.status == ReturnOrderStatus.PENDING %}
|
||||
{% if order.can_issue %}
|
||||
<button type='button' class='btn btn-primary' id='issue-order' title='{% trans "Issue Order" %}'>
|
||||
<span class='fas fa-paper-plane'></span> {% trans "Issue Order" %}
|
||||
</button>
|
||||
@ -211,7 +214,7 @@ src="{% static 'img/blank_image.png' %}"
|
||||
|
||||
{% if roles.return_order.change %}
|
||||
|
||||
{% if order.status == ReturnOrderStatus.PENDING %}
|
||||
{% if order.can_issue %}
|
||||
$('#issue-order').click(function() {
|
||||
issueReturnOrder({{ order.pk }}, {
|
||||
reload: true,
|
||||
@ -234,7 +237,7 @@ $('#edit-order').click(function() {
|
||||
});
|
||||
});
|
||||
|
||||
{% if order.is_open %}
|
||||
{% if order.can_cancel %}
|
||||
$('#cancel-order').click(function() {
|
||||
cancelReturnOrder(
|
||||
{{ order.pk }},
|
||||
@ -244,6 +247,17 @@ $('#cancel-order').click(function() {
|
||||
);
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if order.can_hold %}
|
||||
$("#hold-order").click(function() {
|
||||
holdOrder(
|
||||
'{% url "api-ro-hold" order.pk %}',
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if report_enabled %}
|
||||
|
@ -73,13 +73,16 @@ src="{% static 'img/blank_image.png' %}"
|
||||
</button>
|
||||
<ul class='dropdown-menu' role='menu'>
|
||||
<li><a class='dropdown-item' href='#' id='edit-order'><span class='fas fa-edit icon-green'></span> {% trans "Edit order" %}</a></li>
|
||||
{% if order.is_open %}
|
||||
{% if order.can_hold %}
|
||||
<li><a class='dropdown-item' href='#' id='hold-order'><span class='fas fa-hand-paper icon-yellow'></span> {% trans "Hold order" %}</a></li>
|
||||
{% endif %}
|
||||
{% if order.can_cancel %}
|
||||
<li><a class='dropdown-item' href='#' id='cancel-order'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel order" %}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class='btn-group' role='group'>
|
||||
{% if order.is_pending %}
|
||||
{% if order.status == SalesOrderStatus.PENDING or order.status == SalesOrderStatus.ON_HOLD %}
|
||||
<button type='button' class='btn btn-primary' id='issue-order' title='{% trans "Issue Order" %}'>
|
||||
<span class='fas fa-paper-plane'></span> {% trans "Issue Order" %}
|
||||
</button>
|
||||
@ -280,6 +283,7 @@ $('#issue-order').click(function() {
|
||||
);
|
||||
});
|
||||
|
||||
{% if order.can_cancel %}
|
||||
$("#cancel-order").click(function() {
|
||||
|
||||
cancelSalesOrder(
|
||||
@ -289,6 +293,20 @@ $("#cancel-order").click(function() {
|
||||
}
|
||||
);
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if order.can_hold %}
|
||||
$('#hold-order').click(function() {
|
||||
holdOrder(
|
||||
'{% url "api-so-hold" order.pk %}',
|
||||
{
|
||||
onSuccess: function() {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
$("#ship-order").click(function() {
|
||||
shipSalesOrder(
|
||||
|
@ -51,8 +51,7 @@ from build import models as BuildModels
|
||||
from build.status_codes import BuildStatusGroups
|
||||
from common.currency import currency_code_default
|
||||
from common.icons import validate_icon
|
||||
from common.models import InvenTreeSetting
|
||||
from common.settings import get_global_setting, set_global_setting
|
||||
from common.settings import get_global_setting
|
||||
from company.models import SupplierPart
|
||||
from InvenTree import helpers, validators
|
||||
from InvenTree.fields import InvenTreeURLField
|
||||
|
@ -8,6 +8,7 @@
|
||||
exportFormatOptions,
|
||||
formatCurrency,
|
||||
getFormFieldValue,
|
||||
handleFormSuccess,
|
||||
inventreeGet,
|
||||
inventreeLoad,
|
||||
inventreeSave,
|
||||
@ -25,6 +26,7 @@
|
||||
createExtraLineItem,
|
||||
editExtraLineItem,
|
||||
exportOrder,
|
||||
holdOrder,
|
||||
issuePurchaseOrder,
|
||||
newPurchaseOrderFromOrderWizard,
|
||||
newSupplierPartFromOrderWizard,
|
||||
@ -38,6 +40,29 @@
|
||||
*/
|
||||
|
||||
|
||||
function holdOrder(url, options={}) {
|
||||
constructForm(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
title: '{% trans "Hold Order" %}',
|
||||
confirm: true,
|
||||
preFormContent: function(opts) {
|
||||
let html = `
|
||||
<div class='alert alert-info alert-block'>
|
||||
{% trans "Are you sure you wish to place this order on hold?" %}
|
||||
</div>`;
|
||||
|
||||
return html;
|
||||
},
|
||||
onSuccess: function(response) {
|
||||
handleFormSuccess(response, options);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/* Construct a set of fields for a OrderExtraLine form */
|
||||
function extraLineFields(options={}) {
|
||||
|
||||
|
Reference in New Issue
Block a user