Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Programme Cycle - Allow deletion of cycle only if no TP / PP linked #4229

Open
wants to merge 28 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
22d07b9
add new property can_remove_cycle
pavlo-mk Sep 11, 2024
b31002e
upd fields list
pavlo-mk Sep 11, 2024
52dddf0
fix serializer
pavlo-mk Sep 11, 2024
725ab2b
upd can_remove_cycle
pavlo-mk Sep 12, 2024
4e71f2e
Merge branch 'develop' into 214700_fix_cycle_deletion_dev
pavlo-mk Sep 13, 2024
f304be2
Merge branch 'develop' into 214700_fix_cycle_deletion_dev
pavlo-mk Sep 13, 2024
efb49ce
Merge branch 'develop' into 214700_fix_cycle_deletion_dev
pavlo-mk Sep 16, 2024
e68a15e
Merge branch 'develop' into 214700_fix_cycle_deletion_dev
pavlo-mk Sep 17, 2024
97a3999
Merge branch 'develop' into 214700_fix_cycle_deletion_dev
pavlo-mk Sep 19, 2024
702eba8
Merge branch 'develop' into 214700_fix_cycle_deletion_dev
pavlo-mk Sep 25, 2024
4bf0a36
Merge remote-tracking branch 'origin/214700_fix_cycle_deletion_dev' i…
pavlo-mk Sep 25, 2024
8951b5c
Merge branch 'develop' into 214700_fix_cycle_deletion_dev
pavlo-mk Sep 26, 2024
9589fdc
Merge branch 'develop' into 214700_fix_cycle_deletion_dev
pavlo-mk Sep 27, 2024
fcfafab
Merge branch 'develop' into 214700_fix_cycle_deletion_dev
pavlo-mk Sep 30, 2024
533b63c
Merge branch 'develop' into 214700_fix_cycle_deletion_dev
pavlo-mk Oct 4, 2024
cf00c2e
Merge branch 'develop' into 214700_fix_cycle_deletion_dev
pavlo-mk Oct 8, 2024
5b37322
allow delete cycle when program doesnt have tp nor rdi
Oct 8, 2024
def24e2
Merge branch 'develop' into 214700_fix_cycle_deletion_dev
mmaciekk Oct 8, 2024
ee50489
add can remove flag
Oct 8, 2024
84bb7e8
can remove only draft cycles
pavlo-mk Oct 8, 2024
afd3fd0
Merge branch 'develop' into 214700_fix_cycle_deletion_dev
pavlo-mk Oct 8, 2024
02be8e0
add delete permission
Oct 8, 2024
9a27683
add perm + flag to delete
Oct 8, 2024
7464eda
Merge branch 'develop' into 214700_fix_cycle_deletion_dev
mmaciekk Oct 8, 2024
7ce761a
Merge branch 'develop' into 214700_fix_cycle_deletion_dev
pavlo-mk Oct 9, 2024
2895814
Merge remote-tracking branch 'origin' into 214700_fix_cycle_deletion_dev
Oct 9, 2024
3b834ea
add more tests for can_remove_cycle field
pavlo-mk Oct 9, 2024
2663951
upd fe types
pavlo-mk Oct 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/frontend/src/api/programCycleApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface ProgramCycle {
total_delivered_quantity_usd: number;
frequency_of_payments: string;
admin_url?: string;
can_remove_cycle: boolean;
}

