Compare commits

..

9 Commits

Author SHA1 Message Date
Jose Antonio Tubau b25d421131 Merge branch 'dev' into 8945-migrateFixedAssetsSection
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-05-08 07:06:16 +00:00
Jose Antonio Tubau 500f38cc21 refactor: refs #8945 add handlePhotoUpdated method to reload image after update
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-05-07 17:09:12 +02:00
Jose Antonio Tubau a2e8f332df refactor: refs #8945 reorganize imports and enhance confirmation dialog for unassigning invoices
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-05-07 10:59:16 +02:00
Jose Antonio Tubau 574bf7f771 refactor: refs #8945 add assignedInvoices localization and update Fixed Asset components
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-05-07 09:43:23 +02:00
Jose Antonio Tubau 3cdfb42386 test: refs #8945 add Cypress tests for Fixed Asset management
gitea/salix-front/pipeline/pr-dev There was a failure building this commit Details
2025-05-06 16:19:24 +02:00
Jose Antonio Tubau d9f7047239 refactor: refs #8945 update paths and error messages for Fixed Asset management 2025-05-06 16:18:48 +02:00
Jose Antonio Tubau f511832fe5 Merge branch 'dev' into 8945-migrateFixedAssetsSection
gitea/salix-front/pipeline/pr-dev This commit looks good Details
2025-05-05 15:58:14 +02:00
Jose Antonio Tubau ca0d90c07e feat: refs #8945 add Fixed Asset management components
gitea/salix-front/pipeline/pr-dev There was a failure building this commit Details
2025-05-05 15:04:58 +02:00
Jose Antonio Tubau 47bde21df5 feat: refs #8945 add fixed asset module with localization and routing 2025-05-05 15:04:06 +02:00
97 changed files with 2771 additions and 537 deletions

View File

@ -64,6 +64,5 @@ export default defineConfig({
...timeouts,
includeShadowDom: true,
waitForAnimations: true,
testIsolation: false,
},
});

View File

