diff --git a/docs/docs/build/build.md b/docs/docs/build/build.md
index 5c889cc808..cf571be503 100644
--- a/docs/docs/build/build.md
+++ b/docs/docs/build/build.md
@@ -79,6 +79,18 @@ Each *Build Order* has an associated *Status* flag, which indicates the state of
| `Cancelled` | Build has been cancelled |
| `Completed` | Build has been completed |
+**Source Code**
+
+Refer to the source code for the Build Order status codes:
+
+::: build.status_codes.BuildStatus
+ options:
+ show_bases: False
+ show_root_heading: False
+ show_root_toc_entry: False
+ show_source: True
+ members: []
+
### Stock Allocations
When a *Build Order* is created, we then have the ability to *allocate* stock items against that build order. The particular parts we need to allocate against the build are specified by the BOM for the part we are assembling.
diff --git a/docs/docs/order/purchase_order.md b/docs/docs/order/purchase_order.md
index b75a6a5d87..066f64d058 100644
--- a/docs/docs/order/purchase_order.md
+++ b/docs/docs/order/purchase_order.md
@@ -22,6 +22,20 @@ Each Purchase Order has a specific status code which indicates the current state
| In Progress | The purchase order has been issued to the supplier, and is in progress |
| 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 |
+| Returned | The purchase order was returned, and is now closed |
+
+**Source Code**
+
+Refer to the source code for the Purchase Order status codes:
+
+::: order.status_codes.PurchaseOrderStatus
+ options:
+ show_bases: False
+ show_root_heading: False
+ show_root_toc_entry: False
+ show_source: True
+ members: []
### Purchase Order Currency
diff --git a/docs/docs/order/return_order.md b/docs/docs/order/return_order.md
index 0a7df0d069..4b134601ae 100644
--- a/docs/docs/order/return_order.md
+++ b/docs/docs/order/return_order.md
@@ -48,6 +48,18 @@ Each Return Order has a specific status code, as follows:
| Complete | The return order was marked as complete, and is now closed |
| Cancelled | The return order was cancelled, and is now closed |
+**Source Code**
+
+Refer to the source code for the Return Order status codes:
+
+::: order.status_codes.ReturnOrderStatus
+ options:
+ show_bases: False
+ show_root_heading: False
+ show_root_toc_entry: False
+ show_source: True
+ members: []
+
## Create a Return Order
From the Return Order index, click on New Return Order which opens the "Create Return Order" form.
diff --git a/docs/docs/order/sales_order.md b/docs/docs/order/sales_order.md
index 1d37e2410d..43ab9cc533 100644
--- a/docs/docs/order/sales_order.md
+++ b/docs/docs/order/sales_order.md
@@ -20,8 +20,23 @@ 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 |
-| Shipped | The sales order has been completed, and is now closed |
+| 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 |
+| Lost | The sales order was lost, and is now closed |
+| Returned | The sales order was returned, and is now closed |
+
+**Source Code**
+
+Refer to the source code for the Sales Order status codes:
+
+::: order.status_codes.SalesOrderStatus
+ options:
+ show_bases: False
+ show_root_heading: False
+ show_root_toc_entry: False
+ show_source: True
+ members: []
### Sales Order Currency
@@ -83,7 +98,7 @@ To view all the completed shipment, click on the Complete Order to mark the sales order as complete.
+Once all items in the sales order have been shipped, click on Complete Order to mark the sales order as shipped. Confirm then click on Submit to complete the order.
### Cancel Order
diff --git a/docs/docs/stock/status.md b/docs/docs/stock/status.md
index 42e5bffe32..3ae6f12823 100644
--- a/docs/docs/stock/status.md
+++ b/docs/docs/stock/status.md
@@ -26,6 +26,18 @@ The *status* of a given stock item is displayed on the stock item detail page:
{% include 'img.html' %}
{% endwith %}
+**Source Code**
+
+Refer to the source code for the Stock status codes:
+
+::: stock.status_codes.StockStatus
+ options:
+ show_bases: False
+ show_root_heading: False
+ show_root_toc_entry: False
+ show_source: True
+ members: []
+
### Default Status Code
The default status code for any newly created Stock Item is OK
diff --git a/src/backend/InvenTree/InvenTree/status_codes.py b/src/backend/InvenTree/InvenTree/status_codes.py
index 2cd3f110ba..81d713f59c 100644
--- a/src/backend/InvenTree/InvenTree/status_codes.py
+++ b/src/backend/InvenTree/InvenTree/status_codes.py
@@ -1,198 +1,9 @@
-"""Status codes for InvenTree."""
+"""Global import of all status codes.
-from django.utils.translation import gettext_lazy as _
+This file remains here for backwards compatibility,
+as external plugins may import status codes from this file.
+"""
-from generic.states import StatusCode
-
-
-class PurchaseOrderStatus(StatusCode):
- """Defines a set of status codes for a PurchaseOrder."""
-
- # Order status codes
- PENDING = 10, _('Pending'), 'secondary' # Order is pending (not yet placed)
- PLACED = 20, _('Placed'), 'primary' # Order has been placed with supplier
- COMPLETE = 30, _('Complete'), 'success' # Order has been completed
- CANCELLED = 40, _('Cancelled'), 'danger' # Order was cancelled
- LOST = 50, _('Lost'), 'warning' # Order was lost
- RETURNED = 60, _('Returned'), 'warning' # Order was returned
-
-
-class PurchaseOrderStatusGroups:
- """Groups for PurchaseOrderStatus codes."""
-
- # Open orders
- OPEN = [PurchaseOrderStatus.PENDING.value, PurchaseOrderStatus.PLACED.value]
-
- # Failed orders
- FAILED = [
- PurchaseOrderStatus.CANCELLED.value,
- PurchaseOrderStatus.LOST.value,
- PurchaseOrderStatus.RETURNED.value,
- ]
-
-
-class SalesOrderStatus(StatusCode):
- """Defines a set of status codes for a SalesOrder."""
-
- PENDING = 10, _('Pending'), 'secondary' # Order is pending
- IN_PROGRESS = (
- 15,
- _('In Progress'),
- 'primary',
- ) # Order has been issued, and is in progress
- SHIPPED = 20, _('Shipped'), 'success' # Order has been shipped to customer
- COMPLETE = 30, _('Complete'), 'success' # Order is complete
- CANCELLED = 40, _('Cancelled'), 'danger' # Order has been cancelled
- LOST = 50, _('Lost'), 'warning' # Order was lost
- RETURNED = 60, _('Returned'), 'warning' # Order was returned
-
-
-class SalesOrderStatusGroups:
- """Groups for SalesOrderStatus codes."""
-
- # Open orders
- OPEN = [SalesOrderStatus.PENDING.value, SalesOrderStatus.IN_PROGRESS.value]
-
- # Completed orders
- COMPLETE = [SalesOrderStatus.SHIPPED.value, SalesOrderStatus.COMPLETE.value]
-
-
-class StockStatus(StatusCode):
- """Status codes for Stock."""
-
- OK = 10, _('OK'), 'success' # Item is OK
- ATTENTION = 50, _('Attention needed'), 'warning' # Item requires attention
- DAMAGED = 55, _('Damaged'), 'warning' # Item is damaged
- DESTROYED = 60, _('Destroyed'), 'danger' # Item is destroyed
- REJECTED = 65, _('Rejected'), 'danger' # Item is rejected
- LOST = 70, _('Lost'), 'dark' # Item has been lost
- QUARANTINED = (
- 75,
- _('Quarantined'),
- 'info',
- ) # Item has been quarantined and is unavailable
- RETURNED = 85, _('Returned'), 'warning' # Item has been returned from a customer
-
-
-class StockStatusGroups:
- """Groups for StockStatus codes."""
-
- # The following codes correspond to parts that are 'available' or 'in stock'
- AVAILABLE_CODES = [
- StockStatus.OK.value,
- StockStatus.ATTENTION.value,
- StockStatus.DAMAGED.value,
- StockStatus.RETURNED.value,
- ]
-
-
-class StockHistoryCode(StatusCode):
- """Status codes for StockHistory."""
-
- LEGACY = 0, _('Legacy stock tracking entry')
-
- CREATED = 1, _('Stock item created')
-
- # Manual editing operations
- EDITED = 5, _('Edited stock item')
- ASSIGNED_SERIAL = 6, _('Assigned serial number')
-
- # Manual stock operations
- STOCK_COUNT = 10, _('Stock counted')
- STOCK_ADD = 11, _('Stock manually added')
- STOCK_REMOVE = 12, _('Stock manually removed')
-
- # Location operations
- STOCK_MOVE = 20, _('Location changed')
- STOCK_UPDATE = 25, _('Stock updated')
-
- # Installation operations
- INSTALLED_INTO_ASSEMBLY = 30, _('Installed into assembly')
- REMOVED_FROM_ASSEMBLY = 31, _('Removed from assembly')
-
- INSTALLED_CHILD_ITEM = 35, _('Installed component item')
- REMOVED_CHILD_ITEM = 36, _('Removed component item')
-
- # Stock splitting operations
- SPLIT_FROM_PARENT = 40, _('Split from parent item')
- SPLIT_CHILD_ITEM = 42, _('Split child item')
-
- # Stock merging operations
- MERGED_STOCK_ITEMS = 45, _('Merged stock items')
-
- # Convert stock item to variant
- CONVERTED_TO_VARIANT = 48, _('Converted to variant')
-
- # Build order codes
- BUILD_OUTPUT_CREATED = 50, _('Build order output created')
- BUILD_OUTPUT_COMPLETED = 55, _('Build order output completed')
- BUILD_OUTPUT_REJECTED = 56, _('Build order output rejected')
- BUILD_CONSUMED = 57, _('Consumed by build order')
-
- # Sales order codes
- SHIPPED_AGAINST_SALES_ORDER = 60, _('Shipped against Sales Order')
-
- # Purchase order codes
- RECEIVED_AGAINST_PURCHASE_ORDER = 70, _('Received against Purchase Order')
-
- # Return order codes
- RETURNED_AGAINST_RETURN_ORDER = 80, _('Returned against Return Order')
-
- # Customer actions
- SENT_TO_CUSTOMER = 100, _('Sent to customer')
- RETURNED_FROM_CUSTOMER = 105, _('Returned from customer')
-
-
-class BuildStatus(StatusCode):
- """Build status codes."""
-
- PENDING = 10, _('Pending'), 'secondary' # Build is pending / active
- PRODUCTION = 20, _('Production'), 'primary' # BuildOrder is in production
- CANCELLED = 30, _('Cancelled'), 'danger' # Build was cancelled
- COMPLETE = 40, _('Complete'), 'success' # Build is complete
-
-
-class BuildStatusGroups:
- """Groups for BuildStatus codes."""
-
- ACTIVE_CODES = [BuildStatus.PENDING.value, BuildStatus.PRODUCTION.value]
-
-
-class ReturnOrderStatus(StatusCode):
- """Defines a set of status codes for a ReturnOrder."""
-
- # Order is pending, waiting for receipt of items
- PENDING = 10, _('Pending'), 'secondary'
-
- # Items have been received, and are being inspected
- IN_PROGRESS = 20, _('In Progress'), 'primary'
-
- COMPLETE = 30, _('Complete'), 'success'
- CANCELLED = 40, _('Cancelled'), 'danger'
-
-
-class ReturnOrderStatusGroups:
- """Groups for ReturnOrderStatus codes."""
-
- OPEN = [ReturnOrderStatus.PENDING.value, ReturnOrderStatus.IN_PROGRESS.value]
-
-
-class ReturnOrderLineStatus(StatusCode):
- """Defines a set of status codes for a ReturnOrderLineItem."""
-
- PENDING = 10, _('Pending'), 'secondary'
-
- # Item is to be returned to customer, no other action
- RETURN = 20, _('Return'), 'success'
-
- # Item is to be repaired, and returned to customer
- REPAIR = 30, _('Repair'), 'primary'
-
- # Item is to be replaced (new item shipped)
- REPLACE = 40, _('Replace'), 'warning'
-
- # Item is to be refunded (cannot be repaired)
- REFUND = 50, _('Refund'), 'info'
-
- # Item is rejected
- REJECT = 60, _('Reject'), 'danger'
+from build.status_codes import *
+from order.status_codes import *
+from stock.status_codes import *
diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py
index e47011ae8b..f84bca1273 100644
--- a/src/backend/InvenTree/build/api.py
+++ b/src/backend/InvenTree/build/api.py
@@ -14,7 +14,7 @@ from django_filters import rest_framework as rest_filters
from InvenTree.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAPIView, MetadataView
from generic.states.api import StatusView
from InvenTree.helpers import str2bool, isNull, DownloadFile
-from InvenTree.status_codes import BuildStatus, BuildStatusGroups
+from build.status_codes import BuildStatus, BuildStatusGroups
from InvenTree.mixins import CreateAPI, RetrieveUpdateDestroyAPI, ListCreateAPI
import common.models
diff --git a/src/backend/InvenTree/build/models.py b/src/backend/InvenTree/build/models.py
index cd1713126e..ffbbc0b544 100644
--- a/src/backend/InvenTree/build/models.py
+++ b/src/backend/InvenTree/build/models.py
@@ -22,7 +22,8 @@ from mptt.exceptions import InvalidMove
from rest_framework import serializers
-from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode, BuildStatusGroups
+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
diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py
index ac19b6ef2c..92180f8d42 100644
--- a/src/backend/InvenTree/build/serializers.py
+++ b/src/backend/InvenTree/build/serializers.py
@@ -18,7 +18,7 @@ from InvenTree.serializers import UserSerializer
import InvenTree.helpers
from InvenTree.serializers import InvenTreeDecimalField
-from InvenTree.status_codes import StockStatus
+from stock.status_codes import StockStatus
from stock.generators import generate_batch_code
from stock.models import StockItem, StockLocation
diff --git a/src/backend/InvenTree/build/status_codes.py b/src/backend/InvenTree/build/status_codes.py
new file mode 100644
index 0000000000..463bd22059
--- /dev/null
+++ b/src/backend/InvenTree/build/status_codes.py
@@ -0,0 +1,20 @@
+"""Build status codes."""
+
+from django.utils.translation import gettext_lazy as _
+
+from generic.states import StatusCode
+
+
+class BuildStatus(StatusCode):
+ """Build status codes."""
+
+ PENDING = 10, _('Pending'), 'secondary' # Build is pending / active
+ PRODUCTION = 20, _('Production'), 'primary' # BuildOrder is in production
+ CANCELLED = 30, _('Cancelled'), 'danger' # Build was cancelled
+ COMPLETE = 40, _('Complete'), 'success' # Build is complete
+
+
+class BuildStatusGroups:
+ """Groups for BuildStatus codes."""
+
+ ACTIVE_CODES = [BuildStatus.PENDING.value, BuildStatus.PRODUCTION.value]
diff --git a/src/backend/InvenTree/build/tasks.py b/src/backend/InvenTree/build/tasks.py
index b30a15f3d2..82828d642d 100644
--- a/src/backend/InvenTree/build/tasks.py
+++ b/src/backend/InvenTree/build/tasks.py
@@ -17,8 +17,8 @@ import InvenTree.email
import InvenTree.helpers
import InvenTree.helpers_model
import InvenTree.tasks
-from InvenTree.status_codes import BuildStatusGroups
from InvenTree.ready import isImportingData
+from build.status_codes import BuildStatusGroups
import part.models as part_models
diff --git a/src/backend/InvenTree/build/test_api.py b/src/backend/InvenTree/build/test_api.py
index 1fc1a368ee..b240521db6 100644
--- a/src/backend/InvenTree/build/test_api.py
+++ b/src/backend/InvenTree/build/test_api.py
@@ -10,7 +10,8 @@ from part.models import Part
from build.models import Build, BuildItem
from stock.models import StockItem
-from InvenTree.status_codes import BuildStatus, StockStatus
+from build.status_codes import BuildStatus
+from stock.status_codes import StockStatus
from InvenTree.unit_test import InvenTreeAPITestCase
diff --git a/src/backend/InvenTree/build/tests.py b/src/backend/InvenTree/build/tests.py
index 904f2a3a62..4dd7ee0fee 100644
--- a/src/backend/InvenTree/build/tests.py
+++ b/src/backend/InvenTree/build/tests.py
@@ -11,7 +11,7 @@ from InvenTree.unit_test import InvenTreeTestCase
from .models import Build
from stock.models import StockItem
-from InvenTree.status_codes import BuildStatus
+from build.status_codes import BuildStatus
class BuildTestSimple(InvenTreeTestCase):
diff --git a/src/backend/InvenTree/build/views.py b/src/backend/InvenTree/build/views.py
index 36422e1688..2668b0fe99 100644
--- a/src/backend/InvenTree/build/views.py
+++ b/src/backend/InvenTree/build/views.py
@@ -5,7 +5,7 @@ from django.views.generic import DetailView, ListView
from .models import Build
from InvenTree.views import InvenTreeRoleMixin
-from InvenTree.status_codes import BuildStatus
+from build.status_codes import BuildStatus
from plugin.views import InvenTreePluginViewMixin
diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py
index 15855b462c..d1671dd68c 100644
--- a/src/backend/InvenTree/common/models.py
+++ b/src/backend/InvenTree/common/models.py
@@ -1874,6 +1874,14 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'default': False,
'validator': bool,
},
+ 'SALESORDER_SHIP_COMPLETE': {
+ 'name': _('Mark Shipped Orders as Complete'),
+ 'description': _(
+ 'Sales orders marked as shipped will automatically be completed, bypassing the "shipped" status'
+ ),
+ 'default': False,
+ 'validator': bool,
+ },
'PURCHASEORDER_REFERENCE_PATTERN': {
'name': _('Purchase Order Reference Pattern'),
'description': _(
diff --git a/src/backend/InvenTree/company/models.py b/src/backend/InvenTree/company/models.py
index eea0aa511a..4e102a160d 100644
--- a/src/backend/InvenTree/company/models.py
+++ b/src/backend/InvenTree/company/models.py
@@ -31,7 +31,7 @@ import InvenTree.tasks
import InvenTree.validators
from common.settings import currency_code_default
from InvenTree.fields import InvenTreeURLField, RoundingDecimalField
-from InvenTree.status_codes import PurchaseOrderStatusGroups
+from order.status_codes import PurchaseOrderStatusGroups
def rename_company_image(instance, filename):
diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py
index 9ee4df9974..7db194469f 100644
--- a/src/backend/InvenTree/order/api.py
+++ b/src/backend/InvenTree/order/api.py
@@ -30,14 +30,6 @@ from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS
from InvenTree.helpers import DownloadFile, str2bool
from InvenTree.helpers_model import construct_absolute_url, get_base_url
from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI, RetrieveUpdateDestroyAPI
-from InvenTree.status_codes import (
- PurchaseOrderStatus,
- PurchaseOrderStatusGroups,
- ReturnOrderLineStatus,
- ReturnOrderStatus,
- SalesOrderStatus,
- SalesOrderStatusGroups,
-)
from order import models, serializers
from order.admin import (
PurchaseOrderExtraLineResource,
@@ -48,6 +40,14 @@ from order.admin import (
SalesOrderLineItemResource,
SalesOrderResource,
)
+from order.status_codes import (
+ PurchaseOrderStatus,
+ PurchaseOrderStatusGroups,
+ ReturnOrderLineStatus,
+ ReturnOrderStatus,
+ SalesOrderStatus,
+ SalesOrderStatusGroups,
+)
from part.models import Part
from users.models import Owner
diff --git a/src/backend/InvenTree/order/migrations/0055_auto_20211025_0645.py b/src/backend/InvenTree/order/migrations/0055_auto_20211025_0645.py
index 0efab70da8..69ef6e6add 100644
--- a/src/backend/InvenTree/order/migrations/0055_auto_20211025_0645.py
+++ b/src/backend/InvenTree/order/migrations/0055_auto_20211025_0645.py
@@ -3,7 +3,7 @@
from django.db import migrations
-from InvenTree.status_codes import SalesOrderStatus
+from order.status_codes import SalesOrderStatus
def add_shipment(apps, schema_editor):
diff --git a/src/backend/InvenTree/order/migrations/0058_auto_20211126_1210.py b/src/backend/InvenTree/order/migrations/0058_auto_20211126_1210.py
index 1377a9a0b9..6c31d93099 100644
--- a/src/backend/InvenTree/order/migrations/0058_auto_20211126_1210.py
+++ b/src/backend/InvenTree/order/migrations/0058_auto_20211126_1210.py
@@ -2,7 +2,7 @@
from django.db import migrations
-from InvenTree.status_codes import SalesOrderStatus
+from order.status_codes import SalesOrderStatus
def calculate_shipped_quantity(apps, schema_editor):
diff --git a/src/backend/InvenTree/order/migrations/0096_alter_returnorderlineitem_outcome.py b/src/backend/InvenTree/order/migrations/0096_alter_returnorderlineitem_outcome.py
index ac95830807..9c80796a6f 100644
--- a/src/backend/InvenTree/order/migrations/0096_alter_returnorderlineitem_outcome.py
+++ b/src/backend/InvenTree/order/migrations/0096_alter_returnorderlineitem_outcome.py
@@ -1,7 +1,7 @@
# Generated by Django 3.2.19 on 2023-06-04 17:43
from django.db import migrations, models
-import InvenTree.status_codes
+import order.status_codes
class Migration(migrations.Migration):
@@ -14,6 +14,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='returnorderlineitem',
name='outcome',
- field=models.PositiveIntegerField(choices=InvenTree.status_codes.ReturnOrderLineStatus.items(), default=10, help_text='Outcome for this line item', verbose_name='Outcome'),
+ field=models.PositiveIntegerField(choices=order.status_codes.ReturnOrderLineStatus.items(), default=10, help_text='Outcome for this line item', verbose_name='Outcome'),
),
]
diff --git a/src/backend/InvenTree/order/migrations/0099_alter_salesorder_status.py b/src/backend/InvenTree/order/migrations/0099_alter_salesorder_status.py
index de56926bc9..23d9b95d86 100644
--- a/src/backend/InvenTree/order/migrations/0099_alter_salesorder_status.py
+++ b/src/backend/InvenTree/order/migrations/0099_alter_salesorder_status.py
@@ -2,7 +2,7 @@
import django.core.validators
from django.db import migrations, models
-import InvenTree.status_codes
+import order.status_codes
class Migration(migrations.Migration):
@@ -15,6 +15,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='salesorder',
name='status',
- field=models.PositiveIntegerField(choices=InvenTree.status_codes.SalesOrderStatus.items(), default=10, help_text='Purchase order status', verbose_name='Status'),
+ field=models.PositiveIntegerField(choices=order.status_codes.SalesOrderStatus.items(), default=10, help_text='Purchase order status', verbose_name='Status'),
),
]
diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py
index 0543fbf88d..1f60121efa 100644
--- a/src/backend/InvenTree/order/models.py
+++ b/src/backend/InvenTree/order/models.py
@@ -45,7 +45,7 @@ from InvenTree.fields import (
)
from InvenTree.helpers import decimal2string, pui_url
from InvenTree.helpers_model import getSetting, notify_responsible
-from InvenTree.status_codes import (
+from order.status_codes import (
PurchaseOrderStatus,
PurchaseOrderStatusGroups,
ReturnOrderLineStatus,
@@ -53,11 +53,10 @@ from InvenTree.status_codes import (
ReturnOrderStatusGroups,
SalesOrderStatus,
SalesOrderStatusGroups,
- StockHistoryCode,
- StockStatus,
)
from part import models as PartModels
from plugin.events import trigger_event
+from stock.status_codes import StockHistoryCode, StockStatus
logger = logging.getLogger('inventree')
@@ -1024,6 +1023,12 @@ class SalesOrder(TotalPriceMixin, Order):
Throws a ValidationError if cannot be completed.
"""
try:
+ if self.status == SalesOrderStatus.COMPLETE.value:
+ raise ValidationError(_('Order is already complete'))
+
+ if self.status == SalesOrderStatus.CANCELLED.value:
+ raise ValidationError(_('Order is already cancelled'))
+
# Only an open order can be marked as shipped
if self.is_open and not self.is_completed:
raise ValidationError(_('Only an open order can be marked as complete'))
@@ -1067,7 +1072,11 @@ class SalesOrder(TotalPriceMixin, Order):
if not self.can_complete(**kwargs):
return False
- if self.status == SalesOrderStatus.SHIPPED:
+ bypass_shipped = InvenTree.helpers.str2bool(
+ common_models.InvenTreeSetting.get_setting('SALESORDER_SHIP_COMPLETE')
+ )
+
+ if bypass_shipped or self.status == SalesOrderStatus.SHIPPED:
self.status = SalesOrderStatus.COMPLETE.value
else:
self.status = SalesOrderStatus.SHIPPED.value
diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py
index 12f8e3b86d..f57491d49b 100644
--- a/src/backend/InvenTree/order/serializers.py
+++ b/src/backend/InvenTree/order/serializers.py
@@ -48,14 +48,14 @@ from InvenTree.serializers import (
InvenTreeModelSerializer,
InvenTreeMoneySerializer,
)
-from InvenTree.status_codes import (
+from order.status_codes import (
PurchaseOrderStatusGroups,
ReturnOrderLineStatus,
ReturnOrderStatus,
SalesOrderStatusGroups,
- StockStatus,
)
from part.serializers import PartBriefSerializer
+from stock.status_codes import StockStatus
from users.serializers import OwnerSerializer
diff --git a/src/backend/InvenTree/order/status_codes.py b/src/backend/InvenTree/order/status_codes.py
new file mode 100644
index 0000000000..cec286dc09
--- /dev/null
+++ b/src/backend/InvenTree/order/status_codes.py
@@ -0,0 +1,97 @@
+"""Order status codes."""
+
+from django.utils.translation import gettext_lazy as _
+
+from generic.states import StatusCode
+
+
+class PurchaseOrderStatus(StatusCode):
+ """Defines a set of status codes for a PurchaseOrder."""
+
+ # Order status codes
+ PENDING = 10, _('Pending'), 'secondary' # Order is pending (not yet placed)
+ PLACED = 20, _('Placed'), 'primary' # Order has been placed with supplier
+ COMPLETE = 30, _('Complete'), 'success' # Order has been completed
+ CANCELLED = 40, _('Cancelled'), 'danger' # Order was cancelled
+ LOST = 50, _('Lost'), 'warning' # Order was lost
+ RETURNED = 60, _('Returned'), 'warning' # Order was returned
+
+
+class PurchaseOrderStatusGroups:
+ """Groups for PurchaseOrderStatus codes."""
+
+ # Open orders
+ OPEN = [PurchaseOrderStatus.PENDING.value, PurchaseOrderStatus.PLACED.value]
+
+ # Failed orders
+ FAILED = [
+ PurchaseOrderStatus.CANCELLED.value,
+ PurchaseOrderStatus.LOST.value,
+ PurchaseOrderStatus.RETURNED.value,
+ ]
+
+
+class SalesOrderStatus(StatusCode):
+ """Defines a set of status codes for a SalesOrder."""
+
+ PENDING = 10, _('Pending'), 'secondary' # Order is pending
+ IN_PROGRESS = (
+ 15,
+ _('In Progress'),
+ 'primary',
+ ) # Order has been issued, and is in progress
+ SHIPPED = 20, _('Shipped'), 'success' # Order has been shipped to customer
+ COMPLETE = 30, _('Complete'), 'success' # Order is complete
+ CANCELLED = 40, _('Cancelled'), 'danger' # Order has been cancelled
+ LOST = 50, _('Lost'), 'warning' # Order was lost
+ RETURNED = 60, _('Returned'), 'warning' # Order was returned
+
+
+class SalesOrderStatusGroups:
+ """Groups for SalesOrderStatus codes."""
+
+ # Open orders
+ OPEN = [SalesOrderStatus.PENDING.value, SalesOrderStatus.IN_PROGRESS.value]
+
+ # Completed orders
+ COMPLETE = [SalesOrderStatus.SHIPPED.value, SalesOrderStatus.COMPLETE.value]
+
+
+class ReturnOrderStatus(StatusCode):
+ """Defines a set of status codes for a ReturnOrder."""
+
+ # Order is pending, waiting for receipt of items
+ PENDING = 10, _('Pending'), 'secondary'
+
+ # Items have been received, and are being inspected
+ IN_PROGRESS = 20, _('In Progress'), 'primary'
+
+ COMPLETE = 30, _('Complete'), 'success'
+ CANCELLED = 40, _('Cancelled'), 'danger'
+
+
+class ReturnOrderStatusGroups:
+ """Groups for ReturnOrderStatus codes."""
+
+ OPEN = [ReturnOrderStatus.PENDING.value, ReturnOrderStatus.IN_PROGRESS.value]
+
+
+class ReturnOrderLineStatus(StatusCode):
+ """Defines a set of status codes for a ReturnOrderLineItem."""
+
+ PENDING = 10, _('Pending'), 'secondary'
+
+ # Item is to be returned to customer, no other action
+ RETURN = 20, _('Return'), 'success'
+
+ # Item is to be repaired, and returned to customer
+ REPAIR = 30, _('Repair'), 'primary'
+
+ # Item is to be replaced (new item shipped)
+ REPLACE = 40, _('Replace'), 'warning'
+
+ # Item is to be refunded (cannot be repaired)
+ REFUND = 50, _('Refund'), 'info'
+
+ # Item is rejected
+ REJECT = 60, _('Reject'), 'danger'
diff --git a/src/backend/InvenTree/order/tasks.py b/src/backend/InvenTree/order/tasks.py
index 0c0c8cea30..1e1231a810 100644
--- a/src/backend/InvenTree/order/tasks.py
+++ b/src/backend/InvenTree/order/tasks.py
@@ -7,8 +7,8 @@ from django.utils.translation import gettext_lazy as _
import common.notifications
import InvenTree.helpers_model
import order.models
-from InvenTree.status_codes import PurchaseOrderStatusGroups, SalesOrderStatusGroups
from InvenTree.tasks import ScheduledTask, scheduled_task
+from order.status_codes import PurchaseOrderStatusGroups, SalesOrderStatusGroups
from plugin.events import trigger_event
diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py
index a9824442be..6e59e9a600 100644
--- a/src/backend/InvenTree/order/test_api.py
+++ b/src/backend/InvenTree/order/test_api.py
@@ -16,18 +16,18 @@ from rest_framework import status
from common.models import InvenTreeSetting
from common.settings import currency_codes
from company.models import Company, SupplierPart, SupplierPriceBreak
-from InvenTree.status_codes import (
+from InvenTree.unit_test import InvenTreeAPITestCase
+from order import models
+from order.status_codes import (
PurchaseOrderStatus,
ReturnOrderLineStatus,
ReturnOrderStatus,
SalesOrderStatus,
SalesOrderStatusGroups,
- StockStatus,
)
-from InvenTree.unit_test import InvenTreeAPITestCase
-from order import models
from part.models import Part
from stock.models import StockItem
+from stock.status_codes import StockStatus
from users.models import Owner
@@ -1476,6 +1476,77 @@ class SalesOrderTest(OrderTest):
expected_fn=f'InvenTree_SalesOrders.{fmt}',
)
+ def test_sales_order_complete(self):
+ """Tests for marking a SalesOrder as complete."""
+ self.assignRole('sales_order.add')
+
+ # Let's create a SalesOrder
+ customer = Company.objects.filter(is_customer=True).first()
+ so = models.SalesOrder.objects.create(
+ customer=customer, reference='SO-12345', description='Test SO'
+ )
+
+ self.assertEqual(so.status, SalesOrderStatus.PENDING.value)
+
+ # Create a line item
+ part = Part.objects.filter(salable=True).first()
+
+ line = models.SalesOrderLineItem.objects.create(
+ order=so, part=part, quantity=10, sale_price=Money(10, 'USD')
+ )
+
+ shipment = so.shipments.first()
+
+ if shipment is None:
+ shipment = models.SalesOrderShipment.objects.create(
+ order=so, reference='SHIP-12345'
+ )
+
+ # Allocate some stock
+ item = StockItem.objects.create(part=part, quantity=100, location=None)
+ models.SalesOrderAllocation.objects.create(
+ quantity=10, line=line, item=item, shipment=shipment
+ )
+
+ # Ship the shipment
+ shipment.complete_shipment(self.user)
+
+ # Ok, now we should be able to "complete" the shipment via the API
+ # The 'SALESORDER_SHIP_COMPLETE' setting determines if the outcome is "SHIPPED" or "COMPLETE"
+ InvenTreeSetting.set_setting('SALESORDER_SHIP_COMPLETE', False)
+
+ url = reverse('api-so-complete', kwargs={'pk': so.pk})
+ self.post(url, {}, expected_code=201)
+
+ so.refresh_from_db()
+ self.assertEqual(so.status, SalesOrderStatus.SHIPPED.value)
+
+ # Now, let's try to "complete" the shipment again
+ # This time it should get marked as "COMPLETE"
+ self.post(url, {}, expected_code=201)
+
+ so.refresh_from_db()
+ self.assertEqual(so.status, SalesOrderStatus.COMPLETE.value)
+
+ # Now, let's try *again* (it should fail as the order is already complete)
+ response = self.post(url, {}, expected_code=400)
+
+ self.assertIn('Order is already complete', str(response.data))
+
+ # Next, we'll change the setting so that the order status jumps straight to "complete"
+ so.status = SalesOrderStatus.PENDING.value
+ so.save()
+ so.refresh_from_db()
+ self.assertEqual(so.status, SalesOrderStatus.PENDING.value)
+
+ InvenTreeSetting.set_setting('SALESORDER_SHIP_COMPLETE', True)
+
+ self.post(url, {}, expected_code=201)
+
+ # The orders status should now be "complete" (not "shipped")
+ so.refresh_from_db()
+ self.assertEqual(so.status, SalesOrderStatus.COMPLETE.value)
+
class SalesOrderLineItemTest(OrderTest):
"""Tests for the SalesOrderLineItem API."""
diff --git a/src/backend/InvenTree/order/test_migrations.py b/src/backend/InvenTree/order/test_migrations.py
index 407468cd51..7eafd2032f 100644
--- a/src/backend/InvenTree/order/test_migrations.py
+++ b/src/backend/InvenTree/order/test_migrations.py
@@ -2,7 +2,7 @@
from django_test_migrations.contrib.unittest_case import MigratorTestCase
-from InvenTree.status_codes import SalesOrderStatus
+from order.status_codes import SalesOrderStatus
class TestRefIntMigrations(MigratorTestCase):
diff --git a/src/backend/InvenTree/order/tests.py b/src/backend/InvenTree/order/tests.py
index 1b6cdb9081..c797149ae5 100644
--- a/src/backend/InvenTree/order/tests.py
+++ b/src/backend/InvenTree/order/tests.py
@@ -14,7 +14,7 @@ from djmoney.money import Money
import common.models
import order.tasks
from company.models import Company, SupplierPart
-from InvenTree.status_codes import PurchaseOrderStatus
+from order.status_codes import PurchaseOrderStatus
from part.models import Part
from stock.models import StockItem, StockLocation
from users.models import Owner
diff --git a/src/backend/InvenTree/part/api.py b/src/backend/InvenTree/part/api.py
index 0c3ba35818..824590e923 100644
--- a/src/backend/InvenTree/part/api.py
+++ b/src/backend/InvenTree/part/api.py
@@ -18,6 +18,7 @@ from rest_framework.response import Response
import order.models
import part.filters
from build.models import Build, BuildItem
+from build.status_codes import BuildStatusGroups
from InvenTree.api import (
APIDownloadMixin,
AttachmentMixin,
@@ -45,11 +46,7 @@ from InvenTree.mixins import (
)
from InvenTree.permissions import RolePermission
from InvenTree.serializers import EmptySerializer
-from InvenTree.status_codes import (
- BuildStatusGroups,
- PurchaseOrderStatusGroups,
- SalesOrderStatusGroups,
-)
+from order.status_codes import PurchaseOrderStatusGroups, SalesOrderStatusGroups
from part.admin import PartCategoryResource, PartResource
from stock.models import StockLocation
diff --git a/src/backend/InvenTree/part/filters.py b/src/backend/InvenTree/part/filters.py
index 4d247529b9..0b9fafb983 100644
--- a/src/backend/InvenTree/part/filters.py
+++ b/src/backend/InvenTree/part/filters.py
@@ -40,11 +40,8 @@ from sql_util.utils import SubquerySum
import part.models
import stock.models
-from InvenTree.status_codes import (
- BuildStatusGroups,
- PurchaseOrderStatusGroups,
- SalesOrderStatusGroups,
-)
+from build.status_codes import BuildStatusGroups
+from order.status_codes import PurchaseOrderStatusGroups, SalesOrderStatusGroups
def annotate_in_production_quantity(reference=''):
diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py
index 6364683c5d..9dc2148fb6 100644
--- a/src/backend/InvenTree/part/models.py
+++ b/src/backend/InvenTree/part/models.py
@@ -46,20 +46,20 @@ import part.settings as part_settings
import report.mixins
import users.models
from build import models as BuildModels
+from build.status_codes import BuildStatusGroups
from common.models import InvenTreeSetting
from common.settings import currency_code_default
from company.models import SupplierPart
from InvenTree import helpers, validators
from InvenTree.fields import InvenTreeURLField
from InvenTree.helpers import decimal2money, decimal2string, normalize, str2bool
-from InvenTree.status_codes import (
- BuildStatusGroups,
+from order import models as OrderModels
+from order.status_codes import (
PurchaseOrderStatus,
PurchaseOrderStatusGroups,
SalesOrderStatus,
SalesOrderStatusGroups,
)
-from order import models as OrderModels
from stock import models as StockModels
logger = logging.getLogger('inventree')
diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py
index 3df52b22ca..9ddf1447db 100644
--- a/src/backend/InvenTree/part/serializers.py
+++ b/src/backend/InvenTree/part/serializers.py
@@ -33,7 +33,7 @@ import part.stocktake
import part.tasks
import stock.models
import users.models
-from InvenTree.status_codes import BuildStatusGroups
+from build.status_codes import BuildStatusGroups
from InvenTree.tasks import offload_task
from .models import (
diff --git a/src/backend/InvenTree/part/test_api.py b/src/backend/InvenTree/part/test_api.py
index 77396176e3..b39ecc4834 100644
--- a/src/backend/InvenTree/part/test_api.py
+++ b/src/backend/InvenTree/part/test_api.py
@@ -19,11 +19,12 @@ from rest_framework.test import APIClient
import build.models
import company.models
import order.models
+from build.status_codes import BuildStatus
from common.models import InvenTreeSetting
from company.models import Company, SupplierPart
from InvenTree.settings import BASE_DIR
-from InvenTree.status_codes import BuildStatus, PurchaseOrderStatusGroups, StockStatus
from InvenTree.unit_test import InvenTreeAPITestCase
+from order.status_codes import PurchaseOrderStatusGroups
from part.models import (
BomItem,
BomItemSubstitute,
@@ -37,6 +38,7 @@ from part.models import (
PartTestTemplate,
)
from stock.models import StockItem, StockLocation
+from stock.status_codes import StockStatus
class PartCategoryAPITest(InvenTreeAPITestCase):
diff --git a/src/backend/InvenTree/part/test_pricing.py b/src/backend/InvenTree/part/test_pricing.py
index ba4e7d54ef..a9572b2f16 100644
--- a/src/backend/InvenTree/part/test_pricing.py
+++ b/src/backend/InvenTree/part/test_pricing.py
@@ -11,8 +11,8 @@ import company.models
import order.models
import part.models
import stock.models
-from InvenTree.status_codes import PurchaseOrderStatus
from InvenTree.unit_test import InvenTreeTestCase
+from order.status_codes import PurchaseOrderStatus
class PartPricingTests(InvenTreeTestCase):
diff --git a/src/backend/InvenTree/plugin/base/barcodes/serializers.py b/src/backend/InvenTree/plugin/base/barcodes/serializers.py
index b614bf1a0b..6ad15713b7 100644
--- a/src/backend/InvenTree/plugin/base/barcodes/serializers.py
+++ b/src/backend/InvenTree/plugin/base/barcodes/serializers.py
@@ -7,7 +7,7 @@ from rest_framework import serializers
import order.models
import stock.models
-from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
+from order.status_codes import PurchaseOrderStatus, SalesOrderStatus
from plugin.builtin.barcodes.inventree_barcode import InvenTreeInternalBarcodePlugin
diff --git a/src/backend/InvenTree/plugin/samples/integration/transition.py b/src/backend/InvenTree/plugin/samples/integration/transition.py
index 97166506bd..b0a6ff1826 100644
--- a/src/backend/InvenTree/plugin/samples/integration/transition.py
+++ b/src/backend/InvenTree/plugin/samples/integration/transition.py
@@ -2,8 +2,8 @@
from common.notifications import trigger_notification
from generic.states import TransitionMethod
-from InvenTree.status_codes import ReturnOrderStatus
from order.models import ReturnOrder
+from order.status_codes import ReturnOrderStatus
from plugin import InvenTreePlugin
diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py
index 60402584ba..e78fd277a0 100644
--- a/src/backend/InvenTree/stock/api.py
+++ b/src/backend/InvenTree/stock/api.py
@@ -56,7 +56,6 @@ from InvenTree.mixins import (
RetrieveAPI,
RetrieveUpdateDestroyAPI,
)
-from InvenTree.status_codes import StockHistoryCode, StockStatus
from order.models import PurchaseOrder, ReturnOrder, SalesOrder, SalesOrderAllocation
from order.serializers import (
PurchaseOrderSerializer,
@@ -75,6 +74,7 @@ from stock.models import (
StockLocation,
StockLocationType,
)
+from stock.status_codes import StockHistoryCode, StockStatus
class GenerateBatchCode(GenericAPIView):
diff --git a/src/backend/InvenTree/stock/migrations/0061_auto_20210511_0911.py b/src/backend/InvenTree/stock/migrations/0061_auto_20210511_0911.py
index aacaf01edc..ba44e9c1bd 100644
--- a/src/backend/InvenTree/stock/migrations/0061_auto_20210511_0911.py
+++ b/src/backend/InvenTree/stock/migrations/0061_auto_20210511_0911.py
@@ -4,7 +4,7 @@ import re
from django.db import migrations
-from InvenTree.status_codes import StockHistoryCode
+from stock.status_codes import StockHistoryCode
def update_history(apps, schema_editor):
diff --git a/src/backend/InvenTree/stock/migrations/0096_auto_20230330_1121.py b/src/backend/InvenTree/stock/migrations/0096_auto_20230330_1121.py
index b2dd16cc03..e8db1c5f77 100644
--- a/src/backend/InvenTree/stock/migrations/0096_auto_20230330_1121.py
+++ b/src/backend/InvenTree/stock/migrations/0096_auto_20230330_1121.py
@@ -15,7 +15,7 @@ def update_stock_history(apps, schema_editor):
- Add the appropriate history!
"""
- from InvenTree.status_codes import StockHistoryCode
+ from stock.status_codes import StockHistoryCode
StockItem = apps.get_model('stock', 'stockitem')
StockItemTracking = apps.get_model('stock', 'stockitemtracking')
diff --git a/src/backend/InvenTree/stock/migrations/0102_alter_stockitem_status.py b/src/backend/InvenTree/stock/migrations/0102_alter_stockitem_status.py
index 7b8cf4d239..90807a8c10 100644
--- a/src/backend/InvenTree/stock/migrations/0102_alter_stockitem_status.py
+++ b/src/backend/InvenTree/stock/migrations/0102_alter_stockitem_status.py
@@ -2,7 +2,7 @@
import django.core.validators
from django.db import migrations, models
-import InvenTree.status_codes
+import stock.status_codes
class Migration(migrations.Migration):
@@ -15,6 +15,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='stockitem',
name='status',
- field=models.PositiveIntegerField(choices=InvenTree.status_codes.StockStatus.items(), default=10, validators=[django.core.validators.MinValueValidator(0)]),
+ field=models.PositiveIntegerField(choices=stock.status_codes.StockStatus.items(), default=10, validators=[django.core.validators.MinValueValidator(0)]),
),
]
diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py
index e4eb90192e..1b9cfa8905 100644
--- a/src/backend/InvenTree/stock/models.py
+++ b/src/backend/InvenTree/stock/models.py
@@ -34,15 +34,11 @@ import report.mixins
import report.models
from company import models as CompanyModels
from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField
-from InvenTree.status_codes import (
- SalesOrderStatusGroups,
- StockHistoryCode,
- StockStatus,
- StockStatusGroups,
-)
+from order.status_codes import SalesOrderStatusGroups
from part import models as PartModels
from plugin.events import trigger_event
from stock.generators import generate_batch_code
+from stock.status_codes import StockHistoryCode, StockStatus, StockStatusGroups
from users.models import Owner
logger = logging.getLogger('inventree')
@@ -348,7 +344,7 @@ class StockItem(
stocktake_user: User that performed the most recent stocktake
review_needed: Flag if StockItem needs review
delete_on_deplete: If True, StockItem will be deleted when the stock level gets to zero
- status: Status of this StockItem (ref: InvenTree.status_codes.StockStatus)
+ status: Status of this StockItem (ref: stock.status_codes.StockStatus)
notes: Extra notes field
build: Link to a Build (if this stock item was created from a build)
is_building: Boolean field indicating if this stock item is currently being built (or is "in production")
diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py
index 112647d6ce..91638c62ff 100644
--- a/src/backend/InvenTree/stock/serializers.py
+++ b/src/backend/InvenTree/stock/serializers.py
@@ -20,11 +20,11 @@ import common.models
import company.models
import InvenTree.helpers
import InvenTree.serializers
-import InvenTree.status_codes
import order.models
import part.filters as part_filters
import part.models as part_models
import stock.filters
+import stock.status_codes
from company.serializers import SupplierPartSerializer
from InvenTree.serializers import InvenTreeCurrencySerializer, InvenTreeDecimalField
from part.serializers import PartBriefSerializer, PartTestTemplateSerializer
@@ -925,8 +925,8 @@ class StockChangeStatusSerializer(serializers.Serializer):
return items
status = serializers.ChoiceField(
- choices=InvenTree.status_codes.StockStatus.items(),
- default=InvenTree.status_codes.StockStatus.OK.value,
+ choices=stock.status_codes.StockStatus.items(),
+ default=stock.status_codes.StockStatus.OK.value,
label=_('Status'),
)
@@ -973,7 +973,7 @@ class StockChangeStatusSerializer(serializers.Serializer):
transaction_notes.append(
StockItemTracking(
item=item,
- tracking_type=InvenTree.status_codes.StockHistoryCode.EDITED.value,
+ tracking_type=stock.status_codes.StockHistoryCode.EDITED.value,
date=now,
deltas=deltas,
user=user,
@@ -1419,7 +1419,7 @@ def stock_item_adjust_status_options():
In particular, include a Null option for the status field.
"""
- return [(None, _('No Change'))] + InvenTree.status_codes.StockStatus.items()
+ return [(None, _('No Change'))] + stock.status_codes.StockStatus.items()
class StockAdjustmentItemSerializer(serializers.Serializer):
diff --git a/src/backend/InvenTree/stock/status_codes.py b/src/backend/InvenTree/stock/status_codes.py
new file mode 100644
index 0000000000..3c646bc455
--- /dev/null
+++ b/src/backend/InvenTree/stock/status_codes.py
@@ -0,0 +1,91 @@
+"""Stock status codes."""
+
+from django.utils.translation import gettext_lazy as _
+
+from generic.states import StatusCode
+
+
+class StockStatus(StatusCode):
+ """Status codes for Stock."""
+
+ OK = 10, _('OK'), 'success' # Item is OK
+ ATTENTION = 50, _('Attention needed'), 'warning' # Item requires attention
+ DAMAGED = 55, _('Damaged'), 'warning' # Item is damaged
+ DESTROYED = 60, _('Destroyed'), 'danger' # Item is destroyed
+ REJECTED = 65, _('Rejected'), 'danger' # Item is rejected
+ LOST = 70, _('Lost'), 'dark' # Item has been lost
+ QUARANTINED = (
+ 75,
+ _('Quarantined'),
+ 'info',
+ ) # Item has been quarantined and is unavailable
+ RETURNED = 85, _('Returned'), 'warning' # Item has been returned from a customer
+
+
+class StockStatusGroups:
+ """Groups for StockStatus codes."""
+
+ # The following codes correspond to parts that are 'available' or 'in stock'
+ AVAILABLE_CODES = [
+ StockStatus.OK.value,
+ StockStatus.ATTENTION.value,
+ StockStatus.DAMAGED.value,
+ StockStatus.RETURNED.value,
+ ]
+
+
+class StockHistoryCode(StatusCode):
+ """Status codes for StockHistory."""
+
+ LEGACY = 0, _('Legacy stock tracking entry')
+
+ CREATED = 1, _('Stock item created')
+
+ # Manual editing operations
+ EDITED = 5, _('Edited stock item')
+ ASSIGNED_SERIAL = 6, _('Assigned serial number')
+
+ # Manual stock operations
+ STOCK_COUNT = 10, _('Stock counted')
+ STOCK_ADD = 11, _('Stock manually added')
+ STOCK_REMOVE = 12, _('Stock manually removed')
+
+ # Location operations
+ STOCK_MOVE = 20, _('Location changed')
+ STOCK_UPDATE = 25, _('Stock updated')
+
+ # Installation operations
+ INSTALLED_INTO_ASSEMBLY = 30, _('Installed into assembly')
+ REMOVED_FROM_ASSEMBLY = 31, _('Removed from assembly')
+
+ INSTALLED_CHILD_ITEM = 35, _('Installed component item')
+ REMOVED_CHILD_ITEM = 36, _('Removed component item')
+
+ # Stock splitting operations
+ SPLIT_FROM_PARENT = 40, _('Split from parent item')
+ SPLIT_CHILD_ITEM = 42, _('Split child item')
+
+ # Stock merging operations
+ MERGED_STOCK_ITEMS = 45, _('Merged stock items')
+
+ # Convert stock item to variant
+ CONVERTED_TO_VARIANT = 48, _('Converted to variant')
+
+ # Build order codes
+ BUILD_OUTPUT_CREATED = 50, _('Build order output created')
+ BUILD_OUTPUT_COMPLETED = 55, _('Build order output completed')
+ BUILD_OUTPUT_REJECTED = 56, _('Build order output rejected')
+ BUILD_CONSUMED = 57, _('Consumed by build order')
+
+ # Sales order codes
+ SHIPPED_AGAINST_SALES_ORDER = 60, _('Shipped against Sales Order')
+
+ # Purchase order codes
+ RECEIVED_AGAINST_PURCHASE_ORDER = 70, _('Received against Purchase Order')
+
+ # Return order codes
+ RETURNED_AGAINST_RETURN_ORDER = 80, _('Returned against Return Order')
+
+ # Customer actions
+ SENT_TO_CUSTOMER = 100, _('Sent to customer')
+ RETURNED_FROM_CUSTOMER = 105, _('Returned from customer')
diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py
index 42c29c4abf..07b08334ea 100644
--- a/src/backend/InvenTree/stock/test_api.py
+++ b/src/backend/InvenTree/stock/test_api.py
@@ -17,7 +17,6 @@ import build.models
import company.models
import part.models
from common.models import InvenTreeSetting
-from InvenTree.status_codes import StockHistoryCode, StockStatus
from InvenTree.unit_test import InvenTreeAPITestCase
from part.models import Part, PartTestTemplate
from stock.models import (
@@ -26,6 +25,7 @@ from stock.models import (
StockLocation,
StockLocationType,
)
+from stock.status_codes import StockHistoryCode, StockStatus
class StockAPITestCase(InvenTreeAPITestCase):
diff --git a/src/backend/InvenTree/stock/test_views.py b/src/backend/InvenTree/stock/test_views.py
index e6bb420636..0552a5af7a 100644
--- a/src/backend/InvenTree/stock/test_views.py
+++ b/src/backend/InvenTree/stock/test_views.py
@@ -5,9 +5,9 @@ from django.test import tag
from django.urls import reverse
from common.models import InvenTreeSetting
-from InvenTree.status_codes import StockStatus
from InvenTree.unit_test import InvenTreeTestCase
from stock.models import StockItem, StockLocation
+from stock.status_codes import StockStatus
from users.models import Owner
diff --git a/src/backend/InvenTree/stock/tests.py b/src/backend/InvenTree/stock/tests.py
index 89d9e8e9d2..fe7a2b0e9d 100644
--- a/src/backend/InvenTree/stock/tests.py
+++ b/src/backend/InvenTree/stock/tests.py
@@ -10,10 +10,10 @@ from django.test import override_settings
from build.models import Build
from common.models import InvenTreeSetting
from company.models import Company
-from InvenTree.status_codes import StockHistoryCode
from InvenTree.unit_test import InvenTreeTestCase
from order.models import SalesOrder
from part.models import Part, PartTestTemplate
+from stock.status_codes import StockHistoryCode
from .models import StockItem, StockItemTestResult, StockItemTracking, StockLocation
diff --git a/src/backend/InvenTree/templates/InvenTree/settings/so.html b/src/backend/InvenTree/templates/InvenTree/settings/so.html
index 9be6df6025..493ac32407 100644
--- a/src/backend/InvenTree/templates/InvenTree/settings/so.html
+++ b/src/backend/InvenTree/templates/InvenTree/settings/so.html
@@ -15,6 +15,7 @@
{% include "InvenTree/settings/setting.html" with key="SALESORDER_REQUIRE_RESPONSIBLE" %}
{% include "InvenTree/settings/setting.html" with key="SALESORDER_DEFAULT_SHIPMENT" icon="fa-truck-loading" %}
{% include "InvenTree/settings/setting.html" with key="SALESORDER_EDIT_COMPLETED_ORDERS" icon='fa-edit' %}
+ {% include "InvenTree/settings/setting.html" with key="SALESORDER_SHIP_COMPLETE" %}
diff --git a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx
index 904869bb7e..7feb204820 100644
--- a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx
+++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx
@@ -263,7 +263,8 @@ export default function SystemSettings() {
'SALESORDER_REFERENCE_PATTERN',
'SALESORDER_REQUIRE_RESPONSIBLE',
'SALESORDER_DEFAULT_SHIPMENT',
- 'SALESORDER_EDIT_COMPLETED_ORDERS'
+ 'SALESORDER_EDIT_COMPLETED_ORDERS',
+ 'SALESORDER_SHIP_COMPLETE'
]}
/>
)