mirror of
https://github.com/inventree/InvenTree.git
synced 2025-08-07 12:22:11 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into pui-maintine-v7
This commit is contained in:
5
.github/workflows/backport.yml
vendored
5
.github/workflows/backport.yml
vendored
@@ -9,15 +9,13 @@ on:
|
|||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [ "labeled", "closed" ]
|
types: [ "labeled", "closed" ]
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
backport:
|
backport:
|
||||||
name: Backport PR
|
name: Backport PR
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
if: |
|
if: |
|
||||||
github.event.pull_request.merged == true
|
github.event.pull_request.merged == true
|
||||||
&& contains(github.event.pull_request.labels.*.name, 'backport')
|
&& contains(github.event.pull_request.labels.*.name, 'backport')
|
||||||
@@ -31,7 +29,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
auto_backport_label_prefix: backport-to-
|
auto_backport_label_prefix: backport-to-
|
||||||
add_original_reviewers: true
|
|
||||||
|
|
||||||
- name: Info log
|
- name: Info log
|
||||||
if: ${{ success() }}
|
if: ${{ success() }}
|
||||||
|
2
.github/workflows/docker.yaml
vendored
2
.github/workflows/docker.yaml
vendored
@@ -128,7 +128,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@2b51285047da1547ffb1b2203d8be4c0af6b1f20 # pin@v3.2.0
|
uses: docker/setup-buildx-action@2b51285047da1547ffb1b2203d8be4c0af6b1f20 # pin@v3.2.0
|
||||||
- name: Set up cosign
|
- name: Set up cosign
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: sigstore/cosign-installer@e1523de7571e31dbe865fd2e80c5c7c23ae71eb4 # pin@v3.4.0
|
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # pin@v3.5.0
|
||||||
- name: Check if Dockerhub login is required
|
- name: Check if Dockerhub login is required
|
||||||
id: docker_login
|
id: docker_login
|
||||||
run: |
|
run: |
|
||||||
|
2
.github/workflows/qc_checks.yaml
vendored
2
.github/workflows/qc_checks.yaml
vendored
@@ -213,7 +213,7 @@ jobs:
|
|||||||
echo "Version: $version"
|
echo "Version: $version"
|
||||||
mkdir export/${version}
|
mkdir export/${version}
|
||||||
mv schema.yml export/${version}/api.yaml
|
mv schema.yml export/${version}/api.yaml
|
||||||
- uses: stefanzweifel/git-auto-commit-action@8756aa072ef5b4a080af5dc8fef36c5d586e521d # v5.0.0
|
- uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 # v5.0.1
|
||||||
with:
|
with:
|
||||||
commit_message: "Update API schema for ${version}"
|
commit_message: "Update API schema for ${version}"
|
||||||
|
|
||||||
|
2
.github/workflows/scorecard.yml
vendored
2
.github/workflows/scorecard.yml
vendored
@@ -67,6 +67,6 @@ jobs:
|
|||||||
|
|
||||||
# Upload the results to GitHub's code scanning dashboard.
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10
|
uses: github/codeql-action/upload-sarif@df5a14dc28094dc936e103b37d749c6628682b60 # v3.25.0
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
2
Procfile
2
Procfile
@@ -1,7 +1,7 @@
|
|||||||
# Web process: gunicorn
|
# Web process: gunicorn
|
||||||
web: env/bin/gunicorn --chdir $APP_HOME/src/backend/InvenTree -c src/backend/InvenTree/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:$PORT
|
web: env/bin/gunicorn --chdir $APP_HOME/src/backend/InvenTree -c src/backend/InvenTree/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:$PORT
|
||||||
# Worker process: qcluster
|
# Worker process: qcluster
|
||||||
worker: env/bin/python src/backendInvenTree/manage.py qcluster
|
worker: env/bin/python src/backend/InvenTree/manage.py qcluster
|
||||||
# Invoke commands
|
# Invoke commands
|
||||||
invoke: echo "" | echo "" && . env/bin/activate && invoke
|
invoke: echo "" | echo "" && . env/bin/activate && invoke
|
||||||
# CLI: Provided for backwards compatibility
|
# CLI: Provided for backwards compatibility
|
||||||
|
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"repoOwner": "Oliver Walters",
|
|
||||||
"repoName": "InvenTree",
|
|
||||||
"targetBranchChoices": [],
|
|
||||||
"branchLabelMapping": {
|
|
||||||
"^backport-to-(.+)$": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,5 +1,11 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { SegmentedControl, SimpleGrid, Stack } from '@mantine/core';
|
import {
|
||||||
|
Group,
|
||||||
|
SegmentedControl,
|
||||||
|
SimpleGrid,
|
||||||
|
Stack,
|
||||||
|
Text
|
||||||
|
} from '@mantine/core';
|
||||||
import { ReactNode, useMemo, useState } from 'react';
|
import { ReactNode, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Bar,
|
Bar,
|
||||||
@@ -17,6 +23,7 @@ import {
|
|||||||
import { CHART_COLORS } from '../../../components/charts/colors';
|
import { CHART_COLORS } from '../../../components/charts/colors';
|
||||||
import { formatDecimal, formatPriceRange } from '../../../defaults/formatters';
|
import { formatDecimal, formatPriceRange } from '../../../defaults/formatters';
|
||||||
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
|
||||||
|
import { ModelType } from '../../../enums/ModelType';
|
||||||
import { useTable } from '../../../hooks/UseTable';
|
import { useTable } from '../../../hooks/UseTable';
|
||||||
import { apiUrl } from '../../../states/ApiState';
|
import { apiUrl } from '../../../states/ApiState';
|
||||||
import { TableColumn } from '../../../tables/Column';
|
import { TableColumn } from '../../../tables/Column';
|
||||||
@@ -110,7 +117,17 @@ export default function BomPricingPanel({
|
|||||||
title: t`Quantity`,
|
title: t`Quantity`,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
render: (record: any) => formatDecimal(record.quantity)
|
render: (record: any) => {
|
||||||
|
let quantity = formatDecimal(record.quantity);
|
||||||
|
let units = record.sub_part_detail?.units;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group spacing="apart" grow>
|
||||||
|
<Text>{quantity}</Text>
|
||||||
|
{units && <Text size="xs">[{units}]</Text>}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'unit_price',
|
accessor: 'unit_price',
|
||||||
@@ -178,7 +195,9 @@ export default function BomPricingPanel({
|
|||||||
sub_part_detail: true,
|
sub_part_detail: true,
|
||||||
has_pricing: true
|
has_pricing: true
|
||||||
},
|
},
|
||||||
enableSelection: false
|
enableSelection: false,
|
||||||
|
modelType: ModelType.part,
|
||||||
|
modelField: 'sub_part'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{bomPricingData.length > 0 ? (
|
{bomPricingData.length > 0 ? (
|
||||||
|
@@ -14,6 +14,7 @@ import {
|
|||||||
import { CHART_COLORS } from '../../../components/charts/colors';
|
import { CHART_COLORS } from '../../../components/charts/colors';
|
||||||
import { formatCurrency } from '../../../defaults/formatters';
|
import { formatCurrency } from '../../../defaults/formatters';
|
||||||
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
|
||||||
|
import { ModelType } from '../../../enums/ModelType';
|
||||||
import { useTable } from '../../../hooks/UseTable';
|
import { useTable } from '../../../hooks/UseTable';
|
||||||
import { apiUrl } from '../../../states/ApiState';
|
import { apiUrl } from '../../../states/ApiState';
|
||||||
import { TableColumn } from '../../../tables/Column';
|
import { TableColumn } from '../../../tables/Column';
|
||||||
@@ -37,7 +38,7 @@ export default function VariantPricingPanel({
|
|||||||
title: t`Variant Part`,
|
title: t`Variant Part`,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
render: (record: any) => PartColumn(record)
|
render: (record: any) => PartColumn(record, true)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'pricing_min',
|
accessor: 'pricing_min',
|
||||||
@@ -90,7 +91,8 @@ export default function VariantPricingPanel({
|
|||||||
ancestor: part?.pk,
|
ancestor: part?.pk,
|
||||||
has_pricing: true
|
has_pricing: true
|
||||||
},
|
},
|
||||||
enablePagination: false
|
enablePagination: true,
|
||||||
|
modelType: ModelType.part
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{variantPricingData.length > 0 ? (
|
{variantPricingData.length > 0 ? (
|
||||||
|
@@ -16,8 +16,13 @@ import { TableColumn } from './Column';
|
|||||||
import { ProjectCodeHoverCard } from './TableHoverCard';
|
import { ProjectCodeHoverCard } from './TableHoverCard';
|
||||||
|
|
||||||
// Render a Part instance within a table
|
// Render a Part instance within a table
|
||||||
export function PartColumn(part: any) {
|
export function PartColumn(part: any, full_name?: boolean) {
|
||||||
return <Thumbnail src={part?.thumbnail ?? part.image} text={part.name} />;
|
return (
|
||||||
|
<Thumbnail
|
||||||
|
src={part?.thumbnail ?? part.image}
|
||||||
|
text={full_name ? part.full_name : part.name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BooleanColumn({
|
export function BooleanColumn({
|
||||||
|
@@ -1,21 +1,26 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Text } from '@mantine/core';
|
import { Group, Text } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconArrowRight,
|
IconArrowRight,
|
||||||
IconCircleCheck,
|
IconCircleCheck,
|
||||||
IconSwitch3
|
IconSwitch3
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { ReactNode, useCallback, useMemo } from 'react';
|
import { ReactNode, useCallback, useMemo, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||||
import { YesNoButton } from '../../components/buttons/YesNoButton';
|
import { YesNoButton } from '../../components/buttons/YesNoButton';
|
||||||
import { Thumbnail } from '../../components/images/Thumbnail';
|
import { Thumbnail } from '../../components/images/Thumbnail';
|
||||||
import { formatPriceRange } from '../../defaults/formatters';
|
import { formatDecimal, formatPriceRange } from '../../defaults/formatters';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import { bomItemFields } from '../../forms/BomForms';
|
import { bomItemFields } from '../../forms/BomForms';
|
||||||
import { openDeleteApiForm, openEditApiForm } from '../../functions/forms';
|
import {
|
||||||
|
useCreateApiFormModal,
|
||||||
|
useDeleteApiFormModal,
|
||||||
|
useEditApiFormModal
|
||||||
|
} from '../../hooks/UseForm';
|
||||||
import { useTable } from '../../hooks/UseTable';
|
import { useTable } from '../../hooks/UseTable';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
@@ -98,9 +103,19 @@ export function BomTable({
|
|||||||
{
|
{
|
||||||
accessor: 'quantity',
|
accessor: 'quantity',
|
||||||
switchable: false,
|
switchable: false,
|
||||||
sortable: true
|
sortable: true,
|
||||||
// TODO: Custom quantity renderer
|
render: (record: any) => {
|
||||||
// TODO: see bom.js for existing implementation
|
let quantity = formatDecimal(record.quantity);
|
||||||
|
let units = record.sub_part_detail?.units;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group position="apart" grow>
|
||||||
|
<Text>{quantity}</Text>
|
||||||
|
{record.overage && <Text size="xs">+{record.overage}</Text>}
|
||||||
|
{units && <Text size="xs">{units}</Text>}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'substitutes',
|
accessor: 'substitutes',
|
||||||
@@ -131,12 +146,22 @@ export function BomTable({
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
accessor: 'price_range',
|
accessor: 'price_range',
|
||||||
title: t`Price Range`,
|
title: t`Unit Price`,
|
||||||
|
ordering: 'pricing_max',
|
||||||
sortable: false,
|
sortable: true,
|
||||||
|
switchable: true,
|
||||||
render: (record: any) =>
|
render: (record: any) =>
|
||||||
formatPriceRange(record.pricing_min, record.pricing_max)
|
formatPriceRange(record.pricing_min, record.pricing_max)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessor: 'total_price',
|
||||||
|
title: t`Total Price`,
|
||||||
|
ordering: 'pricing_max_total',
|
||||||
|
sortable: true,
|
||||||
|
switchable: true,
|
||||||
|
render: (record: any) =>
|
||||||
|
formatPriceRange(record.pricing_min_total, record.pricing_max_total)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessor: 'available_stock',
|
accessor: 'available_stock',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
@@ -277,6 +302,36 @@ export function BomTable({
|
|||||||
];
|
];
|
||||||
}, [partId, params]);
|
}, [partId, params]);
|
||||||
|
|
||||||
|
const [selectedBomItem, setSelectedBomItem] = useState<number>(0);
|
||||||
|
|
||||||
|
const newBomItem = useCreateApiFormModal({
|
||||||
|
url: ApiEndpoints.bom_list,
|
||||||
|
title: t`Create BOM Item`,
|
||||||
|
fields: bomItemFields(),
|
||||||
|
initialData: {
|
||||||
|
part: partId
|
||||||
|
},
|
||||||
|
successMessage: t`BOM item created`,
|
||||||
|
onFormSuccess: table.refreshTable
|
||||||
|
});
|
||||||
|
|
||||||
|
const editBomItem = useEditApiFormModal({
|
||||||
|
url: ApiEndpoints.bom_list,
|
||||||
|
pk: selectedBomItem,
|
||||||
|
title: t`Edit BOM Item`,
|
||||||
|
fields: bomItemFields(),
|
||||||
|
successMessage: t`BOM item updated`,
|
||||||
|
onFormSuccess: table.refreshTable
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteBomItem = useDeleteApiFormModal({
|
||||||
|
url: ApiEndpoints.bom_list,
|
||||||
|
pk: selectedBomItem,
|
||||||
|
title: t`Delete BOM Item`,
|
||||||
|
successMessage: t`BOM item deleted`,
|
||||||
|
onFormSuccess: table.refreshTable
|
||||||
|
});
|
||||||
|
|
||||||
const rowActions = useCallback(
|
const rowActions = useCallback(
|
||||||
(record: any) => {
|
(record: any) => {
|
||||||
// If this BOM item is defined for a *different* parent, then it cannot be edited
|
// If this BOM item is defined for a *different* parent, then it cannot be edited
|
||||||
@@ -313,14 +368,8 @@ export function BomTable({
|
|||||||
RowEditAction({
|
RowEditAction({
|
||||||
hidden: !user.hasChangeRole(UserRoles.part),
|
hidden: !user.hasChangeRole(UserRoles.part),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
openEditApiForm({
|
setSelectedBomItem(record.pk);
|
||||||
url: ApiEndpoints.bom_list,
|
editBomItem.open();
|
||||||
pk: record.pk,
|
|
||||||
title: t`Edit Bom Item`,
|
|
||||||
fields: bomItemFields(),
|
|
||||||
successMessage: t`Bom item updated`,
|
|
||||||
onFormSuccess: table.refreshTable
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -330,14 +379,8 @@ export function BomTable({
|
|||||||
RowDeleteAction({
|
RowDeleteAction({
|
||||||
hidden: !user.hasDeleteRole(UserRoles.part),
|
hidden: !user.hasDeleteRole(UserRoles.part),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
openDeleteApiForm({
|
setSelectedBomItem(record.pk);
|
||||||
url: ApiEndpoints.bom_list,
|
deleteBomItem.open();
|
||||||
pk: record.pk,
|
|
||||||
title: t`Delete Bom Item`,
|
|
||||||
successMessage: t`Bom item deleted`,
|
|
||||||
onFormSuccess: table.refreshTable,
|
|
||||||
preFormWarning: t`Are you sure you want to remove this BOM item?`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -347,22 +390,38 @@ export function BomTable({
|
|||||||
[partId, user]
|
[partId, user]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tableActions = useMemo(() => {
|
||||||
|
return [
|
||||||
|
<AddItemButton
|
||||||
|
hidden={!user.hasAddRole(UserRoles.part)}
|
||||||
|
tooltip={t`Add BOM Item`}
|
||||||
|
onClick={() => newBomItem.open()}
|
||||||
|
/>
|
||||||
|
];
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InvenTreeTable
|
<>
|
||||||
url={apiUrl(ApiEndpoints.bom_list)}
|
{newBomItem.modal}
|
||||||
tableState={table}
|
{editBomItem.modal}
|
||||||
columns={tableColumns}
|
{deleteBomItem.modal}
|
||||||
props={{
|
<InvenTreeTable
|
||||||
params: {
|
url={apiUrl(ApiEndpoints.bom_list)}
|
||||||
...params,
|
tableState={table}
|
||||||
part: partId,
|
columns={tableColumns}
|
||||||
part_detail: true,
|
props={{
|
||||||
sub_part_detail: true
|
params: {
|
||||||
},
|
...params,
|
||||||
tableFilters: tableFilters,
|
part: partId,
|
||||||
modelType: ModelType.part,
|
part_detail: true,
|
||||||
rowActions: rowActions
|
sub_part_detail: true
|
||||||
}}
|
},
|
||||||
/>
|
tableActions: tableActions,
|
||||||
|
tableFilters: tableFilters,
|
||||||
|
modelType: ModelType.part,
|
||||||
|
rowActions: rowActions
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { Group, Text } from '@mantine/core';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { PartHoverCard } from '../../components/images/Thumbnail';
|
import { PartHoverCard } from '../../components/images/Thumbnail';
|
||||||
|
import { formatDecimal } from '../../defaults/formatters';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { useTable } from '../../hooks/UseTable';
|
import { useTable } from '../../hooks/UseTable';
|
||||||
@@ -39,8 +41,15 @@ export function UsedInTable({
|
|||||||
{
|
{
|
||||||
accessor: 'quantity',
|
accessor: 'quantity',
|
||||||
render: (record: any) => {
|
render: (record: any) => {
|
||||||
// TODO: render units if appropriate
|
let quantity = formatDecimal(record.quantity);
|
||||||
return record.quantity;
|
let units = record.sub_part_detail?.units;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group position="apart" grow>
|
||||||
|
<Text>{quantity}</Text>
|
||||||
|
{units && <Text size="xs">{units}</Text>}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ReferenceColumn()
|
ReferenceColumn()
|
||||||
|
@@ -158,7 +158,8 @@ function partTableColumns(): TableColumn[] {
|
|||||||
{
|
{
|
||||||
accessor: 'price_range',
|
accessor: 'price_range',
|
||||||
title: t`Price Range`,
|
title: t`Price Range`,
|
||||||
sortable: false,
|
sortable: true,
|
||||||
|
ordering: 'pricing_max',
|
||||||
render: (record: any) =>
|
render: (record: any) =>
|
||||||
formatPriceRange(record.pricing_min, record.pricing_max)
|
formatPriceRange(record.pricing_min, record.pricing_max)
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user