@ -7,7 +7,7 @@ import { QLayout } from 'quasar';
import mainShortcutMixin from './mainShortcutMixin';
import { useCau } from 'src/composables/useCau';
export default boot(({ app, router }) => {
export default boot(({ app }) => {
QForm.mixins = [qFormMixin];
QLayout.mixins = [mainShortcutMixin];
@ -22,14 +22,6 @@ export default boot(({ app, router }) => {
}
switch (response?.status) {
case 401:
if (!router.currentRoute.value.name.toLowerCase().includes('login')) {
message = 'errors.sessionExpired';
} else message = 'login.loginError';
break;
case 403:
if (!message || message.toLowerCase() === 'access denied')
message = 'errors.accessDenied';
case 422:
if (error.name == 'ValidationError')
message += ` "${responseError.details.context}.${Object.keys(

View File

@ -1,6 +1,6 @@
<script setup>
import axios from 'axios';
import { computed, ref, useAttrs, watch, nextTick } from 'vue';
import { computed, ref, useAttrs, watch } from 'vue';
import { useRouter, onBeforeRouteLeave } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
@ -42,15 +42,7 @@ const $props = defineProps({
},
dataRequired: {
type: Object,
default: () => ({}),
},
dataDefault: {
type: Object,
default: () => ({}),
},
insertOnLoad: {
type: Boolean,
default: false,
default: () => {},
},
defaultSave: {
type: Boolean,
@ -95,7 +87,6 @@ const formData = ref();
const saveButtonRef = ref(null);
const watchChanges = ref();
const formUrl = computed(() => $props.url);
const rowsContainer = ref(null);
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
@ -131,11 +122,9 @@ async function fetch(data) {
const rows = keyData ? data[keyData] : data;
resetData(rows);
emit('onFetch', rows);
$props.insertOnLoad && await insert();
return rows;
}
function resetData(data) {
if (!data) return;
if (data && Array.isArray(data)) {
@ -146,16 +135,9 @@ function resetData(data) {
formData.value = JSON.parse(JSON.stringify(data));
if (watchChanges.value) watchChanges.value(); //destroy watcher
watchChanges.value = watch(formData, (nVal) => {
hasChanges.value = false;
const filteredNewData = nVal.filter(row => !isRowEmpty(row) || row[$props.primaryKey]);
const filteredOriginal = originalData.value.filter(row => row[$props.primaryKey]);
const changes = getDifferences(filteredOriginal, filteredNewData);
hasChanges.value = !isEmpty(changes);
}, { deep: true });
watchChanges.value = watch(formData, () => (hasChanges.value = true), { deep: true });
}
async function reset() {
await fetch(originalData.value);
hasChanges.value = false;
@ -183,9 +165,7 @@ async function onSubmit() {
});
}
isLoading.value = true;
await saveChanges($props.saveFn ? formData.value : null);
}
async function onSubmitAndGo() {
@ -194,10 +174,6 @@ async function onSubmitAndGo() {
}
async function saveChanges(data) {
formData.value = formData.value.filter(row =>
row[$props.primaryKey] || !isRowEmpty(row)
);
if ($props.saveFn) {
$props.saveFn(data, getChanges);
isLoading.value = false;
@ -227,32 +203,14 @@ async function saveChanges(data) {
});
}
async function insert(pushData = { ...$props.dataRequired, ...$props.dataDefault }) {
formData.value = formData.value.filter(row => !isRowEmpty(row));
const lastRow = formData.value.at(-1);
const isLastRowEmpty = lastRow ? isRowEmpty(lastRow) : false;
if (formData.value.length && isLastRowEmpty) return;
const $index = formData.value.length ? formData.value.at(-1).$index + 1 : 0;
const nRow = Object.assign({ $index }, pushData);
formData.value.push(nRow);
const hasChange = Object.keys(nRow).some(key => !isChange(nRow, key));
if (hasChange) hasChanges.value = true;
async function insert(pushData = $props.dataRequired) {
const $index = formData.value.length
? formData.value[formData.value.length - 1].$index + 1
: 0;
formData.value.push(Object.assign({ $index }, pushData));
hasChanges.value = true;
}
function isRowEmpty(row) {
return Object.keys(row).every(key => isChange(row, key));
}
function isChange(row,key){
return !row[key] || key == '$index' || Object.hasOwn($props.dataRequired || {}, key);
}
async function remove(data) {
if (!data.length)
return quasar.notify({
@ -269,8 +227,10 @@ async function remove(data) {
newData = newData.filter(
(form) => !preRemove.some((index) => index == form.$index),
);
formData.value = newData;
hasChanges.value = JSON.stringify(removeIndexField(formData.value)) !== JSON.stringify(removeIndexField(originalData.value));
const changes = getChanges();
if (!changes.creates?.length && !changes.updates?.length)
hasChanges.value = false;
fetch(newData);
}
if (ids.length) {
quasar
@ -288,8 +248,9 @@ async function remove(data) {
newData = newData.filter((form) => !ids.some((id) => id == form[pk]));
fetch(newData);
});
} else {
reset();
}
emit('update:selected', []);
}
@ -300,7 +261,7 @@ function getChanges() {
const pk = $props.primaryKey;
for (const [i, row] of formData.value.entries()) {
if (!row[pk]) {
creates.push(Object.assign(row, { ...$props.dataRequired }));
creates.push(row);
} else if (originalData.value[i]) {
const data = getDifferences(originalData.value[i], row);
if (!isEmpty(data)) {
@ -326,33 +287,6 @@ function isEmpty(obj) {
return !Object.keys(obj).length;
}
function removeIndexField(data) {
if (Array.isArray(data)) {
return data.map(({ $index, ...rest }) => rest);
} else if (typeof data === 'object' && data !== null) {
const { $index, ...rest } = data;
return rest;
}
}
async function handleTab(event) {
event.preventDefault();
const { shiftKey, target } = event;
const focusableSelector = `tbody tr td:not(:first-child) :is(a, button, input, textarea, select, details):not([disabled])`;
const focusableElements = rowsContainer.value?.querySelectorAll(focusableSelector);
const currentIndex = Array.prototype.indexOf.call(focusableElements, target);
const index = shiftKey ? currentIndex - 1 : currentIndex + 1;
const isLast = target === focusableElements[focusableElements.length - 1];
const isFirst = currentIndex === 0;
if ((shiftKey && !isFirst) || (!shiftKey && !isLast))
focusableElements[index]?.focus();
else if (isLast) {
await insert();
await nextTick();
}
}
async function reload(params) {
const data = await vnPaginateRef.value.fetch(params);
fetch(data);
@ -378,14 +312,12 @@ watch(formUrl, async () => {
v-bind="$attrs"
>
<template #body v-if="formData">
<div ref="rowsContainer" @keydown.tab="handleTab">
<slot
name="body"
:rows="formData"
:validate="validate"
:filter="filter"
></slot>
</div>
</template>
</VnPaginate>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && hasSubToolbar">

View File

@ -181,10 +181,6 @@ const col = computed(() => {
newColumn.component = 'checkbox';
if ($props.default && !newColumn.component) newColumn.component = $props.default;
if (typeof newColumn.component !== 'string') {
newColumn.attrs = { ...newColumn.component.attrs, autofocus: $props.autofocus };
newColumn.event = { ...newColumn.component.event, ...$props?.eventHandlers };
}
return newColumn;
});

View File

@ -684,10 +684,8 @@ const handleHeaderSelection = (evt, data) => {
ref="CrudModelRef"
@on-fetch="
(...args) => {
if ($props.multiCheck.expand) {
selectAll = false;
selected = [];
}
emit('onFetch', ...args);
}
"

View File

@ -193,11 +193,11 @@ describe('CrudModel', () => {
});
it('should set originalData and formatData with data and generate watchChanges', async () => {
data = [{
data = {
name: 'Tony',
lastName: 'Stark',
age: 42,
}];
};
vm.resetData(data);

View File

@ -72,7 +72,7 @@ const getBankEntities = (data) => {
:acls="[{ model: 'BankEntity', props: '*', accessType: 'WRITE' }]"
:options="bankEntities"
hide-selected
option-label="bic"
option-label="name"
option-value="id"
v-model="bankEntityFk"
@update:model-value="$emit('updateBic', { iban, bankEntityFk })"

View File

@ -30,7 +30,7 @@ const onClick = async () => {
params: { filter: JSON.stringify(filter) },
};
try {
const { data } = await axios.get(props.url, params);
const { data } = axios.get(props.url, params);
rows.value = data;
} catch (error) {
const response = error.response;
@ -83,7 +83,7 @@ defineEmits(['update:selected', 'select:all']);
/>
<span
v-else
v-text="t('records', { rows: rows?.length ?? 0 })"
v-text="t('records', { rows: rows.length ?? 0 })"
/>
</QItemLabel>
</QItemSection>

View File

@ -264,7 +264,7 @@ function deleteDms(dmsFk) {
rows.value.splice(index, 1);
notify(t('globals.dataDeleted'), 'positive');
} catch (e) {
throw e;
notify(t('errorDmsDelete'), 'negative');
}
});
}
@ -435,9 +435,11 @@ defineExpose({
</style>
<i18n>
en:
errorDmsDelete: Error deleting the dms
contentTypesInfo: Allowed file types {allowedContentTypes}
The documentation is available in paper form: The documentation is available in paper form
es:
errorDmsSave: Error al eliminar el dms
contentTypesInfo: Tipos de archivo permitidos {allowedContentTypes}
Generate identifier for original file: Generar identificador para archivo original
Upload file: Subir fichero

View File

@ -1,49 +0,0 @@
<script setup>
import { ref, useTemplateRef } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAccountShortToStandard } from 'src/composables/useAccountShortToStandard';
import VnSelectDialog from './VnSelectDialog.vue';
import CreateNewExpenseForm from '../CreateNewExpenseForm.vue';
import FetchData from '../FetchData.vue';
const model = defineModel({ type: [String, Number, Object] });
const { t } = useI18n();
const expenses = ref([]);
const selectDialogRef = useTemplateRef('selectDialogRef');
async function autocompleteExpense(evt) {
const val = evt.target.value;
if (!val || isNaN(val)) return;
const lookup = expenses.value.find(({ id }) => id == useAccountShortToStandard(val));
if (selectDialogRef.value)
selectDialogRef.value.vnSelectDialogRef.vnSelectRef.toggleOption(lookup);
}
</script>
<template>
<VnSelectDialog
v-bind="$attrs"
ref="selectDialogRef"
v-model="model"
:options="expenses"
option-value="id"
:option-label="(x) => `${x.id}: ${x.name}`"
:filter-options="['id', 'name']"
:tooltip="t('Create a new expense')"
:acls="[{ model: 'Expense', props: '*', accessType: 'WRITE' }]"
@keydown.tab.prevent="autocompleteExpense"
>
<template #form>
<CreateNewExpenseForm @on-data-saved="$refs.expensesRef.fetch()" />
</template>
</VnSelectDialog>
<FetchData
ref="expensesRef"
url="Expenses"
auto-load
@on-fetch="(data) => (expenses = data)"
/>
</template>
<i18n>
es:
Create a new expense: Crear nuevo gasto
</i18n>

View File

@ -40,11 +40,4 @@ describe('VnBankDetail Component', () => {
await vm.autofillBic('ES1234567891324567891234');
expect(vm.bankEntityFk).toBe(null);
});
it('should not update bankEntityFk if IBAN country is not ES', async () => {
vm.bankEntities = bankEntities;
await vm.autofillBic('FR1420041010050500013M02606');
expect(vm.bankEntityFk).toBe(null);
});
});

View File

@ -35,7 +35,6 @@ const $props = defineProps({
selectType: { type: Boolean, default: false },
justInput: { type: Boolean, default: false },
goTo: { type: String, default: '' },
useUserRelation: { type: Boolean, default: true },
});
const { t } = useI18n();
@ -55,26 +54,6 @@ const defaultObservationType = computed(
let savedNote = false;
let originalText;
onBeforeRouteLeave((to, from, next) => {
if (
(newNote.text && !$props.justInput) ||
(newNote.text !== originalText && $props.justInput)
)
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('globals.unsavedPopup.title'),
message: t('globals.unsavedPopup.subtitle'),
promise: () => next(),
},
});
else next();
});
onMounted(() => {
nextTick(() => (componentIsRendered.value = true));
});
function handleClick(e) {
if (e.shiftKey && e.key === 'Enter') return;
if ($props.justInput) confirmAndUpdate();
@ -129,6 +108,22 @@ async function update() {
);
}
onBeforeRouteLeave((to, from, next) => {
if (
(newNote.text && !$props.justInput) ||
(newNote.text !== originalText && $props.justInput)
)
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('globals.unsavedPopup.title'),
message: t('globals.unsavedPopup.subtitle'),
promise: () => next(),
},
});
else next();
});
function fetchData([data]) {
newNote.text = data?.notes;
originalText = data?.notes;
@ -142,38 +137,16 @@ const handleObservationTypes = (data) => {
}
};
onMounted(() => {
nextTick(() => (componentIsRendered.value = true));
});
async function saveAndGo() {
savedNote = false;
await insert();
await savedNote;
router.push({ path: $props.goTo });
}
function getUserFilter() {
const newUserFilter = $props.userFilter ?? {};
const userInclude = {
relation: 'user',
scope: {
fields: ['id', 'nickname', 'name'],
},
};
if (newUserFilter.include) {
if (Array.isArray(newUserFilter.include)) {
newUserFilter.include.push(userInclude);
} else {
newUserFilter.include = [userInclude, newUserFilter.include];
}
} else {
newUserFilter.include = userInclude;
}
if ($props.useUserRelation) {
return {
...newUserFilter,
...$props.userFilter,
};
}
return $props.filter;
}
</script>
<template>
<Teleport
@ -269,7 +242,7 @@ function getUserFilter() {
:url="$props.url"
order="created DESC"
:limit="0"
:user-filter="getUserFilter()"
:user-filter="userFilter"
:filter="filter"
auto-load
ref="vnPaginateRef"
@ -288,15 +261,15 @@ function getUserFilter() {
<QCardSection horizontal>
<VnAvatar
:descriptor="false"
:worker-id="note.user?.id"
:worker-id="note.workerFk"
size="md"
:title="note.user?.nickname"
:title="note.worker?.user.nickname"
/>
<div class="full-width row justify-between q-pa-xs">
<div>
<VnUserLink
:name="`${note.user?.name}`"
:worker-id="note.user?.id"
:name="`${note.worker.user.name}`"
:worker-id="note.worker.id"
/>
<QBadge
class="q-ml-xs"

View File

@ -169,6 +169,7 @@ globals:
selectDocumentId: Select document id
document: Document
import: Import from existing
group: Group
pageTitles:
logIn: Login
addressEdit: Update address
@ -351,7 +352,9 @@ globals:
vehicleList: Vehicles
vehicle: Vehicle
entryPreAccount: Pre-account
fixedAsset: Fixed assets
management: Worker management
assignedInvoices: Assigned Invoices
unsavedPopup:
title: Unsaved changes will be lost
subtitle: Are you sure exit without saving?
@ -400,8 +403,6 @@ errors:
updateUserConfig: Error updating user config
tokenConfig: Error fetching token config
writeRequest: The requested operation could not be completed
sessionExpired: Your session has expired. Please log in again
accessDenied: Access denied
claimBeginningQuantity: Cannot import a line with a claimed quantity of 0
login:
title: Login

View File

@ -173,6 +173,7 @@ globals:
selectDocumentId: Seleccione el id de gestión documental
document: Documento
import: Importar desde existente
group: Grupo
pageTitles:
logIn: Inicio de sesión
addressEdit: Modificar consignatario
@ -354,7 +355,9 @@ globals:
vehicleList: Vehículos
vehicle: Vehículo
entryPreAccount: Precontabilizar
fixedAsset: Inmovilizados
management: Gestión de trabajadores
assignedInvoices: Facturas vinculadas
unsavedPopup:
title: Los cambios que no haya guardado se perderán
subtitle: ¿Seguro que quiere salir sin guardar?
@ -396,8 +399,6 @@ errors:
updateUserConfig: Error al actualizar la configuración de usuario
tokenConfig: Error al obtener configuración de token
writeRequest: No se pudo completar la operación solicitada
sessionExpired: Tu sesión ha expirado, por favor vuelve a iniciar sesión
accessDenied: Acceso denegado
claimBeginningQuantity: No se puede importar una linea sin una cantidad reclamada
login:
title: Inicio de sesión

View File

@ -138,7 +138,6 @@ const columns = computed(() => [
:filter="developmentsFilter"
ref="claimDevelopmentForm"
:data-required="{ claimFk: route.params.id }"
:insert-on-load="true"
v-model:selected="selected"
@save-changes="$router.push(`/claim/${route.params.id}/action`)"
:default-save="false"

View File

@ -1,9 +1,12 @@
<script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useState } from 'src/composables/useState';
import VnNotes from 'src/components/ui/VnNotes.vue';
const route = useRoute();
const state = useState();
const user = state.getUser();
const $props = defineProps({
id: { type: [Number, String], default: null },
@ -12,13 +15,24 @@ const $props = defineProps({
const claimId = computed(() => $props.id || route.params.id);
const claimFilter = {
fields: ['id', 'created', 'userFk', 'text'],
fields: ['id', 'created', 'workerFk', 'text'],
include: {
relation: 'worker',
scope: {
fields: ['id', 'firstName', 'lastName'],
include: {
relation: 'user',
scope: {
fields: ['id', 'nickname', 'name'],
},
},
},
},
};
const body = {
claimFk: claimId.value,
workerFk: user.value.id,
};
</script>
@ -29,9 +43,7 @@ const claimFilter = {
:add-note="$props.addNote"
:user-filter="claimFilter"
:filter="{ where: { claimFk: claimId } }"
:body="{
claimFk: claimId,
}"
:body="body"
v-bind="$attrs"
style="overflow-y: auto"
/>

View File

@ -1,15 +1,12 @@
<script setup>
import VnNotes from 'src/components/ui/VnNotes.vue';
import { useState } from 'src/composables/useState';
const state = useState();
const user = state.getUser();
</script>
<template>
<VnNotes
url="clientObservations"
:add-note="true"
:filter="{ where: { clientFk: $route.params.id } }"
:body="{ clientFk: $route.params.id, userFk: user.id }"
:body="{ clientFk: $route.params.id }"
style="overflow-y: auto"
:select-type="true"
required

View File

@ -76,7 +76,7 @@ const columns = computed(() => [
},
{
align: 'left',
name: 'userFk',
name: 'workerFk',
label: t('Author'),
tooltip: t('Worker who made the last observation'),
columnFilter: {
@ -155,7 +155,7 @@ function exprBuilder(param, value) {
return { [`c.${param}`]: value };
case 'payMethod':
return { [`c.payMethodFk`]: value };
case 'userFk':
case 'workerFk':
return { [`co.${param}`]: value };
case 'departmentFk':
return { [`c.${param}`]: value };
@ -229,10 +229,10 @@ function exprBuilder(param, value) {
<DepartmentDescriptorProxy :id="row.departmentFk" />
</span>
</template>
<template #column-userFk="{ row }">
<template #column-workerFk="{ row }">
<span class="link" @click.stop>
{{ row.workerName }}
<WorkerDescriptorProxy :id="row.userFk" />
<WorkerDescriptorProxy :id="row.workerFk" />
</span>
</template>
</VnTable>

View File

@ -73,7 +73,6 @@ const columns = computed(() => [
optionLabel: 'code',
options: companies.value,
},
orderBy: false,
},
{
name: 'warehouse',

View File

@ -0,0 +1,215 @@
<script setup>
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnAccountNumber from 'src/components/common/VnAccountNumber.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
import { toCurrency } from 'src/filters';
import FetchData from 'src/components/FetchData.vue';
const { t } = useI18n();
const route = useRoute();
const isNew = Boolean(!route.params.id);
const natureOptions = ref([]);
const prueba = ref();
</script>
<template>
<FetchData
url="Ppes/getNatures"
@on-fetch="(data) => (natureOptions = data)"
auto-load
/>
<VnSubToolbar v-if="isNew" />
<div class="q-pa-md">
<FormModel :url-update="`Ppes/${route.params.id}`" model="FixedAsset" auto-load>
<template #form="{ data }">
<VnRow>
<VnInput
:label="t('globals.description')"
v-model="data.description"
fill-input
/>
</VnRow>
<VnRow>
<VnSelect
url="companies"
:label="t('globals.company')"
v-model="data.companyFk"
option-value="id"
option-label="code"
hide-selected
/>
<VnSelect
:label="t('fixedAsset.nature')"
v-model="data.nature"
:options="natureOptions"
option-value="nature"
option-label="nature"
hide-selected
/>
<VnInputNumber
:label="t('fixedAsset.value')"
v-model="data.value"
fill-input
/>
</VnRow>
<VnRow>
<VnSelect
url="Ppes/getSubAccounts"
:label="t('fixedAsset.account')"
v-model="data.account"
option-value="code"
option-label="code"
hide-selected
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt?.description}`
}}</QItemLabel>
<QItemLabel caption>
{{ `#${scope.opt?.code}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect
url="Ppes/getEndowments"
:label="t('fixedAsset.endowment')"
v-model="data.endowment"
option-value="code"
option-label="code"
hide-selected
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt?.description}`
}}</QItemLabel>
<QItemLabel caption>
{{ `#${scope.opt?.code}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnAccountNumber
:label="t('fixedAsset.elementAccount')"
v-model="data.elementAccount"
fill-input
/>
</VnRow>
<VnRow>
<VnSelect
url="PpeLocations"
:label="t('fixedAsset.location')"
v-model="data.locationFk"
option-value="code"
:option-label="(value) => `${value.code} - ${value.description}`"
hide-selected
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt?.description}`
}}</QItemLabel>
<QItemLabel caption>
{{ `#${scope.opt?.code}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect
url="PpeGroups"
:label="t('globals.group')"
v-model="data.groupFk"
option-value="id"
option-label="description"
hide-selected
/>
</VnRow>
<VnRow>
<VnInputDate
:label="t('fixedAsset.firstAmortizated')"
v-model="data.firstAmortizated"
placeholder="dd-mm-aaaa"
fill-input
/>
<VnInputDate
:label="t('fixedAsset.lastAmortizated')"
v-model="data.lastAmortizated"
placeholder="dd-mm-aaaa"
fill-input
/>
</VnRow>
<VnRow>
<VnInputDate
:label="t('fixedAsset.finished')"
v-model="data.finished"
placeholder="dd-mm-aaaa"
fill-input
/>
<VnInputNumber
:label="t('fixedAsset.amortization')"
v-model="data.amortization"
fill-input
/>
<VnSelect
url="PpePlans"
:label="t('fixedAsset.plan')"
v-model="data.planFk"
:option-label="(value) => `${value.rate}% - ${value.days} days`"
hide-selected
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt?.rate}% - ${scope.opt?.days} days`
}}</QItemLabel>
<QItemLabel caption>
{{ `#${scope.opt?.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</VnRow>
<VnRow>
<VnInputDate
:label="t('fixedAsset.discharged')"
v-model="data.discharged"
placeholder="dd-mm-aaaa"
fill-input
/>
<VnInput
:label="t('fixedAsset.cause')"
v-model="data.cause"
fill-input
/>
</VnRow>
<VnRow>
<VnCheckbox
v-model="data.isInvestmentAsset"
:label="t('fixedAsset.isInvestmentAsset')"
/>
<VnCheckbox :label="t('fixedAsset.isDone')" v-model="data.isDone" />
</VnRow>
</template>
</FormModel>
</div>
</template>

View File

@ -0,0 +1,12 @@
<script setup>
import VnCard from 'components/common/VnCard.vue';
import FixedAssetDescriptor from 'pages/FixedAsset/Card/FixedAssetDescriptor.vue';
</script>
<template>
<VnCard
data-key="FixedAsset"
url="Ppes"
:descriptor="FixedAssetDescriptor"
:filter="{ where: { id: $route.params.id } }"
/>
</template>

View File

@ -0,0 +1,107 @@
<script setup>
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import CardDescriptor from 'src/components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import FixedAssetCard from './FixedAssetCard.vue';
import EditPictureForm from 'src/components/EditPictureForm.vue';
import VnImg from 'src/components/ui/VnImg.vue';
import FixedAssetDescriptorMenu from './FixedAssetDescriptorMenu.vue';
const route = useRoute();
const entityId = computed(() => {
return Number(props.id || route.params.id);
});
const props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
summary: {
type: Object,
default: null,
},
});
const image = ref(null);
const showEditPhotoForm = ref(false);
const toggleEditPictureForm = () => {
showEditPhotoForm.value = !showEditPhotoForm.value;
};
const handlePhotoUpdated = (evt = false) => {
image.value.reload(evt);
};
</script>
<template>
<CardDescriptor
v-bind="$attrs"
:id="entityId"
:card="FixedAssetCard"
title="description"
module="FixedAsset"
>
<template #before>
<div class="relative-position">
<VnImg
ref="image"
:id="parseInt(entityId)"
collection="fixedAsset"
resolution="520x520"
class="photo"
>
<template #error>
<div
class="absolute-full picture text-center q-pa-md flex flex-center"
>
<div>
<div
class="text-grey-5"
style="opacity: 0.4; font-size: 5vh"
>
<QIcon name="vn:claims" />
</div>
<div class="text-grey-5" style="opacity: 0.4">
{{ t('fixedAsset.imageNotFound') }}
</div>
</div>
</div>
</template> </VnImg
><QBtn
color="primary"
size="lg"
round
class="edit-photo-btn"
@click="toggleEditPictureForm()"
>
<QIcon name="edit" size="sm" />
<QDialog ref="editPhotoFormDialog" v-model="showEditPhotoForm">
<EditPictureForm
collection="fixedAsset"
:id="entityId"
@close-form="toggleEditPictureForm()"
@on-photo-uploaded="handlePhotoUpdated"
/>
</QDialog>
</QBtn>
</div>
</template>
<template #body="{ entity }">
<VnLv
:label="$t('fixedAsset.elementAccount')"
:value="entity.elementAccount"
copy
/>
<VnLv
:label="$t('fixedAsset.location')"
:value="entity.location.description"
/>
</template>
<template #menu="{ entity }">
<FixedAssetDescriptorMenu :fixedAsset="entity" />
</template>
</CardDescriptor>
</template>

View File

@ -0,0 +1,47 @@
<script setup>
import axios from 'axios';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnConfirm from 'components/ui/VnConfirm.vue';
const props = defineProps({
fixedAsset: {
type: Object,
required: true,
},
});
const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n();
const fixedAssetId = props.fixedAsset.id;
function confirmRemove() {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('fixedAsset.confirmDeletion'),
message: t('fixedAsset.confirmDeletionMessage'),
promise: remove,
},
})
.onOk(async () => await router.push({ name: 'FixedAssetList' }));
}
async function remove() {
await axios.delete(`Ppes/${fixedAssetId}`);
quasar.notify({
message: t('globals.dataDeleted'),
type: 'positive',
});
}
</script>
<template>
<QItem @click="confirmRemove" v-ripple clickable>
<QItemSection avatar>
<QIcon name="delete" />
</QItemSection>
<QItemSection>{{ t('fixedAsset.delete') }}</QItemSection>
</QItem>
</template>

View File

@ -0,0 +1,18 @@
<script setup>
import { ref } from 'vue';
import VnDmsList from 'src/components/common/VnDmsList.vue';
const dmsListRef = ref(null);
</script>
<template>
<VnDmsList
ref="dmsListRef"
model="PpeDms"
update-model="PpeDms"
delete-model="PpeDms"
download-model="dms"
default-dms-code="fixedAssets"
filter="ppeFk"
/>
</template>

View File

@ -0,0 +1,149 @@
<script setup>
import { toDate, toCurrency } from 'src/filters/index';
import { useRoute } from 'vue-router';
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import VnConfirm from 'components/ui/VnConfirm.vue';
import VnTable from 'src/components/VnTable/VnTable.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import InvoiceInDescriptorProxy from 'pages/InvoiceIn/Card/InvoiceInDescriptorProxy.vue';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
const route = useRoute();
const { t } = useI18n();
const { notify } = useNotify();
const quasar = useQuasar();
const tableRef = ref();
const dataKey = 'fixedAssetInvoiceIn';
const columns = computed(() => [
{
align: 'left',
name: 'issued',
label: t('invoiceIn.list.issued'),
columnFilter: {
component: 'date',
},
format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.issued)),
cardVisible: true,
},
{
align: 'left',
name: 'supplierFk',
label: t('invoiceIn.list.supplier'),
columnFilter: {
component: 'select',
attrs: {
url: 'Suppliers',
fields: ['id', 'name'],
},
},
format: ({ supplierName }) => supplierName,
columnClass: 'expand',
cardVisible: true,
},
{
align: 'left',
name: 'supplierRef',
label: t('invoiceIn.supplierRef'),
cardVisible: true,
},
{
align: 'left',
name: 'amount',
label: t('invoiceIn.list.amount'),
format: ({ amount }) => toCurrency(amount),
columnFilter: false,
cardVisible: true,
},
{
align: 'right',
name: 'tableActions',
actions: [
{
title: t('fixedAsset.invoice.unassignInvoice'),
isPrimary: true,
icon: 'delete',
action: ({ id }) => confirmRemove(id),
},
],
},
]);
function confirmRemove(id) {
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('fixedAsset.invoice.unassignInvoice'),
message: t('fixedAsset.invoice.unassignInvoiceConfirmation'),
promise: () => unassignInvoice(id),
},
});
}
async function unassignInvoice(id) {
try {
await axios.delete(`PpeComponents/${id}`);
notify(t('fixedAsset.invoice.unassignedInvoice'), 'positive');
tableRef.value.reload();
} catch (e) {
throw e;
}
}
</script>
<template>
<VnTable
ref="tableRef"
:data-key="dataKey"
:url="`ppes/${route.params.id}/getInvoices`"
:columns="columns"
search-url="fixedAssetInvoiceIns"
:order="['issued DESC', 'supplierRef ASC']"
:create="{
urlCreate: 'ppeComponents',
title: t('fixedAsset.invoice.assignInvoice'),
formInitialData: {
ppeFk: parseInt(route.params.id, 10),
},
onDataSaved: ({ id }) => tableRef.reload(),
}"
:disable-option="{ card: true }"
auto-load
>
<template #column-supplierFk="{ row }">
<span class="link" @click.stop>
{{ row.supplierName }}
<SupplierDescriptorProxy :id="row.supplierId" />
</span>
</template>
<template #column-supplierRef="{ row }">
<span class="link" @click.stop>
{{ row.supplierRef }}
<InvoiceInDescriptorProxy :id="row.invoiceInFk" />
</span>
</template>
<template #more-create-dialog="{ data }">
<VnSelect
url="invoiceIns"
:label="t('invoiceIn.supplierRef')"
:fields="['id', 'supplierRef', 'supplierFk']"
:filter-options="['id', 'supplierRef']"
v-model="data.invoiceInFk"
option-label="supplierRef"
:required="true"
>
</VnSelect>
<VnInputNumber
:label="t('invoiceIn.list.amount')"
v-model="data.amount"
required
/>
</template>
</VnTable>
</template>

View File

@ -0,0 +1,6 @@
<script setup>
import VnLog from 'src/components/common/VnLog.vue';
</script>
<template>
<VnLog model="Ppe" url="/FixedAssetLogs" />
</template>

View File

@ -0,0 +1,348 @@
<script setup>
import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { dashIfEmpty, toDate, toCurrency } from 'src/filters';
import { getTotal } from 'src/composables/getTotal';
import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import { getUrl } from 'src/composables/getUrl';
import VnTitle from 'src/components/common/VnTitle.vue';
import VnToSummary from 'src/components/ui/VnToSummary.vue';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
import InvoiceInDescriptorProxy from 'src/pages/InvoiceIn/Card/InvoiceInDescriptorProxy.vue';
import FixedAssetDescriptorMenu from './FixedAssetDescriptorMenu.vue';
const route = useRoute();
const { t } = useI18n();
const $props = defineProps({
id: {
type: Number,
default: null,
},
});
const dmsColumns = ref([
{
align: 'left',
label: t('globals.id'),
name: 'id',
field: ({ id }) => id,
},
{
align: 'left',
label: t('globals.type'),
name: 'type',
field: ({ type }) => type?.name,
},
{
align: 'left',
label: t('globals.order'),
name: 'order',
field: ({ hardCopyNumber }) => dashIfEmpty(hardCopyNumber),
},
{
align: 'left',
label: t('globals.reference'),
name: 'reference',
field: ({ reference }) => dashIfEmpty(reference),
},
{
align: 'left',
label: t('globals.description'),
name: 'description',
field: ({ description }) => dashIfEmpty(description),
},
{
align: 'left',
label: t('globals.original'),
name: 'hasFile',
toolTip: t('The documentation is available in paper form'),
component: 'checkbox',
field: ({ hasFile }) => hasFile,
},
{
align: 'left',
label: t('globals.worker'),
name: 'worker',
field: ({ worker }) => worker?.name,
},
{
align: 'left',
label: t('globals.created'),
name: 'created',
field: ({ created }) => toDate(created),
},
]);
const entityId = computed(() => $props.id || route.params.id);
const summary = ref();
const fixedAssetUrl = ref();
onMounted(async () => {
fixedAssetUrl.value = (await getUrl('fixed-asset/')) + entityId.value + '/';
});
function toFixedAssetUrl(section) {
return '#/fixed-asset/' + entityId.value + '/' + section;
}
</script>
<template>
<CardSummary
ref="summary"
:url="`Ppes/${entityId}/summary`"
data-key="FixedAssetSummary"
v-bind="$attrs.width"
>
<template #header-left>
<VnToSummary
v-if="route?.name !== 'FixedAssetSummary'"
:route-name="'FixedAssetSummary'"
:entity-id="entityId"
:url="FixedAssetUrl"
/>
</template>
<template #header="{ entity }">
<div>{{ entity.id }} - {{ entity.description }}</div>
</template>
<template #menu="{ entity }">
<FixedAssetDescriptorMenu :fixedAsset="entity" />
</template>
<template #body="{ entity }">
<QCard class="vn-two">
<VnTitle
:url="toFixedAssetUrl('basic-data')"
:text="t('globals.summary.basicData')"
data-cy="titleBasicDataBlock1"
/>
<div class="vn-card-group">
<div class="vn-card-content">
<VnLv
:label="t('globals.description')"
:value="dashIfEmpty(entity.description)"
/>
<VnLv
:label="t('globals.company')"
:value="entity.company.code"
/>
<VnLv
:label="t('fixedAsset.account')"
:value="entity.account"
copy
/>
<VnLv
:label="t('fixedAsset.endowment')"
:value="entity.endowment"
copy
/>
<VnLv
:label="t('fixedAsset.elementAccount')"
:value="entity.elementAccount"
copy
/>
<VnLv
:label="t('fixedAsset.nature')"
:value="dashIfEmpty(entity.nature)"
/>
<VnLv
:label="t('fixedAsset.location')"
:value="
dashIfEmpty(
`${entity.location.code} - ${entity.location.description}`,
)
"
/>
<VnLv
:label="t('globals.group')"
:value="dashIfEmpty(entity.group.description)"
/>
<VnLv
:label="t('fixedAsset.isInvestmentAsset')"
:value="entity.isInvestmentAsset"
/>
</div>
</div>
</QCard>
<QCard class="vn-two">
<VnTitle
:url="toFixedAssetUrl('basic-data')"
:text="t('globals.summary.basicData')"
data-cy="titleBasicDataBlock2"
/>
<div class="vn-card-content">
<VnLv
:label="t('fixedAsset.value')"
:value="dashIfEmpty(toCurrency(entity.value))"
copy
/>
<VnLv
:label="$t('fixedAsset.firstAmortizated')"
:tooltip="$t('fixedAsset.firstAmortizatedTooltip')"
:value="dashIfEmpty(toDate(entity.firstAmortizated))"
/>
<VnLv
:label="t('fixedAsset.lastAmortizated')"
:tooltip="t('fixedAsset.lastAmortizatedTooltip')"
:value="dashIfEmpty(toDate(entity.lastAmortizated))"
/>
<VnLv
:label="$t('fixedAsset.finished')"
:tooltip="$t('fixedAsset.finishedTooltip')"
:value="dashIfEmpty(toDate(entity.finished))"
/>
<VnLv
:label="t('fixedAsset.amortization')"
:value="dashIfEmpty(toCurrency(entity.amortization))"
/>
<VnLv
:label="t('fixedAsset.plan')"
:value="
dashIfEmpty(`${entity.plan.rate}% - ${entity.plan.days}days`)
"
/>
<VnLv
:label="t('fixedAsset.discharged')"
:value="dashIfEmpty(toDate(entity.discharged))"
/>
<VnLv
:label="t('fixedAsset.cause')"
:value="dashIfEmpty(entity.cause)"
/>
<VnLv :label="t('fixedAsset.isDone')" :value="entity.isDone" />
</div>
</QCard>
<QCard v-if="entity?.ppeDms?.length > 0" class="vn-two">
<VnTitle
:url="toFixedAssetUrl('dms')"
:text="t('globals.pageTitles.dms')"
data-cy="titleDmsBlock"
/>
<QTable :columns="dmsColumns" :rows="entity?.ppeDms" flat>
<template #header="props">
<QTr :props="props">
<QTh auto-width class="text-left">{{ t('globals.id') }}</QTh>
<QTh auto-width class="text-left">{{
t('globals.type')
}}</QTh>
<QTh auto-width class="text-left">{{
t('globals.order')
}}</QTh>
<QTh auto-width class="text-left">{{
t('globals.reference')
}}</QTh>
<QTh auto-width class="text-left">{{
t('globals.description')
}}</QTh>
<QTh auto-width class="text-center">{{
t('globals.original')
}}</QTh>
<QTh auto-width class="text-left">{{
t('globals.worker')
}}</QTh>
<QTh auto-width class="text-center">{{
t('globals.created')
}}</QTh>
</QTr>
</template>
<template #body="props">
<QTr :props="props">
<QTd class="text-left">{{ props.row.dms.id }}</QTd>
<QTd class="text-left">{{ props.row.dms.dmsType.name }}</QTd>
<QTd class="text-left">{{
props.row.dms.hardCopyNumber
}}</QTd>
<QTd class="text-left">{{ props.row.dms.reference }}</QTd>
<QTd class="text-left">{{ props.row.dms.description }}</QTd>
<QTd class="text-center"
><VnCheckbox
:disable="true"
v-model="props.row.dms.hasFile"
/></QTd>
<QTd class="text-left"
><span class="link" @click.stop
>{{ props.row.dms.worker.firstName
}}<WorkerDescriptorProxy
:id="props.row.dms.worker.id" /></span
></QTd>
<QTd class="text-center">{{
toDate(props.row.dms.created)
}}</QTd>
</QTr>
</template>
</QTable>
</QCard>
<QCard v-if="entity.ppeComponents?.length > 0" class="vn-two">
<VnTitle
:url="toFixedAssetUrl('invoice')"
:text="$t('globals.pageTitles.assignedInvoices')"
data-cy="titleInvoiceBlock"
/>
<QTable :rows="entity.ppeComponents" style="text-align: center">
<template #body-cell="{ value }">
<QTd>{{ value }}</QTd>
</template>
<template #header="props">
<QTr class="tr-header" :props="props">
<QTh auto-width>{{ $t('invoiceIn.list.issued') }}</QTh>
<QTh auto-width>{{ $t('invoiceIn.list.supplier') }}</QTh>
<QTh auto-width>{{ $t('invoiceIn.supplierRef') }}</QTh>
<QTh auto-width>{{ $t('invoiceIn.list.amount') }}</QTh>
</QTr>
</template>
<template #body="props">
<QTr :props="props">
<QTd>{{ toDate(props.row.invoiceIn.issued) }}</QTd>
<QTd>
<span class="link" data-cy="supplierLink">
{{ props.row.invoiceIn.supplier.name }}
<SupplierDescriptorProxy
:id="props.row.invoiceIn.supplierFk"
/>
</span>
</QTd>
<QTd>
<span class="link" data-cy="invoiceLink">
{{ props.row.invoiceInFk }}
<InvoiceInDescriptorProxy
:id="props.row.invoiceInFk"
/>
</span>
</QTd>
<QTd>{{ toCurrency(props.row.amount) }}</QTd>
</QTr>
</template>
<template #bottom-row>
<QTr class="bg">
<QTd></QTd>
<QTd></QTd>
<QTd></QTd>
<QTd>
{{ toCurrency(getTotal(entity.ppeComponents, 'amount')) }}
</QTd>
</QTr>
</template>
</QTable>
</QCard>
</template>
</CardSummary>
</template>
<style lang="scss" scoped>
.q-card.q-card--dark.q-dark.vn-one {
& > .bodyCard {
padding: 1%;
}
}
.q-table {
tr,
th,
.q-td {
border-bottom: 1px solid black;
}
}
</style>

View File

@ -0,0 +1,207 @@
<script setup>
import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
</script>
<template>
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`fixedAsset.params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params, searchFn }">
<QItem>
<QItemSection>
<VnInput
v-model="params.id"
:label="t('globals.id')"
dense
filled
data-cy="idInput"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
v-model="params.description"
:label="t('globals.description')"
dense
filled
data-cy="nameInput"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
url="Companies"
:label="t('globals.company')"
v-model="params.companyFk"
option-value="id"
option-label="code"
dense
filled
data-cy="fixedAssetGroupSelect"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
v-model="params.firstAmortizated"
:label="t('fixedAsset.firstAmortizated')"
dense
filled
data-cy="firstAmortizatedDateInput"
/>
</QItemSection>
<QItemSection>
<VnInputDate
v-model="params.lastAmortizated"
:label="t('fixedAsset.lastAmortizated')"
dense
filled
data-cy="lastAmortizatedDateInput"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
v-model="params.finished"
:label="t('fixedAsset.finished')"
dense
filled
data-cy="finishedDateInput"
/>
</QItemSection>
<QItemSection>
<VnInputDate
v-model="params.discharged"
:label="t('fixedAsset.discharged')"
dense
filled
data-cy="dischargedDateInput"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
url="Ppes/getNatures"
:label="t('fixedAsset.nature')"
v-model="params.nature"
option-value="nature"
option-label="nature"
dense
filled
data-cy="natureSelect"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
url="PpePlans"
:label="t('fixedAsset.plan')"
v-model="params.planFk"
:option-label="(value) => `${value.rate}% - ${value.days} days`"
dense
filled
data-cy="planSelect"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt?.rate}% - ${scope.opt?.days} days`
}}</QItemLabel>
<QItemLabel caption>
{{ `#${scope.opt?.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
url="PpeGroups"
:label="t('globals.group')"
v-model="params.groupFk"
option-value="id"
option-label="description"
dense
filled
data-cy="groupSelect"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
url="PpeLocations"
:label="t('fixedAsset.location')"
v-model="params.locationFk"
option-value="code"
:option-label="(value) => `${value.code} - ${value.description}`"
dense
filled
data-cy="locationSelect"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.description }}</QItemLabel>
<QItemLabel caption>
{{ scope.opt?.code }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnCheckbox
v-model="params.isInvestmentAsset"
:label="t('fixedAsset.isInvestmentAsset')"
dense
filled
data-cy="isInvestmentAssetCheckbox"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnCheckbox
v-model="params.isDone"
:label="t('fixedAsset.isDone')"
dense
filled
data-cy="isDoneCheckbox"
/>
</QItemSection>
</QItem>
</template>
</VnFilterPanel>
</template>

View File

@ -0,0 +1,366 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState';
import VnTable from 'src/components/VnTable/VnTable.vue';
import VnSection from 'src/components/common/VnSection.vue';
import { dashIfEmpty, toCurrency, toDate } from 'src/filters';
import FixedAssetFilter from './FixedAssetFilter.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnRow from 'src/components/ui/VnRow.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnAccountNumber from 'src/components/common/VnAccountNumber.vue';
const user = useState().getUser();
const { t } = useI18n();
const tableRef = ref();
const dataKey = 'FixedAssetList';
const columns = computed(() => [
{
align: 'right',
name: 'id',
label: t('globals.id'),
width: '35px',
chip: {
condition: () => true,
},
isId: true,
},
{
align: 'left',
name: 'description',
label: t('globals.description'),
cardVisible: true,
columnClass: 'expand',
},
{
name: 'companyFk',
label: t('globals.company'),
component: 'select',
attrs: {
url: 'companies',
optionValue: 'code',
},
format: ({ company }) => company?.code,
cardVisible: true,
},
{
name: 'value',
label: t('fixedAsset.value'),
component: 'number',
format: ({ value }) => toCurrency(value),
cardVisible: true,
columnFilter: false,
},
{
align: 'center',
name: 'firstAmortizated',
label: t('fixedAsset.firstAmortizated'),
toolTip: t('fixedAsset.firstAmortizatedTooltip'),
component: 'date',
format: ({ firstAmortizated }) => dashIfEmpty(toDate(firstAmortizated)),
cardVisible: true,
},
{
align: 'center',
name: 'lastAmortizated',
label: t('fixedAsset.lastAmortizated'),
toolTip: t('fixedAsset.lastAmortizatedTooltip'),
component: 'date',
format: ({ lastAmortizated }) => dashIfEmpty(toDate(lastAmortizated)),
cardVisible: true,
},
{
align: 'center',
name: 'finished',
label: t('fixedAsset.finished'),
toolTip: t('fixedAsset.finishedTooltip'),
component: 'date',
format: ({ finished }) => dashIfEmpty(toDate(finished)),
cardVisible: true,
},
{
align: 'center',
name: 'discharged',
label: t('fixedAsset.discharged'),
toolTip: t('fixedAsset.dischargedTooltip'),
component: 'date',
format: ({ discharged }) => dashIfEmpty(toDate(discharged)),
cardVisible: true,
},
{
name: 'amortization',
label: t('fixedAsset.amortization'),
component: 'number',
format: ({ amortization }) => toCurrency(amortization),
cardVisible: true,
columnFilter: false,
},
{
name: 'nature',
label: t('fixedAsset.nature'),
component: 'select',
attrs: {
url: 'Ppes/getNatures',
optionValue: 'nature',
optionLabel: 'nature',
},
format: ({ nature }) => dashIfEmpty(nature),
cardVisible: true,
},
{
name: 'plan',
label: t('fixedAsset.plan'),
toolTip: t('fixedAsset.planTooltip'),
component: 'select',
attrs: {
url: 'PpePlans',
optionValue: 'rate',
optionLabel: 'days',
},
format: ({ plan }) => dashIfEmpty(`${plan?.rate * 100}% - ${plan?.days} days`),
cardVisible: true,
},
{
name: 'group',
label: t('globals.group'),
component: 'select',
attrs: {
url: 'PpeGroups',
optionValue: 'id',
optionLabel: 'description',
},
format: ({ group }) => dashIfEmpty(group?.description),
cardVisible: true,
},
{
name: 'locationFk',
label: t('fixedAsset.location'),
component: 'select',
attrs: {
url: 'PpeLocations',
optionValue: 'code',
optionLabel: 'description',
},
format: ({ location }) =>
dashIfEmpty(`${location?.code} - ${location?.description}`),
cardVisible: true,
},
{
labelAbbreviation: t('fixedAsset.isInvestmentAssetAbbr'),
label: t('fixedAsset.isInvestmentAsset'),
toolTip: t('fixedAsset.isInvestmentAsset'),
name: 'isInvestmentAsset',
component: 'checkbox',
cardVisible: true,
},
{
labelAbbreviation: t('fixedAsset.isDoneAbbr'),
label: t('fixedAsset.isDone'),
toolTip: t('fixedAsset.isDone'),
name: 'isDone',
component: 'checkbox',
cardVisible: true,
},
]);
</script>
<template>
<VnSection
:data-key="dataKey"
:columns="columns"
prefix="fixedAsset"
:array-data-props="{
url: 'ppes/filter',
}"
>
<template #advanced-menu>
<FixedAssetFilter :data-key="dataKey" />
</template>
<template #body>
<VnTable
ref="tableRef"
:data-key="dataKey"
:columns="columns"
redirect="fixed-asset"
:create="{
urlCreate: 'Ppes/createFixedAsset',
title: t('fixedAsset.createFixedAsset'),
onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {
nature: 'INMOVILIZADO',
companyFk: user.companyFk,
},
}"
:disable-option="{ card: true }"
:right-search="false"
>
<template #more-create-dialog="{ data }">
<div class="col-span-2">
<VnRow>
<VnInput
v-model="data.id"
:label="$t('globals.id')"
required
/>
<VnInputNumber
v-model="data.value"
:label="$t('fixedAsset.value')"
required
/>
</VnRow>
<VnRow>
<VnInput
v-model="data.description"
:label="$t('globals.description')"
required
/>
</VnRow>
<VnRow>
<VnInputDate
v-model="data.firstAmortizated"
:label="$t('fixedAsset.firstAmortizated')"
placeholder="dd-mm-aaaa"
/>
<VnInputDate
v-model="data.lastAmortizated"
:label="$t('fixedAsset.lastAmortizated')"
placeholder="dd-mm-aaaa"
/>
</VnRow>
<VnRow>
<VnSelect
url="Ppes/getSubaccounts"
v-model="data.account"
:label="$t('fixedAsset.account')"
option-value="code"
option-label="code"
hide-selected
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt?.description}`
}}</QItemLabel>
<QItemLabel caption>
{{ `#${scope.opt?.code}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect
url="Ppes/getEndowments"
v-model="data.endowment"
:label="$t('fixedAsset.endowment')"
option-value="code"
option-label="code"
hide-selected
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt?.description}`
}}</QItemLabel>
<QItemLabel caption>
{{ `#${scope.opt?.code}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</VnRow>
<VnRow>
<VnAccountNumber
:label="$t('fixedAsset.elementAccount')"
v-model="data.elementAccount"
required
/>
<VnSelect
url="PpePlans"
v-model="data.planFk"
:label="$t('fixedAsset.plan')"
option-value="id"
:option-label="
(value) => `${value.rate * 100}% - ${value.days} days`
"
hide-selected
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt?.rate * 100}% - ${scope.opt?.days} days`
}}</QItemLabel>
<QItemLabel caption>
{{ `#${scope.opt?.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</VnRow>
<VnRow>
<VnSelect
url="Ppes/getNatures"
v-model="data.nature"
:label="$t('fixedAsset.nature')"
option-value="nature"
option-label="nature"
hide-selected
/>
<VnSelect
url="PpeGroups"
v-model="data.groupFk"
:label="$t('globals.group')"
option-value="id"
option-label="description"
hide-selected
/>
</VnRow>
<VnRow>
<VnSelect
url="Companies"
:fields="['id', 'code']"
v-model="data.companyFk"
:label="$t('globals.company')"
option-value="id"
option-label="code"
hide-selected
/>
<VnSelect
url="PpeLocations"
v-model="data.locationFk"
:label="$t('fixedAsset.location')"
option-value="code"
:option-label="
(value) => `${value.code} - ${value.description}`
"
hide-selected
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt?.description}`
}}</QItemLabel>
<QItemLabel caption>
{{ `#${scope.opt?.code}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</VnRow>
</div>
</template>
</VnTable>
</template>
</VnSection>
</template>