export const fetchProgramCycles = async (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import DownloadIcon from '@mui/icons-material/Download';
import DeleteIcon from '@mui/icons-material/Delete';
import { PaperContainer } from '@components/targeting/PaperContainer';
import { hasPermissions, PERMISSIONS } from 'src/config/permissions';
import {
deleteSupportingDocument,
uploadSupportingDocument,
} from '@api/paymentModuleApi';
import { useConfirmation } from '@components/core/ConfirmationDialog';
import { DropzoneField } from '@components/core/DropzoneField';
import { GreyBox } from '@components/core/GreyBox';
import { GreyText } from '@components/core/GreyText';
import { LoadingButton } from '@components/core/LoadingButton';
import { BlueText } from '@components/grievances/LookUps/LookUpStyles';
import { PaperContainer } from '@components/targeting/PaperContainer';
import { DialogTitleWrapper } from '@containers/dialogs/DialogTitleWrapper';
import { PaymentPlanQuery, PaymentPlanStatus } from '@generated/graphql';
import { useBaseUrl } from '@hooks/useBaseUrl';
import { usePermissions } from '@hooks/usePermissions';
import { useSnackbar } from '@hooks/useSnackBar';
import DeleteIcon from '@mui/icons-material/Delete';
import DownloadIcon from '@mui/icons-material/Download';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import {
Typography,
IconButton,
Box,
Button,
Collapse,
Dialog,
DialogTitle,
TextField,
DialogActions,
Button,
Box,
Grid,
DialogContent,
DialogTitle,
FormHelperText,
Grid,
IconButton,
TextField,
Typography,
} from '@mui/material';
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { usePermissions } from '@hooks/usePermissions';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PaymentPlanQuery, PaymentPlanStatus } from '@generated/graphql';
import { DropzoneField } from '@components/core/DropzoneField';
import { DialogTitleWrapper } from '@containers/dialogs/DialogTitleWrapper';
import {
deleteSupportingDocument,
uploadSupportingDocument,
} from '@api/paymentModuleApi';
import { useSnackbar } from '@hooks/useSnackBar';
import { useBaseUrl } from '@hooks/useBaseUrl';
import { useConfirmation } from '@components/core/ConfirmationDialog';
import { GreyBox } from '@components/core/GreyBox';
import { BlueText } from '@components/grievances/LookUps/LookUpStyles';
import { hasPermissions, PERMISSIONS } from 'src/config/permissions';
import { useDownloadSupportingDocument } from './SupportingDocumentsSectionActions';

