#6942 improve invoiceIn #220

Merged
jorgep merged 63 commits from 6942-improveInvoceIn into dev 2024-05-29 07:03:46 +00:00
34 changed files with 922 additions and 765 deletions

View File

@ -1,6 +1,7 @@
<script setup>
import axios from 'axios';
import { computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useValidator } from 'src/composables/useValidator';
@ -10,6 +11,7 @@ import VnConfirm from 'components/ui/VnConfirm.vue';
import SkeletonTable from 'components/ui/SkeletonTable.vue';
import { tMobile } from 'src/composables/tMobile';
const { push } = useRouter();
const quasar = useQuasar();
const stateStore = useStateStore();
const { t } = useI18n();
@ -60,6 +62,11 @@ const $props = defineProps({
type: Function,
default: null,
},
goTo: {
Review

url a la que redirigir al hacer click en 'save and continue'

url a la que redirigir al hacer click en 'save and continue'
type: String,
default: '',
description: 'It is used for redirect on click "save and continue"',
},
});
const isLoading = ref(false);
@ -128,6 +135,11 @@ async function onSubmit() {
await saveChanges($props.saveFn ? formData.value : null);
}
async function onSumbitAndGo() {
await onSubmit();
push({ path: $props.goTo });
}
async function saveChanges(data) {
if ($props.saveFn) {
$props.saveFn(data, getChanges);
@ -310,7 +322,40 @@ watch(formUrl, async () => {
:title="t('globals.reset')"
v-if="$props.defaultReset"
/>
<QBtnDropdown
Review

Botón desplegable con la op. de guardar y continuar o solo guardar.

Botón desplegable con la op. de guardar y continuar o solo guardar.
v-if="$props.goTo && $props.defaultSave"
@click="onSumbitAndGo"
:label="tMobile('globals.saveAndContinue')"
:title="t('globals.saveAndContinue')"
:disable="!hasChanges"
color="primary"
icon="save"
split
>
<QList>
<QItem
color="primary"
clickable
v-close-popup
@click="onSubmit"
:title="t('globals.save')"
>
<QItemSection>
<QItemLabel>
<QIcon
name="save"
color="white"
class="q-mr-sm"
size="sm"
/>
{{ t('globals.save').toUpperCase() }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QBtnDropdown>
<QBtn
v-else-if="!$props.goTo && $props.defaultSave"
:label="tMobile('globals.save')"
ref="saveButtonRef"
color="primary"
@ -318,7 +363,6 @@ watch(formUrl, async () => {
@click="onSubmit"
:disable="!hasChanges"
:title="t('globals.save')"
v-if="$props.defaultSave"
/>
<slot name="moreAfterActions" />
</QBtnGroup>

View File

@ -24,7 +24,7 @@ const $props = defineProps({
default: '',
},
limit: {
type: String,
type: [String, Number],
default: '',
},
Review

Elimino el warning de consola.

Elimino el warning de consola.
params: {

View File

@ -1,7 +1,7 @@
<script setup>
import axios from 'axios';
import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue';
import { onBeforeRouteLeave } from 'vue-router';
import { onBeforeRouteLeave, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useState } from 'src/composables/useState';
@ -11,7 +11,9 @@ import useNotify from 'src/composables/useNotify.js';
import SkeletonForm from 'components/ui/SkeletonForm.vue';
import VnConfirm from './ui/VnConfirm.vue';
import { tMobile } from 'src/composables/tMobile';
import { useArrayData } from 'src/composables/useArrayData';
const { push } = useRouter();
const quasar = useQuasar();
const state = useState();
const stateStore = useStateStore();
@ -74,55 +76,17 @@ const $props = defineProps({
type: Function,
default: null,
},
goTo: {
Review

Lo mismo que en crudModel

Lo mismo que en crudModel
type: String,
default: '',
description: 'It is used for redirect on click "save and continue"',
},
});
const emit = defineEmits(['onFetch', 'onDataSaved']);
const componentIsRendered = ref(false);
onMounted(async () => {
originalData.value = $props.formInitialData;
nextTick(() => {
componentIsRendered.value = true;
});
// Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla
state.set($props.model, $props.formInitialData);
if ($props.autoLoad && !$props.formInitialData) {
await fetch();
}
// Si así se desea disparamos el watcher del form después de 100ms, asi darle tiempo de que se haya cargado la data inicial
// para evitar que detecte cambios cuando es data inicial default
if ($props.observeFormChanges) {
setTimeout(() => {
startFormWatcher();
}, 100);
}
});
onBeforeRouteLeave((to, from, next) => {
if (hasChanges.value && $props.observeFormChanges)
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('Unsaved changes will be lost'),
message: t('Are you sure exit without saving?'),
promise: () => next(),
},
});
else next();
});
onUnmounted(() => {
// Restauramos los datos originales en el store si se realizaron cambios en el formulario pero no se guardaron, evitando modificaciones erróneas.
if (hasChanges.value) {
state.set($props.model, originalData.value);
return;
}
if ($props.clearStoreOnUnmount) state.unset($props.model);
});
const arrayData = useArrayData($props.model);
const isLoading = ref(false);
// Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas
const isResetting = ref(false);
@ -143,29 +107,72 @@ const defaultButtons = computed(() => ({
},
...$props.defaultButtons,
}));
const startFormWatcher = () => {
onMounted(async () => {
Review

Lo he pasado aquí para que todos los hooks estén agrupados.

Lo he pasado aquí para que todos los hooks estén agrupados.
originalData.value = JSON.parse(JSON.stringify($props.formInitialData ?? {}));
Review

Hay que hacerlo así para que se haga una copia, si no, se hace una referencia al mismo objeto.

Hay que hacerlo así para que se haga una copia, si no, se hace una referencia al mismo objeto.
Review

Se podría poner el comentario en código?

Se podría poner el comentario en código?
nextTick(() => (componentIsRendered.value = true));
// Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla
state.set($props.model, $props.formInitialData);
if ($props.autoLoad && !$props.formInitialData && $props.url) await fetch();
else if (arrayData.store.data) updateAndEmit(arrayData.store.data, 'onFetch');
if ($props.observeFormChanges) {
watch(
() => formData.value,
(newVal, oldVal) => {
if (!oldVal) return;
hasChanges.value =
!isResetting.value &&
JSON.stringify(newVal) !== JSON.stringify(originalData.value);
Review

Para ver si hay cambios comparamos el obj actual con el original. Con la lógica anterior, no funcionaba bien si se usa una store, ya que al emitir este valor se 'actualiza'. Así se hace la comprobación correcta.

Para ver si hay cambios comparamos el obj actual con el original. Con la lógica anterior, no funcionaba bien si se usa una store, ya que al emitir este valor se 'actualiza'. Así se hace la comprobación correcta.
Review

Se podría poner el comentario en código?

Se podría poner el comentario en código?
Review

yo lo veo bien sin comentarios. lo que diga @jgallego

yo lo veo bien sin comentarios. lo que diga @jgallego
Review

Yo no pondría comentarios, pero si en otros sitios está implementado distinto yo lo cambiaría para que quien venga detrás coja siempre una buena implementación

Yo no pondría comentarios, pero si en otros sitios está implementado distinto yo lo cambiaría para que quien venga detrás coja siempre una buena implementación
isResetting.value = false;
},
{ deep: true }
);
}
});
if (!$props.url)
watch(
() => formData.value,
(val) => {
hasChanges.value = !isResetting.value && val;
isResetting.value = false;
},
{ deep: true }
() => arrayData.store.data,
(val) => updateAndEmit(val, 'onFetch')
);
};
watch(formUrl, async () => {
Review

Agrupado con el resto de hooks.

Agrupado con el resto de hooks.
originalData.value = null;
reset();
await fetch();
});
onBeforeRouteLeave((to, from, next) => {
Review

Agrupado con el resto de hooks

Agrupado con el resto de hooks
if (hasChanges.value && $props.observeFormChanges)
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('Unsaved changes will be lost'),
message: t('Are you sure exit without saving?'),
promise: () => next(),
},
});
else next();
});
onUnmounted(() => {
// Restauramos los datos originales en el store si se realizaron cambios en el formulario pero no se guardaron, evitando modificaciones erróneas.
if (hasChanges.value) return state.set($props.model, originalData.value);
if ($props.clearStoreOnUnmount) state.unset($props.model);
});
async function fetch() {
try {
let { data } = await axios.get($props.url, {
params: { filter: JSON.stringify($props.filter) },
});
if (Array.isArray(data)) data = data[0] ?? {};
state.set($props.model, data);
originalData.value = data && JSON.parse(JSON.stringify(data));
emit('onFetch', state.get($props.model));
updateAndEmit(data, 'onFetch');
} catch (error) {
state.set($props.model, {});
originalData.value = {};
@ -173,38 +180,39 @@ async function fetch() {
}
async function save() {
if ($props.observeFormChanges && !hasChanges.value) {
notify('globals.noChanges', 'negative');
return;
}
isLoading.value = true;
if ($props.observeFormChanges && !hasChanges.value)
return notify('globals.noChanges', 'negative');
isLoading.value = true;
try {
const body = $props.mapper ? $props.mapper(formData.value) : formData.value;
const method = $props.urlCreate ? 'post' : 'patch';
const url =
Review

En mi opinión queda mucho más legible así, aunque solo se use 1 vez.

En mi opinión queda mucho más legible así, aunque solo se use 1 vez.
$props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url;
let response;
if ($props.saveFn) response = await $props.saveFn(body);
else
response = await axios[$props.urlCreate ? 'post' : 'patch'](
$props.urlCreate || $props.urlUpdate || $props.url,
body
);
else response = await axios[method](url, body);
if ($props.urlCreate) notify('globals.dataCreated', 'positive');
emit('onDataSaved', formData.value, response?.data);
originalData.value = JSON.parse(JSON.stringify(formData.value));
hasChanges.value = false;
isLoading.value = false;
updateAndEmit(response?.data, 'onDataSaved');
} catch (err) {
console.error(err);
notify('errors.writeRequest', 'negative');
}
isLoading.value = false;
}
async function saveAndGo() {
await save();
push({ path: $props.goTo });
}
function reset() {
state.set($props.model, originalData.value);
originalData.value = JSON.parse(JSON.stringify(originalData.value));
emit('onFetch', state.get($props.model));
updateAndEmit(originalData.value, 'onFetch');
if ($props.observeFormChanges) {
hasChanges.value = false;
isResetting.value = true;
@ -226,17 +234,15 @@ function filter(value, update, filterOptions) {
);
}
watch(formUrl, async () => {
originalData.value = null;
reset();
fetch();
});
function updateAndEmit(val, evt) {
Review

Esta parte se repetía varias veces

Esta parte se repetía varias veces
state.set($props.model, val);
originalData.value = val && JSON.parse(JSON.stringify(val));
if (!$props.url) arrayData.store.data = val;
defineExpose({
save,
isLoading,
hasChanges,
});
emit(evt, state.get($props.model));
}
defineExpose({ save, isLoading, hasChanges });
</script>
<template>
<div class="column items-center full-width">
@ -273,10 +279,42 @@ defineExpose({
:disable="!hasChanges"
:title="t(defaultButtons.reset.label)"
/>
<QBtnDropdown
Review

Lo mismo que crudModel

Lo mismo que crudModel
v-if="$props.goTo"
@click="saveAndGo"
:label="tMobile('globals.saveAndContinue')"
:title="t('globals.saveAndContinue')"
:disable="!hasChanges"
color="primary"
icon="save"
split
>
<QList>
<QItem
clickable
v-close-popup
@click="save"
:title="t('globals.save')"
>
<QItemSection>
<QItemLabel>
<QIcon
name="save"
color="white"
class="q-mr-sm"
size="sm"
/>
{{ t('globals.save').toUpperCase() }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QBtnDropdown>
<QBtn
:label="tMobile(defaultButtons.save.label)"
:color="defaultButtons.save.color"
:icon="defaultButtons.save.icon"
v-else
:label="tMobile('globals.save')"
color="primary"
icon="save"
@click="save"
:disable="!hasChanges"
:title="t(defaultButtons.save.label)"

View File

@ -44,7 +44,7 @@ onBeforeMount(async () => {
if (props.baseUrl) {
onBeforeRouteUpdate(async (to, from) => {

Al no haber cambiado de ruta, se estaba poniendo el mismo id actual.

Al no haber cambiado de ruta, se estaba poniendo el mismo id actual.
if (to.params.id !== from.params.id) {
arrayData.store.url = `${props.baseUrl}/${route.params.id}`;
arrayData.store.url = `${props.baseUrl}/${to.params.id}`;
await arrayData.fetch({ append: false });
}
});

View File

@ -78,6 +78,7 @@ async function save() {
const body = mapperDms(dms.value);
const response = await axios.post(getUrl(), body[0], body[1]);
emit('onDataSaved', body[1].params, response);
return response;
}
function defaultData() {

View File

@ -622,21 +622,6 @@ setLogTree();
</QList>
</div>
</div>
<Teleport v-if="stateStore.isHeaderMounted()" to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click.stop="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
<QDrawer v-model="stateStore.rightDrawer" show-if-above side="right" :width="300">
<QScrollArea class="fit text-grey-8">
<QList dense>

View File

@ -1,16 +1,16 @@
<script setup>
const $props = defineProps({
defineProps({
url: { type: String, default: null },
text: { type: String, default: null },
icon: { type: String, default: 'open_in_new' },
});
</script>
<template>
<div class="titleBox">
<div :class="$q.screen.gt.md ? 'q-pb-lg' : 'q-pb-md'">
<div class="header-link">
<a :href="$props.url" :class="$props.url ? 'link' : 'color-vn-text'">
{{ $props.text }}
<QIcon v-if="url" :name="$props.icon" />
<a :href="url" :class="url ? 'link' : 'color-vn-text'">
{{ text }}
<QIcon v-if="url" :name="icon" />
</a>
</div>
</div>
@ -19,7 +19,4 @@ const $props = defineProps({
a {
font-size: large;
}
.titleBox {
padding-bottom: 2%;
}
</style>

View File

@ -5,6 +5,7 @@ import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
import { useArrayData } from 'composables/useArrayData';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useState } from 'src/composables/useState';
import { useRoute } from 'vue-router';
const $props = defineProps({
url: {
@ -15,21 +16,21 @@ const $props = defineProps({
type: Object,
default: null,
},
module: {
type: String,
required: true,
},
title: {
type: String,
default: '',
},
subtitle: {
type: Number,
default: 0,
default: null,
},
dataKey: {
type: String,
default: '',
default: null,
},
module: {
type: String,
default: null,
},
summary: {
type: Object,
@ -40,21 +41,27 @@ const $props = defineProps({
const state = useState();
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const arrayData = useArrayData($props.dataKey || $props.module, {
url: $props.url,
filter: $props.filter,
skip: 0,
});
const { store } = arrayData;
const entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data));
let arrayData;
let store;
let entity;
const isLoading = ref(false);
defineExpose({
getData,
});
defineExpose({ getData });
onBeforeMount(async () => {
await getData();
watch($props, async () => await getData());
arrayData = useArrayData($props.dataKey, {
url: $props.url,
filter: $props.filter,
skip: 0,
});
store = arrayData.store;
entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data));
// It enables to load data only once if the module is the same as the dataKey
if ($props.dataKey !== useRoute().meta.moduleName) await getData();
Review

Permite cargar los datos solo una vez si el módulo es el mismo que dataKey

Permite cargar los datos solo una vez si el módulo es el mismo que dataKey
Review

Se podría poner el comentario en código?

Se podría poner el comentario en código?
watch(
() => [$props.url, $props.filter],
Review

Solo interesa ver cambios en estas 2 props.

Solo interesa ver cambios en estas 2 props.
async () => await getData()
);
});
async function getData() {
@ -132,7 +139,7 @@ const emit = defineEmits(['onFetch']);
<QItemLabel header class="ellipsis text-h5" :lines="1">
<div class="title">
<span v-if="$props.title" :title="$props.title">
{{ $props.title }}
{{ entity[title] ?? $props.title }}
</span>
<slot v-else name="description" :entity="entity">
<span :title="entity.name">

View File

@ -24,6 +24,7 @@ globals:
create: Create
edit: Edit
save: Save
saveAndContinue: Save and continue
remove: Remove
reset: Reset
close: Close

View File

@ -24,6 +24,7 @@ globals:
create: Crear
edit: Modificar
save: Guardar
saveAndContinue: Guardar y continuar
remove: Eliminar
reset: Restaurar
close: Cerrar

View File

@ -9,18 +9,21 @@ import { downloadFile } from 'src/composables/downloadFile';
import FormModel from 'components/FormModel.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'src/components/FetchData.vue';
import VnRow from 'src/components/ui/VnRow.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnInput from 'src/components/common/VnInput.vue';
const quasar = useQuasar();
const route = useRoute();
const { t } = useI18n();
const dms = ref({});
const route = useRoute();
const editDownloadDisabled = ref(false);
jorgep marked this conversation as resolved Outdated

Usar useRoute().meta.moduleName

Usar useRoute().meta.moduleName
const arrayData = useArrayData('InvoiceIn');
const invoiceIn = computed(() => arrayData.store.data);
const invoiceIn = computed(() => useArrayData(route.meta.moduleName).store.data);
const userConfig = ref(null);
const invoiceId = computed(() => +route.params.id);
const expenses = ref([]);
const currencies = ref([]);
const currenciesRef = ref();
const companies = ref([]);
@ -31,14 +34,11 @@ const warehouses = ref([]);
const warehousesRef = ref();
const allowTypesRef = ref();
const allowedContentTypes = ref([]);
const sageWithholdings = ref([]);
const inputFileRef = ref();
const editDmsRef = ref();
const createDmsRef = ref();
const requiredFieldRule = (val) => val || t('globals.requiredField');
const dateMask = '####-##-##';
const fillMask = '_';
async function checkFileExists(dmsId) {
if (!dmsId) return;
try {
@ -173,11 +173,17 @@ async function upsert() {
@on-fetch="(data) => (userConfig = data)"
auto-load
/>
<FetchData url="Expenses" auto-load @on-fetch="(data) => (expenses = data)" />
<FetchData
url="SageWithholdings"
auto-load
@on-fetch="(data) => (sageWithholdings = data)"
/>
<FormModel
v-if="invoiceIn"
:url="`InvoiceIns/${route.params.id}`"
model="invoiceIn"
:auto-load="true"
model="InvoiceIn"
:go-to="`/invoice-in/${invoiceId}/vat`"
auto-load
:url-update="`InvoiceIns/${invoiceId}/updateInvoiceIn`"
>
<template #form="{ data }">
<VnRow>
@ -201,7 +207,7 @@ async function upsert() {
</QItem>
</template>
</VnSelect>
<QInput
<VnInput
clearable
clear-icon="close"
:label="t('Supplier ref')"
@ -209,69 +215,29 @@ async function upsert() {
/>
</VnRow>
<VnRow>
<QInput
:label="t('Expedition date')"
v-model="data.issued"
:mask="dateMask"
>
<template #append>
<QIcon name="event" class="cursor-pointer" :fill-mask="fillMask">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate v-model="data.issued">
<div class="row items-center justify-end">
<QBtn
v-close-popup
label="Close"
color="primary"
flat
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
<QInput
<VnInputDate :label="t('Expedition date')" v-model="data.issued" />
<VnInputDate
:label="t('Operation date')"
v-model="data.operated"
:mask="dateMask"
:fill-mask="fillMask"
autofocus
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate v-model="data.operated" :mask="dateMask">
<div class="row items-center justify-end">
<QBtn
v-close-popup
label="Close"
color="primary"
flat
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
/>
</VnRow>
<VnRow>
<QInput
<VnSelect
:label="t('Undeductible VAT')"
v-model="data.deductibleExpenseFk"
clearable
clear-icon="close"
/>
<QInput
:options="expenses"
option-value="id"
option-label="id"
:filter-options="['id', 'name']"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
{{ `${scope.opt.id}: ${scope.opt.name}` }}
</QItem>
</template>
</VnSelect>
<VnInput
:label="t('Document')"
v-model="data.dmsFk"
clearable
@ -316,67 +282,11 @@ async function upsert() {
<QTooltip>{{ t('Create document') }}</QTooltip>
</QBtn>
</template>
</QInput>
</VnInput>
</VnRow>
<VnRow>
<QInput
:label="t('Entry date')"
v-model="data.bookEntried"
clearable
clear-icon="close"
:mask="dateMask"
:fill-mask="fillMask"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate v-model="data.bookEntried" :mask="dateMask">
<div class="row items-center justify-end">
<QBtn
v-close-popup
label="Close"
color="primary"
flat
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
<QInput
:label="t('Accounted date')"
v-model="data.booked"
clearable
clear-icon="close"
:mask="dateMask"
:fill-mask="fillMask"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate v-model="data.booked" :mask="maskDate">
<div class="row items-center justify-end">
<QBtn
v-close-popup
label="Close"
color="primary"
flat
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
<VnInputDate :label="t('Entry date')" v-model="data.bookEntried" />
<VnInputDate :label="t('Accounted date')" v-model="data.booked" />
</VnRow>
<VnRow>
<VnSelect
@ -386,6 +296,7 @@ async function upsert() {
option-value="id"
option-label="code"
/>
<VnSelect
v-if="companiesRef"
:label="t('Company')"
@ -395,7 +306,15 @@ async function upsert() {
option-label="code"
/>
</VnRow>
<QCheckbox :label="t('invoiceIn.summary.booked')" v-model="data.isBooked" />
<VnRow>
<VnSelect
:label="t('invoiceIn.summary.sage')"
v-model="data.withholdingSageFk"
:options="sageWithholdings"
option-value="id"
option-label="withholding"
/>
</VnRow>
</template>
</FormModel>
<QDialog ref="editDmsRef">
@ -411,7 +330,7 @@ async function upsert() {
</QCardSection>
<QCardSection class="q-py-none">
<QItem>
<QInput
<VnInput
class="full-width q-pa-xs"
:label="t('Reference')"
v-model="dms.reference"
@ -420,45 +339,45 @@ async function upsert() {
/>
<VnSelect
class="full-width q-pa-xs"
:label="`${t('Company')}*`"
:label="t('Company')"
v-model="dms.companyId"
:options="companies"
option-value="id"
option-label="code"
:rules="[requiredFieldRule]"
:required="true"
/>
</QItem>
<QItem>
<VnSelect
class="full-width q-pa-xs"
:label="`${t('Warehouse')}*`"
:label="t('Warehouse')"
v-model="dms.warehouseId"
:options="warehouses"
option-value="id"
option-label="name"
:rules="[requiredFieldRule]"
:required="true"
/>
<VnSelect
class="full-width q-pa-xs"
:label="`${t('Type')}*`"
:label="t('Type')"
v-model="dms.dmsTypeId"
:options="dmsTypes"
option-value="id"
option-label="name"
:rules="[requiredFieldRule]"
:required="true"
/>
</QItem>
<QItem>
<QInput
class="full-width q-pa-xs"
<VnInput
:label="t('Description')"
v-model="dms.description"
:required="true"
type="textarea"
class="full-width q-pa-xs"
size="lg"
autogrow
:label="`${t('Description')}*`"
v-model="dms.description"
clearable
clear-icon="close"
:rules="[(val) => val.length || t('Required field')]"
/>
</QItem>
<QItem>
@ -522,7 +441,7 @@ async function upsert() {
</QCardSection>
<QCardSection class="q-pb-none">
<QItem>
<QInput
<VnInput
class="full-width q-pa-xs"
:label="t('Reference')"
v-model="dms.reference"
@ -534,7 +453,7 @@ async function upsert() {
:options="companies"
option-value="id"
option-label="code"
:rules="[requiredFieldRule]"
:required="true"
/>
</QItem>
<QItem>
@ -545,7 +464,7 @@ async function upsert() {
:options="warehouses"
option-value="id"
option-label="name"
:rules="[requiredFieldRule]"
:required="true"
/>
<VnSelect
class="full-width q-pa-xs"
@ -554,11 +473,11 @@ async function upsert() {
:options="dmsTypes"
option-value="id"
option-label="name"
:rules="[requiredFieldRule]"
:required="true"
/>
</QItem>
<QItem>
<QInput
<VnInput
class="full-width q-pa-xs"
type="textarea"
size="lg"

View File

@ -1,6 +1,6 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useArrayData } from 'src/composables/useArrayData';
import { useCapitalize } from 'src/composables/useCapitalize';
@ -8,12 +8,11 @@ import CrudModel from 'src/components/CrudModel.vue';
import FetchData from 'src/components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
const router = useRouter();
const route = useRoute();
const { push, currentRoute } = useRouter();
const { t } = useI18n();
const invoiceId = route.params.id;
const arrayData = useArrayData('InvoiceIn');
const invoiceId = +currentRoute.value.params.id;
const arrayData = useArrayData(currentRoute.value.meta.moduleName);
const invoiceIn = computed(() => arrayData.store.data);
const invoiceInCorrectionRef = ref();
const filter = {
@ -74,7 +73,7 @@ const rowsSelected = ref([]);
const requiredFieldRule = (val) => val || t('globals.requiredField');
const onSave = (data) => data.deletes && router.push(`/invoice-in/${invoiceId}/summary`);
const onSave = (data) => data.deletes && push(`/invoice-in/${invoiceId}/summary`);
</script>
<template>
<FetchData

View File

@ -1,12 +1,11 @@
<script setup>
import { ref, reactive, computed, onBeforeMount, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ref, reactive, computed, onBeforeMount } from 'vue';
import { useRouter, onBeforeRouteLeave } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import axios from 'axios';
import { toCurrency, toDate } from 'src/filters';
import { useRole } from 'src/composables/useRole';
import useCardDescription from 'src/composables/useCardDescription';
import { downloadFile } from 'src/composables/downloadFile';
import { useArrayData } from 'src/composables/useArrayData';
import { usePrintService } from 'composables/usePrintService';
@ -17,27 +16,23 @@ import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import { useCapitalize } from 'src/composables/useCapitalize';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
import InvoiceInToBook from '../InvoiceInToBook.vue';
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const $props = defineProps({ id: { type: Number, default: null } });
const { push, currentRoute } = useRouter();
const route = useRoute();
const router = useRouter();
const quasar = useQuasar();
const { hasAny } = useRole();
const { t } = useI18n();
const { openReport, sendEmail } = usePrintService();
const arrayData = useArrayData('InvoiceIn');
const { store } = useArrayData(currentRoute.value.meta.moduleName);
jorgep marked this conversation as resolved Outdated

useRoute().meta.moduleName

useRoute().meta.moduleName
const invoiceIn = computed(() => arrayData.store.data);
const invoiceIn = computed(() => store.data);
const cardDescriptorRef = ref();
const correctionDialogRef = ref();
const entityId = computed(() => $props.id || +route.params.id);
const entityId = computed(() => $props.id || +currentRoute.value.params.id);
const totalAmount = ref();
const currentAction = ref();
const config = ref();
@ -45,28 +40,21 @@ const cplusRectificationTypes = ref([]);
const siiTypeInvoiceOuts = ref([]);
const invoiceCorrectionTypes = ref([]);
const actions = {
jorgep marked this conversation as resolved
Review

Hay 3 acciones y el texto es el mismo, menos el verbo de la acción.

Se puede hacer una traducción con parámetro

Hay 3 acciones y el texto es el mismo, menos el verbo de la acción. Se puede hacer una traducción con parámetro
book: {
title: 'Are you sure you want to book this invoice?',
cb: checkToBook,
action: toBook,
unbook: {
title: t('assertAction', { action: t('unbook') }),
action: toUnbook,
},
delete: {
title: 'Are you sure you want to delete this invoice?',
title: t('assertAction', { action: t('delete') }),
action: deleteInvoice,
},
clone: {
title: 'Are you sure you want to clone this invoice?',
title: t('assertAction', { action: t('clone') }),
action: cloneInvoice,
},
showPdf: {
cb: showPdfInvoice,
},
sendPdf: {
cb: sendPdfInvoiceConfirmation,
},
correct: {
cb: () => correctionDialogRef.value.show(),
},
showPdf: { cb: showPdfInvoice },
sendPdf: { cb: sendPdfInvoiceConfirmation },
correct: { cb: () => correctionDialogRef.value.show() },
};
const filter = {
include: [
@ -94,11 +82,7 @@ const filter = {
},
],
};
const data = ref(useCardDescription());
const invoiceInCorrection = reactive({
correcting: [],
corrected: null,
});
const invoiceInCorrection = reactive({ correcting: [], corrected: null });
const routes = reactive({
getSupplier: (id) => {
return { name: 'SupplierCard', params: { id } };
@ -139,16 +123,17 @@ const correctionFormData = reactive({
});
const isNotFilled = computed(() => Object.values(correctionFormData).includes(null));
onBeforeMount(async () => await setInvoiceCorrection(entityId.value));
onBeforeMount(async () => {
await setInvoiceCorrection(entityId.value);
const { data } = await axios.get(`InvoiceIns/${entityId.value}/getTotals`);
totalAmount.value = data.totalDueDay;
});
watch(
() => route.params.id,
async (newId) => {
invoiceInCorrection.correcting.length = 0;
invoiceInCorrection.corrected = null;
if (newId) await setInvoiceCorrection(entityId.value);
}
);
onBeforeRouteLeave(async (to, from) => {
invoiceInCorrection.correcting.length = 0;
invoiceInCorrection.corrected = null;
if (to.params.id !== from.params.id) await setInvoiceCorrection(entityId.value);
});
async function setInvoiceCorrection(id) {
const [{ data: correctingData }, { data: correctedData }] = await Promise.all([
@ -179,17 +164,6 @@ async function setInvoiceCorrection(id) {
);
}
async function setData(entity) {
data.value = useCardDescription(entity.supplierRef, entity.id);
const { totalDueDay } = await getTotals();
totalAmount.value = totalDueDay;
}
async function getTotals() {
const { data } = await axios.get(`InvoiceIns/${entityId.value}/getTotals`);
return data;
}
function openDialog() {
quasar.dialog({
component: VnConfirm,
@ -200,38 +174,17 @@ function openDialog() {
});
}
async function checkToBook() {
let directBooking = true;
async function toUnbook() {
const { data } = await axios.post(`InvoiceIns/${entityId.value}/toUnbook`);
const { isLinked, bookEntry, accountingEntries } = data;
const totals = await getTotals();
const taxableBaseNotEqualDueDay = totals.totalDueDay != totals.totalTaxableBase;
const vatNotEqualDueDay = totals.totalDueDay != totals.totalVat;
const type = isLinked ? 'warning' : 'positive';
const message = isLinked
? t('isLinked', { bookEntry, accountingEntries })
: t('isNotLinked', { bookEntry });
if (taxableBaseNotEqualDueDay && vatNotEqualDueDay) directBooking = false;
const { data: dueDaysCount } = await axios.get('InvoiceInDueDays/count', {
where: {
invoiceInFk: entityId.value,
dueDated: { gte: Date.vnNew() },
},
});
if (dueDaysCount) directBooking = false;
if (!directBooking) openDialog();
else toBook();
}
async function toBook() {
await axios.post(`InvoiceIns/${entityId.value}/toBook`);
quasar.notify({
type: 'positive',
message: t('globals.dataSaved'),
});
await cardDescriptorRef.value.getData();
setTimeout(() => location.reload(), 500);
quasar.notify({ type, message });
if (!isLinked) store.data.isBooked = false;
}
async function deleteInvoice() {
@ -240,7 +193,7 @@ async function deleteInvoice() {
type: 'positive',
message: t('Invoice deleted'),
});
router.push({ path: '/invoice-in' });
push({ path: '/invoice-in' });
}
async function cloneInvoice() {
@ -249,11 +202,9 @@ async function cloneInvoice() {
type: 'positive',
message: t('Invoice cloned'),
});
router.push({ path: `/invoice-in/${data.id}/summary` });
push({ path: `/invoice-in/${data.id}/summary` });
}
const requiredFieldRule = (val) => val || t('globals.requiredField');
const isAdministrative = () => hasAny(['administrative']);
const isAgricultural = () =>
@ -299,10 +250,9 @@ const createInvoiceInCorrection = async () => {
'InvoiceIns/corrective',
Object.assign(correctionFormData, { id: entityId.value })
);
router.push({ path: `/invoice-in/${correctingId}/summary` });
push({ path: `/invoice-in/${correctingId}/summary` });
};
</script>
<template>
<FetchData
url="InvoiceInConfigs"
@ -329,21 +279,33 @@ const createInvoiceInCorrection = async () => {
<CardDescriptor
ref="cardDescriptorRef"
module="InvoiceIn"
data-key="InvoiceIn"
:url="`InvoiceIns/${entityId}`"
:filter="filter"
:title="data.title"
:subtitle="data.subtitle"
@on-fetch="setData"
data-key="invoiceInData"
title="supplierRef"
>
<template #menu="{ entity }">
<InvoiceInToBook>
<template #content="{ book }">
<QItem
v-if="!entity?.isBooked && isAdministrative()"
v-ripple
clickable
@click="book(entityId)"
>
<QItemSection>{{ t('To book') }}</QItemSection>
</QItem>
</template>
</InvoiceInToBook>
<QItem
v-if="!entity.isBooked && isAdministrative()"
v-if="entity?.isBooked && isAdministrative()"
v-ripple
clickable
@click="triggerMenu('book')"
@click="triggerMenu('unbook')"
>
<QItemSection>{{ t('To book') }}</QItemSection>
<QItemSection>
{{ t('To unbook') }}
</QItemSection>
</QItem>
<QItem
v-if="isAdministrative()"
@ -395,25 +357,24 @@ const createInvoiceInCorrection = async () => {
>
<QItemSection>{{ t('components.smartCard.downloadFile') }}</QItemSection>
</QItem>
<QItem
v-if="entity.dmsFk"
v-ripple
clickable
@click="downloadFile(entity.dmsFk)"
>
<QItemSection>{{ t('components.smartCard.downloadFile') }}</QItemSection>
</QItem>
</template>
<template #body="{ entity }">
<VnLv :label="t('invoiceIn.card.issued')" :value="toDate(entity.issued)" />
<VnLv :label="t('invoiceIn.summary.booked')" :value="toDate(entity.booked)" />
<VnLv :label="t('invoiceIn.card.amount')" :value="toCurrency(totalAmount)" />
<VnLv
:label="t('invoiceIn.summary.supplier')"
:value="entity.supplier?.nickname"
:label="t('invoiceIn.card.amount')"
:value="toCurrency(totalAmount, entity.currency?.code)"
/>
<VnLv :label="t('invoiceIn.summary.supplier')">
<template #value>
<span class="link">
{{ entity?.supplier?.nickname }}
<SupplierDescriptorProxy :id="entity?.supplierFk" />
</span>
</template>
</VnLv>
</template>
<template #actions="{ entity }">
<template #action="{ entity }">
<QCardActions>
<QBtn
size="md"
@ -486,7 +447,7 @@ const createInvoiceInCorrection = async () => {
:options="siiTypeInvoiceOuts"
option-value="id"
option-label="code"
:rules="[requiredFieldRule]"
:required="true"
/>
</QItemSection>
<QItemSection>
@ -496,7 +457,7 @@ const createInvoiceInCorrection = async () => {
:options="cplusRectificationTypes"
option-value="id"
option-label="description"
:rules="[requiredFieldRule]"
:required="true"
/>
<VnSelect
:label="`${useCapitalize(t('globals.reason'))}*`"
@ -504,7 +465,7 @@ const createInvoiceInCorrection = async () => {
:options="invoiceCorrectionTypes"
option-value="id"
option-label="description"
:rules="[requiredFieldRule]"
:required="true"
/>
</QItemSection>
</QItem>
@ -544,11 +505,18 @@ const createInvoiceInCorrection = async () => {
}
</style>
<i18n>
en:
isNotLinked: The entry {bookEntry} has been deleted with {accountingEntries} entries
isLinked: The entry {bookEntry} has been linked to Sage. Please contact administration for further information
assertAction: Are you sure you want to {action} this invoice?
es:
book: asentar
unbook: desasentar
delete: eliminar
clone: clonar
To book: Contabilizar
Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura?
To unbook: Descontabilizar
Delete invoice: Eliminar factura
Are you sure you want to delete this invoice?: Estas seguro de querer eliminar esta factura?
Invoice deleted: Factura eliminada
Clone invoice: Clonar factura
Invoice cloned: Factura clonada
@ -560,4 +528,7 @@ es:
Rectificative invoice: Factura rectificativa
Original invoice: Factura origen
Entry: entrada
isNotLinked: Se ha eliminado el asiento {bookEntry} con {accountingEntries} apuntes
isLinked: El asiento {bookEntry} fue enlazado a Sage, por favor contacta con administración
assertAction: Estas seguro de querer {action} esta factura?
</i18n>

View File

@ -9,24 +9,21 @@ import CrudModel from 'src/components/CrudModel.vue';
import FetchData from 'src/components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue';
import { toCurrency } from 'src/filters';
const route = useRoute();
const { t } = useI18n();
const arrayData = useArrayData('InvoiceIn');
const arrayData = useArrayData(route.meta.moduleName);
const invoiceIn = computed(() => arrayData.store.data);
const rowsSelected = ref([]);
const banks = ref([]);
const invoiceInFormRef = ref();
const invoiceId = route.params.id;
const invoiceId = +route.params.id;
const placeholder = 'yyyy/mm/dd';
const filter = {
where: {
invoiceInFk: invoiceId,
},
};
const filter = { where: { invoiceInFk: invoiceId } };
const columns = computed(() => [
{
@ -73,6 +70,7 @@ async function insert() {
await axios.post('/InvoiceInDueDays/new', { id: +invoiceId });
await invoiceInFormRef.value.reload();
}
const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount, 0);
</script>
<template>
<FetchData
@ -184,6 +182,19 @@ async function insert() {
/>
</QTd>
</template>
<template #bottom-row>
<QTr class="bg">
<QTd />
jorgep marked this conversation as resolved Outdated

Divisa no es seleccionable? Quiero decir estña el campo pero sin acción

Divisa no es seleccionable? Quiero decir estña el campo pero sin acción

Si la divisa es euros no se puede editar ese campo

Si la divisa es euros no se puede editar ese campo
<QTd />
<QTd />
<QTd>
jorgep marked this conversation as resolved
Review

http://localhost:9000/#/invoice-in/4/due-day al entrar en esta seccion me dice Access Denied en ingles con developer, cuando en realidad esta seccion la deben de poder usar el rol administrative.

http://localhost:9000/#/invoice-in/4/due-day al entrar en esta seccion me dice Access Denied en ingles con developer, cuando en realidad esta seccion la deben de poder usar el rol administrative.
Review

A mi no me da ningún fallo . Compruebalo de nuevo y me dices.

A mi no me da ningún fallo . Compruebalo de nuevo y me dices.
Review

No me da fallo.
Pregunta: la fecha y el placeholder está como yyyy/mm/dd, es correcto?

No me da fallo. Pregunta: la fecha y el placeholder está como yyyy/mm/dd, es correcto?
Review

@jsegarra Ahora se usa VnInputDate.

@jsegarra Ahora se usa VnInputDate.
{{
toCurrency(getTotalAmount(rows), invoiceIn.currency.code)
}}
</QTd>
<QTd />
</QTr>
</template>
<template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
<QCard>
@ -294,7 +305,11 @@ async function insert() {
<QBtn color="primary" icon="add" size="lg" round @click="insert" />
</QPageSticky>
</template>
jorgep marked this conversation as resolved Outdated

Que hace la función de insert?
Restaurar la tabla? Eso ya tiene un botón

Que hace la función de insert? Restaurar la tabla? Eso ya tiene un botón

Llama al back InvoiceInDueDays/new (llama a invoiceIn_calculate) y recarga el formulario...

Llama al back InvoiceInDueDays/new (llama a invoiceIn_calculate) y recarga el formulario...
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.bg {
background-color: var(--vn-light-gray);
}
</style>
<i18n>
es:
Date: Fecha

View File

@ -6,26 +6,21 @@ import { toCurrency } from 'src/filters';
import CrudModel from 'src/components/CrudModel.vue';
import FetchData from 'src/components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import { useArrayData } from 'src/composables/useArrayData';
const { t } = useI18n();
const route = useRoute();
const route = useRoute();
const currency = computed(
() => useArrayData(route.meta.moduleName).store.data?.currency?.code
);
const invoceInIntrastat = ref([]);
const amountTotal = computed(() => getTotal('amount'));
const netTotal = computed(() => getTotal('net'));
const stemsTotal = computed(() => getTotal('stems'));
const rowsSelected = ref([]);
const countries = ref([]);
const intrastats = ref([]);
const invoiceInFormRef = ref();
const filter = {
where: {
invoiceInFk: route.params.id,
},
};
const invoiceInId = computed(() => +route.params.id);
const filter = { where: { invoiceInFk: invoiceInId.value } };
const columns = computed(() => [
{
name: 'code',
@ -77,13 +72,8 @@ const columns = computed(() => [
},
]);

Si se escribe la coma, te devuelve NaN

Si se escribe la coma, te devuelve NaN
function getTotal(type) {
if (!invoceInIntrastat.value.length) return 0.0;
return invoceInIntrastat.value.reduce(
(total, intrastat) => total + intrastat[type],
0.0
);
}
const getTotal = (data, key) =>
data.reduce((acc, cur) => acc + +String(cur[key]).replace(',', '.'), 0);
</script>
<template>
<FetchData
@ -99,30 +89,12 @@ function getTotal(type) {
@on-fetch="(data) => (intrastats = data)"
/>
<div class="invoiceIn-intrastat">
<QCard v-if="invoceInIntrastat.length" class="full-width q-mb-md q-pa-sm">
<QItem class="justify-end">
<div>
<QItemLabel>
<VnLv
:label="t('Total amount')"
:value="toCurrency(amountTotal)"
/>
</QItemLabel>
<QItemLabel>
<VnLv :label="t('Total net')" :value="netTotal" />
</QItemLabel>
<QItemLabel>
<VnLv :label="t('Total stems')" :value="stemsTotal" />
</QItemLabel>
</div>
</QItem>
</QCard>
<CrudModel
ref="invoiceInFormRef"

espera a que se carguen los datos de arrayData, ya que hace falta saber el currency

espera a que se carguen los datos de arrayData, ya que hace falta saber el currency
data-key="InvoiceInIntrastats"
url="InvoiceInIntrastats"
auto-load
:data-required="{ invoiceInFk: route.params.id }"
:auto-load="!currency"
:data-required="{ invoiceInFk: invoiceInId }"
:filter="filter"
v-model:selected="rowsSelected"
@on-fetch="(data) => (invoceInIntrastat = data)"
@ -172,6 +144,22 @@ function getTotal(type) {
/>
</QTd>
</template>
<template #bottom-row>
<QTr class="bg">
<QTd />
<QTd />
<QTd>
{{ toCurrency(getTotal(rows, 'amount'), currency) }}
</QTd>
<QTd>
{{ getTotal(rows, 'net') }}
</QTd>
<QTd>
{{ getTotal(rows, 'stems') }}
</QTd>
<QTd />
</QTr>
</template>
<template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
<QCard>

View File

@ -3,32 +3,24 @@ import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toCurrency, toDate } from 'src/filters';
import { useArrayData } from 'src/composables/useArrayData';
import { getUrl } from 'src/composables/getUrl';
import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
import InvoiceIntoBook from '../InvoiceInToBook.vue';
import VnTitle from 'src/components/common/VnTitle.vue';
onMounted(async () => {
salixUrl.value = await getUrl('');
invoiceInUrl.value = salixUrl.value + `invoiceIn/${entityId.value}/`;
});
const route = useRoute();
const props = defineProps({ id: { type: [Number, String], default: 0 } });
const { t } = useI18n();
const route = useRoute();
const $props = defineProps({
id: {
type: Number,
default: 0,
},
});
const entityId = computed(() => $props.id || route.params.id);
const salixUrl = ref();
const entityId = computed(() => props.id || +route.params.id);
const invoiceIn = computed(() => useArrayData(route.meta.moduleName).store.data);
const currency = computed(() => invoiceIn.value?.currency?.code);
const invoiceInUrl = ref();
const amountsNotMatch = ref(null);
const intrastatTotals = ref({});
const intrastatTotals = ref({ amount: 0, net: 0, stems: 0 });
const vatColumns = ref([
{
@ -42,14 +34,16 @@ const vatColumns = ref([
name: 'landed',
label: 'invoiceIn.summary.taxableBase',
field: (row) => row.taxableBase,
format: (value) => toCurrency(value),
format: (value) => toCurrency(value, currency.value),
sortable: true,
align: 'left',
},
{
name: 'vat',
label: 'invoiceIn.summary.sageVat',
field: (row) => row.taxTypeSage?.vat,
field: (row) => {
if (row.taxTypeSage) return `#${row.taxTypeSage.id} : ${row.taxTypeSage.vat}`;
},
format: (value) => value,
sortable: true,
align: 'left',
@ -57,7 +51,10 @@ const vatColumns = ref([
{
name: 'transaction',
label: 'invoiceIn.summary.sageTransaction',
field: (row) => row.transactionTypeSage?.transaction,
field: (row) => {
if (row.transactionTypeSage)
return `#${row.transactionTypeSage.id} : ${row.transactionTypeSage?.transaction}`;
},
format: (value) => value,
sortable: true,
align: 'left',
@ -66,9 +63,9 @@ const vatColumns = ref([
name: 'rate',
label: 'invoiceIn.summary.rate',
field: (row) => taxRate(row.taxableBase, row.taxTypeSage?.rate),
format: (value) => toCurrency(value),
format: (value) => toCurrency(value, currency.value),
sortable: true,
align: 'left',
align: 'center',
},
{
name: 'currency',
@ -99,7 +96,7 @@ const dueDayColumns = ref([
name: 'amount',
label: 'invoiceIn.summary.amount',
field: (row) => row.amount,
format: (value) => toCurrency(value),
format: (value) => toCurrency(value, currency.value),
sortable: true,
align: 'left',
},
@ -122,13 +119,15 @@ const intrastatColumns = ref([
},
sortable: true,
align: 'left',
style: 'width: 10px',
},
{
name: 'amount',
label: 'invoiceIn.summary.amount',
field: (row) => toCurrency(row.amount),
field: (row) => toCurrency(row.amount, currency.value),
sortable: true,
align: 'left',
style: 'width: 10px',
},
{
name: 'net',
@ -155,58 +154,55 @@ const intrastatColumns = ref([
},
]);
function getAmountNotMatch(totals) {
return (
onMounted(async () => {
invoiceInUrl.value = `${await getUrl('')}invoiceIn/${entityId.value}/`;
});
const init = (data) => {
if (!data) return;
const { totals, invoiceInIntrastat } = data;
amountsNotMatch.value =
totals.totalDueDay != totals.totalTaxableBase &&
totals.totalDueDay != totals.totalVat
);
}
totals.totalDueDay != totals.totalVat;
function getIntrastatTotals(intrastat) {
const totals = {
amount: intrastat.reduce((acc, cur) => acc + cur.amount, 0),
net: intrastat.reduce((acc, cur) => acc + cur.net, 0),
stems: intrastat.reduce((acc, cur) => acc + cur.stems, 0),
};
invoiceInIntrastat.forEach((val) => {
intrastatTotals.value.amount += val.amount;
intrastatTotals.value.net += val.net;
intrastatTotals.value.stems += val.stems;
});
};
return totals;
}
const taxRate = (taxableBase = 0, rate = 0) => (rate / 100) * taxableBase;
function getTaxTotal(tax) {
return tax.reduce(
(acc, cur) => acc + taxRate(cur.taxableBase, cur.taxTypeSage?.rate),
0
);
}
const getTotalTax = (tax) =>
tax.reduce((acc, cur) => acc + taxRate(cur.taxableBase, cur.taxTypeSage?.rate), 0);
function setData(entity) {
if (!entity) return false;
amountsNotMatch.value = getAmountNotMatch(entity.totals);
if (entity.invoiceInIntrastat.length)
intrastatTotals.value = { ...getIntrastatTotals(entity.invoiceInIntrastat) };
}
function taxRate(taxableBase = 0, rate = 0) {
return (rate / 100) * taxableBase;
}
function getLink(param) {
return `#/invoice-in/${entityId.value}/${param}`;
}
const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
</script>
<template>
<CardSummary
data-key="InvoiceInSummary"
:url="`InvoiceIns/${entityId}/summary`"
@on-fetch="(data) => setData(data)"
@on-fetch="(data) => init(data)"
>
<template #header="{ entity: invoiceIn }">
<div>{{ invoiceIn.id }} - {{ invoiceIn.supplier?.name }}</div>
<template #header="{ entity }">
<div>{{ entity.id }} - {{ entity.supplier?.name }}</div>
</template>
<template #body="{ entity: invoiceIn }">
<template #header-right v-if="!invoiceIn?.isBooked">
<InvoiceIntoBook>
<template #content="{ book }">
<QBtn
:label="t('To book')"
color="orange-11"
text-color="black"
@click="book(entityId)"
/>
</template>
</InvoiceIntoBook>
</template>
<template #body="{ entity }">
<!--Basic Data-->
<QCard class="vn-one">
<QCardSection class="q-pa-none">
@ -217,19 +213,26 @@ function getLink(param) {
</QCardSection>
<VnLv
:label="t('invoiceIn.summary.supplier')"
:value="invoiceIn.supplier?.name"
/>
:value="entity.supplier?.name"
>
<template #value>
<span class="link">
{{ entity.supplier?.name }}
<SupplierDescriptorProxy :id="entity.supplierFk" />
</span>
</template>
</VnLv>
<VnLv
:label="t('invoiceIn.summary.supplierRef')"
:value="invoiceIn.supplierRef"
:value="entity.supplierRef"
/>
<VnLv
:label="t('invoiceIn.summary.currency')"
:value="invoiceIn.currency?.code"
:value="entity.currency?.code"
/>
<VnLv
:label="t('invoiceIn.summary.docNumber')"
:value="`${invoiceIn.serial}/${invoiceIn.serialNumber}`"
:value="`${entity.serial}/${entity.serialNumber}`"
/>
</QCard>
<QCard class="vn-one">
@ -242,19 +245,19 @@ function getLink(param) {
<VnLv
:ellipsis-value="false"
:label="t('invoiceIn.summary.issued')"
:value="toDate(invoiceIn.issued)"
:value="toDate(entity.issued)"
/>
<VnLv
:label="t('invoiceIn.summary.operated')"
:value="toDate(invoiceIn.operated)"
:value="toDate(entity.operated)"
/>
<VnLv
:label="t('invoiceIn.summary.bookEntried')"
:value="toDate(invoiceIn.bookEntried)"
:value="toDate(entity.bookEntried)"
/>
<VnLv
:label="t('invoiceIn.summary.bookedDate')"
:value="toDate(invoiceIn.booked)"
:value="toDate(entity.booked)"
/>
</QCard>
<QCard class="vn-one">
@ -266,20 +269,19 @@ function getLink(param) {
</QCardSection>
<VnLv
:label="t('invoiceIn.summary.sage')"
:value="invoiceIn.sageWithholding?.withholding"
:value="entity.sageWithholding?.withholding"
/>
<VnLv
:label="t('invoiceIn.summary.vat')"
:value="invoiceIn.expenseDeductible?.name"
:value="entity.expenseDeductible?.name"
/>
<VnLv
:label="t('invoiceIn.summary.company')"
:value="invoiceIn.company?.code"
:value="entity.company?.code"
/>
<QCheckbox
<VnLv
:label="t('invoiceIn.summary.booked')"
v-model="invoiceIn.isBooked"
:disable="true"
:value="invoiceIn?.isBooked"
/>
</QCard>
<QCard class="vn-one">
@ -290,58 +292,62 @@ function getLink(param) {
/>
</QCardSection>
<QCardSection class="q-pa-none">
<div class="bordered q-px-sm q-mx-auto">
<VnLv
:label="t('invoiceIn.summary.taxableBase')"
:value="toCurrency(invoiceIn.totals.totalTaxableBase)"
/>
<VnLv
label="Total"
:value="toCurrency(invoiceIn.totals.totalVat)"
/>
<VnLv :label="t('invoiceIn.summary.dueTotal')">
<template #value>
<QChip
dense
class="q-pa-xs"
:color="amountsNotMatch ? 'negative' : 'transparent'"
:title="
amountsNotMatch
? t('invoiceIn.summary.noMatch')
: t('invoiceIn.summary.dueTotal')
"
>
{{ toCurrency(invoiceIn.totals.totalDueDay) }}
</QChip>
</template>
</VnLv>
</div>
<VnLv
:label="t('invoiceIn.summary.taxableBase')"
:value="toCurrency(entity.totals.totalTaxableBase, currency)"
/>
<VnLv
label="Total"
:value="toCurrency(entity.totals.totalVat, currency)"
/>
<VnLv :label="t('invoiceIn.summary.dueTotal')">
<template #value>
<QChip
dense
class="q-pa-xs"
:color="amountsNotMatch ? 'negative' : 'transparent'"
:title="
amountsNotMatch
? t('invoiceIn.summary.noMatch')
: t('invoiceIn.summary.dueTotal')
"
>
jorgep marked this conversation as resolved Outdated

lo pongo aqui pero no es esta linea
en la seccion iva del summary en los campos sage vat y sage transaccion quieren en la columna sage vat que aparezca:
8 : H.P. IVA 21% CEE
20 : Adquisic.intracomunitarias de bienes y serv.corr.

es decir, añadir la clave primaria

lo pongo aqui pero no es esta linea en la seccion iva del summary en los campos sage vat y sage transaccion quieren en la columna sage vat que aparezca: 8 : H.P. IVA 21% CEE 20 : Adquisic.intracomunitarias de bienes y serv.corr. es decir, añadir la clave primaria
{{ toCurrency(entity.totals.totalDueDay, currency) }}
</QChip>
</template>
</VnLv>
</QCardSection>
</QCard>
<!--Vat-->
<QCard v-if="invoiceIn.invoiceInTax.length">
<QCard v-if="entity.invoiceInTax.length" class="vat">
<VnTitle :url="getLink('vat')" :text="t('invoiceIn.card.vat')" />
<QTable
:columns="vatColumns"
:rows="invoiceIn.invoiceInTax"
:rows="entity.invoiceInTax"
jorgep marked this conversation as resolved Outdated

Warning en la 338, 366 y 394

Warning en la 338, 366 y 394
flat
hide-pagination
>
<template #header="props">
<QTr :props="props" class="bg">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
<template #header="vatProps">
<QTr :props="vatProps" class="bg">
<QTh
v-for="col in vatProps.cols"
:key="col.name"
:props="vatProps"
>
{{ t(col.label) }}
</QTh>
</QTr>
</template>
<template #bottom-row>
<QTr class="bg">
<QTd></QTd>
<QTd>{{ toCurrency(invoiceIn.totals.totalTaxableBase) }}</QTd>
<QTd></QTd>
<QTd></QTd>
<QTd>{{
toCurrency(getTaxTotal(invoiceIn.invoiceInTax))
toCurrency(entity.totals.totalTaxableBase, currency)
}}</QTd>
<QTd></QTd>
<QTd></QTd>
<QTd class="text-center">{{
toCurrency(getTotalTax(entity.invoiceInTax, currency))
}}</QTd>
<QTd></QTd>
</QTr>
@ -349,17 +355,17 @@ function getLink(param) {
</QTable>
</QCard>
<!--Due Day-->
<QCard v-if="invoiceIn.invoiceInDueDay.length">
<QCard v-if="entity.invoiceInDueDay.length" class="due-day">
<VnTitle :url="getLink('due-day')" :text="t('invoiceIn.card.dueDay')" />
<QTable
class="full-width"
:columns="dueDayColumns"
:rows="invoiceIn.invoiceInDueDay"
flat
>
<template #header="props">
<QTr :props="props" class="bg">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
<QTable :columns="dueDayColumns" :rows="entity.invoiceInDueDay" flat>
<template #header="dueDayProps">
<QTr :props="dueDayProps" class="bg">
<QTh
table-header-style="max-width:50%"
v-for="col in dueDayProps.cols"
:key="col.name"
:props="dueDayProps"
>
{{ t(col.label) }}
</QTh>
</QTr>
@ -368,26 +374,32 @@ function getLink(param) {
<QTr class="bg">
<QTd></QTd>
<QTd></QTd>
<QTd>{{ toCurrency(invoiceIn.totals.totalDueDay) }}</QTd>
<QTd>
{{ toCurrency(entity.totals.totalDueDay, currency) }}
</QTd>
<QTd></QTd>
</QTr>
</template>
</QTable>
</QCard>
<!--Intrastat-->
<QCard v-if="invoiceIn.invoiceInIntrastat.length">
<QCard v-if="entity.invoiceInIntrastat.length">
<VnTitle
:url="getLink('intrastat')"
:text="t('invoiceIn.card.intrastat')"
/>
<QTable
:columns="intrastatColumns"
:rows="invoiceIn.invoiceInIntrastat"
:rows="entity.invoiceInIntrastat"
flat
>
<template #header="props">
<QTr :props="props" class="bg">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
<template #header="intrastatProps">
<QTr :props="intrastatProps" class="bg">
<QTh
v-for="col in intrastatProps.cols"
:key="col.name"
:props="intrastatProps"
>
{{ t(col.label) }}
</QTh>
</QTr>
@ -395,7 +407,7 @@ function getLink(param) {
<template #bottom-row>
<QTr class="bg">
<QTd></QTd>
<QTd>{{ toCurrency(intrastatTotals.amount) }}</QTd>
<QTd>{{ toCurrency(intrastatTotals.amount, currency) }}</QTd>
<QTd>{{ intrastatTotals.net }}</QTd>
<QTd>{{ intrastatTotals.stems }}</QTd>
<QTd></QTd>
@ -410,13 +422,28 @@ function getLink(param) {
.bg {
background-color: var(--vn-accent-color);
}
.bordered {
border: 1px solid var(--vn-text-color);
max-width: 18em;
@media (min-width: $breakpoint-md) {
.summaryBody {
.vat {
flex: 65%;
}
.due-day {
flex: 30%;
}
.vat,
.due-day {
.q-table th {
padding-right: 0;
}
}
}
}
</style>
<i18n>
es:
Search invoice: Buscar factura recibida
You can search by invoice reference: Puedes buscar por referencia de la factura
Totals: Totales
To book: Contabilizar
</i18n>

View File

@ -11,13 +11,14 @@ import VnSelect from 'src/components/common/VnSelect.vue';
import CrudModel from 'src/components/CrudModel.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue';
const route = useRoute();
const router = useRoute();
const { t } = useI18n();
const quasar = useQuasar();
const arrayData = useArrayData('InvoiceIn');
const arrayData = useArrayData(router.meta.moduleName);
const invoiceIn = computed(() => arrayData.store.data);
const invoiceId = +router.params.id;
const currency = computed(() => invoiceIn.value?.currency?.code);
const expenses = ref([]);
const sageTaxTypes = ref([]);
const sageTransactionTypes = ref([]);
@ -55,7 +56,7 @@ const columns = computed(() => [
{
name: 'taxablebase',
label: t('Taxable base'),
field: (row) => toCurrency(row.taxableBase),
field: (row) => toCurrency(row.taxableBase, currency.value),
model: 'taxableBase',
sortable: true,
tabIndex: 2,
@ -68,7 +69,7 @@ const columns = computed(() => [
options: sageTaxTypes.value,
model: 'taxTypeSageFk',
optionValue: 'id',
optionLabel: 'vat',
optionLabel: 'id',
sortable: true,
tabindex: 3,
align: 'left',
@ -80,7 +81,7 @@ const columns = computed(() => [
options: sageTransactionTypes.value,
model: 'transactionTypeSageFk',
optionValue: 'id',
optionLabel: 'transaction',
optionLabel: 'id',
sortable: true,
tabIndex: 4,
align: 'left',
@ -90,7 +91,7 @@ const columns = computed(() => [
label: t('Rate'),
sortable: true,
tabIndex: 5,
field: (row) => toCurrency(taxRate(row, row.taxTypeSageFk)),
field: (row) => toCurrency(taxRate(row, row.taxTypeSageFk), currency.value),
align: 'left',
},
{
@ -114,7 +115,7 @@ const filter = {
'transactionTypeSageFk',
],
where: {
invoiceInFk: route.params.id,
invoiceInFk: invoiceId,
},
};
@ -161,6 +162,9 @@ async function addExpense() {
});
}
}
const getTotalTaxableBase = (rows) =>
rows.reduce((acc, { taxableBase }) => acc + +taxableBase, 0);
const getTotalRate = (rows) => rows.reduce((acc, cur) => acc + +taxRate(cur), 0);
</script>
<template>
<FetchData
@ -181,9 +185,10 @@ async function addExpense() {
data-key="InvoiceInTaxes"
url="InvoiceInTaxes"
:filter="filter"
:data-required="{ invoiceInFk: route.params.id }"
:data-required="{ invoiceInFk: invoiceId }"
auto-load
v-model:selected="rowsSelected"
:go-to="`/invoice-in/${invoiceId}/due-day`"
>
<template #body="{ rows }">
<QTable
@ -191,7 +196,7 @@ async function addExpense() {
selection="multiple"
:columns="columns"
:rows="rows"
row-key="$index"
row-key="$index"
:grid="$q.screen.lt.sm"
>
<template #body-cell-expense="{ row, col }">
@ -303,6 +308,19 @@ async function addExpense() {
/>
</QTd>
</template>
<template #bottom-row>
<QTr class="bg">
<QTd />
<QTd />
<QTd>
{{ toCurrency(getTotalTaxableBase(rows), currency) }}
</QTd>
<QTd />
jorgep marked this conversation as resolved
Review

En esta seccion en los desplegables Sage iva, transactionTypeSageFk no quieren ver la descripcion solo la clave primaria de la tabla. si despliegan veran el valor

En esta seccion en los desplegables Sage iva, transactionTypeSageFk no quieren ver la descripcion solo la clave primaria de la tabla. si despliegan veran el valor
<QTd />
<QTd> {{ toCurrency(getTotalRate(rows), currency) }}</QTd>
<QTd />
</QTr>
</template>
<template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
<QCard bordered flat class="q-my-xs">
@ -391,7 +409,7 @@ async function addExpense() {
</VnSelect>
</QItem>
<QItem>
{{ toCurrency(taxRate(props.row)) }}
{{ toCurrency(taxRate(props.row), currency) }}
</QItem>
<QItem>
<QInput
@ -458,6 +476,10 @@ async function addExpense() {
</QPageSticky>
</template>
<style lang="scss" scoped>
.bg {
background-color: var(--vn-light-gray);
}
@media (max-width: $breakpoint-xs) {
.q-dialog {
.q-card {

View File

@ -0,0 +1,124 @@
<script setup>
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { useStateStore } from 'stores/useStateStore';
import { useState } from 'src/composables/useState';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnInput from 'src/components/common/VnInput.vue';
const state = useState();
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const stateStore = useStateStore();
const user = state.getUser();
const newInvoiceIn = reactive({
supplierFk: +route.query?.supplierFk || null,
supplierRef: null,
companyFk: user.value.companyFk || null,
issued: Date.vnNew(),
});
const suppliersOptions = ref([]);
const companiesOptions = ref([]);
const redirectToInvoiceInBasicData = ({ id }) => {
router.push({ name: 'InvoiceInBasicData', params: { id } });
};
</script>
<template>
<FetchData
url="Suppliers"
:filter="{ fields: ['id', 'nickname'] }"
order="nickname"
@on-fetch="(data) => (suppliersOptions = data)"
auto-load
/>
<FetchData
ref="companiesRef"
url="Companies"
:filter="{ fields: ['id', 'code'] }"
order="code"
@on-fetch="(data) => (companiesOptions = data)"
auto-load
/>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
custom-route-redirect-name="InvoiceInSummary"
data-key="InvoiceInSummary"
/>
</Teleport>
</template>
<QPage>
<VnSubToolbar />
<FormModel
url-create="InvoiceIns"
model="InvoiceIn"
:form-initial-data="newInvoiceIn"
@on-data-saved="redirectToInvoiceInBasicData"
>
<template #form="{ data, validate }">
<VnRow>
<VnSelect
:label="t('Supplier')"
v-model="data.supplierFk"
:options="suppliersOptions"
option-value="id"
option-label="nickname"
hide-selected
:required="true"
:rules="validate('entry.supplierFk')"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.nickname }}</QItemLabel>
<QItemLabel caption>
#{{ scope.opt?.id }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnInput
:label="t('invoiceIn.summary.supplierRef')"
v-model="data.supplierRef"
/>
</VnRow>
<VnRow>
<VnSelect
:label="t('Company')"
v-model="data.companyFk"
:options="companiesOptions"
option-value="id"
option-label="code"
map-options
hide-selected
:required="true"
:rules="validate('invoiceIn.companyFk')"
/>
<VnInputDate
:label="t('invoiceIn.summary.issued')"
v-model="data.issued"
/>
</VnRow>
</template>
</FormModel>
</QPage>
</template>
<i18n>
es:
Supplier: Proveedor
Travel: Envío
Company: Empresa
</i18n>

View File

@ -4,33 +4,17 @@ import { useI18n } from 'vue-i18n';
import VnSelect from 'components/common/VnSelect.vue';
Review

Se han reordenado los campos en base a como están en Salix.

Se han reordenado los campos en base a como están en Salix.
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
defineProps({ dataKey: { type: String, required: true } });
const suppliers = ref([]);
const suppliersRef = ref();
</script>
<template>
<FetchData
ref="suppliersRef"
url="Suppliers"
:filter="{ fields: ['id', 'nickname'] }"
order="nickname"
limit="30"
@on-fetch="(data) => (suppliers = data)"
auto-load
/>
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<VnFilterPanel :data-key="dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
@ -38,22 +22,6 @@ const suppliersRef = ref();
</div>
</template>
<template #body="{ params, searchFn }">
<QItem>
<QItemSection>
<VnSelect
:label="t('params.supplierFk')"
v-model="params.supplierFk"
:options="suppliers"
option-value="id"
option-label="nickname"
@input-value="suppliersRef.fetch()"
dense
outlined
rounded
>
</VnSelect>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
@ -68,21 +36,11 @@ const suppliersRef = ref();
</VnInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate :label="t('From')" v-model="params.from" is-outlined />
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate :label="t('To')" v-model="params.to" is-outlined />
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('params.serial')"
v-model="params.serial"
:label="t('params.fi')"
v-model="params.fi"
is-outlined
lazy-rules
>
@ -92,11 +50,61 @@ const suppliersRef = ref();
</VnInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
Review

Se corrige el filtrado de suppliers.

Se corrige el filtrado de suppliers.
v-model="params.supplierFk"
url="Suppliers"
:fields="['id', 'nickname']"
:label="t('params.supplierFk')"
option-value="id"
option-label="nickname"
:options="suppliers"
dense
outlined
rounded
>
</VnSelect>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('params.account')"
v-model="params.account"
is-outlined
lazy-rules
>
<template #prepend>
<QIcon name="person" size="sm" />
</template>
</VnInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnCurrency v-model="params.amount" is-outlined />
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate :label="t('From')" v-model="params.from" is-outlined />
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate :label="t('To')" v-model="params.to" is-outlined />
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
:label="t('Issued')"
v-model="params.issued"
is-outlined
/>
</QItemSection>
</QItem>
<QItem class="q-mb-md">
<QItemSection>
<QCheckbox
@ -111,8 +119,8 @@ const suppliersRef = ref();
<QItem>
<QItemSection>
<VnInput
:label="t('params.fi')"
v-model="params.fi"
:label="t('params.serialNumber')"
v-model="params.serialNumber"
is-outlined
lazy-rules
>
@ -125,8 +133,8 @@ const suppliersRef = ref();
<QItem>
<QItemSection>
<VnInput
:label="t('params.serialNumber')"
v-model="params.serialNumber"
:label="t('params.serial')"
v-model="params.serial"
is-outlined
lazy-rules
>
@ -150,29 +158,6 @@ const suppliersRef = ref();
</VnInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('params.account')"
v-model="params.account"
is-outlined
lazy-rules
>
<template #prepend>
<QIcon name="person" size="sm" />
</template>
</VnInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
:label="t('Issued')"
v-model="params.issued"
is-outlined
/>
</QItemSection>
</QItem>
</QExpansionItem>
</template>
</VnFilterPanel>

View File

@ -13,6 +13,7 @@ import InvoiceInFilter from './InvoiceInFilter.vue';
import { getUrl } from 'src/composables/getUrl';
import InvoiceInSummary from './Card/InvoiceInSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
const stateStore = useStateStore();
const router = useRouter();
@ -85,7 +86,15 @@ function navigate(id) {
<VnLv
:label="t('invoiceIn.list.supplier')"
:value="row.supplierName"
/>
@click.stop
>
<template #value>
<span class="link">
{{ row.supplierName }}
<SupplierDescriptorProxy :id="row.supplierFk" />
</span>
</template>
</VnLv>
<VnLv
:label="t('invoiceIn.list.serialNumber')"
:value="row.serialNumber"
@ -137,13 +146,7 @@ function navigate(id) {
</div>
</QPage>
<QPageSticky position="bottom-right" :offset="[20, 20]">
<QBtn
color="primary"
icon="add"
size="lg"
round
:href="`${url}invoice-in/create`"
/>
<QBtn color="primary" icon="add" size="lg" round :href="`/#/invoice-in/create`" />
</QPageSticky>
</template>

View File

@ -0,0 +1,63 @@
<script setup>
import { useRoute } from 'vue-router';
import axios from 'axios';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import { useArrayData } from 'src/composables/useArrayData';
const { notify, dialog } = useQuasar();
const { t } = useI18n();
defineExpose({ checkToBook });
const { store } = useArrayData(useRoute().meta.moduleName);
async function checkToBook(id) {
let directBooking = true;
const { data: totals } = await axios.get(`InvoiceIns/${id}/getTotals`);
const taxableBaseNotEqualDueDay = totals.totalDueDay != totals.totalTaxableBase;
const vatNotEqualDueDay = totals.totalDueDay != totals.totalVat;
if (taxableBaseNotEqualDueDay && vatNotEqualDueDay) directBooking = false;
const { data: dueDaysCount } = await axios.get('InvoiceInDueDays/count', {
where: {
invoiceInFk: id,
dueDated: { gte: Date.vnNew() },
},
});
if (dueDaysCount) directBooking = false;
if (directBooking) return toBook(id);
dialog({
component: VnConfirm,
componentProps: { title: t('Are you sure you want to book this invoice?') },
}).onOk(async () => await toBook(id));
}
async function toBook(id) {
let type = 'positive';
let message = t('globals.dataSaved');
try {
await axios.post(`InvoiceIns/${id}/toBook`);
store.data.isBooked = true;
} catch (e) {
type = 'negative';
message = t('It was not able to book the invoice');
} finally {
notify({ type, message });
}
}
</script>
<template>
<slot name="content" :book="checkToBook" />
</template>
<i18n>
es:
Are you sure you want to book this invoice?: ¿Estás seguro de querer asentar esta factura?
It was not able to book the invoice: No se pudo contabilizar la factura
</i18n>

View File

@ -183,7 +183,7 @@ const getEntryQueryParams = (supplier) => {
<QTooltip>{{ t('Go to client') }}</QTooltip>
</QBtn>
<QBtn
:href="`${url}invoice-in/create?supplierFk=${entity.id}`"
:href="`#/invoice-in/create?supplierFk=${entity.id}`"
jorgep marked this conversation as resolved
Review

Esta en la ruta actual?

Esta en la ruta actual?
Review

Sí, antes redirigia a Salix.

Sí, antes redirigia a Salix.
size="md"
icon="vn:invoice-in-create"
color="primary"

View File

@ -27,7 +27,7 @@ const save = async (data) => {
const lockerId = data.id ?? originaLockerId.value;
const workerFk = lockerId == originaLockerId.value ? null : entityId.value;
await axios.patch(`Lockers/${lockerId}`, { workerFk });
return axios.patch(`Lockers/${lockerId}`, { workerFk });
};
const init = async (data) => {

View File

@ -37,6 +37,15 @@ export default {
},
component: () => import('src/pages/InvoiceIn/InvoiceInList.vue'),
},
{
path: 'create',
name: 'InvoiceInCreare',
meta: {
title: 'invoiceInCreate',
icon: 'create',
},
component: () => import('src/pages/InvoiceIn/InvoiceInCreate.vue'),
},
],
},
{

View File

@ -2,8 +2,7 @@
describe('InvoiceInBasicData', () => {
const formInputs = '.q-form > .q-card input';
const firstFormSelect = '.q-card > .vn-row:nth-child(1) > .q-select';
const appendBtns = '.q-form label button';
const dialogAppendBtns = '.q-dialog label button';
const documentBtns = '.q-form .q-field button';
const dialogInputs = '.q-dialog input';
const dialogActionBtns = '.q-card__actions button';
@ -15,8 +14,7 @@ describe('InvoiceInBasicData', () => {
it('should edit the provideer and supplier ref', () => {
cy.selectOption(firstFormSelect, 'Bros');
cy.get('[title="Reset"]').click();
cy.get(appendBtns).eq(0).click();
cy.get(formInputs).eq(1).type(4739);
cy.get(formInputs).eq(1).type('{selectall}4739');
cy.saveCard();
cy.get(`${firstFormSelect} input`).invoke('val').should('eq', 'Plants nick');
@ -27,22 +25,20 @@ describe('InvoiceInBasicData', () => {
const firtsInput = 'Ticket:65';
const secondInput = "I don't know what posting here!";
cy.get(appendBtns).eq(3).click();
cy.get(dialogAppendBtns).eq(0).click();
cy.get(dialogInputs).eq(0).type(firtsInput);
cy.get(dialogAppendBtns).eq(1).click();
cy.get('textarea').type(secondInput);
cy.get(documentBtns).eq(1).click();
cy.get(dialogInputs).eq(0).type(`{selectall}${firtsInput}`);
cy.get('textarea').type(`{selectall}${secondInput}`);
cy.get(dialogActionBtns).eq(1).click();
cy.get(appendBtns).eq(3).click();
cy.get(documentBtns).eq(1).click();
cy.get(dialogInputs).eq(0).invoke('val').should('eq', firtsInput);
cy.get('textarea').invoke('val').should('eq', secondInput);
});
it('should throw an error creating a new dms if a file is not attached', () => {
cy.get(appendBtns).eq(2).click();
cy.get(appendBtns).eq(1).click();
cy.get(formInputs).eq(5).click();
cy.get(formInputs).eq(5).type('{selectall}{backspace}');
cy.get(documentBtns).eq(0).click();
cy.get(dialogActionBtns).eq(1).click();
cy.get('.q-notification__message').should(
'have.text',

View File

@ -0,0 +1,24 @@
describe('InvoiceInDescriptor', () => {
const dialogBtns = '.q-card__actions button';
const firstDescritorOpt = '.q-menu > .q-list > :nth-child(1) > .q-item__section';
const isBookedField =
'.q-card:nth-child(3) .vn-label-value:nth-child(5) > .value > span';
it('should booking and unbooking the invoice properly', () => {
cy.login('developer');
cy.visit(`/#/invoice-in/1/summary?limit=10`);
cy.openLeftMenu();
cy.openActionsDescriptor();
cy.get(firstDescritorOpt).click();
cy.get(dialogBtns).eq(1).click();
cy.get('.fullscreen').first().click();
cy.get(isBookedField).should('have.attr', 'title', 'true');
cy.openLeftMenu();
cy.openActionsDescriptor();
cy.get(firstDescritorOpt).click();
cy.get(dialogBtns).eq(1).click();
cy.get(isBookedField).should('have.attr', 'title', 'false');
});
});

View File

@ -18,7 +18,7 @@ describe('InvoiceInVat', () => {
cy.saveCard();
cy.visit(`/#/invoice-in/1/vat`);
cy.getValue(firstLineVat).should('equal', 'H.P. IVA 21% CEE');
cy.getValue(firstLineVat).should('equal', '8');
Review

Se está comprobando el valor del input, no el "label"

Se está comprobando el valor del input, no el "label"
});
it('should add a new row', () => {

View File

@ -9,15 +9,15 @@ describe('VnLog', () => {
cy.visit(`/#/claim/${1}/log`);
cy.openRightMenu();
});
it('should filter by insert actions', () => {
// Se tiene que cambiar el Accept-Language a 'en', ya hay una tarea para eso #7189.
xit('should filter by insert actions', () => {
cy.checkOption(':nth-child(7) > .q-checkbox');
cy.get('.q-page').click();
cy.validateContent(chips[0], 'Document');
cy.validateContent(chips[1], 'Beginning');
});
it('should filter by entity', () => {
xit('should filter by entity', () => {
cy.selectOption('.q-drawer--right .q-item > .q-select', 'Claim');
cy.get('.q-page').click();
cy.validateContent(chips[0], 'Claim');

View File

@ -1,4 +1,4 @@
describe('WorkerList', () => {
describe('WorkerLocker', () => {
const workerId = 1109;
const lockerCode = '2F';
const input = '.q-card input';

View File

@ -92,8 +92,13 @@ Cypress.Commands.add('checkOption', (selector) => {
// Global buttons
Cypress.Commands.add('saveCard', () => {
Review

Se adapta para que te deje guardar en caso de haber un desplegable

Se adapta para que te deje guardar en caso de haber un desplegable
const dropdownArrow = '.q-btn-dropdown__arrow-container > .q-btn__content > .q-icon';
cy.get('#st-actions').then(($el) => {
if ($el.find(dropdownArrow).length) cy.get(dropdownArrow).click();
});
cy.get('[title="Save"]').click();
});
Cypress.Commands.add('resetCard', () => {
cy.get('[title="Reset"]').click();
});

View File

@ -1,34 +0,0 @@
import { vi, describe, expect, it, beforeAll } from 'vitest';

Ya se comprueba en e2e

Ya se comprueba en e2e
import { createWrapper, axios } from 'app/test/vitest/helper';
import InvoiceInBasicData from 'src/pages/InvoiceIn/Card/InvoiceInBasicData.vue';
describe('InvoiceInBasicData', () => {
let vm;
beforeAll(() => {
vm = createWrapper(InvoiceInBasicData, {
global: {
stubs: [],
mocks: {
fetch: vi.fn(),
},
},
}).vm;
});
describe('upsert()', () => {
it('should throw an error when data is empty', async () => {
vi.spyOn(axios, 'post').mockResolvedValue({ data: [] });
vi.spyOn(vm.quasar, 'notify');
await vm.upsert();
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: `The company can't be empty`,
type: 'negative',
})
);
});
});
});

View File

@ -19,13 +19,13 @@ describe('InvoiceInIntrastat', () => {
describe('getTotal()', () => {
it('should correctly handle the sum', () => {
vm.invoceInIntrastat = [
const invoceInIntrastat = [
Review

Ahora a la fn hay que pasarle el array. Así los valoraes se actualizan de forma reactiva.

Ahora a la fn hay que pasarle el array. Así los valoraes se actualizan de forma reactiva.
{ amount: 10, stems: 162 },
{ amount: 20, stems: 21 },
];
const totalAmount = vm.getTotal('amount');
const totalStems = vm.getTotal('stems');
const totalAmount = vm.getTotal(invoceInIntrastat, 'amount');
const totalStems = vm.getTotal(invoceInIntrastat, 'stems');
expect(totalAmount).toBe(10 + 20);
expect(totalStems).toBe(162 + 21);

View File

@ -1,5 +1,5 @@
import { vi, describe, expect, it, beforeAll } from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper';
import { createWrapper } from 'app/test/vitest/helper';
import InvoiceInVat from 'src/pages/InvoiceIn/Card/InvoiceInVat.vue';
describe('InvoiceInVat', () => {
@ -16,41 +16,6 @@ describe('InvoiceInVat', () => {
}).vm;
});
describe('addExpense()', () => {
Review

ya se comprueba en e2e

ya se comprueba en e2e
beforeAll(() => {
vi.spyOn(axios, 'post').mockResolvedValue({ data: [] });
vi.spyOn(axios, 'get').mockResolvedValue({ data: [] });
vi.spyOn(vm.quasar, 'notify');
});
it('should throw an error when the code property is undefined', async () => {
await vm.addExpense();
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: `The code can't be empty`,
type: 'negative',
})
);
});
it('should correctly handle expense addition', async () => {
vm.newExpense = {
code: 123,
isWithheld: false,
description: 'Descripción del gasto',
};
await vm.addExpense();
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Data saved',
type: 'positive',
})
);
});
});
describe('taxRate()', () => {
it('should correctly compute the tax rate', () => {
const invoiceInTax = { taxableBase: 100, taxTypeSageFk: 1 };

View File

@ -24,6 +24,7 @@ vi.mock('vue-router', () => ({
params: {
id: 1,
},
meta: { moduleName: 'mockName' },
Review

Lo pongo en ambos sitios ya de paso.

Lo pongo en ambos sitios ya de paso.
},
},
}),
@ -31,6 +32,7 @@ vi.mock('vue-router', () => ({
matched: [],
query: {},
params: {},
meta: { moduleName: 'mockName' },
}),
}));