View File

@ -0,0 +1,50 @@
fixedAsset:
search: Search fixed assets
searchInfo: Search fixed assets by id
value: Value
firstAmortizated: Amort. start
firstAmortizatedTooltip: Amortization start date
lastAmortizated: Amort. end
lastAmortizatedTooltip: Amortization end date
amortization: Amortization
finished: Final date
finishedTooltip: Final date
discharged: Discharged
dischargedTooltip: Discharged
nature: Nature
plan: Amort. plan
planTooltip: Amortization plan
cause: Cause of discharge
location: Location
elementAccount: Element account
account: Subaccount
endowment: Endowment
isInvestmentAsset: Investment asset
isInvestmentAssetAbbr: IA
isDone: Completed
isDoneAbbr: Co
createFixedAsset: Create fixed asset
confirmDeletion: Confirm deletion
confirmDeletionMessage: Are you sure you want to delete this fixed asset?
delete: Delete fixed asset
params:
id: Id
description: Description
companyFk: Company
firstAmortizated: Amort. start
lastAmortizated: Amort. end
finished: Final date
discharged: Discharged
nature: Nature
planFk: Amort. plan
groupFk: Group
locationFk: Location
isInvestmentAsset: Investment asset
isDone: Completed
issued: Issued
invoice:
assignedInvoices: Assigned invoices
assignInvoice: Assign invoice
unassignedInvoice: Unassigned invoice
unassignInvoice: Unassign invoice
unassignInvoiceConfirmation: This invoice will be unlinked from this fixed asset, continue anyway?

