diff --git a/package.json b/package.json
index 64004f37..da3b8435 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,7 @@
         "salix": "cd ../salix && gulp back",
         "db": "cd ../salix && gulp docker",
         "cy:open": "npm run db && cypress open",
-        "test:e2e": "npm run db && cypress run",
+        "test:e2e": "npm run db && cypress run --headed --config video=false",
         "test:unit": "vitest",
         "build": "rm -rf dist/ ; quasar build",
         "clean": "rm -rf dist/",
diff --git a/quasar.config.js b/quasar.config.js
index 84e934d1..a9d50c62 100644
--- a/quasar.config.js
+++ b/quasar.config.js
@@ -23,7 +23,7 @@ module.exports = configure(function (ctx) {
         // app boot file (/src/boot)
         // --> boot files are part of "main.js"
         // https://v2.quasar.dev/quasar-cli-webpack/boot-files
-        boot: ['i18n', 'axios', 'vnDate', 'error-handler', 'app'],
+        boot: ['i18n', 'axios', 'vnDate', 'app'],
 
         // https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css
         css: ['app.scss', 'width.scss', 'responsive.scss'],
diff --git a/src/boot/error-handler.js b/src/boot/error-handler.js
deleted file mode 100644
index 1e11c952..00000000
--- a/src/boot/error-handler.js
+++ /dev/null
@@ -1,66 +0,0 @@
-export default async ({ app }) => {
-    /*
-  window.addEventListener('error',
-    e => onWindowError(e));
-  window.addEventListener('unhandledrejection',
-    e => onWindowRejection(e));
-
-  ,onWindowError(event) {
-    errorHandler(event.error);
-  }
-  ,onWindowRejection(event) {
-    errorHandler(event.reason);
-  }
-*/
-    app.config.errorHandler = (err, vm, info) => {
-        errorHandler(err, vm)
-    }
-
-    function errorHandler (err, vm) {
-        let message
-        let tMessage
-        let res = err.response
-
-        // XXX: Compatibility with old JSON service
-        if (err.name === 'JsonException') {
-            res = {
-                status: err.statusCode,
-                data: { error: { message: err.message } }
-            }
-        }
-
-        if (res) {
-            const status = res.status
-
-            if (status >= 400 && status < 500) {
-                switch (status) {
-                    case 401:
-                        tMessage = 'loginFailed'
-                        break
-                    case 403:
-                        tMessage = 'authenticationRequired'
-                        vm.$router.push('/login')
-                        break
-                    case 404:
-                        tMessage = 'notFound'
-                        break
-                    default:
-                        message = res.data.error.message
-                }
-            } else if (status >= 500) {
-                tMessage = 'internalServerError'
-            }
-        } else {
-            tMessage = 'somethingWentWrong'
-            console.error(err)
-        }
-
-        if (tMessage) {
-            message = vm.$t(tMessage)
-        }
-        vm.$q.notify({
-            message,
-            type: 'negative'
-        })
-    }
-}
diff --git a/src/components/common/FetchData.vue b/src/components/common/FetchData.vue
new file mode 100644
index 00000000..26ec77a4
--- /dev/null
+++ b/src/components/common/FetchData.vue
@@ -0,0 +1,66 @@
+<script setup>
+import { onMounted, inject } from 'vue';
+
+const $props = defineProps({
+    autoLoad: {
+        type: Boolean,
+        default: false
+    },
+    url: {
+        type: String,
+        default: ''
+    },
+    filter: {
+        type: Object,
+        default: null
+    },
+    where: {
+        type: Object,
+        default: null
+    },
+    sortBy: {
+        type: String,
+        default: ''
+    },
+    limit: {
+        type: [String, Number],
+        default: ''
+    },
+    params: {
+        type: Object,
+        default: null
+    }
+});
+
+const emit = defineEmits(['onFetch']);
+const api = inject('api');
+
+defineExpose({ fetch });
+
+onMounted(async () => {
+    if ($props.autoLoad) {
+        await fetch();
+    }
+});
+
+async function fetch(fetchFilter = {}) {
+    try {
+        const filter = { ...fetchFilter, ...$props.filter }; // eslint-disable-line vue/no-dupe-keys
+        if ($props.where && !fetchFilter.where) filter.where = $props.where;
+        if ($props.sortBy) filter.order = $props.sortBy;
+        if ($props.limit) filter.limit = $props.limit;
+
+        const { data } = await api.get($props.url, {
+            params: { filter: JSON.stringify(filter), ...$props.params }
+        });
+
+        emit('onFetch', data);
+        return data;
+    } catch (e) {
+        //
+    }
+}
+</script>
+<template>
+    <template></template>
+</template>
diff --git a/src/components/common/FormModel.vue b/src/components/common/FormModel.vue
new file mode 100644
index 00000000..fc5d1217
--- /dev/null
+++ b/src/components/common/FormModel.vue
@@ -0,0 +1,231 @@
+<script setup>
+import { ref, inject, onMounted, computed, Teleport, watch } from 'vue';
+import { useI18n } from 'vue-i18n';
+
+import { useAppStore } from 'stores/app';
+import { storeToRefs } from 'pinia';
+import useNotify from 'src/composables/useNotify.js';
+
+const props = defineProps({
+    title: {
+        type: String,
+        default: ''
+    },
+    table: {
+        type: String,
+        default: ''
+    },
+    schema: {
+        type: String,
+        default: ''
+    },
+    // Objeto con los datos iniciales del form, si este objeto es definido, no se ejecuta la query fetch
+    formInitialData: {
+        type: Object,
+        default: () => {}
+    },
+    autoLoad: {
+        type: Boolean,
+        default: true
+    },
+    defaultActions: {
+        type: Boolean,
+        default: true
+    },
+    showBottomActions: {
+        type: Boolean,
+        default: false
+    },
+    saveFn: {
+        type: Function,
+        default: null
+    },
+    separationBetweenInputs: {
+        type: String,
+        default: 'xs'
+    },
+    url: {
+        type: String,
+        default: ''
+    },
+    urlUpdate: {
+        type: String,
+        default: null
+    },
+    urlCreate: {
+        type: String,
+        default: null
+    },
+    observeFormChanges: {
+        type: Boolean,
+        default: true,
+        description:
+            'Esto se usa principalmente para permitir guardar sin hacer cambios (Útil para la feature de clonar ya que en este caso queremos poder guardar de primeras)'
+    },
+    mapper: {
+        type: Function,
+        default: null
+    },
+    filter: {
+        type: Object,
+        default: null
+    }
+});
+
+const emit = defineEmits(['onDataSaved', 'onDataFetched']);
+const api = inject('api');
+const { t } = useI18n();
+const { notify } = useNotify();
+const appStore = useAppStore();
+const { isHeaderMounted } = storeToRefs(appStore);
+
+const isLoading = ref(false);
+const formData = ref({});
+const formModelRef = ref(null);
+const hasChanges = ref(!props.observeFormChanges);
+const isResetting = ref(false);
+const originalData = ref(null);
+
+const separationBetweenInputs = computed(() => {
+    return `q-gutter-y-${props.separationBetweenInputs}`;
+});
+
+const onSubmitSuccess = () => {
+    emit('onDataSaved');
+    notify(t('dataSaved'), 'positive');
+};
+
+onMounted(async () => {
+    if (!props.formInitialData) {
+        if (props.autoLoad && props.url) await fetch();
+        originalData.value = { ...formData.value };
+    } else {
+        formData.value = { ...props.formInitialData };
+        originalData.value = { ...props.formInitialData };
+    }
+
+    if (props.observeFormChanges) {
+        watch(
+            () => formData.value,
+            (newVal, oldVal) => {
+                if (!oldVal) return;
+                hasChanges.value =
+                    !isResetting.value &&
+                    JSON.stringify(newVal) !==
+                        JSON.stringify(originalData.value);
+                isResetting.value = false;
+            },
+            { deep: true }
+        );
+    }
+});
+
+async function fetch() {
+    try {
+        let { data } = await api.get(props.url, {
+            params: { filter: JSON.stringify(props.filter) }
+        });
+        if (Array.isArray(data)) data = data[0] ?? {};
+        formData.value = { ...data };
+        emit('onDataFetched', formData.value);
+    } catch (e) {
+        throw e;
+    }
+}
+
+const submitForm = async evt => {
+    const isFormValid = await formModelRef.value.validate();
+    if (isFormValid) await save(evt);
+};
+
+async function save() {
+    if (props.observeFormChanges && !hasChanges.value)
+        return notify('noChanges', 'negative');
+
+    isLoading.value = true;
+    try {
+        const body = props.mapper
+            ? props.mapper(formData.value, originalData.value)
+            : formData.value;
+        const method = props.urlCreate ? 'post' : 'patch';
+        const url = props.urlCreate || props.urlUpdate || props.url;
+
+        await Promise.resolve(
+            props.saveFn ? props.saveFn(body) : api[method](url, body)
+        );
+
+        onSubmitSuccess();
+        hasChanges.value = false;
+    } finally {
+        isLoading.value = false;
+    }
+}
+
+defineExpose({
+    formData,
+    submitForm
+});
+</script>
+
+<template>
+    <QCard class="form-container" v-bind="$attrs">
+        <QForm ref="formModelRef" class="form" :class="separationBetweenInputs">
+            <span v-if="title" class="text-h6 text-bold">
+                {{ title }}
+            </span>
+            <slot name="form" :data="formData" />
+            <slot name="extraForm" :data="formData" />
+            <component
+                v-if="isHeaderMounted"
+                :is="showBottomActions ? 'div' : Teleport"
+                to="#actions"
+                class="flex row justify-end q-gutter-x-sm"
+                :class="{ 'q-mt-md': showBottomActions }"
+            >
+                <QBtn
+                    v-if="defaultActions && showBottomActions"
+                    :label="t('cancel')"
+                    :icon="showBottomActions ? undefined : 'check'"
+                    rounded
+                    no-caps
+                    flat
+                    v-close-popup
+                >
+                    <QTooltip>{{ t('cancel') }}</QTooltip>
+                </QBtn>
+                <QBtn
+                    v-if="defaultActions"
+                    :label="t('save')"
+                    :icon="showBottomActions ? undefined : 'check'"
+                    rounded
+                    no-caps
+                    flat
+                    :loading="isLoading"
+                    :disabled="!showBottomActions && !hasChanges"
+                    @click="submitForm()"
+                    data-cy="formModelDefaultSaveButton"
+                >
+                    <QTooltip>{{ t('save') }}</QTooltip>
+                </QBtn>
+                <slot name="actions" :data="formData" />
+            </component>
+        </QForm>
+    </QCard>
+</template>
+
+<style lang="scss" scoped>
+.form-container {
+    width: 100%;
+    height: max-content;
+    padding: 32px;
+    max-width: 544px;
+    display: flex;
+    justify-content: center;
+}
+
+.form {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+}
+</style>
diff --git a/src/components/common/VnInput.vue b/src/components/common/VnInput.vue
index 508ce132..f20754ee 100644
--- a/src/components/common/VnInput.vue
+++ b/src/components/common/VnInput.vue
@@ -1,15 +1,20 @@
 <script setup>
-import { computed, ref } from 'vue';
+import { computed, ref, useAttrs, nextTick } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { useRequired } from 'src/composables/useRequired';
 
+const $attrs = useAttrs();
+const { isRequired, requiredFieldRule } = useRequired($attrs);
+const { t } = useI18n();
 const emit = defineEmits([
     'update:modelValue',
     'update:options',
     'keyup.enter',
-    'remove'
+    'remove',
+    'blur'
 ]);
 
-const props = defineProps({
+const $props = defineProps({
     modelValue: {
         type: [String, Number],
         default: null
@@ -25,24 +30,43 @@ const props = defineProps({
     clearable: {
         type: Boolean,
         default: true
+    },
+    emptyToNull: {
+        type: Boolean,
+        default: true
+    },
+    insertable: {
+        type: Boolean,
+        default: false
+    },
+    maxlength: {
+        type: Number,
+        default: null
+    },
+    uppercase: {
+        type: Boolean,
+        default: false
     }
 });
 
-const { t } = useI18n();
-const requiredFieldRule = val => !!val || t('globals.fieldRequired');
 const vnInputRef = ref(null);
 const value = computed({
     get() {
-        return props.modelValue;
+        return $props.modelValue;
     },
     set(value) {
+        if ($props.emptyToNull && value === '') value = null;
         emit('update:modelValue', value);
     }
 });
 const hover = ref(false);
 const styleAttrs = computed(() => {
-    return props.isOutlined
-        ? { dense: true, outlined: true, rounded: true }
+    return $props.isOutlined
+        ? {
+              dense: true,
+              outlined: true,
+              rounded: true
+          }
         : {};
 });
 
@@ -51,66 +75,119 @@ const focus = () => {
 };
 
 defineExpose({
-    focus
+    focus,
+    vnInputRef
 });
 
-const inputRules = [
+const mixinRules = [
+    requiredFieldRule,
+    ...($attrs.rules ?? []),
     val => {
+        const { maxlength } = vnInputRef.value;
+        if (maxlength && +val.length > maxlength)
+            return t(`maxLength`, { value: maxlength });
         const { min, max } = vnInputRef.value.$attrs;
-        if (min >= 0) {
+        if (!min) return null;
+        if (min >= 0)
             if (Math.floor(val) < min) return t('inputMin', { value: min });
-        }
+        if (!max) return null;
         if (max > 0) {
             if (Math.floor(val) > max) return t('inputMax', { value: max });
         }
     }
 ];
+
+const handleKeydown = e => {
+    if (e.key === 'Backspace') return;
+
+    if ($props.insertable && e.key.match(/[0-9]/)) {
+        handleInsertMode(e);
+    }
+};
+
+const handleInsertMode = e => {
+    e.preventDefault();
+    const input = e.target;
+    const cursorPos = input.selectionStart;
+    const { maxlength } = vnInputRef.value;
+    let currentValue = value.value;
+    if (!currentValue) currentValue = e.key;
+    const newValue = e.key;
+    if (newValue && !isNaN(newValue) && cursorPos < maxlength) {
+        value.value =
+            currentValue.substring(0, cursorPos) +
+            newValue +
+            currentValue.substring(cursorPos + 1);
+    }
+    nextTick(() => {
+        input.setSelectionRange(cursorPos + 1, cursorPos + 1);
+    });
+};
+
+const handleUppercase = () => {
+    value.value = value.value?.toUpperCase() || '';
+};
 </script>
 
 <template>
-    <div
-        :rules="$attrs.required ? [requiredFieldRule] : null"
-        @mouseover="hover = true"
-        @mouseleave="hover = false"
-    >
+    <div @mouseover="hover = true" @mouseleave="hover = false">
         <QInput
             ref="vnInputRef"
             v-model="value"
             v-bind="{ ...$attrs, ...styleAttrs }"
             :type="$attrs.type"
-            :class="{ required: $attrs.required }"
+            :class="{ required: isRequired }"
+            @keyup.enter="emit('keyup.enter')"
+            @blur="emit('blur')"
+            @keydown="handleKeydown"
             :clearable="false"
-            :rules="inputRules"
+            :rules="mixinRules"
             :lazy-rules="true"
             hide-bottom-space
-            @keyup.enter="emit('keyup.enter')"
+            :data-cy="$attrs['data-cy'] ?? $attrs.label + '_input'"
         >
-            <template
-                v-if="$slots.prepend"
-                #prepend
-            >
+            <template #prepend v-if="$slots.prepend">
                 <slot name="prepend" />
             </template>
             <template #append>
-                <slot
-                    v-if="$slots.append && !$attrs.disabled"
-                    name="append"
-                />
                 <QIcon
-                    v-if="hover && value && !$attrs.disabled && props.clearable"
                     name="close"
                     size="xs"
+                    :style="{
+                        visibility:
+                            hover &&
+                            value &&
+                            !$attrs.disabled &&
+                            !$attrs.readonly &&
+                            $props.clearable
+                                ? 'visible'
+                                : 'hidden'
+                    }"
                     @click="
                         () => {
                             value = null;
+                            vnInputRef.focus();
                             emit('remove');
                         }
                     "
-                />
+                ></QIcon>
+
                 <QIcon
-                    v-if="info"
-                    name="info"
+                    name="match_case"
+                    size="xs"
+                    v-if="
+                        !$attrs.disabled && !$attrs.readonly && $props.uppercase
+                    "
+                    @click="handleUppercase"
+                    class="uppercase-icon"
                 >
+                    <QTooltip>
+                        {{ t('Convert to uppercase') }}
+                    </QTooltip>
+                </QIcon>
+
+                <slot name="append" v-if="$slots.append && !$attrs.disabled" />
+                <QIcon v-if="info" name="info">
                     <QTooltip max-width="350px">
                         {{ info }}
                     </QTooltip>
@@ -120,20 +197,44 @@ const inputRules = [
     </div>
 </template>
 
+<style>
+.uppercase-icon {
+    transition:
+        color 0.3s,
+        transform 0.2s;
+    cursor: pointer;
+}
+
+.uppercase-icon:hover {
+    color: #ed9937;
+    transform: scale(1.2);
+}
+</style>
+
 <i18n lang="yaml">
 en-US:
     inputMin: Must be more than {value}
+    maxLength: The value exceeds {value} characters
     inputMax: Must be less than {value}
+    Convert to uppercase: Convert to uppercase
 es-ES:
     inputMin: Debe ser mayor a {value}
+    maxLength: El valor excede los {value} carácteres
     inputMax: Debe ser menor a {value}
+    Convert to uppercase: Convertir a mayúsculas
 ca-ES:
     inputMin: Ha de ser més gran que {value}
-    inputMax: Ha de ser menys que {value}
+    maxLength: El valor excedeix els {value} caràcters
+    inputMax: Ha de ser menor que {value}
+    Convert to uppercase: Convertir a majúscules
 fr-FR:
     inputMin: Doit être supérieur à {value}
-    inputMax: Doit être supérieur à {value}
+    maxLength: La valeur dépasse {value} caractères
+    inputMax: Doit être inférieur à {value}
+    Convert to uppercase: Convertir en majuscules
 pt-PT:
     inputMin: Deve ser maior que {value}
-    inputMax: Deve ser maior que {value}
+    maxLength: O valor excede {value} caracteres
+    inputMax: Deve ser menor que {value}
+    Convert to uppercase: Converter para maiúsculas
 </i18n>
diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue
index c4a3e1df..65dbb2f2 100644
--- a/src/components/common/VnSelect.vue
+++ b/src/components/common/VnSelect.vue
@@ -67,7 +67,7 @@ const $props = defineProps({
 });
 
 const { t } = useI18n();
-const requiredFieldRule = val => val ?? t('globals.fieldRequired');
+const requiredFieldRule = val => val ?? t('fieldRequired');
 
 const { optionLabel, optionValue, options } = toRefs($props);
 const myOptions = ref([]);
diff --git a/src/composables/useRequired.js b/src/composables/useRequired.js
new file mode 100644
index 00000000..a77345b5
--- /dev/null
+++ b/src/composables/useRequired.js
@@ -0,0 +1,17 @@
+import { useI18n } from 'vue-i18n';
+
+export function useRequired($attrs) {
+    const { t } = useI18n();
+
+    const isRequired =
+        typeof $attrs['required'] === 'boolean'
+            ? $attrs['required']
+            : Object.keys($attrs).includes('required');
+    const requiredFieldRule = val =>
+        isRequired ? !!val || t('fieldRequired') : null;
+
+    return {
+        isRequired,
+        requiredFieldRule
+    };
+}
diff --git a/src/i18n/ca-ES/index.js b/src/i18n/ca-ES/index.js
index 456795a5..48d214e0 100644
--- a/src/i18n/ca-ES/index.js
+++ b/src/i18n/ca-ES/index.js
@@ -134,6 +134,8 @@ export default {
     introduceSearchTerm: 'Introdueix un terme de cerca',
     noOrdersFound: `No s'han trobat comandes`,
     send: 'Enviar',
+    fieldRequired: 'Aquest camp és obligatori',
+    noChanges: 'No s’han fet canvis',
     // Image related translations
     'Cant lock cache': 'No es pot bloquejar la memòria cau',
     'Bad file format': 'Format de fitxer no reconegut',
diff --git a/src/i18n/en-US/index.js b/src/i18n/en-US/index.js
index 3e21b66d..80107dd5 100644
--- a/src/i18n/en-US/index.js
+++ b/src/i18n/en-US/index.js
@@ -167,6 +167,8 @@ export default {
     introduceSearchTerm: 'Enter a search term',
     noOrdersFound: 'No orders found',
     send: 'Send',
+    fieldRequired: 'Field required',
+    noChanges: 'No changes',
     // Image related translations
     'Cant lock cache': 'The cache could not be blocked',
     'Bad file format': 'Unrecognized file format',
diff --git a/src/i18n/es-ES/index.js b/src/i18n/es-ES/index.js
index 5034ff70..15639c2d 100644
--- a/src/i18n/es-ES/index.js
+++ b/src/i18n/es-ES/index.js
@@ -166,6 +166,8 @@ export default {
     introduceSearchTerm: 'Introduce un término de búsqueda',
     noOrdersFound: 'No se encontrado pedidos',
     send: 'Enviar',
+    fieldRequired: 'Campo requerido',
+    noChanges: 'No se han hecho cambios',
     // Image related translations
     'Cant lock cache': 'La caché no pudo ser bloqueada',
     'Bad file format': 'Formato de archivo no reconocido',
diff --git a/src/i18n/fr-FR/index.js b/src/i18n/fr-FR/index.js
index d30dee52..2eea9a38 100644
--- a/src/i18n/fr-FR/index.js
+++ b/src/i18n/fr-FR/index.js
@@ -134,6 +134,8 @@ export default {
     introduceSearchTerm: 'Entrez un terme de recherche',
     noOrdersFound: 'Aucune commande trouvée',
     send: 'Envoyer',
+    fieldRequired: 'Champ obligatoire',
+    noChanges: 'Aucun changement',
     // Image related translations
     'Cant lock cache': "Le cache n'a pas pu être verrouillé",
     'Bad file format': 'Format de fichier non reconnu',
diff --git a/src/i18n/pt-PT/index.js b/src/i18n/pt-PT/index.js
index 39bbe5de..460335e0 100644
--- a/src/i18n/pt-PT/index.js
+++ b/src/i18n/pt-PT/index.js
@@ -133,6 +133,8 @@ export default {
     introduceSearchTerm: 'Digite um termo de pesquisa',
     noOrdersFound: 'Nenhum pedido encontrado',
     send: 'Enviar',
+    fieldRequired: 'Campo obrigatório',
+    noChanges: 'Sem alterações',
     // Image related translations
     'Cant lock cache': 'O cache não pôde ser bloqueado',
     'Bad file format': 'Formato de arquivo inválido',
diff --git a/src/pages/Account/AccountConfig.vue b/src/pages/Account/AccountConfig.vue
index 0aec3240..edbb181e 100644
--- a/src/pages/Account/AccountConfig.vue
+++ b/src/pages/Account/AccountConfig.vue
@@ -3,8 +3,9 @@ import { ref, inject, onMounted, computed } from 'vue';
 import { useI18n } from 'vue-i18n';
 import VnInput from 'src/components/common/VnInput.vue';
 import VnSelect from 'src/components/common/VnSelect.vue';
-import VnForm from 'src/components/common/VnForm.vue';
 import ChangePasswordForm from 'src/components/ui/ChangePasswordForm.vue';
+import FormModel from 'src/components/common/FormModel.vue';
+import useNotify from 'src/composables/useNotify.js';
 
 import { useUserStore } from 'stores/user';
 import { useAppStore } from 'stores/app';
@@ -12,54 +13,76 @@ import { storeToRefs } from 'pinia';
 
 const userStore = useUserStore();
 const { t } = useI18n();
-const jApi = inject('jApi');
+const api = inject('api');
 const appStore = useAppStore();
 const { isHeaderMounted } = storeToRefs(appStore);
 const { user } = storeToRefs(userStore);
+const { notify } = useNotify();
 
 const vnFormRef = ref(null);
-const vnFormRef2 = ref(null);
 const changePasswordFormDialog = ref(null);
 const showChangePasswordForm = ref(false);
 const langOptions = ref([]);
-const pks = computed(() => ({ id: userStore?.user?.id }));
-const fetchConfigDataSql = {
-    query: `
-        SELECT u.id, u.name, u.email, u.nickname,
-        u.lang, c.isToBeMailed, c.id clientFk
-        FROM account.myUser u
-        LEFT JOIN myClient c
-        ON u.id = c.id`,
-    params: {}
-};
+const formInitialData = ref({});
+const showForm = ref(false);
 
-const fetchLanguagesSql = async () => {
+const fetchLanguages = async () => {
     try {
-        const data = await jApi.query(
-            'SELECT code, name FROM language WHERE isActive'
-        );
+        const filter = { fields: ['code', 'name'], where: { isActive: true } };
+        const { data } = await api.get('/languages', {
+            params: { filter: JSON.stringify(filter) }
+        });
         langOptions.value = data;
     } catch (error) {
         console.error(error);
     }
 };
 
+const fetchFormInitialData = async () => {
+    try {
+        const filter = {
+            where: { id: user?.value?.id },
+            fields: ['id', 'name', 'isToBeMailed'],
+            include: {
+                relation: 'user',
+                scope: {
+                    fields: ['nickname', 'lang', 'email']
+                }
+            }
+        };
+        const { data } = await api.get('/Clients', {
+            params: { filter: JSON.stringify(filter) }
+        });
+
+        const { user: userData, ...restOfData } = data[0];
+
+        formInitialData.value = {
+            ...restOfData,
+            nickname: userData?.nickname,
+            lang: userData?.lang,
+            email: userData?.email
+        };
+    } catch (error) {
+        console.error(error);
+    } finally {
+        showForm.value = true;
+    }
+};
+
 const updateUserNickname = async nickname => {
     try {
-        await vnFormRef.value.submit();
+        await submitAccountData(nickname);
+        await submitNickname(nickname);
         user.value.nickname = nickname;
     } catch (error) {
         console.error(error);
     }
 };
 
-const formatMailData = data => {
-    data.isToBeMailed = Boolean(data.isToBeMailed);
-};
-
 const updateConfigLang = async lang => {
     try {
-        await vnFormRef.value.submit();
+        if (!lang) return;
+        await submitAccountData({ lang });
         userStore.updateUserLang(lang);
         const siteLocaleLang = appStore.localeOptions.find(
             locale => locale.value === lang
@@ -70,7 +93,32 @@ const updateConfigLang = async lang => {
     }
 };
 
-onMounted(() => fetchLanguagesSql());
+const submitAccountData = async data => {
+    try {
+        const params = {
+            ...data
+        };
+        await api.patch(`/VnUsers/${user?.value?.id}`, params);
+        notify(t('dataSaved'), 'positive');
+    } catch (error) {
+        console.error(error);
+    }
+};
+
+const submitIsToBeMailed = async isToBeMailed => {
+    try {
+        const payload = { isToBeMailed };
+        await api.patch(`/Clients/${user.value.id}`, payload);
+        notify(t('dataSaved'), 'positive');
+    } catch (error) {
+        console.error(error);
+    }
+};
+
+onMounted(async () => {
+    fetchLanguages();
+    fetchFormInitialData();
+});
 </script>
 
 <template>
@@ -92,14 +140,14 @@ onMounted(() => fetchLanguagesSql());
                     @click="showChangePasswordForm = true"
                 />
             </Teleport>
-            <VnForm
+            <FormModel
+                v-if="showForm"
                 ref="vnFormRef"
+                :save-fn="submitFormFn"
+                :form-initial-data="formInitialData"
                 :title="t('personalInformation')"
-                :fetch-form-data-sql="fetchConfigDataSql"
-                :pks="pks"
-                table="myUser"
-                schema="account"
-                :default-actions="false"
+                :show-bottom-actions="false"
+                :defaultActions="false"
             >
                 <template #form="{ data }">
                     <VnInput
@@ -111,14 +159,16 @@ onMounted(() => fetchLanguagesSql());
                     <VnInput
                         v-model="data.email"
                         :label="t('email')"
-                        @keyup.enter="vnFormRef.submit()"
-                        @blur="vnFormRef.submit()"
+                        @keyup.enter="submitAccountData({ email: data.email })"
+                        @blur="submitAccountData({ email: data.email })"
                     />
                     <VnInput
                         v-model="data.nickname"
                         :label="t('nickname')"
-                        @keyup.enter="updateUserNickname(data.nickname)"
-                        @blur="updateUserNickname(data.nickname)"
+                        @keyup.enter="
+                            updateUserNickname({ nickname: data.nickname })
+                        "
+                        @blur="updateUserNickname({ nickname: data.nickname })"
                         data-cy="configViewNickname"
                     />
                     <VnSelect
@@ -130,30 +180,17 @@ onMounted(() => fetchLanguagesSql());
                         @update:model-value="updateConfigLang(data.lang)"
                         data-cy="configViewLang"
                     />
+                    <QCheckbox
+                        v-model="data.isToBeMailed"
+                        :label="t('isToBeMailed')"
+                        :toggle-indeterminate="false"
+                        @update:model-value="
+                            submitIsToBeMailed(data.isToBeMailed)
+                        "
+                        dense
+                    />
                 </template>
-                <template #extraForm>
-                    <VnForm
-                        class="no-form-container q-mt-md"
-                        ref="vnFormRef2"
-                        :pks="pks"
-                        table="myClient"
-                        schema="hedera"
-                        :fetch-form-data-sql="fetchConfigDataSql"
-                        :default-actions="false"
-                        @on-data-fetched="$event => formatMailData($event)"
-                    >
-                        <template #form="{ data }">
-                            <QCheckbox
-                                v-model="data.isToBeMailed"
-                                :label="t('isToBeMailed')"
-                                :toggle-indeterminate="false"
-                                @update:model-value="vnFormRef2.submit()"
-                                dense
-                            />
-                        </template>
-                    </VnForm>
-                </template>
-            </VnForm>
+            </FormModel>
         </QPage>
         <QDialog
             ref="changePasswordFormDialog"
diff --git a/src/pages/Account/AddressDetails.vue b/src/pages/Account/AddressDetails.vue
index 9e4210d7..d60684ad 100644
--- a/src/pages/Account/AddressDetails.vue
+++ b/src/pages/Account/AddressDetails.vue
@@ -5,32 +5,26 @@ import { useRouter, useRoute } from 'vue-router';
 
 import VnInput from 'src/components/common/VnInput.vue';
 import VnSelect from 'src/components/common/VnSelect.vue';
-import VnForm from 'src/components/common/VnForm.vue';
+import FormModel from 'src/components/common/FormModel.vue';
 
+import { useUserStore } from 'stores/user';
 import { useAppStore } from 'stores/app';
 import { storeToRefs } from 'pinia';
 
 const router = useRouter();
 const route = useRoute();
 const { t } = useI18n();
-const jApi = inject('jApi');
+const api = inject('api');
 const appStore = useAppStore();
+const userStore = useUserStore();
 const { isHeaderMounted } = storeToRefs(appStore);
 
 const vnFormRef = ref(null);
 const countriesOptions = ref([]);
 const provincesOptions = ref([]);
-const pks = { id: route.params.id };
 const isEditMode = route.params.id !== '0';
-const fetchAddressDataSql = {
-    query: `
-        SELECT a.id, a.street, a.nickname, a.city, a.postalCode, a.provinceFk, p.countryFk
-        FROM myAddress a
-        LEFT JOIN vn.province p ON p.id = a.provinceFk
-        WHERE a.id = #address
-    `,
-    params: { address: route.params.id }
-};
+const editAddressData = ref(null);
+const showForm = ref(false);
 
 watch(
     () => vnFormRef?.value?.formData?.countryFk,
@@ -40,23 +34,49 @@ watch(
 const goBack = () => router.push({ name: 'addressesList' });
 
 const getCountries = async () => {
-    countriesOptions.value = await jApi.query(
-        `SELECT id, name FROM vn.country
-        ORDER BY name`
-    );
+    const filter = { fields: ['id', 'name'], order: 'name' };
+    const { data } = await api.get('Countries', {
+        params: { filter: JSON.stringify(filter) }
+    });
+    countriesOptions.value = data;
 };
 
 const getProvinces = async countryFk => {
     if (!countryFk) return;
-    provincesOptions.value = await jApi.query(
-        `SELECT id, name FROM vn.province
-        WHERE countryFk = #id
-        ORDER BY name`,
-        { id: countryFk }
-    );
+
+    const filter = {
+        where: { countryFk },
+        fields: ['id', 'name'],
+        order: 'name'
+    };
+    const { data } = await api.get('Provinces', {
+        params: { filter: JSON.stringify(filter) }
+    });
+    provincesOptions.value = data;
 };
 
-onMounted(() => getCountries());
+const getAddressDetails = async () => {
+    const { data } = await api.get(`Addresses/${route.params.id}`);
+    if (!data) return;
+
+    const { nickname, street, city, postalCode, province, provinceFk } = data;
+    editAddressData.value = {
+        nickname,
+        street,
+        city,
+        postalCode,
+        countryFk: province?.countryFk,
+        provinceFk
+    };
+};
+
+onMounted(async () => {
+    if (isEditMode) {
+        await getAddressDetails();
+    }
+    getCountries();
+    showForm.value = true;
+});
 </script>
 
 <template>
@@ -74,42 +94,49 @@ onMounted(() => getCountries());
                 </QTooltip>
             </QBtn>
         </Teleport>
-        <VnForm
+        <FormModel
+            v-if="showForm"
             ref="vnFormRef"
-            :fetch-form-data-sql="fetchAddressDataSql"
-            :columns-to-ignore-update="['countryFk']"
-            :create-model-default="{
-                field: 'clientFk',
-                value: 'account.myUser_getId()'
-            }"
-            :pks="pks"
-            :is-edit-mode="isEditMode"
+            :url-create="
+                !isEditMode
+                    ? `Clients/${userStore?.user?.id}/createAddress`
+                    : ''
+            "
+            :url-update="
+                isEditMode
+                    ? `Clients/${userStore?.user?.id}/updateAddress/${route?.params?.id}`
+                    : ''
+            "
+            :form-initial-data="editAddressData"
             :title="t(isEditMode ? 'editAddress' : 'addAddress')"
-            table="myAddress"
-            schema="hedera"
             @on-data-saved="goBack()"
+            :show-bottom-actions="false"
         >
             <template #form="{ data }">
                 <VnInput
                     v-model="data.nickname"
                     :label="t('name')"
                     data-cy="addressFormNickname"
+                    required
                 />
                 <VnInput
                     v-model="data.street"
                     :label="t('address')"
                     data-cy="addressFormStreet"
+                    required
                 />
                 <VnInput
                     v-model="data.city"
                     :label="t('city')"
                     data-cy="addressFormCity"
+                    required
                 />
                 <VnInput
                     v-model="data.postalCode"
                     type="number"
                     :label="t('postalCode')"
                     data-cy="addressFormPostcode"
+                    required
                 />
                 <VnSelect
                     v-model="data.countryFk"
@@ -117,15 +144,17 @@ onMounted(() => getCountries());
                     :options="countriesOptions"
                     @update:model-value="data.provinceFk = null"
                     data-cy="addressFormCountry"
+                    required
                 />
                 <VnSelect
                     v-model="data.provinceFk"
                     :label="t('province')"
                     :options="provincesOptions"
                     data-cy="addressFormProvince"
+                    required
                 />
             </template>
-        </VnForm>
+        </FormModel>
     </QPage>
 </template>
 
diff --git a/src/pages/Account/AddressList.vue b/src/pages/Account/AddressList.vue
index a603715c..9939c926 100644
--- a/src/pages/Account/AddressList.vue
+++ b/src/pages/Account/AddressList.vue
@@ -5,19 +5,24 @@ import { useRouter } from 'vue-router';
 
 import CardList from 'src/components/ui/CardList.vue';
 import VnList from 'src/components/ui/VnList.vue';
+import FetchData from 'src/components/common/FetchData.vue';
 
 import useNotify from 'src/composables/useNotify.js';
 import { useVnConfirm } from 'src/composables/useVnConfirm.js';
 import { useAppStore } from 'stores/app';
 import { storeToRefs } from 'pinia';
+import { useUserStore } from 'stores/user';
 
 const router = useRouter();
 const jApi = inject('jApi');
+const api = inject('api');
 const { notify } = useNotify();
 const { t } = useI18n();
 const { openConfirmationModal } = useVnConfirm();
 const appStore = useAppStore();
+const userStore = useUserStore();
 const { isHeaderMounted } = storeToRefs(appStore);
+const fetchAddressesRef = ref(null);
 
 const addresses = ref([]);
 const defaultAddress = ref(null);
@@ -38,19 +43,6 @@ const getDefaultAddress = async () => {
     }
 };
 
-const getActiveAddresses = async () => {
-    try {
-        addresses.value = await jApi.query(
-            `SELECT a.id, a.nickname, p.name province, a.postalCode, a.city, a.street, a.isActive
-        FROM myAddress a
-        LEFT JOIN vn.province p ON p.id = a.provinceFk
-        WHERE a.isActive`
-        );
-    } catch (error) {
-        console.error('Error getting active addresses:', error);
-    }
-};
-
 const changeDefaultAddress = async () => {
     if (!clientId.value) return;
     await jApi.execQuery(
@@ -65,32 +57,46 @@ const changeDefaultAddress = async () => {
     notify(t('defaultAddressModified'), 'positive');
 };
 
-const removeAddress = async id => {
+async function removeAddress(address) {
     try {
-        await jApi.execQuery(
-            `START TRANSACTION;
-            UPDATE hedera.myAddress SET isActive = FALSE
-            WHERE ((id = #id));
-            SELECT isActive FROM hedera.myAddress WHERE ((id = #id));
-            COMMIT`,
+        await api.patch(
+            `/Clients/${userStore?.user?.id}/updateAddress/${address.id}`,
             {
-                id
+                ...address,
+                isActive: false
             }
         );
-        getActiveAddresses();
+        fetchAddressesRef.value.fetch();
         notify(t('dataSaved'), 'positive');
     } catch (error) {
         console.error('Error removing address:', error);
     }
-};
+}
 
 onMounted(async () => {
     getDefaultAddress();
-    getActiveAddresses();
 });
 </script>
 
 <template>
+    <FetchData
+        v-if="userStore?.user?.id"
+        ref="fetchAddressesRef"
+        url="Addresses"
+        :filter="{
+            where: { clientFk: userStore.user.id, isActive: true },
+            fields: [
+                'id',
+                'nickname',
+                'postalCode',
+                'city',
+                'street',
+                'isActive'
+            ]
+        }"
+        auto-load
+        @on-fetch="data => (addresses = data)"
+    />
     <Teleport v-if="isHeaderMounted" to="#actions">
         <QBtn
             :label="t('addAddress')"
@@ -145,7 +151,7 @@ onMounted(async () => {
                             openConfirmationModal(
                                 null,
                                 t('confirmDeleteAddress'),
-                                () => removeAddress(address.id)
+                                () => removeAddress(address)
                             )
                         "
                     >
diff --git a/src/pages/Cms/HomeView.vue b/src/pages/Cms/HomeView.vue
index 442dfbc9..7dff6165 100644
--- a/src/pages/Cms/HomeView.vue
+++ b/src/pages/Cms/HomeView.vue
@@ -1,16 +1,15 @@
 <script setup>
 import { ref, onMounted, inject } from 'vue';
 const jApi = inject('jApi');
+const api = inject('api');
 const news = ref([]);
 const showPreview = ref(false);
 const selectedImageSrc = ref('');
 
 const fetchData = async () => {
-    news.value = await jApi.query(
-        `SELECT title, text, image, id
-        FROM news
-        ORDER BY priority, created DESC`
-    );
+  const newsResponse = await api.get('News');
+
+  news.value = newsResponse.data;
 };
 
 const showImagePreview = src => {
diff --git a/src/stores/app.js b/src/stores/app.js
index 45077306..d5183dc8 100644
--- a/src/stores/app.js
+++ b/src/stores/app.js
@@ -32,7 +32,10 @@ export const useAppStore = defineStore('hedera', {
     }),
     actions: {
         async getMenuLinks() {
-            const sections = await jApi.query('SELECT * FROM myMenu');
+            const { data: sections } = await api.get('MyMenus');
+
+            if (!sections) return;
+
             const sectionMap = new Map();
             for (const section of sections) {
                 sectionMap.set(section.id, section);
diff --git a/src/stores/user.js b/src/stores/user.js
index 91278afa..8f170752 100644
--- a/src/stores/user.js
+++ b/src/stores/user.js
@@ -38,8 +38,8 @@ export const useUserStore = defineStore('user', () => {
                 router.push({ name: 'login' });
             }
         } else {
-            await fetchTokenConfig();
             await fetchUser();
+            await fetchTokenConfig();
             await supplantInit();
             startInterval();
         }
@@ -248,11 +248,10 @@ export const useUserStore = defineStore('user', () => {
 
     const fetchUser = async (userType = 'user') => {
         try {
-            const userData = await jApi.getObject(
-                'SELECT id, nickname, name, lang FROM account.myUser'
-            );
-            if (userType === 'user') mainUser.value = userData;
-            else supplantedUser.value = userData;
+            const userData = await api.get('VnUsers/getCurrentUserData');
+
+            if (userType === 'user') mainUser.value = userData.data;
+            else supplantedUser.value = userData.data;
         } catch (error) {
             console.error('Error fetching user: ', error);
         }
diff --git a/src/test/cypress/integration/config/AddresList.spec.js b/src/test/cypress/integration/config/AddresList.spec.js
index d8a39271..6b5ad682 100644
--- a/src/test/cypress/integration/config/AddresList.spec.js
+++ b/src/test/cypress/integration/config/AddresList.spec.js
@@ -46,15 +46,28 @@ describe('PendingOrders', () => {
             .should('contain', data.postcode);
     };
 
+    it('should fail if we enter a wrong postcode', () => {
+        cy.dataCy('newAddressBtn').should('exist');
+        cy.dataCy('newAddressBtn').click();
+        cy.dataCy('formModelDefaultSaveButton').should('exist');
+        cy.dataCy('formModelDefaultSaveButton').should('be.disabled');
+        const addressFormData = getRandomAddressFormData();
+        fillFormWithData(addressFormData);
+        cy.dataCy('formModelDefaultSaveButton').should('not.be.disabled');
+        cy.dataCy('formModelDefaultSaveButton').click();
+        cy.checkNotify('negative');
+    });
+
     it('should create a new address', () => {
         cy.dataCy('newAddressBtn').should('exist');
         cy.dataCy('newAddressBtn').click();
-        cy.dataCy('formDefaultSaveButton').should('exist');
-        cy.dataCy('formDefaultSaveButton').should('be.disabled');
+        cy.dataCy('formModelDefaultSaveButton').should('exist');
+        cy.dataCy('formModelDefaultSaveButton').should('be.disabled');
         const addressFormData = getRandomAddressFormData();
+        addressFormData.postcode = '46460'; // Usamos un postcode válido
         fillFormWithData(addressFormData);
-        cy.dataCy('formDefaultSaveButton').should('not.be.disabled');
-        cy.dataCy('formDefaultSaveButton').click();
+        cy.dataCy('formModelDefaultSaveButton').should('not.be.disabled');
+        cy.dataCy('formModelDefaultSaveButton').click();
         cy.checkNotify('positive', 'Datos guardados');
         verifyAddressCardData(addressFormData);
     });
@@ -71,9 +84,10 @@ describe('PendingOrders', () => {
         });
         // Fill form with new data
         const addressFormData = getRandomAddressFormData();
+        addressFormData.postcode = '46460'; // Usamos un postcode válido
         fillFormWithData(addressFormData);
-        cy.dataCy('formDefaultSaveButton').should('not.be.disabled');
-        cy.dataCy('formDefaultSaveButton').click();
+        cy.dataCy('formModelDefaultSaveButton').should('not.be.disabled');
+        cy.dataCy('formModelDefaultSaveButton').click();
         cy.checkNotify('positive', 'Datos guardados');
         verifyAddressCardData(addressFormData);
     });
diff --git a/src/test/cypress/integration/UserFlows.spec.js b/src/test/cypress/integration/flows/UserFlows.spec.js
similarity index 100%
rename from src/test/cypress/integration/UserFlows.spec.js
rename to src/test/cypress/integration/flows/UserFlows.spec.js
diff --git a/src/test/cypress/support/commands.js b/src/test/cypress/support/commands.js
index 2d4de848..cb22f86a 100644
--- a/src/test/cypress/support/commands.js
+++ b/src/test/cypress/support/commands.js
@@ -90,5 +90,6 @@ Cypress.Commands.add('setConfirmDialog', () => {
 });
 
 Cypress.Commands.add('checkNotify', (status, content) => {
-    cy.dataCy(`${status}Notify`).should('contain', content);
+    if (content) cy.dataCy(`${status}Notify`).should('contain', content);
+    else cy.dataCy(`${status}Notify`).should('exist');
 });