mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
PO shipped complete (#7367)
* Add new setting to bypass "shipped" status * Bypass "shipped" status optionally * Update setting description string * Update unit tests * Refactor location of status_codes * Link source code into docs * Add stock status codes * And the build order too * Fix import
This commit is contained in:
parent
fb193cae3d
commit
798c0ed322
12
docs/docs/build/build.md
vendored
12
docs/docs/build/build.md
vendored
@ -79,6 +79,18 @@ Each *Build Order* has an associated *Status* flag, which indicates the state of
|
|||||||
| `Cancelled` | Build has been cancelled |
|
| `Cancelled` | Build has been cancelled |
|
||||||
| `Completed` | Build has been completed |
|
| `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
|
### 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.
|
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.
|
||||||
|
@ -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 |
|
| 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 |
|
| Complete | The purchase order has been completed, and is now closed |
|
||||||
| Cancelled | The purchase order was cancelled, 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
|
### Purchase Order Currency
|
||||||
|
|
||||||
|
@ -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 |
|
| Complete | The return order was marked as complete, and is now closed |
|
||||||
| Cancelled | The return order was cancelled, 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
|
## Create a Return Order
|
||||||
|
|
||||||
From the Return Order index, click on <span class='badge inventree add'><span class='fas fa-plus-circle'></span> New Return Order</span> which opens the "Create Return Order" form.
|
From the Return Order index, click on <span class='badge inventree add'><span class='fas fa-plus-circle'></span> New Return Order</span> which opens the "Create Return Order" form.
|
||||||
|
@ -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 |
|
| 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 |
|
| 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 |
|
| 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
|
### Sales Order Currency
|
||||||
|
|
||||||
@ -83,7 +98,7 @@ To view all the completed shipment, click on the <span class="badge inventree na
|
|||||||
|
|
||||||
### Complete Order
|
### Complete Order
|
||||||
|
|
||||||
Once all items in the sales order have been shipped, click on <span class="badge inventree add"><span class='fas fa-check-circle'></span> Complete Order</span> to mark the sales order as complete.
|
Once all items in the sales order have been shipped, click on <span class="badge inventree add"><span class='fas fa-check-circle'></span> Complete Order</span> to mark the sales order as shipped. Confirm then click on <span class="badge inventree confirm">Submit</span> to complete the order.
|
||||||
|
|
||||||
### Cancel Order
|
### Cancel Order
|
||||||
|
|
||||||
|
@ -26,6 +26,18 @@ The *status* of a given stock item is displayed on the stock item detail page:
|
|||||||
{% include 'img.html' %}
|
{% include 'img.html' %}
|
||||||
{% endwith %}
|
{% 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
|
### Default Status Code
|
||||||
|
|
||||||
The default status code for any newly created Stock Item is <span class='badge inventree success'>OK</span>
|
The default status code for any newly created Stock Item is <span class='badge inventree success'>OK</span>
|
||||||
|
@ -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
|
from build.status_codes import *
|
||||||
|
from order.status_codes import *
|
||||||
|
from stock.status_codes import *
|
||||||
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'
|
|
||||||
|
@ -14,7 +14,7 @@ from django_filters import rest_framework as rest_filters
|
|||||||
from InvenTree.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAPIView, MetadataView
|
from InvenTree.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAPIView, MetadataView
|
||||||
from generic.states.api import StatusView
|
from generic.states.api import StatusView
|
||||||
from InvenTree.helpers import str2bool, isNull, DownloadFile
|
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
|
from InvenTree.mixins import CreateAPI, RetrieveUpdateDestroyAPI, ListCreateAPI
|
||||||
|
|
||||||
import common.models
|
import common.models
|
||||||
|
@ -22,7 +22,8 @@ from mptt.exceptions import InvalidMove
|
|||||||
|
|
||||||
from rest_framework import serializers
|
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
|
from build.validators import generate_next_build_reference, validate_build_order_reference
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ from InvenTree.serializers import UserSerializer
|
|||||||
|
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
from InvenTree.serializers import InvenTreeDecimalField
|
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.generators import generate_batch_code
|
||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
|
20
src/backend/InvenTree/build/status_codes.py
Normal file
20
src/backend/InvenTree/build/status_codes.py
Normal file
@ -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]
|
@ -17,8 +17,8 @@ import InvenTree.email
|
|||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import InvenTree.helpers_model
|
import InvenTree.helpers_model
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
from InvenTree.status_codes import BuildStatusGroups
|
|
||||||
from InvenTree.ready import isImportingData
|
from InvenTree.ready import isImportingData
|
||||||
|
from build.status_codes import BuildStatusGroups
|
||||||
|
|
||||||
import part.models as part_models
|
import part.models as part_models
|
||||||
|
|
||||||
|
@ -10,7 +10,8 @@ from part.models import Part
|
|||||||
from build.models import Build, BuildItem
|
from build.models import Build, BuildItem
|
||||||
from stock.models import StockItem
|
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
|
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ from InvenTree.unit_test import InvenTreeTestCase
|
|||||||
from .models import Build
|
from .models import Build
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus
|
from build.status_codes import BuildStatus
|
||||||
|
|
||||||
|
|
||||||
class BuildTestSimple(InvenTreeTestCase):
|
class BuildTestSimple(InvenTreeTestCase):
|
||||||
|
@ -5,7 +5,7 @@ from django.views.generic import DetailView, ListView
|
|||||||
from .models import Build
|
from .models import Build
|
||||||
|
|
||||||
from InvenTree.views import InvenTreeRoleMixin
|
from InvenTree.views import InvenTreeRoleMixin
|
||||||
from InvenTree.status_codes import BuildStatus
|
from build.status_codes import BuildStatus
|
||||||
|
|
||||||
from plugin.views import InvenTreePluginViewMixin
|
from plugin.views import InvenTreePluginViewMixin
|
||||||
|
|
||||||
|
@ -1874,6 +1874,14 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
'default': False,
|
'default': False,
|
||||||
'validator': bool,
|
'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': {
|
'PURCHASEORDER_REFERENCE_PATTERN': {
|
||||||
'name': _('Purchase Order Reference Pattern'),
|
'name': _('Purchase Order Reference Pattern'),
|
||||||
'description': _(
|
'description': _(
|
||||||
|
@ -31,7 +31,7 @@ import InvenTree.tasks
|
|||||||
import InvenTree.validators
|
import InvenTree.validators
|
||||||
from common.settings import currency_code_default
|
from common.settings import currency_code_default
|
||||||
from InvenTree.fields import InvenTreeURLField, RoundingDecimalField
|
from InvenTree.fields import InvenTreeURLField, RoundingDecimalField
|
||||||
from InvenTree.status_codes import PurchaseOrderStatusGroups
|
from order.status_codes import PurchaseOrderStatusGroups
|
||||||
|
|
||||||
|
|
||||||
def rename_company_image(instance, filename):
|
def rename_company_image(instance, filename):
|
||||||
|
@ -30,14 +30,6 @@ from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS
|
|||||||
from InvenTree.helpers import DownloadFile, str2bool
|
from InvenTree.helpers import DownloadFile, str2bool
|
||||||
from InvenTree.helpers_model import construct_absolute_url, get_base_url
|
from InvenTree.helpers_model import construct_absolute_url, get_base_url
|
||||||
from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI, RetrieveUpdateDestroyAPI
|
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 import models, serializers
|
||||||
from order.admin import (
|
from order.admin import (
|
||||||
PurchaseOrderExtraLineResource,
|
PurchaseOrderExtraLineResource,
|
||||||
@ -48,6 +40,14 @@ from order.admin import (
|
|||||||
SalesOrderLineItemResource,
|
SalesOrderLineItemResource,
|
||||||
SalesOrderResource,
|
SalesOrderResource,
|
||||||
)
|
)
|
||||||
|
from order.status_codes import (
|
||||||
|
PurchaseOrderStatus,
|
||||||
|
PurchaseOrderStatusGroups,
|
||||||
|
ReturnOrderLineStatus,
|
||||||
|
ReturnOrderStatus,
|
||||||
|
SalesOrderStatus,
|
||||||
|
SalesOrderStatusGroups,
|
||||||
|
)
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
from users.models import Owner
|
from users.models import Owner
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
from InvenTree.status_codes import SalesOrderStatus
|
from order.status_codes import SalesOrderStatus
|
||||||
|
|
||||||
|
|
||||||
def add_shipment(apps, schema_editor):
|
def add_shipment(apps, schema_editor):
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
from InvenTree.status_codes import SalesOrderStatus
|
from order.status_codes import SalesOrderStatus
|
||||||
|
|
||||||
|
|
||||||
def calculate_shipped_quantity(apps, schema_editor):
|
def calculate_shipped_quantity(apps, schema_editor):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Generated by Django 3.2.19 on 2023-06-04 17:43
|
# Generated by Django 3.2.19 on 2023-06-04 17:43
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import InvenTree.status_codes
|
import order.status_codes
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -14,6 +14,6 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='returnorderlineitem',
|
model_name='returnorderlineitem',
|
||||||
name='outcome',
|
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'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import InvenTree.status_codes
|
import order.status_codes
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -15,6 +15,6 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='salesorder',
|
model_name='salesorder',
|
||||||
name='status',
|
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'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -45,7 +45,7 @@ from InvenTree.fields import (
|
|||||||
)
|
)
|
||||||
from InvenTree.helpers import decimal2string, pui_url
|
from InvenTree.helpers import decimal2string, pui_url
|
||||||
from InvenTree.helpers_model import getSetting, notify_responsible
|
from InvenTree.helpers_model import getSetting, notify_responsible
|
||||||
from InvenTree.status_codes import (
|
from order.status_codes import (
|
||||||
PurchaseOrderStatus,
|
PurchaseOrderStatus,
|
||||||
PurchaseOrderStatusGroups,
|
PurchaseOrderStatusGroups,
|
||||||
ReturnOrderLineStatus,
|
ReturnOrderLineStatus,
|
||||||
@ -53,11 +53,10 @@ from InvenTree.status_codes import (
|
|||||||
ReturnOrderStatusGroups,
|
ReturnOrderStatusGroups,
|
||||||
SalesOrderStatus,
|
SalesOrderStatus,
|
||||||
SalesOrderStatusGroups,
|
SalesOrderStatusGroups,
|
||||||
StockHistoryCode,
|
|
||||||
StockStatus,
|
|
||||||
)
|
)
|
||||||
from part import models as PartModels
|
from part import models as PartModels
|
||||||
from plugin.events import trigger_event
|
from plugin.events import trigger_event
|
||||||
|
from stock.status_codes import StockHistoryCode, StockStatus
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
@ -1024,6 +1023,12 @@ class SalesOrder(TotalPriceMixin, Order):
|
|||||||
Throws a ValidationError if cannot be completed.
|
Throws a ValidationError if cannot be completed.
|
||||||
"""
|
"""
|
||||||
try:
|
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
|
# Only an open order can be marked as shipped
|
||||||
if self.is_open and not self.is_completed:
|
if self.is_open and not self.is_completed:
|
||||||
raise ValidationError(_('Only an open order can be marked as complete'))
|
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):
|
if not self.can_complete(**kwargs):
|
||||||
return False
|
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
|
self.status = SalesOrderStatus.COMPLETE.value
|
||||||
else:
|
else:
|
||||||
self.status = SalesOrderStatus.SHIPPED.value
|
self.status = SalesOrderStatus.SHIPPED.value
|
||||||
|
@ -48,14 +48,14 @@ from InvenTree.serializers import (
|
|||||||
InvenTreeModelSerializer,
|
InvenTreeModelSerializer,
|
||||||
InvenTreeMoneySerializer,
|
InvenTreeMoneySerializer,
|
||||||
)
|
)
|
||||||
from InvenTree.status_codes import (
|
from order.status_codes import (
|
||||||
PurchaseOrderStatusGroups,
|
PurchaseOrderStatusGroups,
|
||||||
ReturnOrderLineStatus,
|
ReturnOrderLineStatus,
|
||||||
ReturnOrderStatus,
|
ReturnOrderStatus,
|
||||||
SalesOrderStatusGroups,
|
SalesOrderStatusGroups,
|
||||||
StockStatus,
|
|
||||||
)
|
)
|
||||||
from part.serializers import PartBriefSerializer
|
from part.serializers import PartBriefSerializer
|
||||||
|
from stock.status_codes import StockStatus
|
||||||
from users.serializers import OwnerSerializer
|
from users.serializers import OwnerSerializer
|
||||||
|
|
||||||
|
|
||||||
|
97
src/backend/InvenTree/order/status_codes.py
Normal file
97
src/backend/InvenTree/order/status_codes.py
Normal file
@ -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'
|
@ -7,8 +7,8 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
import common.notifications
|
import common.notifications
|
||||||
import InvenTree.helpers_model
|
import InvenTree.helpers_model
|
||||||
import order.models
|
import order.models
|
||||||
from InvenTree.status_codes import PurchaseOrderStatusGroups, SalesOrderStatusGroups
|
|
||||||
from InvenTree.tasks import ScheduledTask, scheduled_task
|
from InvenTree.tasks import ScheduledTask, scheduled_task
|
||||||
|
from order.status_codes import PurchaseOrderStatusGroups, SalesOrderStatusGroups
|
||||||
from plugin.events import trigger_event
|
from plugin.events import trigger_event
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,18 +16,18 @@ from rest_framework import status
|
|||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from common.settings import currency_codes
|
from common.settings import currency_codes
|
||||||
from company.models import Company, SupplierPart, SupplierPriceBreak
|
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,
|
PurchaseOrderStatus,
|
||||||
ReturnOrderLineStatus,
|
ReturnOrderLineStatus,
|
||||||
ReturnOrderStatus,
|
ReturnOrderStatus,
|
||||||
SalesOrderStatus,
|
SalesOrderStatus,
|
||||||
SalesOrderStatusGroups,
|
SalesOrderStatusGroups,
|
||||||
StockStatus,
|
|
||||||
)
|
)
|
||||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
|
||||||
from order import models
|
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
|
from stock.status_codes import StockStatus
|
||||||
from users.models import Owner
|
from users.models import Owner
|
||||||
|
|
||||||
|
|
||||||
@ -1476,6 +1476,77 @@ class SalesOrderTest(OrderTest):
|
|||||||
expected_fn=f'InvenTree_SalesOrders.{fmt}',
|
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):
|
class SalesOrderLineItemTest(OrderTest):
|
||||||
"""Tests for the SalesOrderLineItem API."""
|
"""Tests for the SalesOrderLineItem API."""
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from django_test_migrations.contrib.unittest_case import MigratorTestCase
|
from django_test_migrations.contrib.unittest_case import MigratorTestCase
|
||||||
|
|
||||||
from InvenTree.status_codes import SalesOrderStatus
|
from order.status_codes import SalesOrderStatus
|
||||||
|
|
||||||
|
|
||||||
class TestRefIntMigrations(MigratorTestCase):
|
class TestRefIntMigrations(MigratorTestCase):
|
||||||
|
@ -14,7 +14,7 @@ from djmoney.money import Money
|
|||||||
import common.models
|
import common.models
|
||||||
import order.tasks
|
import order.tasks
|
||||||
from company.models import Company, SupplierPart
|
from company.models import Company, SupplierPart
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus
|
from order.status_codes import PurchaseOrderStatus
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
from users.models import Owner
|
from users.models import Owner
|
||||||
|
@ -18,6 +18,7 @@ from rest_framework.response import Response
|
|||||||
import order.models
|
import order.models
|
||||||
import part.filters
|
import part.filters
|
||||||
from build.models import Build, BuildItem
|
from build.models import Build, BuildItem
|
||||||
|
from build.status_codes import BuildStatusGroups
|
||||||
from InvenTree.api import (
|
from InvenTree.api import (
|
||||||
APIDownloadMixin,
|
APIDownloadMixin,
|
||||||
AttachmentMixin,
|
AttachmentMixin,
|
||||||
@ -45,11 +46,7 @@ from InvenTree.mixins import (
|
|||||||
)
|
)
|
||||||
from InvenTree.permissions import RolePermission
|
from InvenTree.permissions import RolePermission
|
||||||
from InvenTree.serializers import EmptySerializer
|
from InvenTree.serializers import EmptySerializer
|
||||||
from InvenTree.status_codes import (
|
from order.status_codes import PurchaseOrderStatusGroups, SalesOrderStatusGroups
|
||||||
BuildStatusGroups,
|
|
||||||
PurchaseOrderStatusGroups,
|
|
||||||
SalesOrderStatusGroups,
|
|
||||||
)
|
|
||||||
from part.admin import PartCategoryResource, PartResource
|
from part.admin import PartCategoryResource, PartResource
|
||||||
from stock.models import StockLocation
|
from stock.models import StockLocation
|
||||||
|
|
||||||
|
@ -40,11 +40,8 @@ from sql_util.utils import SubquerySum
|
|||||||
|
|
||||||
import part.models
|
import part.models
|
||||||
import stock.models
|
import stock.models
|
||||||
from InvenTree.status_codes import (
|
from build.status_codes import BuildStatusGroups
|
||||||
BuildStatusGroups,
|
from order.status_codes import PurchaseOrderStatusGroups, SalesOrderStatusGroups
|
||||||
PurchaseOrderStatusGroups,
|
|
||||||
SalesOrderStatusGroups,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def annotate_in_production_quantity(reference=''):
|
def annotate_in_production_quantity(reference=''):
|
||||||
|
@ -46,20 +46,20 @@ import part.settings as part_settings
|
|||||||
import report.mixins
|
import report.mixins
|
||||||
import users.models
|
import users.models
|
||||||
from build import models as BuildModels
|
from build import models as BuildModels
|
||||||
|
from build.status_codes import BuildStatusGroups
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from common.settings import currency_code_default
|
from common.settings import currency_code_default
|
||||||
from company.models import SupplierPart
|
from company.models import SupplierPart
|
||||||
from InvenTree import helpers, validators
|
from InvenTree import helpers, validators
|
||||||
from InvenTree.fields import InvenTreeURLField
|
from InvenTree.fields import InvenTreeURLField
|
||||||
from InvenTree.helpers import decimal2money, decimal2string, normalize, str2bool
|
from InvenTree.helpers import decimal2money, decimal2string, normalize, str2bool
|
||||||
from InvenTree.status_codes import (
|
from order import models as OrderModels
|
||||||
BuildStatusGroups,
|
from order.status_codes import (
|
||||||
PurchaseOrderStatus,
|
PurchaseOrderStatus,
|
||||||
PurchaseOrderStatusGroups,
|
PurchaseOrderStatusGroups,
|
||||||
SalesOrderStatus,
|
SalesOrderStatus,
|
||||||
SalesOrderStatusGroups,
|
SalesOrderStatusGroups,
|
||||||
)
|
)
|
||||||
from order import models as OrderModels
|
|
||||||
from stock import models as StockModels
|
from stock import models as StockModels
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
|
@ -33,7 +33,7 @@ import part.stocktake
|
|||||||
import part.tasks
|
import part.tasks
|
||||||
import stock.models
|
import stock.models
|
||||||
import users.models
|
import users.models
|
||||||
from InvenTree.status_codes import BuildStatusGroups
|
from build.status_codes import BuildStatusGroups
|
||||||
from InvenTree.tasks import offload_task
|
from InvenTree.tasks import offload_task
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
|
@ -19,11 +19,12 @@ from rest_framework.test import APIClient
|
|||||||
import build.models
|
import build.models
|
||||||
import company.models
|
import company.models
|
||||||
import order.models
|
import order.models
|
||||||
|
from build.status_codes import BuildStatus
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from company.models import Company, SupplierPart
|
from company.models import Company, SupplierPart
|
||||||
from InvenTree.settings import BASE_DIR
|
from InvenTree.settings import BASE_DIR
|
||||||
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatusGroups, StockStatus
|
|
||||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||||
|
from order.status_codes import PurchaseOrderStatusGroups
|
||||||
from part.models import (
|
from part.models import (
|
||||||
BomItem,
|
BomItem,
|
||||||
BomItemSubstitute,
|
BomItemSubstitute,
|
||||||
@ -37,6 +38,7 @@ from part.models import (
|
|||||||
PartTestTemplate,
|
PartTestTemplate,
|
||||||
)
|
)
|
||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
|
from stock.status_codes import StockStatus
|
||||||
|
|
||||||
|
|
||||||
class PartCategoryAPITest(InvenTreeAPITestCase):
|
class PartCategoryAPITest(InvenTreeAPITestCase):
|
||||||
|
@ -11,8 +11,8 @@ import company.models
|
|||||||
import order.models
|
import order.models
|
||||||
import part.models
|
import part.models
|
||||||
import stock.models
|
import stock.models
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus
|
|
||||||
from InvenTree.unit_test import InvenTreeTestCase
|
from InvenTree.unit_test import InvenTreeTestCase
|
||||||
|
from order.status_codes import PurchaseOrderStatus
|
||||||
|
|
||||||
|
|
||||||
class PartPricingTests(InvenTreeTestCase):
|
class PartPricingTests(InvenTreeTestCase):
|
||||||
|
@ -7,7 +7,7 @@ from rest_framework import serializers
|
|||||||
|
|
||||||
import order.models
|
import order.models
|
||||||
import stock.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
|
from plugin.builtin.barcodes.inventree_barcode import InvenTreeInternalBarcodePlugin
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
from common.notifications import trigger_notification
|
from common.notifications import trigger_notification
|
||||||
from generic.states import TransitionMethod
|
from generic.states import TransitionMethod
|
||||||
from InvenTree.status_codes import ReturnOrderStatus
|
|
||||||
from order.models import ReturnOrder
|
from order.models import ReturnOrder
|
||||||
|
from order.status_codes import ReturnOrderStatus
|
||||||
from plugin import InvenTreePlugin
|
from plugin import InvenTreePlugin
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,7 +56,6 @@ from InvenTree.mixins import (
|
|||||||
RetrieveAPI,
|
RetrieveAPI,
|
||||||
RetrieveUpdateDestroyAPI,
|
RetrieveUpdateDestroyAPI,
|
||||||
)
|
)
|
||||||
from InvenTree.status_codes import StockHistoryCode, StockStatus
|
|
||||||
from order.models import PurchaseOrder, ReturnOrder, SalesOrder, SalesOrderAllocation
|
from order.models import PurchaseOrder, ReturnOrder, SalesOrder, SalesOrderAllocation
|
||||||
from order.serializers import (
|
from order.serializers import (
|
||||||
PurchaseOrderSerializer,
|
PurchaseOrderSerializer,
|
||||||
@ -75,6 +74,7 @@ from stock.models import (
|
|||||||
StockLocation,
|
StockLocation,
|
||||||
StockLocationType,
|
StockLocationType,
|
||||||
)
|
)
|
||||||
|
from stock.status_codes import StockHistoryCode, StockStatus
|
||||||
|
|
||||||
|
|
||||||
class GenerateBatchCode(GenericAPIView):
|
class GenerateBatchCode(GenericAPIView):
|
||||||
|
@ -4,7 +4,7 @@ import re
|
|||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
from InvenTree.status_codes import StockHistoryCode
|
from stock.status_codes import StockHistoryCode
|
||||||
|
|
||||||
|
|
||||||
def update_history(apps, schema_editor):
|
def update_history(apps, schema_editor):
|
||||||
|
@ -15,7 +15,7 @@ def update_stock_history(apps, schema_editor):
|
|||||||
- Add the appropriate history!
|
- Add the appropriate history!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from InvenTree.status_codes import StockHistoryCode
|
from stock.status_codes import StockHistoryCode
|
||||||
|
|
||||||
StockItem = apps.get_model('stock', 'stockitem')
|
StockItem = apps.get_model('stock', 'stockitem')
|
||||||
StockItemTracking = apps.get_model('stock', 'stockitemtracking')
|
StockItemTracking = apps.get_model('stock', 'stockitemtracking')
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import InvenTree.status_codes
|
import stock.status_codes
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -15,6 +15,6 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='stockitem',
|
model_name='stockitem',
|
||||||
name='status',
|
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)]),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -34,15 +34,11 @@ import report.mixins
|
|||||||
import report.models
|
import report.models
|
||||||
from company import models as CompanyModels
|
from company import models as CompanyModels
|
||||||
from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField
|
from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField
|
||||||
from InvenTree.status_codes import (
|
from order.status_codes import SalesOrderStatusGroups
|
||||||
SalesOrderStatusGroups,
|
|
||||||
StockHistoryCode,
|
|
||||||
StockStatus,
|
|
||||||
StockStatusGroups,
|
|
||||||
)
|
|
||||||
from part import models as PartModels
|
from part import models as PartModels
|
||||||
from plugin.events import trigger_event
|
from plugin.events import trigger_event
|
||||||
from stock.generators import generate_batch_code
|
from stock.generators import generate_batch_code
|
||||||
|
from stock.status_codes import StockHistoryCode, StockStatus, StockStatusGroups
|
||||||
from users.models import Owner
|
from users.models import Owner
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
@ -348,7 +344,7 @@ class StockItem(
|
|||||||
stocktake_user: User that performed the most recent stocktake
|
stocktake_user: User that performed the most recent stocktake
|
||||||
review_needed: Flag if StockItem needs review
|
review_needed: Flag if StockItem needs review
|
||||||
delete_on_deplete: If True, StockItem will be deleted when the stock level gets to zero
|
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
|
notes: Extra notes field
|
||||||
build: Link to a Build (if this stock item was created from a build)
|
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")
|
is_building: Boolean field indicating if this stock item is currently being built (or is "in production")
|
||||||
|
@ -20,11 +20,11 @@ import common.models
|
|||||||
import company.models
|
import company.models
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import InvenTree.serializers
|
import InvenTree.serializers
|
||||||
import InvenTree.status_codes
|
|
||||||
import order.models
|
import order.models
|
||||||
import part.filters as part_filters
|
import part.filters as part_filters
|
||||||
import part.models as part_models
|
import part.models as part_models
|
||||||
import stock.filters
|
import stock.filters
|
||||||
|
import stock.status_codes
|
||||||
from company.serializers import SupplierPartSerializer
|
from company.serializers import SupplierPartSerializer
|
||||||
from InvenTree.serializers import InvenTreeCurrencySerializer, InvenTreeDecimalField
|
from InvenTree.serializers import InvenTreeCurrencySerializer, InvenTreeDecimalField
|
||||||
from part.serializers import PartBriefSerializer, PartTestTemplateSerializer
|
from part.serializers import PartBriefSerializer, PartTestTemplateSerializer
|
||||||
@ -925,8 +925,8 @@ class StockChangeStatusSerializer(serializers.Serializer):
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
status = serializers.ChoiceField(
|
status = serializers.ChoiceField(
|
||||||
choices=InvenTree.status_codes.StockStatus.items(),
|
choices=stock.status_codes.StockStatus.items(),
|
||||||
default=InvenTree.status_codes.StockStatus.OK.value,
|
default=stock.status_codes.StockStatus.OK.value,
|
||||||
label=_('Status'),
|
label=_('Status'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -973,7 +973,7 @@ class StockChangeStatusSerializer(serializers.Serializer):
|
|||||||
transaction_notes.append(
|
transaction_notes.append(
|
||||||
StockItemTracking(
|
StockItemTracking(
|
||||||
item=item,
|
item=item,
|
||||||
tracking_type=InvenTree.status_codes.StockHistoryCode.EDITED.value,
|
tracking_type=stock.status_codes.StockHistoryCode.EDITED.value,
|
||||||
date=now,
|
date=now,
|
||||||
deltas=deltas,
|
deltas=deltas,
|
||||||
user=user,
|
user=user,
|
||||||
@ -1419,7 +1419,7 @@ def stock_item_adjust_status_options():
|
|||||||
|
|
||||||
In particular, include a Null option for the status field.
|
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):
|
class StockAdjustmentItemSerializer(serializers.Serializer):
|
||||||
|
91
src/backend/InvenTree/stock/status_codes.py
Normal file
91
src/backend/InvenTree/stock/status_codes.py
Normal file
@ -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')
|
@ -17,7 +17,6 @@ import build.models
|
|||||||
import company.models
|
import company.models
|
||||||
import part.models
|
import part.models
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from InvenTree.status_codes import StockHistoryCode, StockStatus
|
|
||||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||||
from part.models import Part, PartTestTemplate
|
from part.models import Part, PartTestTemplate
|
||||||
from stock.models import (
|
from stock.models import (
|
||||||
@ -26,6 +25,7 @@ from stock.models import (
|
|||||||
StockLocation,
|
StockLocation,
|
||||||
StockLocationType,
|
StockLocationType,
|
||||||
)
|
)
|
||||||
|
from stock.status_codes import StockHistoryCode, StockStatus
|
||||||
|
|
||||||
|
|
||||||
class StockAPITestCase(InvenTreeAPITestCase):
|
class StockAPITestCase(InvenTreeAPITestCase):
|
||||||
|
@ -5,9 +5,9 @@ from django.test import tag
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from InvenTree.status_codes import StockStatus
|
|
||||||
from InvenTree.unit_test import InvenTreeTestCase
|
from InvenTree.unit_test import InvenTreeTestCase
|
||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
|
from stock.status_codes import StockStatus
|
||||||
from users.models import Owner
|
from users.models import Owner
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,10 +10,10 @@ from django.test import override_settings
|
|||||||
from build.models import Build
|
from build.models import Build
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from company.models import Company
|
from company.models import Company
|
||||||
from InvenTree.status_codes import StockHistoryCode
|
|
||||||
from InvenTree.unit_test import InvenTreeTestCase
|
from InvenTree.unit_test import InvenTreeTestCase
|
||||||
from order.models import SalesOrder
|
from order.models import SalesOrder
|
||||||
from part.models import Part, PartTestTemplate
|
from part.models import Part, PartTestTemplate
|
||||||
|
from stock.status_codes import StockHistoryCode
|
||||||
|
|
||||||
from .models import StockItem, StockItemTestResult, StockItemTracking, StockLocation
|
from .models import StockItem, StockItemTestResult, StockItemTracking, StockLocation
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
{% include "InvenTree/settings/setting.html" with key="SALESORDER_REQUIRE_RESPONSIBLE" %}
|
{% 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_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_EDIT_COMPLETED_ORDERS" icon='fa-edit' %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="SALESORDER_SHIP_COMPLETE" %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -263,7 +263,8 @@ export default function SystemSettings() {
|
|||||||
'SALESORDER_REFERENCE_PATTERN',
|
'SALESORDER_REFERENCE_PATTERN',
|
||||||
'SALESORDER_REQUIRE_RESPONSIBLE',
|
'SALESORDER_REQUIRE_RESPONSIBLE',
|
||||||
'SALESORDER_DEFAULT_SHIPMENT',
|
'SALESORDER_DEFAULT_SHIPMENT',
|
||||||
'SALESORDER_EDIT_COMPLETED_ORDERS'
|
'SALESORDER_EDIT_COMPLETED_ORDERS',
|
||||||
|
'SALESORDER_SHIP_COMPLETE'
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user