View File

@ -0,0 +1,51 @@
fixedAsset:
search: Buscar inmovilizados
searchInfo: Buscar inmovilizados por id
value: Valor
firstAmortizated: F. ini. amort.
firstAmortizatedTooltip: Fecha inicial de amortización.
lastAmortizated: F. fin. amort.
lastAmortizatedTooltip: Fecha final de amortización
amortization: Amortización
finished: F. final
finishedTooltip: Fecha final
discharged: F. baja
dischargedTooltip: Fecha de baja
nature: Naturaleza
plan: Plan amort.
planTooltip: Plan de amortización
cause: Causa de la baja
location: Emplazamiento
elementAccount: Cuenta de elemento
account: Subcuenta
endowment: Dotación
isInvestmentAsset: Bien de inversión
isInvestmentAssetAbbr: BI
isDone: Completado
isDoneAbbr: Co
createFixedAsset: Crear inmovilizado
confirmDeletion: Confirmar eliminación
confirmDeletionMessage: Seguro que quieres eliminar este inmovilizado?
delete: Eliminar inmovilizado
params:
search: Búsqueda general
id: Id
description: Descripción
companyFk: Empresa
firstAmortizated: F. ini. amort.
lastAmortizated: F. fin. amort.
finished: F. final
discharged: F. baja
nature: Naturaleza
planFk: Plan amort.
groupFk: Grupo
locationFk: Emplazamiento
isInvestmentAsset: Bien de inversión
isDone: Completado
issued: F. emisión
invoice:
assignedInvoices: Facturas vinculadas
assignInvoice: Vincular factura
unassignedInvoice: Factura desvinculada
unassignInvoice: Desvincular factura
unassignInvoiceConfirmation: Esta factura se desvinculará de este inmovilizado, ¿Continuar de todas formas?

View File

