Skip to content

Commit

Permalink
Adds "ON HOLD" status to order models (#7807)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
SchrodingersGat and matmair authored Aug 7, 2024
1 parent 25f162f commit 0e8c297
Show file tree
Hide file tree
Showing 46 changed files with 1,283 additions and 221 deletions.
7 changes: 5 additions & 2 deletions .github/scripts/version_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
GITHUB_API_URL = os.getenv('GITHUB_API_URL', 'https://api.github.com')


def get_existing_release_tags():
def get_existing_release_tags(include_prerelease=True):
"""Request information on existing releases via the GitHub API."""
# Check for github token
token = os.getenv('GITHUB_TOKEN', None)
Expand Down Expand Up @@ -51,6 +51,9 @@ def get_existing_release_tags():
print(f"Version '{tag}' did not match expected pattern")
continue

if not include_prerelease and release['prerelease']:
continue

tags.append([int(x) for x in match.groups()])

return tags
Expand All @@ -74,7 +77,7 @@ def check_version_number(version_string, allow_duplicate=False):
version_tuple = [int(x) for x in match.groups()]

# Look through the existing releases
existing = get_existing_release_tags()
existing = get_existing_release_tags(include_prerelease=False)

# Assume that this is the highest release, unless told otherwise
highest_release = True
Expand Down
9 changes: 5 additions & 4 deletions docs/docs/build/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,11 @@ Each *Build Order* has an associated *Status* flag, which indicates the state of

| Status | Description |
| ----------- | ----------- |
| `Pending` | Build has been created and build is ready for subpart allocation |
| `Production` | One or more build outputs have been created for this build |
| `Cancelled` | Build has been cancelled |
| `Completed` | Build has been completed |
| `Pending` | Build order has been created, but is not yet in production |
| `Production` | Build order is currently in production |
| `On Hold` | Build order has been placed on hold, but is still active |
| `Cancelled` | Build order has been cancelled |
| `Completed` | Build order has been completed |

**Source Code**

Expand Down
1 change: 1 addition & 0 deletions docs/docs/order/purchase_order.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Each Purchase Order has a specific status code which indicates the current state
| --- | --- |
| Pending | The purchase order has been created, but has not been submitted to the supplier |
| In Progress | The purchase order has been issued to the supplier, and is in progress |
| On Hold | The purchase order has been placed on hold, but is still active |
| Complete | The purchase order has been completed, and is now closed |
| Cancelled | The purchase order was cancelled, and is now closed |
| Lost | The purchase order was lost, and is now closed |
Expand Down
1 change: 1 addition & 0 deletions docs/docs/order/return_order.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Each Return Order has a specific status code, as follows:
| --- | --- |
| Pending | The return order has been created, but not sent to the customer |
| In Progress | The return order has been issued to the customer |
| On Hold | The return order has been placed on hold, but is still active |
| Complete | The return order was marked as complete, and is now closed |
| Cancelled | The return order was cancelled, and is now closed |

Expand Down
1 change: 1 addition & 0 deletions docs/docs/order/sales_order.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Each Sales Order has a specific status code, which represents the state of the o
| --- | --- |
| Pending | The sales order has been created, but has not been finalized or submitted |
| In Progress | The sales order has been issued, and is in progress |
| On Hold | The sales order has been placed on hold, but is still active |
| Shipped | The sales order has been shipped, but is not yet complete |
| Complete | The sales order is fully completed, and is now closed |
| Cancelled | The sales order was cancelled, and is now closed |
Expand Down
8 changes: 7 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
29 changes: 26 additions & 3 deletions src/backend/InvenTree/build/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -484,7 +494,6 @@ class BuildAutoAllocate(BuildOrderContextMixin, CreateAPI):
"""

queryset = Build.objects.none()

serializer_class = build.serializers.BuildAutoAllocationSerializer


Expand All @@ -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."""

Expand Down Expand Up @@ -663,6 +684,8 @@ def filter_queryset(self, queryset):
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'),
Expand Down
81 changes: 79 additions & 2 deletions src/backend/InvenTree/build/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import decimal
import logging
import os
from datetime import datetime
from django.conf import settings

Expand All @@ -26,6 +25,7 @@
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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -574,6 +575,10 @@ def can_complete(self):
- 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

Expand Down Expand Up @@ -602,8 +607,18 @@ def complete_allocations(self, user):
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

Expand Down Expand Up @@ -665,6 +680,59 @@ def complete_build(self, user, trim_allocated_stock=False):
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.
Expand All @@ -674,8 +742,17 @@ def cancel_build(self, user, **kwargs):
- 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)

Expand Down Expand Up @@ -1276,7 +1353,7 @@ def is_active(self):
@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):
Expand Down
31 changes: 31 additions & 0 deletions src/backend/InvenTree/build/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from users.serializers import OwnerSerializer

from .models import Build, BuildLine, BuildItem
from .status_codes import BuildStatus


class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTreeModelSerializer):
Expand Down Expand Up @@ -597,6 +598,33 @@ def save(self):
)


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"""

Expand Down Expand Up @@ -737,6 +765,9 @@ def validate(self, data):
"""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"))

Expand Down
2 changes: 2 additions & 0 deletions src/backend/InvenTree/build/status_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -19,5 +20,6 @@ class BuildStatusGroups:

ACTIVE_CODES = [
BuildStatus.PENDING.value,
BuildStatus.ON_HOLD.value,
BuildStatus.PRODUCTION.value,
]
Loading

0 comments on commit 0e8c297

Please sign in to comment.