interface SupportingDocumentsSectionProps {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ProgramQuery, ProgramStatus } from '@generated/graphql';
import { UniversalRestTable } from '@components/rest/UniversalRestTable/UniversalRestTable';
import React, { ReactElement, useState } from 'react';
import { ReactElement, useState } from 'react';
import { ClickableTableRow } from '@core/Table/ClickableTableRow';
import TableCell from '@mui/material/TableCell';
import { UniversalMoment } from '@core/UniversalMoment';
Expand Down Expand Up @@ -34,6 +34,10 @@ export const ProgramCyclesTableProgramDetails = ({
const canCreateProgramCycle =
program.status === ProgramStatus.Active &&
hasPermissions(PERMISSIONS.PM_PROGRAMME_CYCLE_CREATE, permissions);
const canDeleteProgramCycle = hasPermissions(
PERMISSIONS.PM_PROGRAMME_CYCLE_DELETE,
permissions,
);

const { data, error, isLoading } = useQuery({
queryKey: ['programCycles', businessArea, program.id, queryVariables],
Expand All @@ -46,13 +50,15 @@ export const ProgramCyclesTableProgramDetails = ({

const renderRow = (row: ProgramCycle): ReactElement => {
const detailsUrl = `/${baseUrl}/payment-module/program-cycles/${row.id}`;

const canEditProgramCycle =
(row.status === 'Draft' || row.status === 'Active') &&
hasPermissions(PERMISSIONS.PM_PROGRAMME_CYCLE_UPDATE, permissions);
const canDeleteProgramCycle =
row.status === 'Draft' &&
data.results.length > 1 &&
hasPermissions(PERMISSIONS.PM_PROGRAMME_CYCLE_DELETE, permissions);

const hasPermissionToDelete = hasPermissions(
PERMISSIONS.PM_PROGRAMME_CYCLE_DELETE,
permissions,
);
return (
<ClickableTableRow key={row.id} data-cy="program-cycle-row">
<TableCell data-cy="program-cycle-title">
Expand Down Expand Up @@ -100,7 +106,7 @@ export const ProgramCyclesTableProgramDetails = ({
<EditProgramCycle program={program} programCycle={row} />
)}

{canDeleteProgramCycle && (
{row.can_remove_cycle && hasPermissionToDelete && (
<DeleteProgramCycle program={program} programCycle={row} />
)}
</>
Expand Down
5 changes: 5 additions & 0 deletions src/hct_mis_api/apps/program/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class ProgramCycleListSerializer(EncodedIdSerializerMixin):
program_start_date = serializers.DateField(format="%Y-%m-%d")
program_end_date = serializers.DateField(format="%Y-%m-%d")
admin_url = serializers.SerializerMethodField()
can_remove_cycle = serializers.SerializerMethodField()

class Meta:
model = ProgramCycle
Expand All @@ -63,6 +64,7 @@ class Meta:
"frequency_of_payments",
"created_by",
"admin_url",
"can_remove_cycle",
)

def get_created_by(self, obj: ProgramCycle) -> str:
Expand All @@ -74,6 +76,9 @@ def get_admin_url(self, obj: ProgramCycle) -> Optional[str]:
user = self.context["request"].user
return obj.admin_url if user.is_superuser else None

def get_can_remove_cycle(self, obj: ProgramCycle) -> bool:
return obj.can_remove_cycle


class ProgramCycleCreateSerializer(EncodedIdSerializerMixin):
title = serializers.CharField(required=True)
Expand Down
9 changes: 9 additions & 0 deletions src/hct_mis_api/apps/program/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,15 @@ def save(self, *args: Any, **kwargs: Any) -> None:
def __str__(self) -> str:
return f"{self.title} ({self.status})"

@property
def can_remove_cycle(self) -> bool:
return (
not self.target_populations.exists()
and not self.payment_plans.exists()
and self.program.cycles.count() > 1
and self.status == ProgramCycle.DRAFT
)

@property
def total_entitled_quantity_usd(self) -> Decimal:
total_entitled = self.payment_plans.aggregate(total_entitled=Sum("total_entitled_quantity_usd"))[
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/apps/program/test_program_cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def test_set_active(self) -> None:
self.cycle.set_active()
self.cycle.refresh_from_db()
self.assertEqual(self.cycle.status, ProgramCycle.ACTIVE)
self.assertFalse(self.cycle.can_remove_cycle)

def test_set_draft(self) -> None:
with self.assertRaisesMessage(ValidationError, "Program should be within Active status."):
Expand All @@ -88,6 +89,7 @@ def test_set_draft(self) -> None:
self.cycle.set_draft()
self.cycle.refresh_from_db()
self.assertEqual(self.cycle.status, ProgramCycle.DRAFT)
self.assertTrue(self.cycle.can_remove_cycle)

def test_set_finish(self) -> None:
with self.assertRaisesMessage(ValidationError, "Program should be within Active status."):
Expand All @@ -105,6 +107,7 @@ def test_set_finish(self) -> None:
self.cycle.set_finish()
self.cycle.refresh_from_db()
self.assertEqual(self.cycle.status, ProgramCycle.FINISHED)
self.assertFalse(self.cycle.can_remove_cycle)

def test_total_entitled_quantity_usd(self) -> None:
self.assertEqual(self.cycle.total_entitled_quantity_usd, Decimal("0.0"))
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/apps/program/test_program_cycle_rest_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ def test_list_program_cycles(self) -> None:
cycles = ProgramCycle.objects.filter(program=self.program)
self.assertEqual(int(response.data["count"]), cycles.count())

results = response.data["results"]
first_cycle = results[0]
second_cycle = results[1]
last_cycle = results[2]
# check can_remove_cycle
self.assertEqual(first_cycle["can_remove_cycle"], False)
self.assertEqual(second_cycle["can_remove_cycle"], False)
self.assertEqual(last_cycle["status"], "Draft")
self.assertEqual(last_cycle["can_remove_cycle"], True)

def test_retrieve_program_cycle(self) -> None:
self.client.force_authenticate(user=self.user)
response = self.client.get(self.cycle_1_detail_url)
Expand Down
Loading