@ -90,7 +90,6 @@ const columns = computed(() => [
auto-load
:data-required="{ invoiceInFk: invoiceInId }"
:filter="filter"
:insert-on-load="true"
v-model:selected="rowsSelected"
@on-fetch="(data) => (invoceInIntrastat = data)"
>

View File

@ -1,16 +1,21 @@
<script setup>
import { ref, computed, markRaw } from 'vue';
import { ref, computed, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useArrayData } from 'src/composables/useArrayData';
import { getTotal } from 'src/composables/getTotal';
import { toCurrency } from 'src/filters';
import FetchData from 'src/components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import CrudModel from 'src/components/CrudModel.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
import CreateNewExpenseForm from 'src/components/CreateNewExpenseForm.vue';
import { getExchange } from 'src/composables/getExchange';
import VnTable from 'src/components/VnTable/VnTable.vue';
import VnSelectExpense from 'src/components/common/VnSelectExpense.vue';
import { useAccountShortToStandard } from 'src/composables/useAccountShortToStandard';
const { t } = useI18n();
const arrayData = useArrayData();
const route = useRoute();
const invoiceIn = computed(() => arrayData.store.data);
@ -19,142 +24,100 @@ const expenses = ref([]);
const sageTaxTypes = ref([]);
const sageTransactionTypes = ref([]);
const rowsSelected = ref([]);
const invoiceInVatTableRef = ref();
const invoiceInFormRef = ref();
defineProps({ actionIcon: { type: String, default: 'add' } });
function taxRate(invoiceInTax) {
const sageTaxTypeId = invoiceInTax.taxTypeSageFk;
const taxRateSelection = sageTaxTypes.value.find(
(transaction) => transaction.id == sageTaxTypeId,
);
const taxTypeSage = taxRateSelection?.rate ?? 0;
const taxableBase = invoiceInTax?.taxableBase ?? 0;
return (taxTypeSage / 100) * taxableBase;
}
defineProps({
actionIcon: {
type: String,
default: 'add',
},
});
const columns = computed(() => [
{
name: 'expenseFk',
name: 'expense',
label: t('Expense'),
component: markRaw(VnSelectExpense),
format: (row) => {
const expense = expenses.value.find((e) => e.id === row.expenseFk);
return expense ? `${expense.id}: ${expense.name}` : row.expenseFk;
},
field: (row) => row.expenseFk,
options: expenses.value,
model: 'expenseFk',
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.name}`,
sortable: true,
align: 'left',
isEditable: true,
create: true,
width: '250px',
},
{
name: 'taxableBase',
name: 'taxablebase',
label: t('Taxable base'),
component: 'number',
attrs: {
clearable: true,
'clear-icon': 'close',
},
field: (row) => row.taxableBase,
model: 'taxableBase',
sortable: true,
align: 'left',
isEditable: true,
create: true,
},
{
name: 'isDeductible',
label: t('invoiceIn.isDeductible'),
component: 'checkbox',
field: (row) => row.isDeductible,
model: 'isDeductible',
align: 'center',
isEditable: true,
create: true,
createAttrs: {
defaultValue: true,
},
width: '100px',
},
{
name: 'taxTypeSageFk',
name: 'sageiva',
label: t('Sage iva'),
component: 'select',
attrs: {
field: (row) => row.taxTypeSageFk,
options: sageTaxTypes.value,
model: 'taxTypeSageFk',
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.vat}`,
filterOptions: ['id', 'vat'],
'data-cy': 'vat-sageiva',
},
format: (row) => {
const taxType = sageTaxTypes.value.find((t) => t.id === row.taxTypeSageFk);
return taxType ? `${taxType.id}: ${taxType.vat}` : row.taxTypeSageFk;
},
sortable: true,
align: 'left',
isEditable: true,
create: true,
},
{
name: 'transactionTypeSageFk',
name: 'sagetransaction',
label: t('Sage transaction'),
component: 'select',
attrs: {
field: (row) => row.transactionTypeSageFk,
options: sageTransactionTypes.value,
model: 'transactionTypeSageFk',
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.transaction}`,
filterOptions: ['id', 'transaction'],
},
format: (row) => {
const transType = sageTransactionTypes.value.find(
(t) => t.id === row.transactionTypeSageFk,
);
return transType
? `${transType.id}: ${transType.transaction}`
: row.transactionTypeSageFk;
},
sortable: true,
align: 'left',
isEditable: true,
create: true,
},
{
name: 'rate',
label: t('Rate'),
sortable: false,
format: (row) => taxRate(row).toFixed(2),
sortable: true,
field: (row) => taxRate(row, row.taxTypeSageFk),
align: 'left',
},
{
name: 'foreignValue',
name: 'foreignvalue',
label: t('Foreign value'),
component: 'number',
sortable: true,
field: (row) => row.foreignValue,
align: 'left',
create: true,
disable: !isNotEuro(currency.value),
},
{
name: 'total',
label: t('Total'),
label: 'Total',
align: 'left',
format: (row) => (Number(row.taxableBase || 0) + Number(taxRate(row))).toFixed(2),
},
]);
const tableRows = computed(
() => invoiceInVatTableRef.value?.CrudModelRef?.formData || [],
);
const taxableBaseTotal = computed(() => {
return getTotal(tableRows.value, 'taxableBase');
return getTotal(invoiceInFormRef.value.formData, 'taxableBase');
});
const taxRateTotal = computed(() => {
return tableRows.value.reduce((sum, row) => sum + Number(taxRate(row)), 0);
return getTotal(invoiceInFormRef.value.formData, null, {
cb: taxRate,
});
});
const combinedTotal = computed(() => {
return +taxableBaseTotal.value + +taxRateTotal.value;
});
const filter = computed(() => ({
const filter = {
fields: [
'id',
'invoiceInFk',
@ -168,75 +131,388 @@ const filter = computed(() => ({
where: {
invoiceInFk: route.params.id,
},
}));
};
const isNotEuro = (code) => code != 'EUR';
async function handleForeignValueUpdate(val, row) {
if (!isNotEuro(currency.value)) return;
row.taxableBase = await getExchange(
val,
invoiceIn.value?.currencyFk,
invoiceIn.value?.issued,
function taxRate(invoiceInTax) {
const sageTaxTypeId = invoiceInTax.taxTypeSageFk;
const taxRateSelection = sageTaxTypes.value.find(
(transaction) => transaction.id == sageTaxTypeId,
);
const taxTypeSage = taxRateSelection?.rate ?? 0;
const taxableBase = invoiceInTax?.taxableBase ?? 0;
return ((taxTypeSage / 100) * taxableBase).toFixed(2);
}
function autocompleteExpense(evt, row, col, ref) {
const val = evt.target.value;
if (!val) return;
const param = isNaN(val) ? row[col.model] : val;
const lookup = expenses.value.find(
({ id }) => id == useAccountShortToStandard(param),
);
ref.vnSelectDialogRef.vnSelectRef.toggleOption(lookup);
}
function setCursor(ref) {
nextTick(() => {
const select = ref.vnSelectDialogRef
? ref.vnSelectDialogRef.vnSelectRef
: ref.vnSelectRef;
select.$el.querySelector('input').setSelectionRange(0, 0);
});
}
</script>
<template>
<FetchData url="Expenses" auto-load @on-fetch="(data) => (expenses = data)" />
<FetchData
ref="expensesRef"
url="Expenses"
auto-load
@on-fetch="(data) => (expenses = data)"
/>
<FetchData url="SageTaxTypes" auto-load @on-fetch="(data) => (sageTaxTypes = data)" />
<FetchData
url="sageTransactionTypes"
auto-load
@on-fetch="(data) => (sageTransactionTypes = data)"
/>
<VnTable
<CrudModel
ref="invoiceInFormRef"
v-if="invoiceIn"
ref="invoiceInVatTableRef"
data-key="InvoiceInTaxes"
url="InvoiceInTaxes"
save-url="InvoiceInTaxes/crud"
:filter="filter"
:data-required="{ invoiceInFk: $route.params.id }"
:insert-on-load="true"
auto-load
v-model:selected="rowsSelected"
:columns="columns"
:is-editable="true"
:table="{ selection: 'multiple', 'row-key': '$index' }"
footer
:right-search="false"
:column-search="false"
:disable-option="{ card: true }"
class="q-pa-none"
:create="{
urlCreate: 'InvoiceInTaxes',
title: t('Add tax'),
formInitialData: { invoiceInFk: $route.params.id, isDeductible: true },
onDataSaved: () => invoiceInVatTableRef.reload(),
}"
:crud-model="{ goTo: `/invoice-in/${$route.params.id}/due-day` }"
:go-to="`/invoice-in/${$route.params.id}/due-day`"
>
<template #column-footer-taxableBase>
<template #body="{ rows }">
<QTable
v-model:selected="rowsSelected"
selection="multiple"
:columns="columns"
:rows="rows"
row-key="$index"
:grid="$q.screen.lt.sm"
>
<template #body-cell-expense="{ row, col }">
<QTd>
<VnSelectDialog
:ref="`expenseRef-${row.$index}`"
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:filter-options="['id', 'name']"
:tooltip="t('Create a new expense')"
:acls="[
{ model: 'Expense', props: '*', accessType: 'WRITE' },
]"
@keydown.tab.prevent="
autocompleteExpense(
$event,
row,
col,
$refs[`expenseRef-${row.$index}`],
)
"
@update:model-value="
setCursor($refs[`expenseRef-${row.$index}`])
"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
{{ `${scope.opt.id}: ${scope.opt.name}` }}
</QItem>
</template>
<template #form>
<CreateNewExpenseForm
@on-data-saved="$refs.expensesRef.fetch()"
/>
</template>
</VnSelectDialog>
</QTd>
</template>
<template #body-cell-isDeductible="{ row }">
<QTd align="center">
<QCheckbox
v-model="row.isDeductible"
data-cy="isDeductible_checkbox"
/>
</QTd>
</template>
<template #body-cell-taxablebase="{ row }">
<QTd shrink>
<VnInputNumber
clear-icon="close"
v-model="row.taxableBase"
clearable
/>
</QTd>
</template>
<template #body-cell-sageiva="{ row, col }">
<QTd>
<VnSelect
:ref="`sageivaRef-${row.$index}`"
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:filter-options="['id', 'vat']"
data-cy="vat-sageiva"
@update:model-value="
setCursor($refs[`sageivaRef-${row.$index}`])
"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt.vat }}</QItemLabel>
<QItemLabel>
{{ `#${scope.opt.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QTd>
</template>
<template #body-cell-sagetransaction="{ row, col }">
<QTd>
<VnSelect
:ref="`sagetransactionRef-${row.$index}`"
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:filter-options="['id', 'transaction']"
@update:model-value="
setCursor($refs[`sagetransactionRef-${row.$index}`])
"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
scope.opt.transaction
}}</QItemLabel>
<QItemLabel>
{{ `#${scope.opt.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QTd>
</template>
<template #body-cell-foreignvalue="{ row }">
<QTd shrink>
<VnInputNumber
:class="{
'no-pointer-events': !isNotEuro(currency),
}"
:disable="!isNotEuro(currency)"
v-model="row.foreignValue"
@update:model-value="
async (val) => {
if (!isNotEuro(currency)) return;
row.taxableBase = await getExchange(
val,
row.currencyFk,
invoiceIn.issued,
);
}
"
/>
</QTd>
</template>
<template #bottom-row>
<QTr class="bg">
<QTd />
<QTd />
<QTd>
{{ toCurrency(taxableBaseTotal) }}
</template>
<template #column-footer-rate>
</QTd>
<QTd />
<QTd />
<QTd />
<QTd>
{{ toCurrency(taxRateTotal) }}
</template>
<template #column-footer-total>
</QTd>
<QTd />
<QTd>
{{ toCurrency(combinedTotal) }}
</QTd>
</QTr>
</template>
</VnTable>
<template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
<QCard bordered flat class="q-my-xs">
<QCardSection>
<QCheckbox v-model="props.selected" dense />
</QCardSection>
<QSeparator />
<QList>
<QItem>
<VnSelectDialog
:label="t('Expense')"
class="full-width"
v-model="props.row['expenseFk']"
:options="expenses"
option-value="id"
:option-label="(row) => `${row.id}:${row.name}`"
:filter-options="['id', 'name']"
:tooltip="t('Create a new expense')"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
{{ `${scope.opt.id}: ${scope.opt.name}` }}
</QItem>
</template>
<template #form>
<CreateNewExpenseForm />
</template>
</VnSelectDialog>
</QItem>
<QItem>
<VnInputNumber
:label="t('Taxable base')"
:class="{
'no-pointer-events': isNotEuro(currency),
}"
class="full-width"
:disable="isNotEuro(currency)"
clear-icon="close"
v-model="props.row.taxableBase"
clearable
/>
</QItem>
<QItem>
<VnSelect
:label="t('Sage iva')"
class="full-width"
v-model="props.row['taxTypeSageFk']"
:options="sageTaxTypes"
option-value="id"
:option-label="(row) => `${row.id}:${row.vat}`"
:filter-options="['id', 'vat']"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
scope.opt.vat
}}</QItemLabel>
<QItemLabel>
{{ `#${scope.opt.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItem>
<QItem>
<VnSelect
class="full-width"
v-model="props.row['transactionTypeSageFk']"
:options="sageTransactionTypes"
option-value="id"
:option-label="
(row) => `${row.id}:${row.transaction}`
"
:filter-options="['id', 'transaction']"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
scope.opt.transaction
}}</QItemLabel>
<QItemLabel>
{{ `#${scope.opt.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItem>
<QItem>
{{ toCurrency(taxRate(props.row), currency) }}
</QItem>
<QItem>
<VnInputNumber
:label="t('Foreign value')"
class="full-width"
:class="{
'no-pointer-events': !isNotEuro(currency),
}"
:disable="!isNotEuro(currency)"
v-model="props.row.foreignValue"
/>
</QItem>
</QList>
</QCard>
</div>
</template>
</QTable>
</template>
</CrudModel>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn
color="primary"
icon="add"
size="lg"
v-shortcut="'+'"
round
@click="invoiceInFormRef.insert()"
>
<QTooltip>{{ t('Add tax') }}</QTooltip>
</QBtn>
</QPageSticky>
</template>
<style lang="scss" scoped>
.bg {
background-color: var(--vn-light-gray);
}
@media (max-width: $breakpoint-xs) {
.q-dialog {
.q-card {
&__section:not(:first-child) {
.q-item {
flex-direction: column;
.q-checkbox {
margin-top: 2rem;
}
}
}
}
}
}
.q-item {
min-height: 0;
}
.default-icon {
cursor: pointer;
border-radius: 50px;
background-color: $primary;
}
</style>
<i18n>
es:
Expense: Gasto
Create a new expense: Crear nuevo gasto
Add tax: Añadir Gasto/IVA # Changed label slightly
Add tax: Crear gasto
Taxable base: Base imp.
Sage iva: Sage iva # Kept original label
Sage tax: Sage iva
Sage transaction: Sage transacción
Rate: Cuota # Changed label
Rate: Tasa
Foreign value: Divisa
Total: Total
invoiceIn.isDeductible: Deducible
</i18n>

View File

@ -51,7 +51,6 @@ const submit = async (rows) => {
<CrudModel
:data-required="{ itemFk: route.params.id }"
:default-remove="false"
:insert-on-load="true"
:filter="{
fields: ['id', 'itemFk', 'code'],
where: { itemFk: route.params.id },

View File

@ -76,22 +76,15 @@ const insertTag = (rows) => {
model="ItemTags"
url="ItemTags"
:data-required="{
$index: undefined,
itemFk: route.params.id,
priority: undefined,
tag: {
isFree: true,
value: undefined,
name: undefined,
},
}"
:data-default="{
tag: {
isFree: true,
isFree: undefined,
value: undefined,
name: undefined,
},
tagFk: undefined,
priority: undefined,
}"
:default-remove="false"
:user-filter="{

View File

@ -1,11 +1,12 @@
<script setup>
import { ref } from 'vue';
import { Notify } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnInputPassword from 'src/components/common/VnInputPassword.vue';
import { useSession } from 'src/composables/useSession';
import { useLogin } from 'src/composables/useLogin';
import useNotify from 'src/composables/useNotify';
import VnLogo from 'components/ui/VnLogo.vue';
import VnInput from 'src/components/common/VnInput.vue';
import axios from 'axios';
@ -14,14 +15,16 @@ const session = useSession();
const loginCache = useLogin();
const router = useRouter();
const { t } = useI18n();
const { notify } = useNotify();
const username = ref('');
const password = ref('');
const keepLogin = ref(true);
async function onSubmit() {
const params = { user: username.value, password: password.value };
const params = {
user: username.value,
password: password.value,
};
try {
const { data } = await axios.post('Accounts/login', params);
if (!data) return;
@ -30,7 +33,11 @@ async function onSubmit() {
await session.setLogin(data);
} catch (res) {
if (res.response?.data?.error?.code === 'REQUIRES_2FA') {
notify(t('login.twoFactorRequired'), 'warning', 'phoneLink_lock');
Notify.create({
message: t('login.twoFactorRequired'),
icon: 'phoneLink_lock',
type: 'warning',
});
params.keepLogin = keepLogin.value;
loginCache.setUser(params);
return router.push({
@ -38,7 +45,10 @@ async function onSubmit() {
query: router.currentRoute.value?.query,
});
}
throw res;
Notify.create({
message: t('login.loginError'),
type: 'negative',
});
}
}
</script>

View File

@ -18,7 +18,7 @@ const noteFilter = computed(() => {
const body = {
vehicleFk: vehicleId.value,
userFk: user.value.id,
workerFk: user.value.id,
};
</script>

View File

@ -24,10 +24,10 @@ const crudModelFilter = reactive({
where: { ticketFk: route.params.id },
});
const crudModelDefaultData = computed(() => ({
created: Date.vnNew(),
const crudModelRequiredData = computed(() => ({
packagingFk: null,
quantity: 0,
created: Date.vnNew(),
ticketFk: route.params.id,
}));
@ -63,7 +63,7 @@ watch(
url="TicketPackagings"
model="TicketPackagings"
:filter="crudModelFilter"
:data-default="crudModelDefaultData"
:data-required="crudModelRequiredData"
:default-remove="false"
auto-load
>

View File

@ -719,7 +719,6 @@ watch(
:create-as-dialog="false"
:crud-model="{
disableInfiniteScroll: true,
insertOnLoad: false,
}"
:default-remove="false"
:default-reset="false"

View File

@ -181,7 +181,6 @@ const setUserParams = (params) => {
:create="false"
:crud-model="{
disableInfiniteScroll: true,
insertOnLoad: false,
}"
:table="{
'row-key': 'itemFk',

View File

@ -547,7 +547,6 @@ watch(route, () => {
},
]"
v-text="col.value"
:data-cy="`extra-community-${col.name}`"
/>
<TravelDescriptorProxy
v-if="col.name === 'id'"

View File

@ -99,10 +99,6 @@ const columns = computed(() => [
workerFk: entityId,
},
}"
:crud-model="{
insertOnLoad: false,
}"
order="paymentDate DESC"
:columns="columns"
auto-load

View File

@ -128,9 +128,6 @@ const columns = computed(() => [
workerFk: entityId,
},
}"
:crud-model="{
insertOnLoad: false,
}"
order="id DESC"
:columns="columns"
auto-load

View File

@ -109,9 +109,6 @@ const columns = [
workerFk: entityId,
},
}"
:crud-model="{
insertOnLoad: false,
}"
order="date DESC"
:columns="columns"
auto-load

View File

@ -1,26 +1,28 @@
<script setup>
import { useRoute } from 'vue-router';
import { useState } from 'src/composables/useState';
import VnNotes from 'src/components/ui/VnNotes.vue';
const route = useRoute();
const state = useState();
const user = state.getUser();
const userFilter = {
order: 'created DESC',
include: {
relation: 'worker',
scope: {
fields: ['id', 'firstName', 'lastName'],
include: {
relation: 'user',
scope: {
fields: ['id', 'nickname', 'name'],
},
},
},
},
};
const body = {
workerFk: route.params.id,
userFk: user.value.id,
};
const body = { workerFk: route.params.id };
</script>
<template>

View File

@ -170,9 +170,6 @@ function isSigned(row) {
'row-key': 'deviceProductionFk',
selection: 'multiple',
}"
:crud-model="{
insertOnLoad: false,
}"
:table-filter="{ hiddenTags: ['userFk'] }"
>
<template #moreBeforeActions>

View File

@ -66,8 +66,6 @@ const excludeType = computed({
const arrayData = useArrayData('ZoneEvents');
const exclusionGeoCreate = async () => {
if (await zoneHasTickets(route.params.id, dated.value)) return;
const params = {
zoneFk: parseInt(route.params.id),
date: dated.value,
@ -89,8 +87,6 @@ const exclusionCreate = async () => {
};
const zoneIds = props.zoneIds?.length ? props.zoneIds : [route.params.id];
for (const id of zoneIds) {
if (await zoneHasTickets(id, dated.value)) return;
const url = `Zones/${id}/exclusions`;
let today = moment(dated.value);
let lastDay = today.clone().add(nMonths, 'months').endOf('month');
@ -127,26 +123,6 @@ const exclusionCreate = async () => {
await refetchEvents();
};
const zoneHasTickets = async (zoneId, date) => {
const filter = {
where: {
zoneFk: zoneId,
shipped: date,
},
};
const params = { filter: JSON.stringify(filter) };
const { data } = await axios.get('Tickets', { params });
if (data.length > 0) {
quasar.notify({
message: t('eventsExclusionForm.cantCloseZone'),
type: 'negative',
});
await refetchEvents();
return true;
}
return false;
};
const onSubmit = async () => {
if (excludeType.value === 'all') exclusionCreate();
else exclusionGeoCreate();

View File

@ -80,7 +80,6 @@ eventsExclusionForm:
all: All
specificLocations: Specific locations
rootTreeLabel: Locations where it is not distributed
cantCloseZone: Can not close this zone because there are tickets programmed for that day
eventsInclusionForm:
addEvent: Add event
editEvent: Edit event

View File

@ -81,7 +81,6 @@ eventsExclusionForm:
all: Todo
specificLocations: Localizaciones concretas
rootTreeLabel: Localizaciones en las que no se reparte
cantCloseZone: No se puede cerrar la zona porque hay tickets programados para ese día
eventsInclusionForm:
addEvent: Añadir evento
editEvent: Editar evento

View File

@ -0,0 +1,99 @@
import { RouterView } from 'vue-router';
const fixedAssetCard = {
name: 'FixedAssetCard',
path: ':id',
component: () => import('src/pages/FixedAsset/Card/FixedAssetCard.vue'),
redirect: { name: 'FixedAssetSummary' },
meta: {
menu: ['FixedAssetBasicData', 'FixedAssetInvoice', 'FixedAssetDms', 'FixedAssetLog'],
},
children: [
{
name: 'FixedAssetSummary',
path: 'summary',
meta: {
title: 'summary',
icon: 'view_list',
},
component: () => import('src/pages/FixedAsset/Card/FixedAssetSummary.vue'),
},
{
name: 'FixedAssetBasicData',
path: 'basic-data',
meta: {
title: 'basicData',
icon: 'vn:settings',
},
component: () => import('src/pages/FixedAsset/Card/FixedAssetBasicData.vue'),
},
{
name: 'FixedAssetInvoice',
path: 'invoice',
meta: {
title: 'invoiceIns',
icon: 'vn:invoice-in',
},
component: () => import('pages/FixedAsset/Card/FixedAssetInvoice.vue'),
},
{
name: 'FixedAssetDms',
path: 'dms',
meta: {
title: 'dms',
icon: 'cloud_upload',
},
component: () => import('src/pages/FixedAsset/Card/FixedAssetDms.vue'),
},
{
name: 'FixedAssetLog',
path: 'history',
meta: {
title: 'history',
icon: 'history',
},
component: () => import('src/pages/FixedAsset/Card/FixedAssetLog.vue'),
},
],
};
export default {
name: 'FixedAsset',
path: '/fixed-asset',
meta: {
title: 'fixedAsset',
icon: 'inventory_2',
moduleName: 'FixedAsset',
menu: ['FixedAssetList'],
},
component: RouterView,
redirect: { name: 'FixedAssetMain' },
children: [
{
name: 'FixedAssetMain',
path: '',
component: () => import('src/components/common/VnModule.vue'),
redirect: { name: 'FixedAssetIndexMain' },
children: [
{
path: '',
name: 'FixedAssetIndexMain',
component: () => import('src/pages/FixedAsset/FixedAssetList.vue'),
redirect: { name: 'FixedAssetList' },
children: [
{
name: 'FixedAssetList',
path: 'list',
meta: {
title: 'list',
icon: 'view_list',
},
component: () => import('src/pages/FixedAsset/FixedAssetList.vue'),
},
fixedAssetCard,
],
},
],
},
],
};

View File

@ -15,6 +15,7 @@ import Entry from './entry';
import Zone from './zone';
import Account from './account';
import Monitor from './monitor';
import FixedAsset from './fixedAsset';
export default [
Item,
@ -34,4 +35,5 @@ export default [
Zone,
Account,
Monitor,
FixedAsset,
];

View File

@ -15,6 +15,7 @@ import entry from 'src/router/modules/entry';
import zone from 'src/router/modules/zone';
import account from './modules/account';
import monitor from 'src/router/modules/monitor';
import fixedAsset from 'src/router/modules/fixedAsset';
const routes = [
{
@ -83,6 +84,7 @@ const routes = [
entry,
zone,
account,
fixedAsset,
{
path: '/:catchAll(.*)*',
name: 'NotFound',

View File

@ -18,6 +18,7 @@ export const useNavigationStore = defineStore('navigationStore', () => {
'monitor',
'supplier',
'claim',
'fixedAsset',
'route',
'ticket',
'worker',

View File

@ -24,8 +24,13 @@ export CI=true
export TZ=Europe/Madrid
# IMAGES
docker-compose -f test/cypress/docker-compose.yml --project-directory . pull db
docker-compose -f test/cypress/docker-compose.yml --project-directory . pull back
docker build -t registry.verdnatura.es/salix-back:dev -f "$salix_dir/back/Dockerfile" "$salix_dir"
cd "$salix_dir" && npx myt run -t
docker exec vn-database sh -c "rm -rf /mysql-template"
docker exec vn-database sh -c "cp -a /var/lib/mysql /mysql-template"
docker commit vn-database registry.verdnatura.es/salix-db:dev
docker rm -f vn-database
cd "$current_dir"
docker build -f ./docs/Dockerfile.dev -t lilium-dev .
# END IMAGES

View File

@ -1,4 +1,4 @@
describe('Account descriptor', { testIsolation: true }, () => {
describe('Account descriptor', () => {
const descriptorOptions = '[data-cy="descriptor-more-opts-menu"] > .q-list';
const url = '/#/account/1/summary';

View File

@ -1,5 +1,5 @@
/// <reference types="cypress" />
describe('ClaimDevelopment', { testIsolation: true }, () => {
describe('ClaimDevelopment', () => {
const claimId = 1;
const firstLineReason = 'tbody > :nth-child(1) > :nth-child(2)';
const thirdRow = 'tbody > :nth-child(3)';

View File

@ -1,5 +1,5 @@
/// <reference types="cypress" />
describe('Client fiscal data', { testIsolation: true }, () => {
describe('Client fiscal data', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');

View File

@ -1,5 +1,5 @@
/// <reference types="cypress" />
describe('Client list', { testIsolation: true }, () => {
describe('Client list', () => {
beforeEach(() => {
cy.login('developer');
cy.visit('/#/customer/list', {

View File

@ -1,5 +1,5 @@
/// <reference types="cypress" />
describe('Entry PreAccount Functionality', { testIsolation: true }, () => {
describe('Entry PreAccount Functionality', () => {
beforeEach(() => {
cy.login('administrative');
cy.visit('/#/entry/pre-account');

View File

@ -0,0 +1,57 @@
describe('FixedAssetBasicData', () => {
const selectors = {
resetBtn: '#st-actions > .q-btn-group > .q-btn[title="Reset"]',
saveBtn: '#st-actions > .q-btn-group > .q-btn[title="Save"]',
labelDescription: '[data-cy="Description_input"]',
investmentCheckbox: 'vnCheckboxInvestment asset',
completedCheckbox: 'vnCheckboxCompleted',
};
const updateData = {
Id: { val: '12' },
Value: { val: '100000' },
Description: { val: 'F.R.I.D.A.Y.' },
Company: { val: 'CCs', type: 'select' },
Subaccount: { val: '2800000000', type: 'select' },
Endowment: { val: '6810000000', type: 'select' },
'Element account': { val: '561.12' },
'Amort. start': { val: '1.5', type: 'date' },
'Amort. end': { val: '1.10.21', type: 'date' },
'Final date': { val: '1.11.21', type: 'date' },
Amortization: { val: '640' },
'Amort. plan': { val: '2', type: 'select' },
Group: { val: 'Chitauri', type: 'select' },
Location: { val: 'Stark tower', type: 'select' },
Discharged: { val: '1.4.25', type: 'date' },
'Cause of discharge': { val: 'Venta' },
};
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('administrative');
cy.visit('#/fixed-asset/1/basic-data');
});
it('Should reset fixed asset basic data', () => {
cy.get(selectors.labelDescription)
.should('be.visible')
.click()
.invoke('text')
.then((name) => {
name = name.trim();
cy.get(selectors.labelDescription)
.click()
.type(`{selectall}{backspace}Tony Stark`);
cy.get(selectors.resetBtn).click();
cy.containContent(selectors.labelDescription, name);
});
});
it('Should edit fixed asset basic data', () => {
cy.fillInForm(updateData);
cy.dataCy(selectors.investmentCheckbox).click();
cy.dataCy(selectors.completedCheckbox).click();
cy.get(selectors.saveBtn).click();
cy.checkNotification('Data saved');
});
});

View File

@ -0,0 +1,19 @@
describe('FixedAssetDescriptor', () => {
const selectors = {
listItem: '[role="menu"] .q-list .q-item',
deleteOpt: 'Delete fixed asset',
};
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/fixed-asset/2/summary');
});
it('should delete the fixed-asset', () => {
cy.openActionsDescriptor();
cy.contains(selectors.listItem, selectors.deleteOpt).click();
cy.clickConfirm();
cy.checkNotification('Data deleted');
});
});

View File

@ -0,0 +1,122 @@
describe('FixedAssetDms', () => {
const getBtnSelector = (trPosition, btnPosition) =>
`tr:${trPosition}-child > .text-right > .no-wrap > :nth-child(${btnPosition}) > .q-btn > .q-btn__content > .q-icon`;
const selectors = {
firstRowDownloadBtn: getBtnSelector('first', 1),
firstRowEditBtn: getBtnSelector('first', 2),
firstRowDeleteBtn: getBtnSelector('first', 3),
firstRowReference:
'tr:first-child > :nth-child(5) > .q-tr > :nth-child(1) > span',
firstRowId: 'tr:first-child > :nth-child(2) > .q-tr > :nth-child(1) > span',
descriptorTitle: '.descriptor .title',
lastRowWorkerLink: 'tr:last-child > :nth-child(8) > .q-tr > .link',
summaryGoToSummaryBtn: '.summaryHeader [data-cy="goToSummaryBtn"]',
descriptorOpenSummaryBtn: '.q-menu > .descriptor [data-cy="openSummaryBtn"]',
descriptorGoToSummaryBtn: '.q-menu .descriptor [data-cy="goToSummaryBtn"]',
summaryTitle: '.summaryHeader',
referenceInput: 'Reference_input',
companySelect: 'Company_select',
warehouseSelect: 'Warehouse_select',
fileInput: 'VnDms_inputFile',
importBtn: 'importBtn',
addBtn: 'addButton',
saveFormBtn: 'FormModelPopup_save',
};
const data = {
Reference: { val: 'FixedAsset:Peter Parker' },
Company: { val: 'VNL', type: 'select' },
Warehouse: { val: 'Warehouse One', type: 'select' },
};
const updateData = {
Reference: { val: 'FixedAsset:Peter Parker House' },
Company: { val: 'CCs', type: 'select' },
Warehouse: { val: 'Warehouse Two', type: 'select' },
};
const workerSummaryUrlRegex = /worker\/\d+\/summary/;
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit(`/#/fixed-asset/1/dms`);
});
it('Should create new DMS', () => {
cy.dataCy(selectors.addBtn).click();
cy.fillInForm(data);
cy.dataCy(selectors.fileInput).selectFile('test/cypress/fixtures/image.jpg', {
force: true,
});
cy.dataCy(selectors.saveFormBtn).click();
cy.checkNotification('Data saved');
});
/*
TODO: #8946 REDMINE
*/
it.skip('Should download DMS', () => {
let fileName;
cy.get(selectors.firstRowId)
.invoke('text')
.then((label) => {
label = label.trim();
fileName = `${label}.jpg`;
});
cy.intercept('GET', /\/api\/dms\/\d+\/downloadFile/).as('download');
cy.get(selectors.firstRowDownloadBtn).click();
cy.wait('@download').then((interception) => {
expect(interception.response.statusCode).to.equal(200);
expect(interception.response.headers['content-disposition']).to.contain(
fileName,
);
});
});
it('Should edit DMS', () => {
cy.get(selectors.firstRowEditBtn).click();
cy.fillInForm(updateData);
cy.dataCy(selectors.saveFormBtn).click();
cy.checkNotification('Data saved');
cy.validateContent(selectors.firstRowReference, updateData.Reference.val);
});
it('Should delete DMS', () => {
cy.get(selectors.firstRowDeleteBtn).click();
cy.clickConfirm();
cy.checkNotification('Data deleted');
cy.validateContent(selectors.firstRowReference, 'FixedAsset: Laser');
});
describe('Worker pop-ups', () => {
it('Should redirect to the worker summary from worker descriptor pop-up', () => {
cy.get(selectors.lastRowWorkerLink)
.should('be.visible')
.click()
.invoke('text')
.then((workerName) => {
workerName = workerName.trim();
cy.get(selectors.descriptorGoToSummaryBtn).click();
cy.location().should('match', workerSummaryUrlRegex);
cy.containContent(selectors.descriptorTitle, workerName);
});
});
it('Should redirect to the worker summary from summary pop-up from the worker descriptor pop-up', () => {
cy.get(selectors.lastRowWorkerLink)
.should('be.visible')
.click()
.invoke('text')
.then((workerName) => {
workerName = workerName.trim();
cy.get(selectors.descriptorOpenSummaryBtn).click();
cy.get(selectors.summaryGoToSummaryBtn).click();
cy.location().should('match', workerSummaryUrlRegex);
cy.containContent(selectors.descriptorTitle, workerName);
});
});
});
});

