diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 14c4f62e5b..9e3d1d5bb6 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,13 +1,18 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 297 +INVENTREE_API_VERSION = 298 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v298 - 2025-01-07 - https://github.com/inventree/InvenTree/pull/8848 + - Adds 'created_by' field to PurchaseOrder API endpoints + - Adds 'created_by' field to SalesOrder API endpoints + - Adds 'created_by' field to ReturnOrder API endpoints + v297 - 2024-12-29 - https://github.com/inventree/InvenTree/pull/8438 - Adjustments to the CustomUserState API endpoints and serializers diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index 52520808dd..ab4f677a56 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -5,6 +5,7 @@ from typing import cast from django.conf import settings from django.contrib.auth import authenticate, login +from django.contrib.auth.models import User from django.db.models import F, Q from django.http.response import JsonResponse from django.urls import include, path, re_path @@ -168,6 +169,10 @@ class OrderFilter(rest_filters.FilterSet): queryset=Owner.objects.all(), field_name='responsible', label=_('Responsible') ) + created_by = rest_filters.ModelChoiceFilter( + queryset=User.objects.all(), field_name='created_by', label=_('Created By') + ) + created_before = InvenTreeDateFilter( label=_('Created Before'), field_name='creation_date', lookup_expr='lt' ) @@ -328,6 +333,7 @@ class PurchaseOrderList( ordering_fields = [ 'creation_date', + 'created_by', 'reference', 'supplier__name', 'target_date', @@ -785,6 +791,7 @@ class SalesOrderList( ordering_fields = [ 'creation_date', + 'created_by', 'reference', 'customer__name', 'customer_reference', @@ -1369,6 +1376,7 @@ class ReturnOrderList( ordering_fields = [ 'creation_date', + 'created_by', 'reference', 'customer__name', 'customer_reference', diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index de12e62f27..be0f59bd38 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -50,6 +50,7 @@ from InvenTree.serializers import ( InvenTreeModelSerializer, InvenTreeMoneySerializer, NotesFieldMixin, + UserSerializer, ) from order.status_codes import ( PurchaseOrderStatusGroups, @@ -158,6 +159,8 @@ class AbstractOrderSerializer(DataImportExportSerializerMixin, serializers.Seria required=False, allow_null=True, label=_('Creation Date') ) + created_by = UserSerializer(read_only=True) + duplicate = DuplicateOrderSerializer( label=_('Duplicate Order'), help_text=_('Specify options for duplicating this order'), @@ -174,6 +177,7 @@ class AbstractOrderSerializer(DataImportExportSerializerMixin, serializers.Seria def annotate_queryset(queryset): """Add extra information to the queryset.""" queryset = queryset.annotate(line_items=SubqueryCount('lines')) + queryset = queryset.select_related('created_by') return queryset @@ -183,6 +187,7 @@ class AbstractOrderSerializer(DataImportExportSerializerMixin, serializers.Seria return [ 'pk', 'creation_date', + 'created_by', 'target_date', 'description', 'line_items', diff --git a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx index 22ec2225b4..2d5e13e719 100644 --- a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx +++ b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx @@ -217,6 +217,13 @@ export default function PurchaseOrderDetail() { icon: 'reference', copy: true, hidden: !order.project_code + }, + { + type: 'text', + name: 'responsible', + label: t`Responsible`, + badge: 'owner', + hidden: !order.responsible } ]; @@ -225,6 +232,7 @@ export default function PurchaseOrderDetail() { type: 'date', name: 'creation_date', label: t`Creation Date`, + copy: true, icon: 'calendar' }, { @@ -240,6 +248,7 @@ export default function PurchaseOrderDetail() { name: 'target_date', label: t`Target Date`, icon: 'calendar', + copy: true, hidden: !order.target_date }, { @@ -249,13 +258,6 @@ export default function PurchaseOrderDetail() { label: t`Completion Date`, copy: true, hidden: !order.complete_date - }, - { - type: 'text', - name: 'responsible', - label: t`Responsible`, - badge: 'owner', - hidden: !order.responsible } ]; diff --git a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx index bbba1340c5..d598134805 100644 --- a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx +++ b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx @@ -187,6 +187,13 @@ export default function ReturnOrderDetail() { icon: 'reference', copy: true, hidden: !order.project_code + }, + { + type: 'text', + name: 'responsible', + label: t`Responsible`, + badge: 'owner', + hidden: !order.responsible } ]; @@ -221,13 +228,6 @@ export default function ReturnOrderDetail() { label: t`Completion Date`, copy: true, hidden: !order.complete_date - }, - { - type: 'text', - name: 'responsible', - label: t`Responsible`, - badge: 'owner', - hidden: !order.responsible } ]; diff --git a/src/frontend/src/pages/sales/SalesOrderDetail.tsx b/src/frontend/src/pages/sales/SalesOrderDetail.tsx index c02627361c..c0b7e0fa23 100644 --- a/src/frontend/src/pages/sales/SalesOrderDetail.tsx +++ b/src/frontend/src/pages/sales/SalesOrderDetail.tsx @@ -199,6 +199,13 @@ export default function SalesOrderDetail() { icon: 'reference', copy: true, hidden: !order.project_code + }, + { + type: 'text', + name: 'responsible', + label: t`Responsible`, + badge: 'owner', + hidden: !order.responsible } ]; @@ -231,13 +238,6 @@ export default function SalesOrderDetail() { label: t`Completion Date`, hidden: !order.shipment_date, copy: true - }, - { - type: 'text', - name: 'responsible', - label: t`Responsible`, - badge: 'owner', - hidden: !order.responsible } ]; diff --git a/src/frontend/src/tables/ColumnRenderers.tsx b/src/frontend/src/tables/ColumnRenderers.tsx index 0d25ff8b91..d01d62f8ef 100644 --- a/src/frontend/src/tables/ColumnRenderers.tsx +++ b/src/frontend/src/tables/ColumnRenderers.tsx @@ -9,7 +9,7 @@ import { YesNoButton } from '../components/buttons/YesNoButton'; import { Thumbnail } from '../components/images/Thumbnail'; import { ProgressBar } from '../components/items/ProgressBar'; import { TableStatusRenderer } from '../components/render/StatusRenderer'; -import { RenderOwner } from '../components/render/User'; +import { RenderOwner, RenderUser } from '../components/render/User'; import { formatCurrency, formatDate } from '../defaults/formatters'; import type { ModelType } from '../enums/ModelType'; import { resolveItem } from '../functions/conversion'; @@ -202,6 +202,18 @@ export function StatusColumn({ }; } +export function CreatedByColumn(props: TableColumnProps): TableColumn { + return { + accessor: 'created_by', + title: t`Created By`, + sortable: true, + switchable: true, + render: (record: any) => + record.created_by && RenderUser({ instance: record.created_by }), + ...props + }; +} + export function ResponsibleColumn(props: TableColumnProps): TableColumn { return { accessor: 'responsible', diff --git a/src/frontend/src/tables/Filter.tsx b/src/frontend/src/tables/Filter.tsx index f53d5e37f4..dbeca42138 100644 --- a/src/frontend/src/tables/Filter.tsx +++ b/src/frontend/src/tables/Filter.tsx @@ -205,3 +205,47 @@ export function HasProjectCodeFilter(): TableFilter { description: t`Show orders with an assigned project code` }; } + +export function OrderStatusFilter({ + model +}: { model: ModelType }): TableFilter { + return { + name: 'status', + label: t`Status`, + description: t`Filter by order status`, + choiceFunction: StatusFilterOptions(model) + }; +} + +export function ProjectCodeFilter({ + choices +}: { choices: TableFilterChoice[] }): TableFilter { + return { + name: 'project_code', + label: t`Project Code`, + description: t`Filter by project code`, + choices: choices + }; +} + +export function ResponsibleFilter({ + choices +}: { choices: TableFilterChoice[] }): TableFilter { + return { + name: 'assigned_to', + label: t`Responsible`, + description: t`Filter by responsible owner`, + choices: choices + }; +} + +export function CreatedByFilter({ + choices +}: { choices: TableFilterChoice[] }): TableFilter { + return { + name: 'created_by', + label: t`Created By`, + description: t`Filter by user who created the order`, + choices: choices + }; +} diff --git a/src/frontend/src/tables/build/BuildOrderTable.tsx b/src/frontend/src/tables/build/BuildOrderTable.tsx index 01531866f0..b8c397ce06 100644 --- a/src/frontend/src/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/tables/build/BuildOrderTable.tsx @@ -8,7 +8,11 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { useBuildOrderFields } from '../../forms/BuildForms'; -import { useOwnerFilters, useProjectCodeFilters } from '../../hooks/UseFilter'; +import { + useOwnerFilters, + useProjectCodeFilters, + useUserFilters +} from '../../hooks/UseFilter'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; @@ -32,8 +36,11 @@ import { HasProjectCodeFilter, MaxDateFilter, MinDateFilter, + OrderStatusFilter, + OutstandingFilter, OverdueFilter, - StatusFilterOptions, + ProjectCodeFilter, + ResponsibleFilter, type TableFilter, TargetDateAfterFilter, TargetDateBeforeFilter @@ -117,21 +124,12 @@ export function BuildOrderTable({ const projectCodeFilters = useProjectCodeFilters(); const ownerFilters = useOwnerFilters(); + const userFilters = useUserFilters(); const tableFilters: TableFilter[] = useMemo(() => { const filters: TableFilter[] = [ - { - name: 'outstanding', - type: 'boolean', - label: t`Outstanding`, - description: t`Show outstanding orders` - }, - { - name: 'status', - label: t`Status`, - description: t`Filter by order status`, - choiceFunction: StatusFilterOptions(ModelType.build) - }, + OutstandingFilter(), + OrderStatusFilter({ model: ModelType.build }), OverdueFilter(), AssignedToMeFilter(), MinDateFilter(), @@ -142,25 +140,15 @@ export function BuildOrderTable({ TargetDateAfterFilter(), CompletedBeforeFilter(), CompletedAfterFilter(), - { - name: 'project_code', - label: t`Project Code`, - description: t`Filter by project code`, - choices: projectCodeFilters.choices - }, + ProjectCodeFilter({ choices: projectCodeFilters.choices }), HasProjectCodeFilter(), { name: 'issued_by', label: t`Issued By`, description: t`Filter by user who issued this order`, - choices: ownerFilters.choices + choices: userFilters.choices }, - { - name: 'assigned_to', - label: t`Responsible`, - description: t`Filter by responsible owner`, - choices: ownerFilters.choices - } + ResponsibleFilter({ choices: ownerFilters.choices }) ]; // If we are filtering on a specific part, we can include the "include variants" filter @@ -174,7 +162,12 @@ export function BuildOrderTable({ } return filters; - }, [partId, projectCodeFilters.choices, ownerFilters.choices]); + }, [ + partId, + projectCodeFilters.choices, + ownerFilters.choices, + userFilters.choices + ]); const user = useUserState(); diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx index 72c763b35d..36c343bad9 100644 --- a/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx +++ b/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx @@ -8,13 +8,18 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms'; -import { useOwnerFilters, useProjectCodeFilters } from '../../hooks/UseFilter'; +import { + useOwnerFilters, + useProjectCodeFilters, + useUserFilters +} from '../../hooks/UseFilter'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { CompletionDateColumn, + CreatedByColumn, CreationDateColumn, DescriptionColumn, LineItemsProgressColumn, @@ -30,12 +35,15 @@ import { CompletedBeforeFilter, CreatedAfterFilter, CreatedBeforeFilter, + CreatedByFilter, HasProjectCodeFilter, MaxDateFilter, MinDateFilter, + OrderStatusFilter, OutstandingFilter, OverdueFilter, - StatusFilterOptions, + ProjectCodeFilter, + ResponsibleFilter, type TableFilter, TargetDateAfterFilter, TargetDateBeforeFilter @@ -57,15 +65,11 @@ export function PurchaseOrderTable({ const projectCodeFilters = useProjectCodeFilters(); const responsibleFilters = useOwnerFilters(); + const createdByFilters = useUserFilters(); const tableFilters: TableFilter[] = useMemo(() => { return [ - { - name: 'status', - label: t`Status`, - description: t`Filter by order status`, - choiceFunction: StatusFilterOptions(ModelType.purchaseorder) - }, + OrderStatusFilter({ model: ModelType.purchaseorder }), OutstandingFilter(), OverdueFilter(), AssignedToMeFilter(), @@ -77,21 +81,16 @@ export function PurchaseOrderTable({ TargetDateAfterFilter(), CompletedBeforeFilter(), CompletedAfterFilter(), - { - name: 'project_code', - label: t`Project Code`, - description: t`Filter by project code`, - choices: projectCodeFilters.choices - }, + ProjectCodeFilter({ choices: projectCodeFilters.choices }), HasProjectCodeFilter(), - { - name: 'assigned_to', - label: t`Responsible`, - description: t`Filter by responsible owner`, - choices: responsibleFilters.choices - } + ResponsibleFilter({ choices: responsibleFilters.choices }), + CreatedByFilter({ choices: createdByFilters.choices }) ]; - }, [projectCodeFilters.choices, responsibleFilters.choices]); + }, [ + projectCodeFilters.choices, + responsibleFilters.choices, + createdByFilters.choices + ]); const tableColumns = useMemo(() => { return [ @@ -120,6 +119,7 @@ export function PurchaseOrderTable({ StatusColumn({ model: ModelType.purchaseorder }), ProjectCodeColumn({}), CreationDateColumn({}), + CreatedByColumn({}), TargetDateColumn({}), CompletionDateColumn({ accessor: 'complete_date' diff --git a/src/frontend/src/tables/sales/ReturnOrderTable.tsx b/src/frontend/src/tables/sales/ReturnOrderTable.tsx index 6a5f6f9694..d1715a3658 100644 --- a/src/frontend/src/tables/sales/ReturnOrderTable.tsx +++ b/src/frontend/src/tables/sales/ReturnOrderTable.tsx @@ -8,13 +8,18 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { useReturnOrderFields } from '../../forms/ReturnOrderForms'; -import { useOwnerFilters, useProjectCodeFilters } from '../../hooks/UseFilter'; +import { + useOwnerFilters, + useProjectCodeFilters, + useUserFilters +} from '../../hooks/UseFilter'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { CompletionDateColumn, + CreatedByColumn, CreationDateColumn, DescriptionColumn, LineItemsProgressColumn, @@ -30,12 +35,15 @@ import { CompletedBeforeFilter, CreatedAfterFilter, CreatedBeforeFilter, + CreatedByFilter, HasProjectCodeFilter, MaxDateFilter, MinDateFilter, + OrderStatusFilter, OutstandingFilter, OverdueFilter, - StatusFilterOptions, + ProjectCodeFilter, + ResponsibleFilter, type TableFilter, TargetDateAfterFilter, TargetDateBeforeFilter @@ -54,15 +62,11 @@ export function ReturnOrderTable({ const projectCodeFilters = useProjectCodeFilters(); const responsibleFilters = useOwnerFilters(); + const createdByFilters = useUserFilters(); const tableFilters: TableFilter[] = useMemo(() => { const filters: TableFilter[] = [ - { - name: 'status', - label: t`Status`, - description: t`Filter by order status`, - choiceFunction: StatusFilterOptions(ModelType.returnorder) - }, + OrderStatusFilter({ model: ModelType.returnorder }), OutstandingFilter(), OverdueFilter(), AssignedToMeFilter(), @@ -74,19 +78,10 @@ export function ReturnOrderTable({ TargetDateAfterFilter(), CompletedBeforeFilter(), CompletedAfterFilter(), - { - name: 'project_code', - label: t`Project Code`, - description: t`Filter by project code`, - choices: projectCodeFilters.choices - }, HasProjectCodeFilter(), - { - name: 'assigned_to', - label: t`Responsible`, - description: t`Filter by responsible owner`, - choices: responsibleFilters.choices - } + ProjectCodeFilter({ choices: projectCodeFilters.choices }), + ResponsibleFilter({ choices: responsibleFilters.choices }), + CreatedByFilter({ choices: createdByFilters.choices }) ]; if (!!partId) { @@ -99,7 +94,12 @@ export function ReturnOrderTable({ } return filters; - }, [partId, projectCodeFilters.choices, responsibleFilters.choices]); + }, [ + partId, + projectCodeFilters.choices, + responsibleFilters.choices, + createdByFilters.choices + ]); const tableColumns = useMemo(() => { return [ @@ -128,6 +128,7 @@ export function ReturnOrderTable({ StatusColumn({ model: ModelType.returnorder }), ProjectCodeColumn({}), CreationDateColumn({}), + CreatedByColumn({}), TargetDateColumn({}), CompletionDateColumn({ accessor: 'complete_date' diff --git a/src/frontend/src/tables/sales/SalesOrderTable.tsx b/src/frontend/src/tables/sales/SalesOrderTable.tsx index 265ec9a214..62adb55f86 100644 --- a/src/frontend/src/tables/sales/SalesOrderTable.tsx +++ b/src/frontend/src/tables/sales/SalesOrderTable.tsx @@ -9,12 +9,17 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { useSalesOrderFields } from '../../forms/SalesOrderForms'; -import { useOwnerFilters, useProjectCodeFilters } from '../../hooks/UseFilter'; +import { + useOwnerFilters, + useProjectCodeFilters, + useUserFilters +} from '../../hooks/UseFilter'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { + CreatedByColumn, CreationDateColumn, DescriptionColumn, LineItemsProgressColumn, @@ -31,12 +36,15 @@ import { CompletedBeforeFilter, CreatedAfterFilter, CreatedBeforeFilter, + CreatedByFilter, HasProjectCodeFilter, MaxDateFilter, MinDateFilter, + OrderStatusFilter, OutstandingFilter, OverdueFilter, - StatusFilterOptions, + ProjectCodeFilter, + ResponsibleFilter, type TableFilter, TargetDateAfterFilter, TargetDateBeforeFilter @@ -55,15 +63,11 @@ export function SalesOrderTable({ const projectCodeFilters = useProjectCodeFilters(); const responsibleFilters = useOwnerFilters(); + const createdByFilters = useUserFilters(); const tableFilters: TableFilter[] = useMemo(() => { const filters: TableFilter[] = [ - { - name: 'status', - label: t`Status`, - description: t`Filter by order status`, - choiceFunction: StatusFilterOptions(ModelType.salesorder) - }, + OrderStatusFilter({ model: ModelType.salesorder }), OutstandingFilter(), OverdueFilter(), AssignedToMeFilter(), @@ -75,19 +79,10 @@ export function SalesOrderTable({ TargetDateAfterFilter(), CompletedBeforeFilter(), CompletedAfterFilter(), - { - name: 'project_code', - label: t`Project Code`, - description: t`Filter by project code`, - choices: projectCodeFilters.choices - }, HasProjectCodeFilter(), - { - name: 'assigned_to', - label: t`Responsible`, - description: t`Filter by responsible owner`, - choices: responsibleFilters.choices - } + ProjectCodeFilter({ choices: projectCodeFilters.choices }), + ResponsibleFilter({ choices: responsibleFilters.choices }), + CreatedByFilter({ choices: createdByFilters.choices }) ]; if (!!partId) { @@ -100,7 +95,12 @@ export function SalesOrderTable({ } return filters; - }, [partId, projectCodeFilters.choices, responsibleFilters.choices]); + }, [ + partId, + projectCodeFilters.choices, + responsibleFilters.choices, + createdByFilters.choices + ]); const salesOrderFields = useSalesOrderFields({}); @@ -165,6 +165,7 @@ export function SalesOrderTable({ StatusColumn({ model: ModelType.salesorder }), ProjectCodeColumn({}), CreationDateColumn({}), + CreatedByColumn({}), TargetDateColumn({}), ShipmentDateColumn({}), ResponsibleColumn({}),