diff --git a/changelogs/unreleased/86726.json b/changelogs/unreleased/86726.json
new file mode 100644
index 0000000000..2a5690c919
--- /dev/null
+++ b/changelogs/unreleased/86726.json
@@ -0,0 +1,5 @@
+{
+ "title": "Expense: add cancel and return to draft status actions",
+ "type": "feat",
+ "packages": "hr"
+}
diff --git a/packages/apps/hr/src/api/expense-api.js b/packages/apps/hr/src/api/expense-api.js
index 25040080bf..1b50da3530 100644
--- a/packages/apps/hr/src/api/expense-api.js
+++ b/packages/apps/hr/src/api/expense-api.js
@@ -17,10 +17,10 @@
*/
import {
- createStandardSearch,
- getSearchCriterias,
createStandardFetch,
+ createStandardSearch,
getActionApi,
+ getSearchCriterias,
getTypes,
} from '@axelor/aos-mobile-core';
@@ -202,3 +202,25 @@ export async function deleteExpense({expenseId}) {
},
});
}
+
+export async function cancelExpense({expenseId, version}) {
+ return getActionApi().send({
+ url: `ws/aos/expense/cancel/${expenseId}`,
+ method: 'put',
+ body: {
+ version,
+ },
+ description: 'cancel expense',
+ });
+}
+
+export async function returnToDraftStatusExpense({expenseId, version}) {
+ return getActionApi().send({
+ url: `ws/aos/expense/draft/${expenseId}`,
+ method: 'put',
+ body: {
+ version,
+ },
+ description: 'return expense to draft status',
+ });
+}
diff --git a/packages/apps/hr/src/api/index.ts b/packages/apps/hr/src/api/index.ts
index 18344c3f74..f60833f5af 100644
--- a/packages/apps/hr/src/api/index.ts
+++ b/packages/apps/hr/src/api/index.ts
@@ -20,11 +20,13 @@ export {searchCurrencies as searchCurrenciesApi} from './currency-api';
export {getDistance as getDistanceApi} from './distance-api';
export {searchManagedEmployee as searchManagedEmployeeApi} from './employee-api';
export {
+ cancelExpense as cancelExpenseApi,
createExpense as createExpenseApi,
deleteExpense as deleteExpenseApi,
getExpense,
quickCreateExpense as quickCreateExpenseApi,
refuseExpense as refuseExpenseApi,
+ returnToDraftStatusExpense as returnToDraftStatusExpenseApi,
searchExpenseDraft as searchExpenseDraftApi,
searchExpenseToValidate as searchExpenseToValidateApi,
searchMyExpense as searchMyExpenseApi,
diff --git a/packages/apps/hr/src/components/molecules/ExpenseDetailsValidationButton/ExpenseDetailsValidationButton.js b/packages/apps/hr/src/components/molecules/ExpenseDetailsValidationButton/ExpenseDetailsValidationButton.tsx
similarity index 52%
rename from packages/apps/hr/src/components/molecules/ExpenseDetailsValidationButton/ExpenseDetailsValidationButton.js
rename to packages/apps/hr/src/components/molecules/ExpenseDetailsValidationButton/ExpenseDetailsValidationButton.tsx
index 221708b018..0ce9f684d1 100644
--- a/packages/apps/hr/src/components/molecules/ExpenseDetailsValidationButton/ExpenseDetailsValidationButton.js
+++ b/packages/apps/hr/src/components/molecules/ExpenseDetailsValidationButton/ExpenseDetailsValidationButton.tsx
@@ -28,13 +28,25 @@ import {
} from '@axelor/aos-mobile-core';
import {Button, useThemeColor} from '@axelor/aos-mobile-ui';
import {
+ cancelExpense,
deleteExpense,
+ returnToDraftStatusExpense,
sendExpense,
validateExpense,
} from '../../../features/expenseSlice';
import {ExpenseRefusalPopup} from '../../templates';
-const ExpenseDetailsValidationButton = ({expense, mode, isManualCreation}) => {
+interface ExpenseDetailsValidationButtonProps {
+ expense: any;
+ mode: string;
+ isManualCreation?: boolean;
+}
+
+const ExpenseDetailsValidationButton = ({
+ expense,
+ mode,
+ isManualCreation = false,
+}: ExpenseDetailsValidationButtonProps) => {
const navigation = useNavigation();
const Colors = useThemeColor();
const I18n = useTranslator();
@@ -50,7 +62,7 @@ const ExpenseDetailsValidationButton = ({expense, mode, isManualCreation}) => {
const sendExpenseAPI = useCallback(() => {
dispatch(
- sendExpense({
+ (sendExpense as any)({
expenseId: expense.id,
version: expense.version,
onExpense: true,
@@ -60,7 +72,7 @@ const ExpenseDetailsValidationButton = ({expense, mode, isManualCreation}) => {
const validateExpenseAPI = useCallback(() => {
dispatch(
- validateExpense({
+ (validateExpense as any)({
expenseId: expense.id,
version: expense.version,
onExpense: true,
@@ -70,10 +82,43 @@ const ExpenseDetailsValidationButton = ({expense, mode, isManualCreation}) => {
}, [dispatch, mode, expense]);
const deleteExpenseAPI = useCallback(() => {
- dispatch(deleteExpense({expenseId: expense.id, userId: user.id}));
+ dispatch((deleteExpense as any)({expenseId: expense.id, userId: user.id}));
navigation.pop();
}, [dispatch, expense.id, navigation, user.id]);
+ const cancelExpenseAPI = useCallback(() => {
+ dispatch(
+ (cancelExpense as any)({
+ expenseId: expense.id,
+ version: expense.version,
+ mode,
+ user,
+ }),
+ );
+ }, [dispatch, expense, mode, user]);
+
+ const returnToDraftStatusExpenseAPI = useCallback(() => {
+ dispatch(
+ (returnToDraftStatusExpense as any)({
+ expenseId: expense.id,
+ version: expense.version,
+ user,
+ }),
+ );
+ }, [dispatch, expense, user]);
+
+ const renderCancelButton = width => {
+ return (
+
+ );
+ };
+
if (readonly) {
return null;
}
@@ -90,41 +135,67 @@ const ExpenseDetailsValidationButton = ({expense, mode, isManualCreation}) => {
iconName={isManualCreation ? 'x-lg' : 'trash3-fill'}
/>
)}
+ {(!canDelete || !isManualCreation) && renderCancelButton('45%')}
);
}
+ if (expense.statusSelect === Expense?.statusSelect.WaitingValidation) {
+ return (
+
+ {(user?.employee?.hrManager ||
+ expense.employee?.managerUser?.id === user.id) && (
+ <>
+
+ );
+ }
+
+ if (expense.statusSelect === Expense?.statusSelect.Validate) {
+ return (
+ {renderCancelButton('94%')}
+ );
+ }
+
if (
- (user?.employee?.hrManager ||
- expense.employee?.managerUser?.id === user.id) &&
- expense.statusSelect === Expense?.statusSelect.WaitingValidation
+ expense.statusSelect === Expense?.statusSelect.Refused ||
+ expense.statusSelect === Expense?.statusSelect.Canceled
) {
return (
setRefusalPopupIsOpen(true)}
- color={Colors.errorColor}
- width="45%"
- iconName="x-lg"
- />
- setRefusalPopupIsOpen(false)}
- />
-
);
@@ -135,6 +206,7 @@ const ExpenseDetailsValidationButton = ({expense, mode, isManualCreation}) => {
const styles = StyleSheet.create({
buttonContainer: {
+ flexWrap: 'wrap',
flexDirection: 'row',
justifyContent: 'space-evenly',
},
diff --git a/packages/apps/hr/src/features/asyncFunctions-index.ts b/packages/apps/hr/src/features/asyncFunctions-index.ts
index bd40e67f44..b3996aae2e 100644
--- a/packages/apps/hr/src/features/asyncFunctions-index.ts
+++ b/packages/apps/hr/src/features/asyncFunctions-index.ts
@@ -34,11 +34,13 @@ export {
updateExpenseLine,
} from './expenseLineSlice';
export {
+ cancelExpense,
createExpense,
deleteExpense,
fetchExpenseById,
quickCreateExpense,
refuseExpense,
+ returnToDraftStatusExpense,
searchExpenseDraft,
searchExpenseToValidate,
searchMyExpense,
diff --git a/packages/apps/hr/src/features/expenseSlice.js b/packages/apps/hr/src/features/expenseSlice.js
index 142c2b5b14..049d5ae89b 100644
--- a/packages/apps/hr/src/features/expenseSlice.js
+++ b/packages/apps/hr/src/features/expenseSlice.js
@@ -23,15 +23,17 @@ import {
updateAgendaItems,
} from '@axelor/aos-mobile-core';
import {
+ cancelExpense as _cancelExpense,
createExpense as _createExpense,
deleteExpense as _deleteExpense,
getExpense,
quickCreateExpense as _quickCreateExpense,
refuseExpense as _refuseExpense,
- sendExpense as _sendExpense,
+ returnToDraftStatusExpense as _returnToDraftStatusExpense,
searchExpenseDraft as _searchExpenseDraft,
searchExpenseToValidate as _searchExpenseToValidate,
searchMyExpense as _searchMyExpense,
+ sendExpense as _sendExpense,
updateExpense as _updateExpense,
validateExpense as _validateExpense,
} from '../api/expense-api';
@@ -272,6 +274,42 @@ export const deleteExpense = createAsyncThunk(
},
);
+export const cancelExpense = createAsyncThunk(
+ 'hr_expense/cancelExpense',
+ async function (data, {getState, dispatch}) {
+ return handlerApiCall({
+ fetchFunction: _cancelExpense,
+ data,
+ action: 'Hr_SliceAction_CancelExpense',
+ getState,
+ responseOptions: {isArrayResponse: false},
+ }).then(() => {
+ dispatch(fetchExpenseById({ExpenseId: data.expenseId}));
+ if (data.mode === Expense.mode.validation) {
+ dispatch(searchExpenseToValidate({user: data.user}));
+ } else {
+ dispatch(searchMyExpense({userId: data.user?.id}));
+ }
+ });
+ },
+);
+
+export const returnToDraftStatusExpense = createAsyncThunk(
+ 'hr_expense/returnToDraftStatusExpense',
+ async function (data, {getState, dispatch}) {
+ return handlerApiCall({
+ fetchFunction: _returnToDraftStatusExpense,
+ data,
+ action: 'Hr_SliceAction_ReturnToDraftStatusExpense',
+ getState,
+ responseOptions: {isArrayResponse: false},
+ }).then(() => {
+ dispatch(fetchExpenseById({ExpenseId: data.expenseId}));
+ dispatch(searchMyExpense({userId: data.user?.id}));
+ });
+ },
+);
+
const initialState = {
loading: true,
expenseDraftList: [],
diff --git a/packages/apps/hr/src/i18n/en.json b/packages/apps/hr/src/i18n/en.json
index d79bdfd469..096b63ea21 100644
--- a/packages/apps/hr/src/i18n/en.json
+++ b/packages/apps/hr/src/i18n/en.json
@@ -100,6 +100,7 @@
"Hr_AvailableQty": "Available quantity",
"Hr_MissingQuantity": "Missing quantity",
"Hr_ExceedingQuantity": "Exceeding quantity",
+ "Hr_ReturnToDraftStatus": "Return to draft status",
"Hr_SliceAction_FetchExpenseLines": "fetch expense lines",
"Hr_SliceAction_SearchDraftExpense": "fetch draft expenses",
"Hr_SliceAction_FetchExpense": "fetch expenses",
@@ -160,5 +161,7 @@
"Hr_SliceAction_CancelLeave": "cancel leave",
"Hr_SliceAction_RejectLeave": "reject leave",
"Hr_SliceAction_DeleteLeave": "delete leave",
- "Hr_SliceAction_CreateLeaveRequest": "create leave requests"
+ "Hr_SliceAction_CreateLeaveRequest": "create leave requests",
+ "Hr_SliceAction_CancelExpense": "cancel expense",
+ "Hr_SliceAction_ReturnToDraftStatusExpense": "return expense to draft status"
}
diff --git a/packages/apps/hr/src/i18n/fr.json b/packages/apps/hr/src/i18n/fr.json
index 041d716e6c..4709436099 100644
--- a/packages/apps/hr/src/i18n/fr.json
+++ b/packages/apps/hr/src/i18n/fr.json
@@ -100,6 +100,7 @@
"Hr_AvailableQty": "Quantité disponible",
"Hr_MissingQuantity": "Quantité manquante",
"Hr_ExceedingQuantity": "Quantité en trop",
+ "Hr_ReturnToDraftStatus": "Retourner à l'état brouillon",
"Hr_SliceAction_FetchExpenseLines": "récupération des lignes de frais",
"Hr_SliceAction_SearchDraftExpense": "récupération des notes de frais brouillon",
"Hr_SliceAction_FetchExpense": "récupération des notes de frais",
@@ -160,5 +161,7 @@
"Hr_SliceAction_CancelLeave": "annulation de la demande de congé",
"Hr_SliceAction_RejectLeave": "rejet de la demande de congé",
"Hr_SliceAction_DeleteLeave": "suppression de la demande de congé",
- "Hr_SliceAction_CreateLeaveRequest": "création des demandes de congés"
+ "Hr_SliceAction_CreateLeaveRequest": "création des demandes de congés",
+ "Hr_SliceAction_CancelExpense": "annulation de la note de frais",
+ "Hr_SliceAction_ReturnToDraftStatusExpense": "retour à l'état brouillon de la note de frais"
}