View File

@ -0,0 +1,86 @@
describe('FixedAssetInvoice', () => {
const getLinkSelector = (colField) =>
`tr:first-child > [data-col-field="${colField}"] > .no-padding > .link`;
const selectors = {
firstRowSupplier: getLinkSelector('supplierFk'),
firstRowInvoice: getLinkSelector('supplierRef'),
descriptorSupplierTitle: '[data-cy="vnDescriptor_description"]',
descriptorInvoiceInTitle: '[data-cy="vnDescriptor_title"]',
descriptorOpenSummaryBtn: '.q-menu > .descriptor [data-cy="openSummaryBtn"]',
descriptorGoToSummaryBtn: '.q-menu > .descriptor [data-cy="goToSummaryBtn"]',
summaryGoToSummaryBtn: '.summaryHeader [data-cy="goToSummaryBtn"]',
unassignBtn: 'tableAction-0',
};
const supplierSummaryUrlRegex = /supplier\/\d+\/summary/;
const invoiceInSummaryUrlRegex = /invoice-in\/\d+\/summary/;
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('administrative');
cy.visit(`/#/fixed-asset/1/invoice`);
});
it('Should assign a new invoice', () => {
const data = {
'Invoice nº': { val: '1243', type: 'select' },
Amount: { val: '1000' },
};
cy.addBtnClick();
cy.fillInForm(data);
cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('Data created');
});
it('Should unassign an invoice', () => {
cy.dataCy(selectors.unassignBtn).last().click();
cy.clickConfirm();
cy.checkNotification('Unassigned invoice');
});
describe('Supplier pop-ups', () => {
it('Should redirect to the supplier summary from the supplier descriptor pop-up', () => {
cy.checkRedirectionFromPopUp({
selectorToClick: selectors.firstRowSupplier,
steps: [selectors.descriptorGoToSummaryBtn],
expectedUrlRegex: supplierSummaryUrlRegex,
expectedTextSelector: selectors.descriptorSupplierTitle,
});
});
it('Should redirect to the supplier summary from summary pop-up from the supplier descriptor pop-up', () => {
cy.checkRedirectionFromPopUp({
selectorToClick: selectors.firstRowSupplier,
steps: [
selectors.descriptorOpenSummaryBtn,
selectors.summaryGoToSummaryBtn,
],
expectedUrlRegex: supplierSummaryUrlRegex,
expectedTextSelector: selectors.descriptorSupplierTitle,
});
});
});
describe('Invoice pop-ups', () => {
it('Should redirect to the invoiceIn summary from the invoice descriptor pop-up', () => {
cy.checkRedirectionFromPopUp({
selectorToClick: selectors.firstRowInvoice,
steps: [selectors.descriptorGoToSummaryBtn],
expectedUrlRegex: invoiceInSummaryUrlRegex,
expectedTextSelector: selectors.descriptorInvoiceInTitle,
});
});
it('Should redirect to the invoiceIn summary from summary pop-up from the invoice descriptor pop-up', () => {
cy.checkRedirectionFromPopUp({
selectorToClick: selectors.firstRowInvoice,
steps: [
selectors.descriptorOpenSummaryBtn,
selectors.summaryGoToSummaryBtn,
],
expectedUrlRegex: invoiceInSummaryUrlRegex,
expectedTextSelector: selectors.descriptorInvoiceInTitle,
});
});
});
});

