From df36f902fa0036f9e327b3b0644e21452e9637dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 21:05:29 +1000 Subject: [PATCH 01/20] Bump idna from 3.6 to 3.7 in /src/backend (#7110) * Bump idna from 3.6 to 3.7 in /src/backend Bumps [idna](https://github.com/kjd/idna) from 3.6 to 3.7. - [Release notes](https://github.com/kjd/idna/releases) - [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst) - [Commits](https://github.com/kjd/idna/compare/v3.6...v3.7) --- updated-dependencies: - dependency-name: idna dependency-type: indirect ... Signed-off-by: dependabot[bot] * fix req --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Matthias Mair --- src/backend/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index ee568519d3..83499b3ee2 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -664,9 +664,9 @@ icalendar==5.0.12 \ --hash=sha256:73f9be68477722c98320621400943705dcfdbbc6c2b565253f72d3f87e514db8 \ --hash=sha256:d873bb859df9c6d0e597b16d247436e0f83f7ac1b90a06429b8393fe8afeba40 # via django-ical -idna==3.6 \ - --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ - --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f +idna==3.7 \ + --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ + --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 # via requests importlib-metadata==7.0.0 \ --hash=sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7 \ From 37956db5fc6de8795dc3f6b169390407d764f329 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Tue, 23 Apr 2024 13:05:48 +0200 Subject: [PATCH 02/20] Add PUI info banner to CUI (#7097) * add intrestials to CUI * Add intrestial to report settings * add option to docs * rename to banner Fixes #6554 Co-authored-by: Oliver * change name --------- Co-authored-by: Oliver --- docs/docs/start/config.md | 1 + src/backend/InvenTree/config_template.yaml | 1 + .../templates/InvenTree/settings/global.html | 2 ++ .../templates/InvenTree/settings/login.html | 2 ++ .../templates/InvenTree/settings/plugin.html | 2 ++ .../templates/InvenTree/settings/report.html | 2 ++ .../InvenTree/templates/account/login.html | 2 ++ src/backend/InvenTree/templates/pui_banner.html | 17 +++++++++++++++++ 8 files changed, 29 insertions(+) create mode 100644 src/backend/InvenTree/templates/pui_banner.html diff --git a/docs/docs/start/config.md b/docs/docs/start/config.md index 7ce2e96784..56d8f06347 100644 --- a/docs/docs/start/config.md +++ b/docs/docs/start/config.md @@ -324,6 +324,7 @@ The logo and custom messages can be changed/set: | INVENTREE_CUSTOM_SPLASH | customize.splash | Path to custom splash screen in the static files directory | *Not specified* | | INVENTREE_CUSTOMIZE | customize.login_message | Custom message for login page | *Not specified* | | INVENTREE_CUSTOMIZE | customize.navbar_message | Custom message for navbar | *Not specified* | +| INVENTREE_CUSTOMIZE | customize.hide_pui_banner | Disable PUI banner | False | If you want to remove the InvenTree branding as far as possible from your end-user also check the [global server settings](../settings/global.md#server-settings). diff --git a/src/backend/InvenTree/config_template.yaml b/src/backend/InvenTree/config_template.yaml index e429aa2bea..8e4efeb54c 100644 --- a/src/backend/InvenTree/config_template.yaml +++ b/src/backend/InvenTree/config_template.yaml @@ -300,6 +300,7 @@ remote_login_header: HTTP_REMOTE_USER # hide_password_reset: true # logo: img/custom_logo.png # splash: img/custom_splash.jpg +# hide_pui_banner: true # Set enabled frontends # Use the environment variable INVENTREE_CLASSIC_FRONTEND diff --git a/src/backend/InvenTree/templates/InvenTree/settings/global.html b/src/backend/InvenTree/templates/InvenTree/settings/global.html index d532bc5727..983700591d 100644 --- a/src/backend/InvenTree/templates/InvenTree/settings/global.html +++ b/src/backend/InvenTree/templates/InvenTree/settings/global.html @@ -10,6 +10,8 @@ {% block content %} +{% include "pui_banner.html" with mode='admin' %} + {% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" icon="fa-building" %} diff --git a/src/backend/InvenTree/templates/InvenTree/settings/login.html b/src/backend/InvenTree/templates/InvenTree/settings/login.html index 2cdd7784f1..d2b557978c 100644 --- a/src/backend/InvenTree/templates/InvenTree/settings/login.html +++ b/src/backend/InvenTree/templates/InvenTree/settings/login.html @@ -16,6 +16,8 @@ {% endif %} +{% include "pui_banner.html" with mode='admin' %} +
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_PWD_FORGOT" icon="fa-user-lock" %} diff --git a/src/backend/InvenTree/templates/InvenTree/settings/plugin.html b/src/backend/InvenTree/templates/InvenTree/settings/plugin.html index ea1cc4341a..f97c98795e 100644 --- a/src/backend/InvenTree/templates/InvenTree/settings/plugin.html +++ b/src/backend/InvenTree/templates/InvenTree/settings/plugin.html @@ -15,6 +15,8 @@ {% trans "Changing the settings below require you to immediately restart the server. Do not change this while under active usage." %} +{% include "pui_banner.html" with mode='admin' %} +
diff --git a/src/backend/InvenTree/templates/InvenTree/settings/report.html b/src/backend/InvenTree/templates/InvenTree/settings/report.html index 683625bd74..6622befc36 100644 --- a/src/backend/InvenTree/templates/InvenTree/settings/report.html +++ b/src/backend/InvenTree/templates/InvenTree/settings/report.html @@ -10,6 +10,8 @@ {% block content %} +{% include "pui_banner.html" with mode='admin' %} +
{% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE" icon="fa-file-pdf" %} diff --git a/src/backend/InvenTree/templates/account/login.html b/src/backend/InvenTree/templates/account/login.html index b31b183b31..71b570a4d8 100644 --- a/src/backend/InvenTree/templates/account/login.html +++ b/src/backend/InvenTree/templates/account/login.html @@ -13,6 +13,8 @@ {% inventree_customize 'login_message' as login_message %} {% mail_configured as mail_conf %} +{% include "pui_banner.html" with mode='user' %} +

{% trans "Sign In" %}

{% include "spacer.html" %} diff --git a/src/backend/InvenTree/templates/pui_banner.html b/src/backend/InvenTree/templates/pui_banner.html new file mode 100644 index 0000000000..d6eb0746f3 --- /dev/null +++ b/src/backend/InvenTree/templates/pui_banner.html @@ -0,0 +1,17 @@ +{% load inventree_extras %} +{% load i18n %} + +{% inventree_customize 'hide_pui_banner' as hidden %} + +{% if not hidden %} +
+ {% if mode == 'admin' %} + {% trans "Platform UI - the new UI for InvenTree - provides more modern administration options." %} + {% endif %} + {% if mode == 'user' %} + {% trans "Platform UI - the new UI for InvenTree - is ready to be tested." %} + {% endif %} +
+ {% trans "Try it out now" %} {% trans "here" %}. +
+{% endif %} From 5f54aef79a3d58539ebff65510db2e35783f67f6 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Wed, 24 Apr 2024 01:59:03 +0200 Subject: [PATCH 03/20] [PUI] Test modals (#7113) * add spotlight * [PUI] Quick commands pallet Fixes #5888 * add testing for new commands * add text input testing * only test backend if code changed * add trans files * fix testing text * always push coverage * add nav state to manage navigation state * add navigation action and test * make test faster * fix typo * use texts instead * fix tests for linux * use var to determine action key * Revert "use texts instead" This reverts commit 77711895569a42426a422d679496661c0ee9d25b. * add wait for input * split out keyboard based tests * split ou test * add upload * revert assert change * adjust reporting settings * ignore error code * fix reporter config * add full info suit (+tests) * make tests more accurate * license modal fixes * unify icons * add custom actions registering with removal on page refresh * only upload report data if the tests failed * Revert "add trans files" This reverts commit 28d96e058febba96979afd3054e04cd207bffba5. * adjust url that iw waited for * try an await and body locator for keypresses * test registering addition actions * extend testing for actions * add doclink and test * merge tests * add modals test * use quick login * reduce diff * fix test * open packages too * expand to frontend * rnsure keys are unique * ensure no errors in console * add QR code modal too --- .../src/components/buttons/ScanButton.tsx | 1 + .../src/components/modals/LicenseModal.tsx | 5 +- src/frontend/tests/modals.spec.ts | 74 +++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/frontend/tests/modals.spec.ts diff --git a/src/frontend/src/components/buttons/ScanButton.tsx b/src/frontend/src/components/buttons/ScanButton.tsx index c3fe6d14b5..83dd990aaf 100644 --- a/src/frontend/src/components/buttons/ScanButton.tsx +++ b/src/frontend/src/components/buttons/ScanButton.tsx @@ -16,6 +16,7 @@ export function ScanButton() { innerProps: {} }) } + title={t`Open QR code scanner`} > diff --git a/src/frontend/src/components/modals/LicenseModal.tsx b/src/frontend/src/components/modals/LicenseModal.tsx index a790e765b5..c321d11532 100644 --- a/src/frontend/src/components/modals/LicenseModal.tsx +++ b/src/frontend/src/components/modals/LicenseModal.tsx @@ -23,7 +23,10 @@ export function LicenceView(entries: Readonly) { {entries?.length > 0 ? ( {entries?.map((entry: any, index: number) => ( - + {entry.name} diff --git a/src/frontend/tests/modals.spec.ts b/src/frontend/tests/modals.spec.ts new file mode 100644 index 0000000000..4d628eaf1f --- /dev/null +++ b/src/frontend/tests/modals.spec.ts @@ -0,0 +1,74 @@ +import { test } from './baseFixtures.js'; +import { doQuickLogin } from './login.js'; + +test('PUI - Modals as admin', async ({ page }) => { + await doQuickLogin(page, 'admin', 'inventree'); + + // Fail on console error + await page.on('console', (msg) => { + if (msg.type() === 'error') test.fail(); + }); + + // use server info + await page.getByRole('button', { name: 'Open spotlight' }).click(); + await page + .getByRole('button', { + name: 'Server Information About this Inventree instance' + }) + .click(); + await page.getByRole('cell', { name: 'Instance Name' }).waitFor(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + + await page.waitForURL('**/platform/home'); + + // use license info + await page.getByRole('button', { name: 'Open spotlight' }).click(); + await page + .getByRole('button', { + name: 'License Information Licenses for dependencies of the service' + }) + .click(); + await page.getByText('License Information').first().waitFor(); + await page.getByRole('tab', { name: 'backend Packages' }).click(); + await page.getByRole('button', { name: 'Babel BSD License' }).click(); + await page.getByText('Copyright (c) 2013-2023 by').waitFor(); + + await page.getByRole('tab', { name: 'frontend Packages' }).click(); + await page.getByRole('button', { name: '@sentry/utils MIT' }).click(); + await page + .getByLabel('@sentry/utilsMIT7.109.0') + .getByText('Copyright (c) 2019 Sentry (') + .waitFor(); + + await page + .getByLabel('License Information') + .getByRole('button') + .first() + .click(); + + // use about + await page.getByRole('button', { name: 'Open spotlight' }).click(); + await page + .getByRole('button', { name: 'About InvenTree About the InvenTree org' }) + .click(); + await page.getByRole('cell', { name: 'InvenTree Version' }).click(); + + await page.goto('./platform/'); + + // qr code modal + await page.getByRole('button', { name: 'Open QR code scanner' }).click(); + await page + .locator('div') + .filter({ hasText: /^Scan QR code$/ }) + .getByRole('button') + .click(); + await page.getByRole('button', { name: 'Open QR code scanner' }).click(); + await page.getByRole('button', { name: 'Close modal' }).click(); + await page.getByRole('button', { name: 'Open QR code scanner' }).click(); + await page.waitForTimeout(500); + await page + .locator('div') + .filter({ hasText: /^Scan QR code$/ }) + .getByRole('button') + .click(); +}); From d30ab932ca418e0dae0e8cdf1aed231a78ac22e6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 25 Apr 2024 10:11:44 +1000 Subject: [PATCH 04/20] [PUI] Implement "build outputs" table (#7115) * Update build line allocation table - Allow display of "tracked" items in main allocation table * Add resolveItem function for finding nested items * Update BuildLineTable * Allow BuildLineList to be ordered by 'trackable' field * Bump API version * Building out columns * Table tweaks * Fetch list of required test templates * Tweaks * Add placeholders for table actions * Add typing * Add placeholder buttons * Update columns * Add button to duplicate build order * Add placeholder cancel action * Refactoring * Edit company from table * Change "create" to "add" * Change more from Create to Add --- src/frontend/src/components/forms/ApiForm.tsx | 15 + .../src/components/items/ActionDropdown.tsx | 20 ++ src/frontend/src/forms/StockForms.tsx | 2 - src/frontend/src/functions/icons.tsx | 10 +- src/frontend/src/hooks/UseTable.tsx | 9 +- src/frontend/src/pages/build/BuildDetail.tsx | 35 ++- .../pages/company/ManufacturerPartDetail.tsx | 20 +- .../src/pages/company/SupplierPartDetail.tsx | 12 +- src/frontend/src/pages/part/PartDetail.tsx | 10 +- .../pages/purchasing/PurchaseOrderDetail.tsx | 33 +- .../src/pages/sales/ReturnOrderDetail.tsx | 32 +- .../src/pages/sales/SalesOrderDetail.tsx | 37 ++- src/frontend/src/pages/stock/StockDetail.tsx | 28 +- src/frontend/src/tables/ColumnRenderers.tsx | 31 +- src/frontend/src/tables/TableHoverCard.tsx | 2 +- src/frontend/src/tables/bom/BomTable.tsx | 2 +- .../src/tables/build/BuildOrderTable.tsx | 10 +- .../src/tables/build/BuildOutputTable.tsx | 284 ++++++++++++++++++ .../src/tables/company/CompanyTable.tsx | 49 ++- .../src/tables/machine/MachineListTable.tsx | 2 +- src/frontend/src/tables/part/PartTable.tsx | 7 +- .../purchasing/ManufacturerPartTable.tsx | 2 +- .../tables/purchasing/PurchaseOrderTable.tsx | 13 +- .../src/tables/sales/ReturnOrderTable.tsx | 15 +- .../src/tables/sales/SalesOrderTable.tsx | 13 +- .../src/tables/settings/TemplateTable.tsx | 2 +- .../src/tables/stock/StockItemTable.tsx | 33 +- 27 files changed, 581 insertions(+), 147 deletions(-) create mode 100644 src/frontend/src/tables/build/BuildOutputTable.tsx diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 315121efef..6b49748fed 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -19,9 +19,11 @@ import { SubmitHandler, useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom'; import { api, queryClient } from '../../App'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { ModelType } from '../../enums/ModelType'; import { NestedDict, constructField, @@ -30,6 +32,7 @@ import { mapFields } from '../../functions/forms'; import { invalidResponse } from '../../functions/notifications'; +import { getDetailUrl } from '../../functions/urls'; import { PathParams } from '../../states/ApiState'; import { ApiFormField, @@ -59,6 +62,8 @@ export interface ApiFormAction { * @param successMessage : Optional message to display on successful form submission * @param onFormSuccess : A callback function to call when the form is submitted successfully. * @param onFormError : A callback function to call when the form is submitted with errors. + * @param modelType : Define a model type for this form + * @param follow : Boolean, follow the result of the form (if possible) */ export interface ApiFormProps { url: ApiEndpoints | string; @@ -79,6 +84,8 @@ export interface ApiFormProps { successMessage?: string; onFormSuccess?: (data: any) => void; onFormError?: () => void; + modelType?: ModelType; + follow?: boolean; actions?: ApiFormAction[]; timeout?: number; } @@ -183,6 +190,8 @@ export function ApiForm({ props: ApiFormProps; optionsLoading: boolean; }) { + const navigate = useNavigate(); + const fields: ApiFormFieldSet = useMemo(() => { return props.fields ?? {}; }, [props.fields]); @@ -384,6 +393,12 @@ export function ApiForm({ props.onFormSuccess(response.data); } + if (props.follow) { + if (props.modelType && response.data?.pk) { + navigate(getDetailUrl(props.modelType, response.data?.pk)); + } + } + // Optionally show a success message if (props.successMessage) { notifications.hide('form-success'); diff --git a/src/frontend/src/components/items/ActionDropdown.tsx b/src/frontend/src/components/items/ActionDropdown.tsx index e3c2f951f1..a677bd6ee5 100644 --- a/src/frontend/src/components/items/ActionDropdown.tsx +++ b/src/frontend/src/components/items/ActionDropdown.tsx @@ -14,8 +14,10 @@ import { IconTrash, IconUnlink } from '@tabler/icons-react'; +import { color } from '@uiw/react-codemirror'; import { ReactNode, useMemo } from 'react'; +import { InvenTreeIcon } from '../../functions/icons'; import { notYetImplemented } from '../../functions/notifications'; export type ActionDropdownItem = { @@ -203,6 +205,24 @@ export function DeleteItemAction({ }; } +export function CancelItemAction({ + hidden = false, + tooltip, + onClick +}: { + hidden?: boolean; + tooltip?: string; + onClick?: () => void; +}): ActionDropdownItem { + return { + icon: , + name: t`Cancel`, + tooltip: tooltip ?? t`Cancel`, + onClick: onClick, + hidden: hidden + }; +} + // Common action button for duplicating an item export function DuplicateItemAction({ hidden = false, diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx index fc023688af..ccc7276ab5 100644 --- a/src/frontend/src/forms/StockForms.tsx +++ b/src/frontend/src/forms/StockForms.tsx @@ -268,8 +268,6 @@ function StockOperationsRow({ }) { const item = input.item; - console.log('rec', record); - const [value, setValue] = useState( add ? 0 : item.quantity ?? 0 ); diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx index b362d08ce5..486cb43343 100644 --- a/src/frontend/src/functions/icons.tsx +++ b/src/frontend/src/functions/icons.tsx @@ -12,8 +12,10 @@ import { IconCalendarStats, IconCategory, IconCheck, + IconCircleCheck, IconCircleMinus, IconCirclePlus, + IconCircleX, IconClipboardList, IconClipboardText, IconCopy, @@ -59,6 +61,7 @@ import { IconTool, IconTools, IconTransfer, + IconTransitionRight, IconTrash, IconTruck, IconTruckDelivery, @@ -130,6 +133,10 @@ const icons = { delete: IconTrash, packaging: IconPackage, packages: IconPackages, + install: IconTransitionRight, + plus: IconCirclePlus, + minus: IconCircleMinus, + cancel: IconCircleX, // Part Icons active: IconCheck, @@ -186,7 +193,8 @@ const icons = { batch_code: IconClipboardText, destination: IconFlag, repeat_destination: IconFlagShare, - unlink: IconUnlink + unlink: IconUnlink, + success: IconCircleCheck }; export type InvenTreeIconType = keyof typeof icons; diff --git a/src/frontend/src/hooks/UseTable.tsx b/src/frontend/src/hooks/UseTable.tsx index 010085861b..19ed88aa56 100644 --- a/src/frontend/src/hooks/UseTable.tsx +++ b/src/frontend/src/hooks/UseTable.tsx @@ -1,5 +1,5 @@ import { randomId, useLocalStorage } from '@mantine/hooks'; -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { TableFilter } from '../tables/Filter'; @@ -22,6 +22,7 @@ export type TableState = { expandedRecords: any[]; setExpandedRecords: (records: any[]) => void; selectedRecords: any[]; + hasSelectedRecords: boolean; setSelectedRecords: (records: any[]) => void; clearSelectedRecords: () => void; hiddenColumns: string[]; @@ -78,6 +79,11 @@ export function useTable(tableName: string): TableState { setSelectedRecords([]); }, []); + const hasSelectedRecords = useMemo( + () => selectedRecords.length > 0, + [selectedRecords] + ); + // Total record count const [recordCount, setRecordCount] = useState(0); @@ -126,6 +132,7 @@ export function useTable(tableName: string): TableState { selectedRecords, setSelectedRecords, clearSelectedRecords, + hasSelectedRecords, hiddenColumns, setHiddenColumns, searchTerm, diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index 1f6bb03f0d..0644ad90aa 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -22,6 +22,7 @@ import { DetailsImage } from '../../components/details/DetailsImage'; import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { ActionDropdown, + CancelItemAction, DuplicateItemAction, EditItemAction, LinkBarcodeAction, @@ -36,12 +37,16 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { useBuildOrderFields } from '../../forms/BuildForms'; -import { useEditApiFormModal } from '../../hooks/UseForm'; +import { + useCreateApiFormModal, + useEditApiFormModal +} from '../../hooks/UseForm'; import { useInstance } from '../../hooks/UseInstance'; import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import BuildLineTable from '../../tables/build/BuildLineTable'; import { BuildOrderTable } from '../../tables/build/BuildOrderTable'; +import BuildOutputTable from '../../tables/build/BuildOutputTable'; import { AttachmentTable } from '../../tables/general/AttachmentTable'; import { StockItemTable } from '../../tables/stock/StockItemTable'; @@ -213,7 +218,12 @@ export default function BuildDetail() { { name: 'incomplete-outputs', label: t`Incomplete Outputs`, - icon: + icon: , + content: build.pk ? ( + + ) : ( + + ) // TODO: Hide if build is complete }, { @@ -290,6 +300,18 @@ export default function BuildDetail() { } }); + const duplicateBuild = useCreateApiFormModal({ + url: ApiEndpoints.build_order_list, + title: t`Add Build Order`, + fields: buildOrderFields, + initialData: { + ...build, + reference: undefined + }, + follow: true, + modelType: ModelType.build + }); + const buildActions = useMemo(() => { // TODO: Disable certain actions based on user permissions return [ @@ -328,7 +350,13 @@ export default function BuildDetail() { onClick: () => editBuild.open(), hidden: !user.hasChangeRole(UserRoles.build) }), - DuplicateItemAction({}) + CancelItemAction({ + tooltip: t`Cancel order` + }), + DuplicateItemAction({ + onClick: () => duplicateBuild.open(), + hidden: !user.hasAddRole(UserRoles.build) + }) ]} /> ]; @@ -349,6 +377,7 @@ export default function BuildDetail() { return ( <> {editBuild.modal} + {duplicateBuild.modal} { return [ } actions={[ DuplicateItemAction({ - hidden: !user.hasAddRole(UserRoles.purchase_order) + hidden: !user.hasAddRole(UserRoles.purchase_order), + onClick: () => duplicateManufacturerPart.open() }), EditItemAction({ hidden: !user.hasChangeRole(UserRoles.purchase_order), @@ -227,6 +242,7 @@ export default function ManufacturerPartDetail() { return ( <> {editManufacturerPart.modal} + {duplicateManufacturerPart.modal} { - if (response.pk) { - navigate(getDetailUrl(ModelType.supplierpart, response.pk)); - } - } + follow: true, + modelType: ModelType.supplierpart }); const breadcrumbs = useMemo(() => { diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 09a1dfa36b..859048e5e5 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -24,7 +24,7 @@ import { } from '@tabler/icons-react'; import { useSuspenseQuery } from '@tanstack/react-query'; import { ReactNode, useMemo, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { api } from '../../App'; import { DetailsField, DetailsTable } from '../../components/details/Details'; @@ -86,7 +86,6 @@ export default function PartDetail() { const { id } = useParams(); const user = useUserState(); - const navigate = useNavigate(); const [treeOpen, setTreeOpen] = useState(false); @@ -680,11 +679,8 @@ export default function PartDetail() { initialData: { ...part }, - onFormSuccess: (response: any) => { - if (response.pk) { - navigate(getDetailUrl(ModelType.part, response.pk)); - } - } + follow: true, + modelType: ModelType.part }); const stockActionProps: StockOperationProps = useMemo(() => { diff --git a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx index a982cf3b23..b0485ed1ec 100644 --- a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx +++ b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx @@ -9,7 +9,7 @@ import { IconPaperclip } from '@tabler/icons-react'; import { ReactNode, useMemo } from 'react'; -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsImage } from '../../components/details/DetailsImage'; @@ -17,7 +17,8 @@ import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { ActionDropdown, BarcodeActionDropdown, - DeleteItemAction, + CancelItemAction, + DuplicateItemAction, EditItemAction, LinkBarcodeAction, UnlinkBarcodeAction, @@ -31,7 +32,11 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms'; -import { useEditApiFormModal } from '../../hooks/UseForm'; +import { getDetailUrl } from '../../functions/urls'; +import { + useCreateApiFormModal, + useEditApiFormModal +} from '../../hooks/UseForm'; import { useInstance } from '../../hooks/UseInstance'; import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; @@ -46,6 +51,7 @@ export default function PurchaseOrderDetail() { const { id } = useParams(); const user = useUserState(); + const navigate = useNavigate(); const { instance: order, @@ -72,6 +78,18 @@ export default function PurchaseOrderDetail() { } }); + const duplicatePurchaseOrder = useCreateApiFormModal({ + url: ApiEndpoints.purchase_order_list, + title: t`Add Purchase Order`, + fields: purchaseOrderFields, + initialData: { + ...order, + reference: undefined + }, + follow: true, + modelType: ModelType.purchaseorder + }); + const detailsPanel = useMemo(() => { if (instanceQuery.isFetching) { return ; @@ -299,8 +317,12 @@ export default function PurchaseOrderDetail() { editPurchaseOrder.open(); } }), - DeleteItemAction({ - hidden: !user.hasDeleteRole(UserRoles.purchase_order) + CancelItemAction({ + tooltip: t`Cancel order` + }), + DuplicateItemAction({ + hidden: !user.hasAddRole(UserRoles.purchase_order), + onClick: () => duplicatePurchaseOrder.open() }) ]} /> @@ -322,6 +344,7 @@ export default function PurchaseOrderDetail() { return ( <> {editPurchaseOrder.modal} + {duplicatePurchaseOrder.modal} { return [ duplicateReturnOrder.open() }) ]} /> @@ -285,6 +306,7 @@ export default function ReturnOrderDetail() { return ( <> {editReturnOrder.modal} + {duplicateReturnOrder.modal} { return [ { @@ -281,13 +300,14 @@ export default function SalesOrderDetail() { actions={[ EditItemAction({ hidden: !user.hasChangeRole(UserRoles.sales_order), - onClick: () => { - editSalesOrder.open(); - } + onClick: () => editSalesOrder.open() }), - DeleteItemAction({ - hidden: !user.hasDeleteRole(UserRoles.sales_order) - // TODO: Delete? + CancelItemAction({ + tooltip: t`Cancel order` + }), + DuplicateItemAction({ + hidden: !user.hasAddRole(UserRoles.sales_order), + onClick: () => duplicateSalesOrder.open() }) ]} /> @@ -309,6 +329,7 @@ export default function SalesOrderDetail() { return ( <> {editSalesOrder.modal} + {duplicateSalesOrder.modal} { - if (response.pk) { - navigate(getDetailUrl(ModelType.stockitem, response.pk)); - } - } + follow: true, + modelType: ModelType.stockitem }); const stockActionProps: StockOperationProps = useMemo(() => { @@ -479,6 +464,11 @@ export default function StockDetail() { return instanceQuery.isLoading ? [] : [ + , { + let location = resolveItem(record, accessor); + + if (!location) { + return {t`No location set`}; + } + + return {location.name}; + } + }; +} + export function BooleanColumn({ accessor, title, diff --git a/src/frontend/src/tables/TableHoverCard.tsx b/src/frontend/src/tables/TableHoverCard.tsx index 8486845638..93893ab834 100644 --- a/src/frontend/src/tables/TableHoverCard.tsx +++ b/src/frontend/src/tables/TableHoverCard.tsx @@ -27,7 +27,7 @@ export function TableHoverCard({ } return ( - + {value} diff --git a/src/frontend/src/tables/bom/BomTable.tsx b/src/frontend/src/tables/bom/BomTable.tsx index 06e7603347..41b358318a 100644 --- a/src/frontend/src/tables/bom/BomTable.tsx +++ b/src/frontend/src/tables/bom/BomTable.tsx @@ -298,7 +298,7 @@ export function BomTable({ const newBomItem = useCreateApiFormModal({ url: ApiEndpoints.bom_list, - title: t`Create BOM Item`, + title: t`Add BOM Item`, fields: bomItemFields(), initialData: { part: partId diff --git a/src/frontend/src/tables/build/BuildOrderTable.tsx b/src/frontend/src/tables/build/BuildOrderTable.tsx index 19f9438380..cfc0a61346 100644 --- a/src/frontend/src/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/tables/build/BuildOrderTable.tsx @@ -1,6 +1,5 @@ import { t } from '@lingui/macro'; import { useMemo } from 'react'; -import { useNavigate } from 'react-router-dom'; import { AddItemButton } from '../../components/buttons/AddItemButton'; import { PartHoverCard } from '../../components/images/Thumbnail'; @@ -11,7 +10,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { useBuildOrderFields } from '../../forms/BuildForms'; -import { getDetailUrl } from '../../functions/urls'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; @@ -130,7 +128,6 @@ export function BuildOrderTable({ ]; }, []); - const navigate = useNavigate(); const user = useUserState(); const table = useTable('buildorder'); @@ -146,11 +143,8 @@ export function BuildOrderTable({ sales_order: salesOrderId, parent: parentBuildId }, - onFormSuccess: (data: any) => { - if (data.pk) { - navigate(getDetailUrl(ModelType.build, data.pk)); - } - } + follow: true, + modelType: ModelType.build }); const tableActions = useMemo(() => { diff --git a/src/frontend/src/tables/build/BuildOutputTable.tsx b/src/frontend/src/tables/build/BuildOutputTable.tsx new file mode 100644 index 0000000000..657151909d --- /dev/null +++ b/src/frontend/src/tables/build/BuildOutputTable.tsx @@ -0,0 +1,284 @@ +import { t } from '@lingui/macro'; +import { Group, Text } from '@mantine/core'; +import { + IconCircleCheck, + IconCircleX, + IconExclamationCircle +} from '@tabler/icons-react'; +import { useQuery } from '@tanstack/react-query'; +import { useCallback, useMemo } from 'react'; + +import { api } from '../../App'; +import { ActionButton } from '../../components/buttons/ActionButton'; +import { AddItemButton } from '../../components/buttons/AddItemButton'; +import { ProgressBar } from '../../components/items/ProgressBar'; +import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { ModelType } from '../../enums/ModelType'; +import { UserRoles } from '../../enums/Roles'; +import { InvenTreeIcon } from '../../functions/icons'; +import { useTable } from '../../hooks/UseTable'; +import { apiUrl } from '../../states/ApiState'; +import { useUserState } from '../../states/UserState'; +import { TableColumn } from '../Column'; +import { LocationColumn, PartColumn } from '../ColumnRenderers'; +import { InvenTreeTable } from '../InvenTreeTable'; +import { RowAction } from '../RowActions'; +import { TableHoverCard } from '../TableHoverCard'; + +type TestResultOverview = { + name: string; + result: boolean; +}; + +export default function BuildOutputTable({ + buildId, + partId +}: { + buildId: number; + partId: number; +}) { + const user = useUserState(); + const table = useTable('build-outputs'); + + // Fetch the test templates associated with the partId + const { data: testTemplates } = useQuery({ + queryKey: ['buildoutputtests', partId], + queryFn: async () => { + if (!partId) { + return []; + } + + return api + .get(apiUrl(ApiEndpoints.part_test_template_list), { + params: { + part: partId, + include_inherited: true, + enabled: true, + required: true + } + }) + .then((response) => response.data) + .catch(() => []); + } + }); + + const hasRequiredTests: boolean = useMemo(() => { + return (testTemplates?.length ?? 0) > 0; + }, [partId, testTemplates]); + + // Format table records + const formatRecords = useCallback( + (records: any[]): any[] => { + records?.forEach((record: any, index: number) => { + let results: TestResultOverview[] = []; + let passCount: number = 0; + + // Iterate through each + testTemplates?.forEach((template: any) => { + // Find the "newest" result for this template in the returned data + let result = record.tests + ?.filter((test: any) => test.template == template.pk) + .sort((a: any, b: any) => { + return a.pk < b.pk ? 1 : -1; + }) + .shift(); + + if (template?.required && result?.result) { + passCount += 1; + } + + results.push({ + name: template.test_name, + result: result?.result ?? false + }); + }); + + records[index].passCount = passCount; + records[index].results = results; + }); + + return records; + }, + [partId, testTemplates] + ); + + const tableActions = useMemo(() => { + // TODO: Button to create new build output + // TODO: Button to complete output(s) + // TODO: Button to cancel output(s) + // TODO: Button to scrap output(s) + return [ +
- + + + + +
@@ -642,23 +646,32 @@ export default function PartDetail() { label={t`In Stock` + `: ${part.in_stock}`} color={part.in_stock >= part.minimum_stock ? 'green' : 'orange'} visible={part.in_stock > 0} + key="in_stock" />, , 0} + key="on_order" />, 0} + key="in_production" />, - + ]; }, [part, instanceQuery]); @@ -706,6 +719,7 @@ export default function PartDetail() { hidden: !part?.barcode_hash || !user.hasChangeRole(UserRoles.part) }) ]} + key="action_dropdown" />, )} - + diff --git a/src/frontend/src/pages/sales/SalesOrderDetail.tsx b/src/frontend/src/pages/sales/SalesOrderDetail.tsx index 4fea24044e..ca96e3f450 100644 --- a/src/frontend/src/pages/sales/SalesOrderDetail.tsx +++ b/src/frontend/src/pages/sales/SalesOrderDetail.tsx @@ -322,6 +322,7 @@ export default function SalesOrderDetail() { status={order.status} type={ModelType.salesorder} options={{ size: 'lg' }} + key={order.pk} /> ]; }, [order, instanceQuery]); diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index b74e2b555d..7fe1649f13 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -473,21 +473,25 @@ export default function StockDetail() { color="blue" label={t`Serial Number` + `: ${stockitem.serial}`} visible={!!stockitem.serial} + key="serial" />, , , ]; }, [stockitem, instanceQuery]); diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index 2b284efcf4..de6fa14838 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -26,7 +26,7 @@ export const useServerApiState = create()( set({ server: response.data }); }) .catch(() => { - console.error('Error fetching server info'); + console.error('ERR: Error fetching server info'); }); // Fetch login/SSO behaviour @@ -38,7 +38,7 @@ export const useServerApiState = create()( set({ auth_settings: response.data }); }) .catch(() => { - console.error('Error fetching SSO information'); + console.error('ERR: Error fetching SSO information'); }); }, status: undefined diff --git a/src/frontend/src/states/SettingsState.tsx b/src/frontend/src/states/SettingsState.tsx index 85fdba8ab3..b5d97592f2 100644 --- a/src/frontend/src/states/SettingsState.tsx +++ b/src/frontend/src/states/SettingsState.tsx @@ -42,7 +42,7 @@ export const useGlobalSettingsState = create( }); }) .catch((_error) => { - console.error('Error fetching global settings'); + console.error('ERR: Error fetching global settings'); }); }, getSetting: (key: string, default_value?: string) => { @@ -76,7 +76,7 @@ export const useUserSettingsState = create((set, get) => ({ }); }) .catch((_error) => { - console.error('Error fetching user settings'); + console.error('ERR: Error fetching user settings'); }); }, getSetting: (key: string, default_value?: string) => { diff --git a/src/frontend/src/states/StatusState.tsx b/src/frontend/src/states/StatusState.tsx index 57e845f33a..f0c27d3ccb 100644 --- a/src/frontend/src/states/StatusState.tsx +++ b/src/frontend/src/states/StatusState.tsx @@ -39,7 +39,7 @@ export const useGlobalStatusState = create()( set({ status: newStatusLookup }); }) .catch(() => { - console.error('Error fetching global status information'); + console.error('ERR: Error fetching global status information'); }); } }), diff --git a/src/frontend/src/states/UserState.tsx b/src/frontend/src/states/UserState.tsx index 35d8a82979..a203de13ae 100644 --- a/src/frontend/src/states/UserState.tsx +++ b/src/frontend/src/states/UserState.tsx @@ -57,7 +57,7 @@ export const useUserState = create((set, get) => ({ set({ user: user }); }) .catch((error) => { - console.error('Error fetching user data'); + console.error('ERR: Error fetching user data'); }); // Fetch role data @@ -75,7 +75,7 @@ export const useUserState = create((set, get) => ({ } }) .catch((_error) => { - console.error('Error fetching user roles'); + console.error('ERR: Error fetching user roles'); }); }, checkUserRole: (role: UserRoles, permission: UserPermissions) => { diff --git a/src/frontend/src/tables/build/BuildOrderTable.tsx b/src/frontend/src/tables/build/BuildOrderTable.tsx index cfc0a61346..405909cfca 100644 --- a/src/frontend/src/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/tables/build/BuildOrderTable.tsx @@ -153,6 +153,7 @@ export function BuildOrderTable({ hidden={!user.hasAddRole(UserRoles.build)} tooltip={t`Add Build Order`} onClick={() => newBuild.open()} + key="add-build-order" /> ]; }, [user]); diff --git a/src/frontend/src/tables/settings/ErrorTable.tsx b/src/frontend/src/tables/settings/ErrorTable.tsx index 0141807ec8..4fec4e3c34 100644 --- a/src/frontend/src/tables/settings/ErrorTable.tsx +++ b/src/frontend/src/tables/settings/ErrorTable.tsx @@ -68,7 +68,11 @@ export default function ErrorReportTable() { onClose={close} > {error.split('\n').map((line: string) => { - return {line}; + return ( + + {line} + + ); })} { + const messages = []; + page.on('console', (msg) => { + const url = msg.location().url; + if ( + msg.type() === 'error' && + !msg.text().startsWith('ERR: ') && + url != 'http://localhost:8000/api/barcode/' && + url != 'http://localhost:8000/api/news/?search=&offset=0&limit=25' && + url != 'https://docs.inventree.org/en/versions.json' && + !url.startsWith('chrome://') + ) + messages.push(msg); + }); + await use(page); + expect(messages).toEqual([]); } }); diff --git a/src/frontend/tests/modals.spec.ts b/src/frontend/tests/modals.spec.ts index 4d628eaf1f..a6cfc0c020 100644 --- a/src/frontend/tests/modals.spec.ts +++ b/src/frontend/tests/modals.spec.ts @@ -4,11 +4,6 @@ import { doQuickLogin } from './login.js'; test('PUI - Modals as admin', async ({ page }) => { await doQuickLogin(page, 'admin', 'inventree'); - // Fail on console error - await page.on('console', (msg) => { - if (msg.type() === 'error') test.fail(); - }); - // use server info await page.getByRole('button', { name: 'Open spotlight' }).click(); await page diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts index 4d4aecf4b7..f0e21f108e 100644 --- a/src/frontend/tests/pages/pui_part.spec.ts +++ b/src/frontend/tests/pages/pui_part.spec.ts @@ -1,5 +1,4 @@ -import { test } from '@playwright/test'; - +import { test } from '../baseFixtures'; import { baseUrl } from '../defaults'; import { doQuickLogin } from '../login'; From d2827df3b8e02e3c4b642f272f8373eed804239c Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Sat, 27 Apr 2024 02:51:42 +0200 Subject: [PATCH 08/20] fix ci filters (#7121) --- .github/workflows/docker.yaml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 7c5b9d9384..9c0b4df2a0 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -46,12 +46,9 @@ jobs: filters: | docker: - .github/workflows/docker.yaml - - docker/** - - docker-compose.yml - - docker.dev.env - - Dockerfile - - InvenTree/settings.py - - requirements.txt + - contrib/container/** + - src/backend/InvenTree/InvenTree/settings.py + - src/backend/requirements.txt - tasks.py # Build the docker image From b711291bebc2723c2481aa560d33f12c28945da5 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 29 Apr 2024 00:17:54 +0200 Subject: [PATCH 09/20] CI: Add pre-commit hook to use modern testing asserts (#7126) * Add test to ensure modern testing syntax is used * auto-fixes for testing formats * remove usage of depreceated assertDictContainsSubset --- .pre-commit-config.yaml | 4 + src/backend/InvenTree/InvenTree/test_api.py | 2 +- src/backend/InvenTree/InvenTree/tests.py | 12 +-- src/backend/InvenTree/InvenTree/unit_test.py | 2 +- src/backend/InvenTree/build/test_api.py | 4 +- src/backend/InvenTree/common/tests.py | 6 +- src/backend/InvenTree/company/test_api.py | 2 +- src/backend/InvenTree/label/tests.py | 6 +- src/backend/InvenTree/machine/test_api.py | 77 +++++++++++-------- src/backend/InvenTree/order/test_api.py | 8 +- src/backend/InvenTree/part/test_api.py | 24 +++--- src/backend/InvenTree/part/test_category.py | 4 +- .../plugin/base/integration/test_mixins.py | 2 +- .../plugin/base/label/test_label_mixin.py | 4 +- .../plugin/base/locate/test_locate.py | 4 +- src/backend/InvenTree/plugin/test_api.py | 5 +- src/backend/InvenTree/plugin/test_helpers.py | 4 +- src/backend/InvenTree/plugin/test_plugin.py | 4 +- src/backend/InvenTree/report/tests.py | 8 +- src/backend/InvenTree/stock/test_api.py | 12 ++- src/backend/InvenTree/users/tests.py | 8 +- src/backend/InvenTree/web/tests.py | 14 ++-- 22 files changed, 115 insertions(+), 101 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0f8057da83..165f25f854 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -84,3 +84,7 @@ repos: # rev: 3.0.0 # hooks: # - id: shellcheck +- repo: https://github.com/isidentical/teyit + rev: 0.4.3 + hooks: + - id: teyit diff --git a/src/backend/InvenTree/InvenTree/test_api.py b/src/backend/InvenTree/InvenTree/test_api.py index 58e18df2e5..ea1a6c5b4a 100644 --- a/src/backend/InvenTree/InvenTree/test_api.py +++ b/src/backend/InvenTree/InvenTree/test_api.py @@ -128,7 +128,7 @@ class APITests(InvenTreeAPITestCase): response = self.client.get(url, format='json') # Not logged in, so cannot access user role data - self.assertTrue(response.status_code in [401, 403]) + self.assertIn(response.status_code, [401, 403]) # Now log in! self.basicAuth() diff --git a/src/backend/InvenTree/InvenTree/tests.py b/src/backend/InvenTree/InvenTree/tests.py index 6f32995592..b0e3e075c2 100644 --- a/src/backend/InvenTree/InvenTree/tests.py +++ b/src/backend/InvenTree/InvenTree/tests.py @@ -1030,7 +1030,7 @@ class TestVersionNumber(TestCase): s = '.'.join([str(i) for i in v]) - self.assertTrue(s in version.inventreeVersion()) + self.assertIn(s, version.inventreeVersion()) def test_comparison(self): """Test direct comparison of version numbers.""" @@ -1039,10 +1039,10 @@ class TestVersionNumber(TestCase): v_c = version.inventreeVersionTuple('1.2.4') v_d = version.inventreeVersionTuple('2.0.0') - self.assertTrue(v_b > v_a) - self.assertTrue(v_c > v_b) - self.assertTrue(v_d > v_c) - self.assertTrue(v_d > v_a) + self.assertGreater(v_b, v_a) + self.assertGreater(v_c, v_b) + self.assertGreater(v_d, v_c) + self.assertGreater(v_d, v_a) def test_commit_info(self): """Test that the git commit information is extracted successfully.""" @@ -1505,7 +1505,7 @@ class MagicLoginTest(InvenTreeTestCase): self.assertEqual(mail.outbox[0].subject, '[InvenTree] Log in to the app') # Check that the token is in the email - self.assertTrue('http://testserver/api/email/login/' in mail.outbox[0].body) + self.assertIn('http://testserver/api/email/login/', mail.outbox[0].body) token = mail.outbox[0].body.split('/')[-1].split('\n')[0][8:] self.assertEqual(get_user(token), self.user) diff --git a/src/backend/InvenTree/InvenTree/unit_test.py b/src/backend/InvenTree/InvenTree/unit_test.py index 4ad2d68b7b..c5f8804d23 100644 --- a/src/backend/InvenTree/InvenTree/unit_test.py +++ b/src/backend/InvenTree/InvenTree/unit_test.py @@ -397,7 +397,7 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase): ): """Helper function to process and validate a downloaded csv file.""" # Check that the correct object type has been passed - self.assertTrue(isinstance(file_object, io.StringIO)) + self.assertIsInstance(file_object, io.StringIO) file_object.seek(0) diff --git a/src/backend/InvenTree/build/test_api.py b/src/backend/InvenTree/build/test_api.py index 131dabaf3f..de8f0bbed8 100644 --- a/src/backend/InvenTree/build/test_api.py +++ b/src/backend/InvenTree/build/test_api.py @@ -247,7 +247,7 @@ class BuildTest(BuildAPITest): expected_code=400 ) - self.assertTrue('accept_unallocated' in response.data) + self.assertIn('accept_unallocated', response.data) # Accept unallocated stock self.post( @@ -934,7 +934,7 @@ class BuildOverallocationTest(BuildAPITest): {}, expected_code=400 ) - self.assertTrue('accept_overallocated' in response.data) + self.assertIn('accept_overallocated', response.data) # Check stock items have not reduced at all for si, oq, _ in self.state.values(): diff --git a/src/backend/InvenTree/common/tests.py b/src/backend/InvenTree/common/tests.py index 6a5b26766d..cc5ac61270 100644 --- a/src/backend/InvenTree/common/tests.py +++ b/src/backend/InvenTree/common/tests.py @@ -50,7 +50,7 @@ class SettingsTest(InvenTreeTestCase): # There should be two settings objects in the database settings = InvenTreeSetting.objects.all() - self.assertTrue(settings.count() >= 2) + self.assertGreaterEqual(settings.count(), 2) instance_name = InvenTreeSetting.objects.get(pk=1) self.assertEqual(instance_name.key, 'INVENTREE_INSTANCE') @@ -207,7 +207,7 @@ class SettingsTest(InvenTreeTestCase): - Ensure that every setting key is valid - Ensure that a validator is supplied """ - self.assertTrue(type(setting) is dict) + self.assertIs(type(setting), dict) name = setting.get('name', None) @@ -726,7 +726,7 @@ class TaskListApiTests(InvenTreeAPITestCase): response = self.get(url, expected_code=200) for task in response.data: - self.assertTrue(task['name'] == 'time.sleep') + self.assertEqual(task['name'], 'time.sleep') class WebhookMessageTests(TestCase): diff --git a/src/backend/InvenTree/company/test_api.py b/src/backend/InvenTree/company/test_api.py index 332292ca3c..65b55089a3 100644 --- a/src/backend/InvenTree/company/test_api.py +++ b/src/backend/InvenTree/company/test_api.py @@ -130,7 +130,7 @@ class CompanyTest(InvenTreeAPITestCase): expected_code=400, ) - self.assertTrue('currency' in response.data) + self.assertIn('currency', response.data) def test_company_active(self): """Test that the 'active' value and filter works.""" diff --git a/src/backend/InvenTree/label/tests.py b/src/backend/InvenTree/label/tests.py index b4afce2216..62a84c6439 100644 --- a/src/backend/InvenTree/label/tests.py +++ b/src/backend/InvenTree/label/tests.py @@ -36,18 +36,18 @@ class LabelTest(InvenTreeAPITestCase): """Test that the default label templates are copied across.""" labels = StockItemLabel.objects.all() - self.assertTrue(labels.count() > 0) + self.assertGreater(labels.count(), 0) labels = StockLocationLabel.objects.all() - self.assertTrue(labels.count() > 0) + self.assertGreater(labels.count(), 0) def test_default_files(self): """Test that label files exist in the MEDIA directory.""" def test_subdir(ref_name): item_dir = settings.MEDIA_ROOT.joinpath('label', 'inventree', ref_name) - self.assertTrue(len([item_dir.iterdir()]) > 0) + self.assertGreater(len([item_dir.iterdir()]), 0) test_subdir('stockitem') test_subdir('stocklocation') diff --git a/src/backend/InvenTree/machine/test_api.py b/src/backend/InvenTree/machine/test_api.py index f67b48d005..1a2e40a517 100644 --- a/src/backend/InvenTree/machine/test_api.py +++ b/src/backend/InvenTree/machine/test_api.py @@ -80,15 +80,18 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase): machine_type = [t for t in response.data if t['slug'] == 'label-printer'] self.assertEqual(len(machine_type), 1) machine_type = machine_type[0] - self.assertDictContainsSubset( - { - 'slug': 'label-printer', - 'name': 'Label Printer', - 'description': 'Directly print labels for various items.', - 'provider_plugin': None, - 'is_builtin': True, - }, + self.assertEqual( machine_type, + { + **machine_type, + **{ + 'slug': 'label-printer', + 'name': 'Label Printer', + 'description': 'Directly print labels for various items.', + 'provider_plugin': None, + 'is_builtin': True, + }, + }, ) self.assertTrue( machine_type['provider_file'].endswith( @@ -102,17 +105,20 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase): driver = [a for a in response.data if a['slug'] == 'test-label-printer-api'] self.assertEqual(len(driver), 1) driver = driver[0] - self.assertDictContainsSubset( - { - 'slug': 'test-label-printer-api', - 'name': 'Test label printer', - 'description': 'This is a test label printer driver for testing.', - 'provider_plugin': None, - 'is_builtin': True, - 'machine_type': 'label-printer', - 'driver_errors': [], - }, + self.assertEqual( driver, + { + **driver, + **{ + 'slug': 'test-label-printer-api', + 'name': 'Test label printer', + 'description': 'This is a test label printer driver for testing.', + 'provider_plugin': None, + 'is_builtin': True, + 'machine_type': 'label-printer', + 'driver_errors': [], + }, + }, ) self.assertEqual(driver['provider_file'], __file__) @@ -163,19 +169,22 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase): response = self.get(reverse('api-machine-list')) self.assertEqual(len(response.data), 1) - self.assertDictContainsSubset( - { - 'name': 'Test Machine', - 'machine_type': 'label-printer', - 'driver': 'test-label-printer-api', - 'initialized': True, - 'active': True, - 'status': 101, - 'status_model': 'LabelPrinterStatus', - 'status_text': '', - 'is_driver_available': True, - }, + self.assertEqual( response.data[0], + { + **response.data[0], + **{ + 'name': 'Test Machine', + 'machine_type': 'label-printer', + 'driver': 'test-label-printer-api', + 'initialized': True, + 'active': True, + 'status': 101, + 'status_model': 'LabelPrinterStatus', + 'status_text': '', + 'is_driver_available': True, + }, + }, ) def test_machine_detail(self): @@ -195,19 +204,21 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase): # Create a machine response = self.post(reverse('api-machine-list'), machine_data) - self.assertDictContainsSubset(machine_data, response.data) + self.assertEqual(response.data, {**response.data, **machine_data}) pk = response.data['pk'] # Retrieve the machine response = self.get(reverse('api-machine-detail', kwargs={'pk': pk})) - self.assertDictContainsSubset(machine_data, response.data) + self.assertEqual(response.data, {**response.data, **machine_data}) # Update the machine response = self.patch( reverse('api-machine-detail', kwargs={'pk': pk}), {'name': 'Updated Machine'}, ) - self.assertDictContainsSubset({'name': 'Updated Machine'}, response.data) + self.assertEqual( + response.data, {**response.data, **{'name': 'Updated Machine'}} + ) self.assertEqual(MachineConfig.objects.get(pk=pk).name, 'Updated Machine') # Delete the machine diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index d9f63d67c0..fb59f8b446 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -418,7 +418,7 @@ class PurchaseOrderTest(OrderTest): po = models.PurchaseOrder.objects.get(pk=1) - self.assertTrue(po.lines.count() > 0) + self.assertGreater(po.lines.count(), 0) lines = [] @@ -839,7 +839,7 @@ class PurchaseOrderDownloadTest(OrderTest): expected_code=200, expected_fn='InvenTree_PurchaseOrderItems.xlsx', ) as file: - self.assertTrue(isinstance(file, io.BytesIO)) + self.assertIsInstance(file, io.BytesIO) class PurchaseOrderReceiveTest(OrderTest): @@ -1570,7 +1570,7 @@ class SalesOrderDownloadTest(OrderTest): expected_fn='InvenTree_SalesOrders.xls', decode=False, ) as file: - self.assertTrue(isinstance(file, io.BytesIO)) + self.assertIsInstance(file, io.BytesIO) def test_download_csv(self): """Test that the list of sales orders can be downloaded as a .csv file.""" @@ -1772,7 +1772,7 @@ class SalesOrderAllocateTest(OrderTest): # At least one item should be allocated, and all should be variants self.assertGreater(self.order.stock_allocations.count(), 0) for allocation in self.order.stock_allocations.all(): - self.assertNotEquals(allocation.item.part.pk, allocation.line.part.pk) + self.assertNotEqual(allocation.item.part.pk, allocation.line.part.pk) def test_shipment_complete(self): """Test that we can complete a shipment via the API.""" diff --git a/src/backend/InvenTree/part/test_api.py b/src/backend/InvenTree/part/test_api.py index 9459ee3bae..f65d06a425 100644 --- a/src/backend/InvenTree/part/test_api.py +++ b/src/backend/InvenTree/part/test_api.py @@ -463,12 +463,12 @@ class PartCategoryAPITest(InvenTreeAPITestCase): response = self.get(url, {'path_detail': False}, expected_code=200) # Check that the path detail information is not included - self.assertFalse('path' in response.data.keys()) + self.assertNotIn('path', response.data.keys()) # Now, request *with* path detail response = self.get(url, {'path_detail': True}, expected_code=200) - self.assertTrue('path' in response.data.keys()) + self.assertIn('path', response.data.keys()) path = response.data['path'] @@ -520,7 +520,7 @@ class PartOptionsAPITest(InvenTreeAPITestCase): # Check that a bunch o' fields are contained for f in ['assembly', 'component', 'description', 'image', 'IPN']: - self.assertTrue(f in actions.keys()) + self.assertIn(f, actions.keys()) # Active is a 'boolean' field active = actions['active'] @@ -553,7 +553,7 @@ class PartOptionsAPITest(InvenTreeAPITestCase): actions = self.getActions(reverse('api-part-category-list')) # actions should *not* contain 'POST' as we do not have the correct role - self.assertFalse('POST' in actions) + self.assertNotIn('POST', actions) self.assignRole('part_category.add') @@ -1055,8 +1055,8 @@ class PartAPITest(PartAPITestBase): # Filter by creation date response = self.get(url, {'created_before': '2019-01-01'}, expected_code=200) - self.assertTrue(len(response.data) < n) - self.assertTrue(len(response.data) > 0) + self.assertLess(len(response.data), n) + self.assertGreater(len(response.data), 0) for item in response.data: self.assertIsNotNone(item['creation_date']) @@ -1066,8 +1066,8 @@ class PartAPITest(PartAPITestBase): response = self.get(url, {'created_after': '2019-01-01'}, expected_code=200) - self.assertTrue(len(response.data) < n) - self.assertTrue(len(response.data) > 0) + self.assertLess(len(response.data), n) + self.assertGreater(len(response.data), 0) for item in response.data: self.assertIsNotNone(item['creation_date']) @@ -2153,7 +2153,7 @@ class BomItemTest(InvenTreeAPITestCase): response = self.get(url, data={'validated': False}, expected_code=200) # There should be at least one non-validated item - self.assertTrue(len(response.data) > 0) + self.assertGreater(len(response.data), 0) # Now, let's validate an item bom_item = BomItem.objects.first() @@ -2169,7 +2169,7 @@ class BomItemTest(InvenTreeAPITestCase): # Each item in response should contain expected keys for el in response.data: for key in ['available_stock', 'available_substitute_stock']: - self.assertTrue(key in el) + self.assertIn(key, el) def test_bom_list_search(self): """Test that we can search the BOM list API endpoint.""" @@ -2208,7 +2208,7 @@ class BomItemTest(InvenTreeAPITestCase): q1 = response.data[0]['quantity'] q2 = response.data[-1]['quantity'] - self.assertTrue(q1 < q2) + self.assertLess(q1, q2) # Order by decreasing quantity response = self.get(f'{url}?ordering=-quantity', expected_code=200) @@ -2255,7 +2255,7 @@ class BomItemTest(InvenTreeAPITestCase): ] for key in expected_values: - self.assertTrue(key in response.data) + self.assertIn(key, response.data) self.assertEqual(int(float(response.data['quantity'])), 25) diff --git a/src/backend/InvenTree/part/test_category.py b/src/backend/InvenTree/part/test_category.py index d3e89994ab..34308bf285 100644 --- a/src/backend/InvenTree/part/test_category.py +++ b/src/backend/InvenTree/part/test_category.py @@ -117,7 +117,7 @@ class CategoryTest(TestCase): child.pathstring, 'Cat/AAAAAAAAAA/BBBBBBBBBB/CCCCCCCCCC/DDDDDDDDDD/EEEEEEEEEE/FFFFFFFFFF/GGGGGGGGGG/HHHHHHHHHH/IIIIIIIIII/JJJJJJJJJJ/KKKKKKKKK...OO/PPPPPPPPPP/QQQQQQQQQQ/RRRRRRRRRR/SSSSSSSSSS/TTTTTTTTTT/UUUUUUUUUU/VVVVVVVVVV/WWWWWWWWWW/XXXXXXXXXX/YYYYYYYYYY/ZZZZZZZZZZ', ) - self.assertTrue(len(child.pathstring) <= 250) + self.assertLessEqual(len(child.pathstring), 250) # Attempt an invalid move with self.assertRaises(ValidationError): @@ -325,7 +325,7 @@ class CategoryTest(TestCase): self.assertEqual(descendants.count(), 3) for loc in [C11, C12, C13]: - self.assertTrue(loc in descendants) + self.assertIn(loc, descendants) # Check category C1x, should be B1 -> C1x for loc in [C11, C12, C13]: diff --git a/src/backend/InvenTree/plugin/base/integration/test_mixins.py b/src/backend/InvenTree/plugin/base/integration/test_mixins.py index 0cba7f5854..00f3d437c0 100644 --- a/src/backend/InvenTree/plugin/base/integration/test_mixins.py +++ b/src/backend/InvenTree/plugin/base/integration/test_mixins.py @@ -351,7 +351,7 @@ class PanelMixinTests(InvenTreeTestCase): """Test that the sample panel plugin is installed.""" plugins = registry.with_mixin('panel') - self.assertTrue(len(plugins) == 0) + self.assertEqual(len(plugins), 0) # Now enable the plugin registry.set_plugin_state('samplepanel', True) diff --git a/src/backend/InvenTree/plugin/base/label/test_label_mixin.py b/src/backend/InvenTree/plugin/base/label/test_label_mixin.py index 29b986af0c..3fa1409687 100644 --- a/src/backend/InvenTree/plugin/base/label/test_label_mixin.py +++ b/src/backend/InvenTree/plugin/base/label/test_label_mixin.py @@ -196,7 +196,7 @@ class LabelMixinTests(InvenTreeAPITestCase): options = self.options( self.do_url(parts, plugin_ref, label), expected_code=200 ).json() - self.assertTrue('amount' in options['actions']['POST']) + self.assertIn('amount', options['actions']['POST']) plg = registry.get_plugin(plugin_ref) with mock.patch.object(plg, 'print_label') as print_label: @@ -206,7 +206,7 @@ class LabelMixinTests(InvenTreeAPITestCase): data={'amount': '-no-valid-int-'}, expected_code=400, ).json() - self.assertTrue('amount' in res) + self.assertIn('amount', res) print_label.assert_not_called() # correct value type diff --git a/src/backend/InvenTree/plugin/base/locate/test_locate.py b/src/backend/InvenTree/plugin/base/locate/test_locate.py index 4bc152d71c..e03070a411 100644 --- a/src/backend/InvenTree/plugin/base/locate/test_locate.py +++ b/src/backend/InvenTree/plugin/base/locate/test_locate.py @@ -26,9 +26,9 @@ class LocatePluginTests(InvenTreeAPITestCase): """Test that a locate plugin is actually installed.""" plugins = registry.with_mixin('locate') - self.assertTrue(len(plugins) > 0) + self.assertGreater(len(plugins), 0) - self.assertTrue('samplelocate' in [p.slug for p in plugins]) + self.assertIn('samplelocate', [p.slug for p in plugins]) def test_locate_fail(self): """Test various API failure modes.""" diff --git a/src/backend/InvenTree/plugin/test_api.py b/src/backend/InvenTree/plugin/test_api.py index b66282f21b..51c2d56037 100644 --- a/src/backend/InvenTree/plugin/test_api.py +++ b/src/backend/InvenTree/plugin/test_api.py @@ -196,8 +196,9 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): mixin_dict = plg.mixins() self.assertIn('base', mixin_dict) - self.assertDictContainsSubset( - {'base': {'key': 'base', 'human_name': 'base'}}, mixin_dict + self.assertEqual( + mixin_dict, + {**mixin_dict, **{'base': {'key': 'base', 'human_name': 'base'}}}, ) # check reload on save diff --git a/src/backend/InvenTree/plugin/test_helpers.py b/src/backend/InvenTree/plugin/test_helpers.py index 4a32c02fb9..95cff3ea95 100644 --- a/src/backend/InvenTree/plugin/test_helpers.py +++ b/src/backend/InvenTree/plugin/test_helpers.py @@ -22,5 +22,5 @@ class HelperTests(TestCase): response = render_template( ErrorSource(), 'sample/wrongsample.html', {'abc': 123} ) - self.assertTrue('lert alert-block alert-danger' in response) - self.assertTrue('Template file sample/wrongsample.html' in response) + self.assertIn('lert alert-block alert-danger', response) + self.assertIn('Template file sample/wrongsample.html', response) diff --git a/src/backend/InvenTree/plugin/test_plugin.py b/src/backend/InvenTree/plugin/test_plugin.py index 557a86b75a..cff4373975 100644 --- a/src/backend/InvenTree/plugin/test_plugin.py +++ b/src/backend/InvenTree/plugin/test_plugin.py @@ -275,13 +275,13 @@ class RegistryTests(TestCase): self.assertEqual(len(registry.errors), 3) # There should be at least one discovery error in the module `broken_file` - self.assertTrue(len(registry.errors.get('discovery')) > 0) + self.assertGreater(len(registry.errors.get('discovery')), 0) self.assertEqual( registry.errors.get('discovery')[0]['broken_file'], "name 'bb' is not defined", ) # There should be at least one load error with an intentional KeyError - self.assertTrue(len(registry.errors.get('load')) > 0) + self.assertGreater(len(registry.errors.get('load')), 0) self.assertEqual( registry.errors.get('load')[0]['broken_sample'], "'This is a dummy error'" ) diff --git a/src/backend/InvenTree/report/tests.py b/src/backend/InvenTree/report/tests.py index e44d19b467..4d40a2c460 100644 --- a/src/backend/InvenTree/report/tests.py +++ b/src/backend/InvenTree/report/tests.py @@ -194,19 +194,19 @@ class BarcodeTagTest(TestCase): """Test the barcode generation tag.""" barcode = barcode_tags.barcode('12345') - self.assertTrue(isinstance(barcode, str)) + self.assertIsInstance(barcode, str) self.assertTrue(barcode.startswith('data:image/png;')) # Try with a different format barcode = barcode_tags.barcode('99999', format='BMP') - self.assertTrue(isinstance(barcode, str)) + self.assertIsInstance(barcode, str) self.assertTrue(barcode.startswith('data:image/bmp;')) def test_qrcode(self): """Test the qrcode generation tag.""" # Test with default settings qrcode = barcode_tags.qrcode('hello world') - self.assertTrue(isinstance(qrcode, str)) + self.assertIsInstance(qrcode, str) self.assertTrue(qrcode.startswith('data:image/png;')) self.assertEqual(len(qrcode), 700) @@ -214,7 +214,7 @@ class BarcodeTagTest(TestCase): qrcode = barcode_tags.qrcode( 'hello_world', version=2, box_size=50, format='BMP' ) - self.assertTrue(isinstance(qrcode, str)) + self.assertIsInstance(qrcode, str) self.assertTrue(qrcode.startswith('data:image/bmp;')) self.assertEqual(len(qrcode), 309720) diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py index 88fe7bb0c0..eff284ad89 100644 --- a/src/backend/InvenTree/stock/test_api.py +++ b/src/backend/InvenTree/stock/test_api.py @@ -552,7 +552,7 @@ class StockItemListTest(StockAPITestCase): response = self.get( self.list_url, {'location': 'null', 'cascade': False}, expected_code=200 ) - self.assertTrue(len(response.data) < StockItem.objects.count()) + self.assertLess(len(response.data), StockItem.objects.count()) # Filter with "cascade=True" response = self.get( @@ -662,10 +662,10 @@ class StockItemListTest(StockAPITestCase): self.assertEqual(n_stock_items, len(with_batch) + len(without_batch)) for item in with_batch: - self.assertFalse(item['batch'] in [None, '']) + self.assertNotIn(item['batch'], [None, '']) for item in without_batch: - self.assertTrue(item['batch'] in [None, '']) + self.assertIn(item['batch'], [None, '']) def test_filter_by_tracked(self): """Test the 'tracked' filter. @@ -746,9 +746,7 @@ class StockItemListTest(StockAPITestCase): self.assertEqual(response.status_code, 200) - self.assertTrue( - isinstance(response, django.http.response.StreamingHttpResponse) - ) + self.assertIsInstance(response, django.http.response.StreamingHttpResponse) file_object = io.StringIO(response.getvalue().decode('utf-8')) @@ -1132,7 +1130,7 @@ class StockItemTest(StockAPITestCase): # Check that each serial number was created for i in range(1, 11): - self.assertTrue(str(i) in sn) + self.assertIn(str(i), sn) # Check the unique stock item has been created diff --git a/src/backend/InvenTree/users/tests.py b/src/backend/InvenTree/users/tests.py index 84f0cd1668..44ea1ac1e8 100644 --- a/src/backend/InvenTree/users/tests.py +++ b/src/backend/InvenTree/users/tests.py @@ -182,13 +182,13 @@ class OwnerModelTest(InvenTreeTestCase): # Get related owners (user + group) related_owners = group_as_owner.get_related_owners(include_group=True) - self.assertTrue(user_as_owner in related_owners) - self.assertTrue(group_as_owner in related_owners) + self.assertIn(user_as_owner, related_owners) + self.assertIn(group_as_owner, related_owners) # Get related owners (only user) related_owners = group_as_owner.get_related_owners(include_group=False) - self.assertTrue(user_as_owner in related_owners) - self.assertFalse(group_as_owner in related_owners) + self.assertIn(user_as_owner, related_owners) + self.assertNotIn(group_as_owner, related_owners) # Get related owners on user related_owners = user_as_owner.get_related_owners() diff --git a/src/backend/InvenTree/web/tests.py b/src/backend/InvenTree/web/tests.py index df13a20d30..d63fe112d0 100644 --- a/src/backend/InvenTree/web/tests.py +++ b/src/backend/InvenTree/web/tests.py @@ -18,10 +18,10 @@ class TemplateTagTest(InvenTreeTestCase): def assertSettings(self, settings_data): """Helper to test if needed args are in the settings.""" - self.assertTrue('debug' in settings_data) - self.assertTrue('server_list' in settings_data) - self.assertTrue('show_server_selector' in settings_data) - self.assertTrue('environment' in settings_data) + self.assertIn('debug', settings_data) + self.assertIn('server_list', settings_data) + self.assertIn('show_server_selector', settings_data) + self.assertIn('environment', settings_data) def test_spa_bundle(self): """Test the 'spa_bundle' template tag.""" @@ -32,8 +32,8 @@ class TemplateTagTest(InvenTreeTestCase): return # pragma: no cover shipped_js = resp.split('