View File

@ -0,0 +1,50 @@
describe('FixedAssetList', () => {
const selectors = {
firstRow: 'tr:first-child > [data-col-field="description"]',
descriptorTitle: '[data-cy="vnDescriptor_title"]',
};
const summaryUrlRegex = /fixed-asset\/\d+\/summary/;
const data = {
Id: { val: '123' },
Value: { val: '100000' },
Description: { val: 'F.R.I.D.A.Y.' },
'Amort. start': { val: '1.5', type: 'date' },
'Amort. end': { val: '1.10.21', type: 'date' },
Subaccount: { val: '2800000000', type: 'select' },
Endowment: { val: '6810000000', type: 'select' },
'Element account': { val: '561.123' },
'Amort. plan': { val: '1', type: 'select' },
Group: { val: 'Avengers', type: 'select' },
Location: { val: 'Stark tower', type: 'select' },
};
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('administrative');
cy.visit('/#/fixed-asset/list');
cy.typeSearchbar('{enter}');
});
it('Should redirect to the fixed asset summary when clicking the row', () => {
cy.get(selectors.firstRow)
.should('be.visible')
.click()
.invoke('text')
.then((name) => {
name = name.trim();
cy.location().should('match', summaryUrlRegex);
cy.containContent(selectors.descriptorTitle, name);
});
});
it('Should create new fixed asset', () => {
cy.addBtnClick();
cy.fillInForm(data);
cy.dataCy('FormModelPopup_save').should('be.visible').click();
cy.checkNotification('Data created');
cy.location().should('match', summaryUrlRegex);
cy.containContent(selectors.descriptorTitle, data.Description.val);
});
});

View File

@ -0,0 +1,143 @@
describe('FixedAsset summary', () => {
const selectors = {
summaryTitle: '.summaryHeader',
descriptorTitle: '[data-cy="vnDescriptor_title"]',
supplierDescriptorTitle: '[data-cy="vnDescriptor_description"]',
descriptorOpenSummaryBtn: '.q-menu > .descriptor [data-cy="openSummaryBtn"]',
descriptorGoToSummaryBtn: '.q-menu > .descriptor [data-cy="goToSummaryBtn"]',
summaryGoToSummaryBtn: '.summaryHeader [data-cy="goToSummaryBtn"]',
summaryBasicDataBlock1Link: 'a.link[data-cy="titleBasicDataBlock1"]',
summaryBasicDataBlock2Link: 'a.link[data-cy="titleBasicDataBlock2"]',
summaryInvoiceBlockLink: 'a.link[data-cy="titleInvoiceBlock"]',
summaryDmsBlockLink: 'a.link[data-cy="titleDmsBlock"]',
dmsFirstRowWorkerLink: '.summaryBody :nth-child(1) > :nth-child(7) .link',
invoiceFirstRowSupplierLink:
':nth-child(1) > :nth-child(2) > [data-cy="supplierLink"]',
invoiceFirstRowSupplierRefLink:
':nth-child(1) > :nth-child(3) > [data-cy="invoiceLink"]',
basicDataIcon: 'FixedAssetBasicData-menu-item',
invoiceIcon: 'FixedAssetInvoice-menu-item',
dmsIcon: 'FixedAssetDms-menu-item',
logIcon: 'FixedAssetLog-menu-item',
};
const url = '/#/fixed-asset/1/summary';
const basicDataUrlRegex = /fixed-asset\/1\/basic-data/;
const invoiceUrlRegex = /fixed-asset\/1\/invoice/;
const dmsUrlRegex = /fixed-asset\/1\/dms/;
const logUrlRegex = /fixed-asset\/1\/history/;
const workerSummaryUrlRegex = /worker\/\d+\/summary/;
const supplierSummaryUrlRegex = /supplier\/\d+\/summary/;
const invoiceSummaryUrlRegex = /invoice-in\/\d+\/summary/;
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('administrative');
cy.visit(url);
});
it('Should redirect to the corresponding section when clicking on the icons in the left menu', () => {
cy.dataCy(selectors.basicDataIcon).click();
cy.location().should('match', basicDataUrlRegex);
cy.visit(url);
cy.dataCy(selectors.invoiceIcon).click();
cy.location().should('match', invoiceUrlRegex);
cy.visit(url);
cy.dataCy(selectors.dmsIcon).click();
cy.location().should('match', dmsUrlRegex);
cy.visit(url);
cy.dataCy(selectors.logIcon).click();
cy.location().should('match', logUrlRegex);
});
it('Should redirect to fixed asset basic-data when clicking on basic-data title links', () => {
cy.get(selectors.summaryBasicDataBlock1Link).click();
cy.location().should('match', basicDataUrlRegex);
cy.visit(url);
cy.get(selectors.summaryBasicDataBlock2Link).click();
cy.location().should('match', basicDataUrlRegex);
cy.visit(url);
});
it('Should redirect to fixed asset invoices when clicking on asigned invoices title', () => {
cy.get(selectors.summaryInvoiceBlockLink).click();
cy.location().should('match', invoiceUrlRegex);
});
it('Should redirect to fixed asset DMS when clicking on file managment title', () => {
cy.get(selectors.summaryDmsBlockLink).click();
cy.location().should('match', dmsUrlRegex);
});
describe('Worker pop-ups', () => {
it('Should redirect to the worker summary from worker descriptor pop-up', () => {
cy.checkRedirectionFromPopUp({
selectorToClick: selectors.dmsFirstRowWorkerLink,
steps: [selectors.descriptorGoToSummaryBtn],
expectedUrlRegex: workerSummaryUrlRegex,
expectedTextSelector: selectors.descriptorTitle,
});
});
it('Should redirect to the worker summary from summary pop-up from the worker descriptor pop-up', () => {
cy.checkRedirectionFromPopUp({
selectorToClick: selectors.dmsFirstRowWorkerLink,
steps: [
selectors.descriptorOpenSummaryBtn,
selectors.summaryGoToSummaryBtn,
],
expectedUrlRegex: workerSummaryUrlRegex,
expectedTextSelector: selectors.descriptorTitle,
});
});
});
describe('Supplier pop-ups', () => {
it('Should redirect to the Supplier summary from Supplier descriptor pop-up', () => {
cy.checkRedirectionFromPopUp({
selectorToClick: selectors.invoiceFirstRowSupplierLink,
steps: [selectors.descriptorGoToSummaryBtn],
expectedUrlRegex: supplierSummaryUrlRegex,
expectedTextSelector: selectors.supplierDescriptorTitle,
});
});
it('Should redirect to the Supplier summary from summary pop-up from the Supplier descriptor pop-up', () => {
cy.checkRedirectionFromPopUp({
selectorToClick: selectors.invoiceFirstRowSupplierLink,
steps: [
selectors.descriptorOpenSummaryBtn,
selectors.summaryGoToSummaryBtn,
],
expectedUrlRegex: supplierSummaryUrlRegex,
expectedTextSelector: selectors.supplierDescriptorTitle,
});
});
});
describe('Invoice in pop-ups', () => {
it('Should redirect to the invoice summary from invoice descriptor pop-up', () => {
cy.checkRedirectionFromPopUp({
selectorToClick: selectors.invoiceFirstRowSupplierRefLink,
steps: [selectors.descriptorGoToSummaryBtn],
expectedUrlRegex: invoiceSummaryUrlRegex,
expectedTextSelector: selectors.descriptorTitle,
});
});
it('Should redirect to the invoice summary from summary pop-up from the invoice descriptor pop-up', () => {
cy.checkRedirectionFromPopUp({
selectorToClick: selectors.invoiceFirstRowSupplierRefLink,
steps: [
selectors.descriptorOpenSummaryBtn,
selectors.summaryGoToSummaryBtn,
],
expectedUrlRegex: invoiceSummaryUrlRegex,
expectedTextSelector: selectors.descriptorTitle,
});
});
});
});

View File

@ -1,6 +1,6 @@
/// <reference types="cypress" />
import moment from 'moment';
describe('InvoiceInBasicData', { testIsolation: true }, () => {
describe('InvoiceInBasicData', () => {
const dialogInputs = '.q-dialog input';
const getDocumentBtns = (opt) => `[data-cy="dms-buttons"] > :nth-child(${opt})`;
const futureDate = moment().add(1, 'days').format('DD-MM-YYYY');

View File

@ -1,4 +1,4 @@
describe('invoiceInCorrective', { testIsolation: true }, () => {
describe('invoiceInCorrective', () => {
beforeEach(() => cy.login('administrative'));
it('should modify the invoice', () => {

View File

@ -1,4 +1,4 @@
describe('InvoiceInDescriptor', { testIsolation: true }, () => {
describe('InvoiceInDescriptor', () => {
beforeEach(() => cy.login('administrative'));
describe('more options', () => {

View File

@ -24,7 +24,6 @@ describe('InvoiceOut list', () => {
});
it.skip('should download all pdfs', () => {
cy.get(columnCheckbox).click();
cy.get(columnCheckbox).click();
cy.dataCy('InvoiceOutDownloadPdfBtn').click();
});

View File

@ -1,5 +1,5 @@
/// <reference types="cypress" />
describe('InvoiceOut summary', { testIsolation: true }, () => {
describe('InvoiceOut summary', () => {
const transferInvoice = {
Client: { val: 'employee', type: 'select' },
Type: { val: 'Error in customer data', type: 'select' },

View File

@ -1,5 +1,5 @@
/// <reference types="cypress" />
describe('ItemBarcodes', { testIsolation: true }, () => {
describe('ItemBarcodes', () => {
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/item/1/barcode`);

View File

@ -1,5 +1,5 @@
/// <reference types="cypress" />
describe('Item summary', { testIsolation: true }, () => {
describe('Item summary', () => {
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/item/1/summary`);

View File

@ -1,4 +1,4 @@
describe('Item tag', { testIsolation: true }, () => {
describe('Item tag', () => {
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/item/1/tags`);

View File

@ -1,5 +1,5 @@
/// <reference types="cypress" />
describe('Login', { testIsolation: true }, () => {
describe('Login', () => {
beforeEach(() => {
cy.visit('/#/login');
cy.get('#switchLanguage').click();

View File

@ -1,17 +1,18 @@
/// <reference types="cypress" />
describe('Logout', { testIsolation: true }, () => {
describe('Logout', () => {
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/dashboard`);
cy.waitForElement('.q-page', 6000);
});
describe('by user', () => {
it('should logout', () => {
cy.get('#user').click();
cy.get('#logout').click();
});
it('should throw session expired error if token has expired or is not valid during navigation', () => {
});
describe('not user', () => {
beforeEach(() => {
cy.intercept('GET', '**StarredModules**', {
statusCode: 401,
body: {
@ -24,9 +25,13 @@ describe('Logout', { testIsolation: true }, () => {
},
statusMessage: 'AUTHORIZATION_REQUIRED',
}).as('badRequest');
});
it('when token not exists', () => {
cy.get('.q-list').should('be.visible').first().should('be.visible').click();
cy.wait('@badRequest');
cy.checkNotification('Your session has expired. Please log in again');
cy.checkNotification('Authorization Required');
});
});
});

View File

@ -1,5 +1,5 @@
/// <reference types="cypress" />
describe('Monitor Tickets Table', { testIsolation: true }, () => {
describe('Monitor Tickets Table', () => {
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('salesPerson');

View File

@ -1,5 +1,5 @@
/// <reference types="cypress" />
describe('OrderCatalog', { testIsolation: true }, () => {
describe.skip('OrderCatalog', () => {
beforeEach(() => {
cy.login('developer');
cy.viewport(1920, 1080);

View File

@ -1,5 +1,5 @@
/// <reference types="cypress" />
describe('OrderList', { testIsolation: true }, () => {
describe('OrderList', () => {
const clientCreateSelect = '#formModel [data-cy="Client_select"]';
const addressCreateSelect = '#formModel [data-cy="Address_select"]';
const agencyCreateSelect = '#formModel [data-cy="Agency_select"]';

View File

@ -1,4 +1,4 @@
describe('Cmr list', { testIsolation: true }, () => {
describe('Cmr list', () => {
const getLinkSelector = (colField) =>
`tr:first-child > [data-col-field="${colField}"] > .no-padding > .link`;

View File

@ -1,4 +1,4 @@
describe.skip('RoadMap', () => {
describe('RoadMap', () => {
const getSelector = (colField) =>
`tr:last-child > [data-col-field="${colField}"] > .no-padding`;

View File

@ -1,4 +1,4 @@
describe('RouteAutonomous', { testIsolation: true }, () => {
describe('RouteAutonomous', () => {
const getLinkSelector = (colField, link = true) =>
`tr:first-child > [data-col-field="${colField}"] > .no-padding${
link ? ' > .link' : ''

View File

@ -1,4 +1,4 @@
describe('Route', { testIsolation: true }, () => {
describe('Route', () => {
const getSelector = (colField) =>
`tr:last-child > [data-col-field="${colField}"] > .no-padding > .link`;

View File

@ -1,4 +1,4 @@
describe('Vehicle DMS', { testIsolation: true }, () => {
describe('Vehicle DMS', () => {
const getSelector = (btnPosition) =>
`tr:last-child > .text-right > .no-wrap > :nth-child(${btnPosition}) > .q-btn > .q-btn__content > .q-icon`;

View File

@ -1,7 +1,7 @@
/// <reference types="cypress" />
const firstRow = 'tbody > :nth-child(1)';
describe('TicketSale', { testIsolation: true }, () => {
describe('TicketSale', () => {
describe('Ticket #23', () => {
beforeEach(() => {
cy.login('claimManager');

View File

@ -1,16 +0,0 @@
describe('Travel Extracommunity', () => {
it('Should show travels', () => {
cy.login('logistic');
cy.visit(`/#/travel/extra-community`);
cy.get('.q-page').should('be.visible');
});
it('Should show travels when user is supplier', () => {
cy.login('petterparker');
cy.visit(`/#/travel/extra-community`);
cy.get('[data-cy="vnFilterPanelChip_continent"] > .q-chip__icon--remove').click();
cy.dataCy('extra-community-cargoSupplierNickname').each(($el) => {
cy.wrap($el).should('contain.text', 'The farmer');
});
});
});

View File

@ -1,5 +1,5 @@
/// <reference types="cypress" />
describe('UserPanel', { testIsolation: true }, () => {
describe('UserPanel', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');

View File

@ -1,5 +1,5 @@
/// <reference types="cypress" />
describe('VnBreadcrumbs', { testIsolation: true }, () => {
describe('VnBreadcrumbs', () => {
const lastBreadcrumb = '.q-breadcrumbs--last > .q-breadcrumbs__el';
beforeEach(() => {
cy.login('developer');

View File

@ -1,6 +1,6 @@
const { randomNumber, randomString } = require('../../support');
describe('VnLocation', { testIsolation: true }, () => {
describe('VnLocation', () => {
const locationOptions = '[role="listbox"] > div.q-virtual-scroll__content > .q-item';
const dialogInputs = '.q-dialog label input';
const createLocationButton = '.q-form > .q-card > .vn-row:nth-child(6) .--add-icon';

View File

@ -1,5 +1,5 @@
/// <reference types="cypress" />
describe('VnLog', { testIsolation: true }, () => {
describe('VnLog', () => {
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/claim/${1}/log`);

View File

@ -1,4 +1,4 @@
describe('WorkerCreate', { testIsolation: true }, () => {
describe('WorkerCreate', () => {
const externalRadio = '.q-radio:nth-child(2)';
const developerBossId = 120;
const payMethodCross =

View File

@ -1,4 +1,4 @@
describe('WorkerManagement', { testIsolation: true }, () => {
describe('WorkerManagement', () => {
const nif = '12091201A';
const searchButton = '.q-scrollarea__content > .q-btn--standard > .q-btn__content';
const url = '/#/worker/management';

View File

@ -1,4 +1,4 @@
describe('WorkerNotificationsManager', { testIsolation: true }, () => {
describe('WorkerNotificationsManager', () => {
const salesPersonId = 18;
const developerId = 9;

View File

@ -1,4 +1,4 @@
describe('ZoneBasicData', { testIsolation: true }, () => {
describe('ZoneBasicData', () => {
const priceBasicData = '[data-cy="ZoneBasicDataPrice"]';
const saveBtn = '.q-btn-group > .q-btn--standard';

View File

@ -1,4 +1,4 @@
describe('ZoneCalendar', { testIsolation: true }, () => {
describe('ZoneCalendar', () => {
const addEventBtn = '.q-page-sticky > div > .q-btn';
const submitBtn = '.q-mt-lg > .q-btn--standard';
const deleteBtn = 'ZoneEventsPanelDeleteBtn';
@ -47,20 +47,4 @@ describe('ZoneCalendar', { testIsolation: true }, () => {
cy.dataCy('ZoneEventExclusionDeleteBtn').click();
cy.dataCy('VnConfirm_confirm').click();
});
it(
'should not exclude an event if there are tickets for that zone and day',
{ testIsoaltion: true },
() => {
cy.visit(`/#/zone/3/events`);
cy.get('.q-mb-sm > .q-radio__inner').click();
cy.get(
'.q-current-day > .q-calendar-month__day--content > [data-cy="ZoneCalendarDay"]',
).click();
cy.get('.q-mt-lg > .q-btn--standard').click();
cy.checkNotification(
'Can not close this zone because there are tickets programmed for that day',
);
},
);
});

View File

@ -1,4 +1,4 @@
describe('ZoneCreate', { testIsolation: true }, () => {
describe('ZoneCreate', () => {
const data = {
Name: { val: 'Zone pickup D' },
Price: { val: '3' },

View File

@ -26,8 +26,27 @@ describe('ZoneDeliveryDays', () => {
});
}).as('events');
cy.selectOption('[data-cy="ZoneDeliveryDaysPostcodeSelect"]', postcode);
cy.selectOption('[data-cy="ZoneDeliveryDaysAgencySelect"]', agency);
cy.dataCy('ZoneDeliveryDaysPostcodeSelect').type(postcode);
cy.get('.q-menu .q-item').contains(postcode).click();
cy.get('.q-menu').then(($menu) => {
if ($menu.is(':visible')) {
cy.get('[data-cy="ZoneDeliveryDaysPostcodeSelect"]')
.as('focusedElement')
.focus();
cy.get('@focusedElement').blur();
}
});
cy.dataCy('ZoneDeliveryDaysAgencySelect').type(agency);
cy.get('.q-menu .q-item').contains(agency).click();
cy.get('.q-menu').then(($menu) => {
if ($menu.is(':visible')) {
cy.get('[data-cy="ZoneDeliveryDaysAgencySelect"]')
.as('focusedElement')
.focus();
cy.get('@focusedElement').blur();
}
});
cy.get(submitForm).click();
cy.wait('@events').then((interception) => {

View File

@ -392,7 +392,6 @@ Cypress.Commands.add('validateContent', (selector, expectedValue) => {
Cypress.Commands.add('containContent', (selector, expectedValue) => {
cy.get(selector)
.should('be.visible')
.invoke('text')
.then((text) => {
expect(text).to.include(expectedValue);