refs #5835 migrateInvoiceIn #110

Merged
jorgep merged 29 commits from 5835-migrateInvoiceIn into dev 2023-12-13 10:25:07 +00:00
24 changed files with 3281 additions and 27 deletions
Showing only changes of commit 112e057cfa - Show all commits

View File

@ -15,6 +15,10 @@ const $props = defineProps({
type: String,
default: '',
},
filterOptions: {
type: Array,
default: () => [],
},
});
const { optionLabel, options } = toRefs($props);
const myOptions = ref([]);
@ -28,18 +32,22 @@ function setOptions(data) {
setOptions(options.value);
const filter = (val, options) => {
const search = val.toLowerCase();
const search = val.toString().toLowerCase();
if (val === '') return options;
if (!search) return options;
return options.filter((row) => {
if ($props.filterOptions.length) {
return $props.filterOptions.some((prop) => {
const propValue = String(row[prop]).toLowerCase();
return propValue.includes(search);
});
}
const id = row.id;
const name = row[$props.optionLabel].toLowerCase();
const optionLabel = String(row[$props.optionLabel]).toLowerCase();
const idMatches = id == search;
const nameMatches = name.indexOf(search) > -1;
return idMatches || nameMatches;
return id == search || optionLabel.includes(search);
});
};
@ -89,7 +97,7 @@ const value = computed({
<QIcon name="close" @click.stop="value = null" class="cursor-pointer" />
</template>
<template v-for="(_, slotName) in $slots" #[slotName]="slotData">
<slot :name="slotName" v-bind="slotData" />
<slot :name="slotName" v-bind="slotData ?? {}" />
</template>
</QSelect>
</template>

View File

@ -76,9 +76,9 @@ async function search() {
const module = route.matched[1];
if (rows.length === 1) {
const [firstRow] = rows;
await router.push({ path: `/${module.name}/${firstRow.id}` });
await router.push({ path: `${module.path}/${firstRow.id}` });
} else if (route.matched.length > 3) {
await router.push({ path: `/${module.name}` });
await router.push({ path: `/${module.path}` });
arrayData.updateStateParams();
}
}

View File

@ -0,0 +1,11 @@
import { useSession } from 'src/composables/useSession';
import { getUrl } from './getUrl';
const session = useSession();
const token = session.getToken();
export async function downloadFile(dmsId) {
let appUrl = await getUrl('', 'lilium');
jorgep marked this conversation as resolved
Review

confirma con @alexm que lilum en el código a piñón es correcto

confirma con @alexm que lilum en el código a piñón es correcto
Review

Lo veo bien. Si se llega a repetir mucho, se podria llegar a hacer getUrlLilium o alho

Lo veo bien. Si se llega a repetir mucho, se podria llegar a hacer getUrlLilium o alho
appUrl = appUrl.replace('/#/', '');
window.open(`${appUrl}/api/dms/${dmsId}/downloadFile?access_token=${token}`);
}

View File

@ -1,11 +1,10 @@
import axios from 'axios';
export async function getUrl(route, appName = 'salix') {
const filter = {
where: { and: [{ appName: appName }, { environment: process.env.NODE_ENV }] },
};
export async function getUrl(route, app = 'salix') {
let url;
const { data } = await axios.get('Urls/findOne', { params: { filter } });
const url = data.url;
return route ? url + route : url;
await axios.get('Urls/getUrl', { params: { app } }).then((res) => {
url = res.data + route;
});
return url;
}

View File

@ -390,6 +390,71 @@ export default {
totalWithVat: 'Amount',
},
},
invoiceIn: {
pageTitles: {
invoiceIns: 'Invoices In',
list: 'List',
createInvoiceIn: 'Create invoice in',
summary: 'Summary',
basicData: 'Basic Data',
vat: 'VAT',
dueDay: 'Due day',
intrastat: 'Intrastat',
log: 'Logs',
},
list: {
ref: 'Reference',
supplier: 'Supplier',
supplierRef: 'Supplier ref.',
serialNumber: 'Serial number',
serial: 'Serial',
file: 'File',
issued: 'Issued',
isBooked: 'Is booked',
awb: 'AWB',
amount: 'Amount',
},
card: {
issued: 'Issued',
amount: 'Amount',
client: 'Client',
company: 'Company',
customerCard: 'Customer card',
ticketList: 'Ticket List',
vat: 'Vat',
dueDay: 'Due day',
intrastat: 'Intrastat',
},
summary: {
supplier: 'Supplier',
supplierRef: 'Supplier ref.',
currency: 'Currency',
docNumber: 'Doc number',
issued: 'Expedition date',
operated: 'Operation date',
bookEntried: 'Entry date',
bookedDate: 'Booked date',
sage: 'Sage withholding',
vat: 'Undeductible VAT',
company: 'Company',
booked: 'Booked',
expense: 'Expense',
taxableBase: 'Taxable base',
rate: 'Rate',
sageVat: 'Sage vat',
sageTransaction: 'Sage transaction',
dueDay: 'Date',
bank: 'Bank',
amount: 'Amount',
foreignValue: 'Foreign value',
dueTotal: 'Due day',
noMatch: 'Do not match',
code: 'Code',
net: 'Net',
stems: 'Stems',
country: 'Country',
},
},
worker: {
pageTitles: {
workers: 'Workers',
@ -514,6 +579,7 @@ export default {
openCard: 'View card',
openSummary: 'Open summary',
viewDescription: 'View description',
downloadFile: 'Download file',
},
cardDescriptor: {
mainList: 'Main list',

View File

@ -390,6 +390,69 @@ export default {
totalWithVat: 'Importe',
},
},
invoiceIn: {
pageTitles: {
invoiceIns: 'Fact. recibidas',
list: 'Listado',
createInvoiceOut: 'Crear fact. recibida',
summary: 'Resumen',
jorgep marked this conversation as resolved Outdated

invoice Out es factura emitida
pero dejalo en Crear factura

invoice Out es factura emitida pero dejalo en Crear factura
basicData: 'Datos básicos',
vat: 'IVA',
dueDay: 'Vencimiento',
intrastat: 'Intrastat',
log: 'Registros de auditoría',
},
jorgep marked this conversation as resolved
Review

aquí creo que en salix es historico, puede ser?

aquí creo que en salix es historico, puede ser?
Review

En salix se llama histórico pero en Lilium le han puesto ese nombre en las otras secciones

En salix se llama histórico pero en Lilium le han puesto ese nombre en las otras secciones
list: {
ref: 'Referencia',
supplier: 'Proveedor',
supplierRef: 'Ref. proveedor',
jorgep marked this conversation as resolved Outdated

invoice out es emitida, mira si esta mal la clave o el valor

invoice out es emitida, mira si esta mal la clave o el valor
serialNumber: 'Num. serie',
shortIssued: 'F. emisión',
serial: 'Serie',
file: 'Fichero',
issued: 'Fecha emisión',
isBooked: 'Conciliada',
awb: 'AWB',
amount: 'Importe',
},
card: {
issued: 'Fecha emisión',
amount: 'Importe',
client: 'Cliente',
company: 'Empresa',
customerCard: 'Ficha del cliente',
ticketList: 'Listado de tickets',
vat: 'Vat',
dueDay: 'Fecha de vencimiento',
},
summary: {
supplier: 'Supplier',
supplierRef: 'Supplier ref.',
currency: 'Divisa',
docNumber: 'Número documento',
issued: 'Fecha de expedición',
operated: 'Fecha operación',
bookEntried: 'Fecha asiento',
bookedDate: 'Fecha contable',
sage: 'Retención sage',
vat: 'Iva no deducible',
company: 'Empresa',
booked: 'Contabilizada',
expense: 'Gasto',
taxableBase: 'Base imp.',
rate: 'Tasa',
sageTransaction: 'Sage transación',
dueDay: 'Fecha',
bank: 'Caja',
amount: 'Importe',
foreignValue: 'Divisa',
dueTotal: 'Vencimiento',
code: 'Código',
net: 'Neto',
stems: 'Tallos',
country: 'País',
},
},
worker: {
pageTitles: {
workers: 'Trabajadores',
@ -514,6 +577,7 @@ export default {
openCard: 'Ver ficha',
openSummary: 'Abrir detalles',
viewDescription: 'Ver descripción',
downloadFile: 'Descargar archivo',
},
cardDescriptor: {
mainList: 'Listado principal',

View File

@ -0,0 +1,742 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useArrayData } from 'src/composables/useArrayData';
import { downloadFile } from 'src/composables/downloadFile';
import FetchData from 'src/components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import axios from 'axios';
const quasar = useQuasar();
const route = useRoute();
const { t } = useI18n();
const dms = ref({});
const editDownloadDisabled = ref(false);
const arrayData = useArrayData('InvoiceIn');
const invoiceIn = computed(() => arrayData.store.data);
const userConfig = ref(null);
const suppliers = ref([]);
const suppliersRef = ref();
const currencies = ref([]);
const currenciesRef = ref();
const companies = ref([]);
const companiesRef = ref();
const dmsTypes = ref([]);
const dmsTypesRef = ref();
const warehouses = ref([]);
const warehousesRef = ref();
const allowTypesRef = ref();
const allowedContentTypes = ref([]);
const inputFileRef = ref();
const editDmsRef = ref();
const createDmsRef = ref();
async function checkFileExists(dmsId) {
if (!dmsId) return;
try {
await axios.get(`Dms/${dmsId}`, { fields: ['id'] });
editDownloadDisabled.value = false;
} catch (e) {
editDownloadDisabled.value = true;
}
}
async function setEditDms(dmsId) {
const { data } = await axios.get(`Dms/${dmsId}`);
dms.value = {
jorgep marked this conversation as resolved
Review

lo miramos

lo miramos
id: data.id,
warehouseId: data.warehouseFk,
companyId: data.companyFk,
dmsTypeId: data.dmsTypeFk,
reference: data.reference,
description: data.description,
hasFile: data.hasFile,
hasFileAttached: data.hasFileAttached,
};
if (!allowedContentTypes.value.length) await allowTypesRef.value.fetch();
editDmsRef.value.show();
}
async function setCreateDms() {
const { data } = await axios.get('DmsTypes/findOne', {
where: { code: 'invoiceIn' },
});
dms.value = {
reference: invoiceIn.value.supplierRef,
warehouseId: userConfig.value.warehouseFk,
companyId: userConfig.value.companyFk,
dmsTypeId: data.id,
description: invoiceIn.value.supplier.name,
hasFile: true,
hasFileAttached: true,
files: null,
};
createDmsRef.value.show();
}
async function edit() {
try {
if (!dms.value.companyId) throw new Error(t(`The company can't be empty`));
jorgep marked this conversation as resolved Outdated

Incluso te diría que en vez de tantos ifs, haría un objeto de validaciones, donde la key es el campo y el value es el mensaje a mostrar.

const validations = {
  companyId:'The company can\'t be empty'
}

Object.entries(validations).forEach(([key, value])=>{
  if(!dms.value[key])
    throw Error(t(value))
});

Para la que aplica en edit, usuaria la bandera que hemos comentado

Incluso te diría que en vez de tantos ifs, haría un objeto de validaciones, donde la key es el campo y el value es el mensaje a mostrar. ``` const validations = { companyId:'The company can\'t be empty' } Object.entries(validations).forEach(([key, value])=>{ if(!dms.value[key]) throw Error(t(value)) }); ``` Para la que aplica en edit, usuaria la bandera que hemos comentado
if (!dms.value.warehouseId) throw new Error(t(`The warehouse can't be empty`));
if (!dms.value.dmsTypeId) throw new Error(t(`The DMS Type can't be empty`));
if (!dms.value.description) throw new Error(t(`The description can't be empty`));
const formData = new FormData();
if (dms.value.files) {
for (let i = 0; i < dms.value.files.length; i++)
formData.append(dms.value.files[i].name, dms.value.files[i]);
dms.value.hasFileAttached = true;
}
const { data } = await axios.post(`dms/${dms.value.id}/updateFile`, formData, {
params: dms.value,
});
if (data.length) invoiceIn.value.dmsFk = data[0].id;
editDmsRef.value.hide();
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
});
} catch (error) {
quasar.notify({
message: t(`${error}`),
type: 'negative',
});
}
}
async function create() {
jorgep marked this conversation as resolved Outdated

Yo movería la lógica de create y save a funciones constantes porque estás duplicando código que hacen lo mismo

Yo movería la lógica de create y save a funciones constantes porque estás duplicando código que hacen lo mismo
try {
if (!dms.value.companyId) throw new Error(t(`The company can't be empty`));
if (!dms.value.warehouseId) throw new Error(t(`The warehouse can't be empty`));
if (!dms.value.dmsTypeId) throw new Error(t(`The DMS Type can't be empty`));
if (!dms.value.description) throw new Error(t(`The description can't be empty`));
if (!dms.value.files) throw new Error(t(`The files can't be empty`));
const formData = new FormData();
if (dms.value.files) {
for (let i = 0; i < dms.value.files.length; i++)
formData.append(dms.value.files[i].name, dms.value.files[i]);
dms.value.hasFileAttached = true;
}
const { data } = await axios.post('Dms/uploadFile', formData, {
params: dms.value,
});
if (data.length) invoiceIn.value.dmsFk = data[0].id;
editDmsRef.value.hide();
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
});
} catch (error) {
quasar.notify({
message: t(`${error}`),
type: 'negative',
});
}
}
</script>
<template>
<FetchData
ref="suppliersRef"
url="Suppliers"
:filter="{ fields: ['id', 'nickname'] }"
limit="30"
@on-fetch="(data) => (suppliers = data)"
/>
<FetchData
ref="currenciesRef"
url="Currencies"
:filter="{ fields: ['id', 'code'] }"
order="code"
@on-fetch="(data) => (currencies = data)"
/>
<FetchData
ref="companiesRef"
url="Companies"
:filter="{ fields: ['id', 'code'] }"
order="code"
@on-fetch="(data) => (companies = data)"
/>
<FetchData
ref="dmsTypesRef"
url="DmsTypes"
:filter="{ fields: ['id', 'name'] }"
order="name"
@on-fetch="(data) => (dmsTypes = data)"
/>
<FetchData
ref="warehousesRef"
url="Warehouses"
:filter="{ fields: ['id', 'name'] }"
order="name"
@on-fetch="(data) => (warehouses = data)"
/>
<FetchData
ref="allowTypesRef"
url="DmsContainers/allowedContentTypes"
@on-fetch="(data) => (allowedContentTypes = data)"
/>
<FetchData
url="UserConfigs/getUserConfig"
@on-fetch="(data) => (userConfig = data)"
auto-load
/>
<div class="column items-center">
<QCard>
<FormModel
v-if="invoiceIn"
:url="`InvoiceIns/${route.params.id}`"
model="invoiceIn"
>
<template #form="{ data }">
<div class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
v-if="suppliersRef"
:label="t('supplierFk')"
v-model="data.supplierFk"
:options="suppliers"
option-value="id"
option-label="nickname"
@input-value="suppliersRef.fetch()"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt.id} - ${scope.opt.nickname}`
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col">
<QInput
clearable
clear-icon="close"
:label="t('Supplier ref')"
v-model="data.supplierRef"
/>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<QInput :label="t('Expedition date')" v-model="data.issued">
<template #append>
<QIcon
name="event"
class="cursor-pointer"
mask="####-##-##"
fill-mask="_"
>
<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>
</div>
<div class="col">
<QInput
:label="t('Operation date')"
v-model="data.operated"
mask="####-##-##"
fill-mask="_"
autofocus
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate
v-model="data.operated"
mask="YYYY-MM-DD"
>
<div class="row items-center justify-end">
<QBtn
v-close-popup
label="Close"
color="primary"
flat
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
:label="t('Undeductible VAT')"
v-model="data.deductibleExpenseFk"
clearable
clear-icon="close"
/>
</div>
<div class="col">
<QInput
:label="t('Document')"
v-model="data.dmsFk"
clearable
clear-icon="close"
@update:model-value="checkFileExists(data.dmsFk)"
>
<template #prepend>
<QBtn
v-if="data.dmsFk"
:class="{
'no-pointer-events': editDownloadDisabled,
}"
:disable="editDownloadDisabled"
icon="cloud_download"
:title="t('Download file')"
padding="xs"
round
@click="downloadFile(data.dmsFk)"
/>
</template>
<template #append>
<QBtn
:class="{
'no-pointer-events': editDownloadDisabled,
}"
:disable="editDownloadDisabled"
v-if="data.dmsFk"
icon="edit"
round
padding="xs"
@click="setEditDms(data.dmsFk)"
>
<QTooltip>{{ t('Edit document') }}</QTooltip>
</QBtn>
<QBtn
v-else
icon="add_circle"
round
padding="xs"
@click="setCreateDms()"
>
<QTooltip>{{ t('Create document') }}</QTooltip>
</QBtn>
</template>
</QInput>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
:label="t('Entry date')"
v-model="data.bookEntried"
clearable
jorgep marked this conversation as resolved
Review

Los valores de las propiedades de mask, podrían ser globales en el componente?

Los valores de las propiedades de mask, podrían ser globales en el componente?
clear-icon="close"
mask="####-##-##"
fill-mask="_"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate
v-model="data.bookEntried"
mask="YYYY-MM-DD"
>
<div class="row items-center justify-end">
<QBtn
v-close-popup
label="Close"
color="primary"
flat
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</div>
<div class="col">
<QInput
:label="t('Accounted date')"
v-model="data.booked"
clearable
clear-icon="close"
mask="####-##-##"
fill-mask="_"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate
v-model="data.booked"
mask="YYYY-MM-DD"
>
<div class="row items-center justify-end">
<QBtn
v-close-popup
label="Close"
color="primary"
flat
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
v-if="currenciesRef"
:label="t('Currency')"
v-model="data.currencyFk"
:options="currencies"
option-value="id"
option-label="code"
@input-value="currenciesRef.fetch()"
/>
</div>
<div class="col">
<VnSelectFilter
v-if="companiesRef"
:label="t('Company')"
v-model="data.companyFk"
:options="companies"
option-value="id"
option-label="code"
@input-value="companiesRef.fetch()"
/>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
jorgep marked this conversation as resolved
Review

Esta rule esta muchas veces en el código y es la misma en todos los casos.
Se podría sacar del HTML y definir en el script setup?

Esta rule esta muchas veces en el código y es la misma en todos los casos. Se podría sacar del HTML y definir en el script setup?
<div class="col">
<QCheckbox
:label="t('invoiceIn.summary.booked')"
v-model="data.isBooked"
/>
</div>
<div class="col"></div>
</div>
</template>
</FormModel>
</QCard>
</div>
<QDialog ref="editDmsRef">
<QCard>
<QCardSection class="q-pb-none">
<QItem class="q-px-none">
<span class="text-primary text-h6 full-width">
<QIcon name="edit" class="q-mr-xs" />
{{ t('Edit document') }}
</span>
<QBtn icon="close" flat round dense v-close-popup />
</QItem>
</QCardSection>
<QCardSection class="q-py-none">
<QItem>
<QInput
class="full-width q-pa-xs"
:label="t('Reference')"
v-model="dms.reference"
/>
<VnSelectFilter
class="full-width q-pa-xs"
:label="`${t('Company')}*`"
v-model="dms.companyId"
:options="companies"
option-value="id"
option-label="code"
@input-value="companiesRef.fetch()"
:rules="[(val) => val || t('Required field')]"
/>
</QItem>
<QItem>
<VnSelectFilter
class="full-width q-pa-xs"
:label="`${t('Warehouse')}*`"
v-model="dms.warehouseId"
:options="warehouses"
option-value="id"
option-label="name"
@input-value="warehousesRef.fetch()"
:rules="[(val) => val || t('Required field')]"
/>
<VnSelectFilter
class="full-width q-pa-xs"
:label="`${t('Type')}*`"
v-model="dms.dmsTypeId"
:options="dmsTypes"
option-value="id"
option-label="name"
@input-value="dmsTypesRef.fetch()"
:rules="[(val) => val || t('Required field')]"
/>
</QItem>
<QItem>
<QInput
class="full-width q-pa-xs"
type="textarea"
size="lg"
autogrow
:label="`${t('Description')}*`"
v-model="dms.description"
clearable
clear-icon="close"
:rules="[(val) => val.length || t('Required field')]"
/>
</QItem>
<QItem>
<QFile
ref="inputFileRef"
class="full-width q-pa-xs"
:label="t('File')"
v-model="dms.files"
multiple
:accept="allowedContentTypes.join(',')"
clearable
clear-icon="close"
>
<template #append>
<QBtn
icon="attach_file_add"
flat
round
padding="xs"
@click="inputFileRef.pickFiles()"
>
<QTooltip>
{{ t('Select a file') }}
</QTooltip>
</QBtn>
<QBtn icon="info" flat round padding="xs">
<QTooltip max-width="30rem">
{{
`${t(
'Allowed content types'
)}: ${allowedContentTypes.join(', ')}`
}}
</QTooltip>
</QBtn>
</template>
</QFile>
</QItem>
<QItem>
<QCheckbox
:label="t('Generate identifier for original file')"
v-model="dms.hasFile"
/>
</QItem>
</QCardSection>
<QCardActions class="justify-end">
<QBtn flat :label="t('globals.close')" color="primary" v-close-popup />
<QBtn :label="t('globals.save')" color="primary" @click="edit" />
</QCardActions>
</QCard>
</QDialog>
<QDialog ref="createDmsRef">
<QCard>
<QCardSection class="q-pb-none">
<QItem>
<span class="text-primary text-h6 full-width">
<QIcon name="edit" class="q-mr-xs" />
{{ t('Create document') }}
</span>
<QBtn icon="close" flat round dense v-close-popup align="right" />
</QItem>
</QCardSection>
<QCardSection class="q-pb-none">
<QItem>
<QInput
class="full-width q-pa-xs"
:label="t('Reference')"
v-model="dms.reference"
/>
<VnSelectFilter
class="full-width q-pa-xs"
:label="`${t('Company')}*`"
v-model="dms.companyId"
:options="companies"
option-value="id"
option-label="code"
@input-value="companiesRef.fetch()"
:rules="[(val) => val || t('Required field')]"
/>
</QItem>
<QItem>
<VnSelectFilter
class="full-width q-pa-xs"
:label="`${t('Warehouse')}*`"
v-model="dms.warehouseId"
:options="warehouses"
option-value="id"
option-label="name"
@input-value="warehousesRef.fetch()"
:rules="[(val) => val || t('Required field')]"
/>
<VnSelectFilter
class="full-width q-pa-xs"
:label="`${t('Type')}*`"
v-model="dms.dmsTypeId"
:options="dmsTypes"
option-value="id"
option-label="name"
@input-value="dmsTypesRef.fetch()"
:rules="[(val) => val || t('Required field')]"
/>
</QItem>
<QItem>
<QInput
class="full-width q-pa-xs"
type="textarea"
size="lg"
autogrow
:label="`${t('Description')}*`"
v-model="dms.description"
clearable
clear-icon="close"
:rules="[(val) => val.length || t('Required field')]"
/>
</QItem>
<QItem>
<QFile
ref="inputFileRef"
class="full-width q-pa-xs"
:label="t('File')"
v-model="dms.files"
multiple
:accept="allowedContentTypes.join(',')"
clearable
clear-icon="close"
>
<template #append>
<QBtn
icon="attach_file_add"
flat
round
padding="xs"
@click="inputFileRef.pickFiles()"
>
<QTooltip>
{{ t('Select a file') }}
</QTooltip>
</QBtn>
<QBtn icon="info" flat round padding="xs">
<QTooltip max-width="30rem">
{{
`${t(
'Allowed content types'
)}: ${allowedContentTypes.join(', ')}`
}}
</QTooltip>
</QBtn>
</template>
</QFile>
</QItem>
<QItem>
<QCheckbox
:label="t('Generate identifier for original file')"
v-model="dms.hasFile"
/>
</QItem>
</QCardSection>
<QCardActions align="right">
<QBtn flat :label="t('globals.close')" color="primary" v-close-popup />
<QBtn :label="t('globals.save')" color="primary" @click="create()" />
</QCardActions>
</QCard>
</QDialog>
</template>
<style lang="scss" scoped>
.column {
.q-card {
width: 100%;
max-width: 60em;
}
}
@media (max-width: $breakpoint-xs) {
.q-dialog {
.q-card {
&__section:not(:first-child) {
.q-item {
flex-direction: column;
}
}
}
}
}
</style>
<i18n>
en:
supplierFk: Supplier
es:
supplierFk: Proveedor
Supplier Ref: Ref. proveedor
Expedition date: Fecha expedición
Operation date: Fecha operación
Undeductible VAT: Iva no deducible
Document: Documento
Download file: Descargar archivo
Entry date: Fecha asiento
Accounted date: Fecha contable
Currency: Moneda
Company: Empresa
Edit document: Editar documento
Reference: Referencia
Type: Tipo
Description: Descripción
Generate identifier for original file: Generar identificador para archivo original
Required field: Campo obligatorio
File: Fichero
Create document: Crear documento
Select a file: Seleccione un fichero
Allowed content types: Tipos de archivo permitidos
The company can't be empty: La empresa no puede estar vacía
The warehouse can't be empty: El almacén no puede estar vacío
The DMS Type can't be empty: El dms no puede estar vacío
The description can't be empty: La descripción no puede estar vacía
The files can't be empty: Los archivos no pueden estar vacíos
</i18n>

View File

@ -0,0 +1,91 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import InvoiceInDescriptor from './InvoiceInDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import { useArrayData } from 'src/composables/useArrayData';
import { onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
const stateStore = useStateStore();
const { t } = useI18n();
const route = useRoute();
const filter = {
include: [
{
relation: 'supplier',
scope: {
include: {
relation: 'contacts',
scope: {
where: {
email: { neq: null },
},
},
},
},
},
{
relation: 'invoiceInDueDay',
},
{
relation: 'company',
},
{
relation: 'currency',
},
],
};
const arrayData = useArrayData('InvoiceIn', {
url: `InvoiceIns/${route.params.id}`,
filter,
});
onMounted(async () => {
await arrayData.fetch({ append: false });
watch(
() => route.params.id,
async (newId, oldId) => {
if (newId) {
arrayData.store.url = `InvoiceIns/${newId}`;
await arrayData.fetch({ append: false });
}
}
);
});
</script>
<template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar
data-key="InvoiceInList"
url="InvoiceIns/filter"
:label="t('Search invoice')"
:info="t('You can search by invoice reference')"
/>
</Teleport>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<InvoiceInDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<QToolbar class="bg-vn-dark justify-end">
<div id="st-data"></div>
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<div class="q-pa-md"><RouterView></RouterView></div>
</QPage>
</QPageContainer>
</template>
<i18n>
es:
Search invoice: Buscar factura emitida
You can search by invoice reference: Puedes buscar por referencia de la factura
</i18n>

View File

@ -0,0 +1,108 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toCurrency, toDate } from 'src/filters';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription';
import FetchData from 'src/components/FetchData.vue';
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const route = useRoute();
const { t } = useI18n();
const entityId = computed(() => {
return $props.id || route.params.id;
});
const totalAmount = ref([]);
// Cuando Carlossa acabe de crear la lógica de card, usar store arrayData en lugar de hacer el fetch en el descriptor
const filter = {
include: [
{
relation: 'supplier',
scope: {
include: {
relation: 'contacts',
scope: {
where: {
email: { neq: null },
},
},
},
},
},
{
relation: 'invoiceInDueDay',
},
{
relation: 'company',
},
{
relation: 'currency',
},
],
};
const data = ref(useCardDescription());
function setData(entity) {
data.value = useCardDescription(entity.supplierRef, entity.id);
}
</script>
<template>
<!--Refactor para añadir en el arrayData-->
<FetchData
:url="`InvoiceIns/${entityId}/getTotals`"
@on-fetch="
(data) => {
totalAmount = data.totalDueDay;
}
"
auto-load
/>
<CardDescriptor
module="InvoiceIn"
:url="`InvoiceIns/${entityId}`"
:filter="filter"
:title="data.title"
:subtitle="data.subtitle"
@on-fetch="setData"
>
<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"
/>
</template>
<template #actions="{ entity }">
<QCardActions>
<!--Sección proveedores no disponible-->
<!--Sección entradas no disponible-->
<QBtn
size="md"
icon="vn:ticket"
color="primary"
:to="{
name: 'InvoiceInList',
query: {
params: JSON.stringify({ supplierFk: entity.supplierFk }),
},
}"
>
<QTooltip>{{ t('invoiceOut.card.ticketList') }}</QTooltip>
</QBtn>
</QCardActions>
</template>
</CardDescriptor>
</template>

View File

@ -0,0 +1,290 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toDate } from 'src/filters';
import { useArrayData } from 'src/composables/useArrayData';
import CrudModel from 'src/components/CrudModel.vue';
import FetchData from 'src/components/FetchData.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
const route = useRoute();
const { t } = useI18n();
const arrayData = useArrayData('InvoiceIn');
const invoiceIn = computed(() => arrayData.store.data);
const rowsSelected = ref([]);
const banks = ref([]);
const invoiceInFormRef = ref();
const filter = {
where: {
invoiceInFk: route.params.id,
},
};
const isNotEuro = (code) => code != 'EUR';
const columns = computed(() => [
{
name: 'duedate',
label: t('Date'),
field: (row) => toDate(row.dueDated),
sortable: true,
tabIndex: 1,
align: 'left',
},
{
name: 'bank',
label: t('Bank'),
field: (row) => row.bankFk,
options: banks.value,
model: 'bankFk',
optionValue: 'id',
optionLabel: 'bank',
sortable: true,
tabIndex: 2,
align: 'left',
},
{
name: 'amount',
label: t('Amount'),
field: (row) => row.amount,
sortable: true,
tabIndex: 3,
align: 'left',
},
{
name: 'foreignvalue',
label: t('Foreign value'),
field: (row) => row.foreignValue,
sortable: true,
tabIndex: 4,
align: 'left',
},
]);
</script>
<template>
<FetchData url="Banks" auto-load limit="30" @on-fetch="(data) => (banks = data)" />
<CrudModel
v-if="invoiceIn"
jorgep marked this conversation as resolved Outdated

TIP: es igual poner Number(invoiceId) que +invoiceId

TIP: es igual poner Number(invoiceId) que +invoiceId
ref="invoiceInFormRef"
data-key="InvoiceInDueDays"
url="InvoiceInDueDays"
:filter="filter"
auto-load
:data-required="{ invoiceInFk: route.params.id }"
v-model:selected="rowsSelected"
>
<template #body="{ rows }">
<QTable
v-model:selected="rowsSelected"
selection="multiple"
:columns="columns"
:rows="rows"
row-key="$index"
hide-pagination
:grid="$q.screen.lt.sm"
>
<template #body-cell-duedate="{ row }">
<QTd>
<QInput
v-model="row.dueDated"
mask="date"
placeholder="yyyy/mm/dd"
clearable
clear-icon="close"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
jorgep marked this conversation as resolved
Review

Quizás debería ser una valor constante

Quizás debería ser una valor constante
transition-hide="scale"
>
<QDate v-model="row.dueDated" landscape>
<div
class="row items-center justify-end q-gutter-sm"
>
<QBtn
:label="t('globals.cancel')"
color="primary"
flat
v-close-popup
/>
<QBtn
:label="t('globals.confirm')"
color="primary"
flat
v-close-popup
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</QTd>
</template>
<template #body-cell-bank="{ row, col }">
<QTd>
<VnSelectFilter
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt.id}: ${scope.opt.bank}`
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QTd>
</template>
<template #body-cell-amount="{ row }">
<QTd>
<QInput v-model="row.amount" clearable clear-icon="close" />
</QTd>
</template>
<template #body-cell-foreignvalue="{ row }">
<QTd>
<QInput
:class="{
'no-pointer-events': !isNotEuro(invoiceIn.currency.code),
}"
:disable="!isNotEuro(invoiceIn.currency.code)"
v-model="row.foreignValue"
clearable
clear-icon="close"
/>
</QTd>
</template>
<template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
<QCard>
<QCardSection>
<QCheckbox v-model="props.selected" dense />
</QCardSection>
<QSeparator />
<QList>
<QItem>
<QInput
class="full-width"
:label="t('Date')"
v-model="props.row.dueDated"
mask="date"
placeholder="yyyy/mm/dd"
clearable
clear-icon="close"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate
v-model="props.row.dueDated"
landscape
>
<div
class="row items-center justify-end q-gutter-sm"
>
<QBtn
:label="
t('globals.cancel')
"
color="primary"
flat
v-close-popup
/>
<QBtn
:label="
t('globals.confirm')
"
color="primary"
flat
v-close-popup
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</QItem>
<QItem>
<VnSelectFilter
:label="t('Bank')"
class="full-width"
v-model="props.row['bankFk']"
:options="banks"
option-value="id"
option-label="bank"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt.id}: ${scope.opt.bank}`
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QItem>
<QItem>
<QInput
:label="t('Amount')"
class="full-width"
v-model="props.row.amount"
clearable
clear-icon="close"
/>
</QItem>
<QItem>
<QInput
:label="t('Foreign value')"
class="full-width"
:class="{
'no-pointer-events': !isNotEuro(
invoiceIn.currency.code
),
}"
:disable="!isNotEuro(invoiceIn.currency.code)"
v-model="props.row.foreignValue"
clearable
clear-icon="close"
/>
</QItem>
</QList>
</QCard>
</div>
</template>
</QTable>
</template>
</CrudModel>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn
color="primary"
icon="add"
size="lg"
round
@click="invoiceInFormRef.insert()"
/>
</QPageSticky>
</template>
<style lang="scss" scoped></style>
<i18n>
es:
Date: Fecha
Bank: Caja
Amount: Importe
Foreign value: Divisa
</i18n>

View File

@ -0,0 +1,280 @@
<script setup>
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toCurrency } from 'src/filters';
import CrudModel from 'src/components/CrudModel.vue';
import FetchData from 'src/components/FetchData.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnLv from 'src/components/ui/VnLv.vue';
const { t } = useI18n();
const route = useRoute();
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 columns = computed(() => [
{
name: 'code',
label: t('Code'),
field: (row) => row.intrastatFk,
options: intrastats.value,
model: 'intrastatFk',
optionValue: 'id',
optionLabel: 'description',
sortable: true,
tabIndex: 1,
align: 'left',
},
{
name: 'amount',
label: t('amount'),
field: (row) => row.amount,
sortable: true,
tabIndex: 2,
align: 'left',
},
{
name: 'net',
label: t('net'),
field: (row) => row.net,
sortable: true,
tabIndex: 3,
align: 'left',
},
{
name: 'stems',
label: t('stems'),
field: (row) => row.stems,
sortable: true,
tabIndex: 4,
align: 'left',
},
{
name: 'country',
label: t('country'),
field: (row) => row.countryFk,
options: countries.value,
model: 'countryFk',
optionValue: 'id',
optionLabel: 'code',
sortable: true,
tabIndex: 5,
align: 'left',
},
]);
function getTotal(type) {
if (!invoceInIntrastat.value.length) return 0.0;
return invoceInIntrastat.value.reduce(
(total, intrastat) => total + intrastat[type],
0.0
);
}
</script>
<template>
<FetchData
url="Countries"
auto-load
@on-fetch="(data) => (countries = data)"
sort-by="country"
/>
<FetchData
url="Intrastats"
sort-by="id"
auto-load
@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"
data-key="InvoiceInIntrastats"
url="InvoiceInIntrastats"
auto-load
:data-required="{ invoiceInFk: route.params.id }"
:filter="filter"
v-model:selected="rowsSelected"
@on-fetch="(data) => (invoceInIntrastat = data)"
>
<template #body="{ rows }">
<QTable
v-model:selected="rowsSelected"
selection="multiple"
:columns="columns"
:rows="rows"
row-key="$index"
hide-pagination
:grid="$q.screen.lt.sm"
>
<template #body-cell="{ row, col }">
<QTd>
<QInput
v-model="row[col.name]"
clearable
clear-icon="close"
/>
</QTd>
</template>
<template #body-cell-code="{ row, col }">
<QTd>
<VnSelectFilter
v-model="row[col.model]"
:options="col.options"
option-value="id"
option-label="description"
:filter-options="['id', 'description']"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
{{ `${scope.opt.id}: ${scope.opt.description}` }}
</QItem>
</template>
</VnSelectFilter>
</QTd>
</template>
<template #body-cell-country="{ row, col }">
<QTd>
<VnSelectFilter
v-model="row[col.model]"
:options="col.options"
option-value="id"
option-label="code"
/>
</QTd>
</template>
<template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
<QCard>
<QCardSection>
<QCheckbox v-model="props.selected" dense />
</QCardSection>
<QSeparator />
<QList>
<QItem>
<VnSelectFilter
:label="t('code')"
class="full-width"
v-model="props.row['intrastatFk']"
:options="intrastats"
option-value="id"
option-label="description"
:filter-options="['id', 'description']"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
{{
`${scope.opt.id}: ${scope.opt.description}`
}}
</QItem>
</template>
</VnSelectFilter>
</QItem>
<QItem
v-for="(value, index) of [
'amount',
'net',
'stems',
]"
:key="index"
>
<QInput
:label="t(value)"
class="full-width"
v-model="props.row[value]"
clearable
clear-icon="close"
/>
</QItem>
<QItem>
<VnSelectFilter
:label="t('country')"
class="full-width"
v-model="props.row['countryFk']"
:options="countries"
option-value="id"
option-label="code"
/>
</QItem>
</QList>
</QCard>
</div>
</template>
</QTable>
</template>
</CrudModel>
</div>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn
color="primary"
icon="add"
size="lg"
round
@click="invoiceInFormRef.insert()"
/>
</QPageSticky>
</template>
<style lang="scss">
.invoiceIn-intrastat {
> .q-card {
.vn-label-value {
display: flex;
gap: 1em;
.label {
flex: 1;
}
.value {
flex: 0.5;
}
}
}
}
</style>
<style lang="scss" scoped></style>
<i18n>
en:
amount: Amount
net: Net
stems: Stems
country: Country
es:
Code: Código
amount: Cantidad
net: Neto
stems: Tallos
country: País
Total amount: Total importe
Total net: Total neto
Total stems: Total tallos
</i18n>

View File

@ -0,0 +1,409 @@
<script setup>
import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toCurrency, toDate } from 'src/filters';
import { getUrl } from 'src/composables/getUrl';
import { downloadFile } from 'src/composables/downloadFile';
import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue';
onMounted(async () => {
salixUrl.value = await getUrl('');
invoiceInUrl.value = salixUrl.value + `invoiceIn/${entityId.value}/`;
});
const route = useRoute();
const { t } = useI18n();
const $props = defineProps({
id: {
type: Number,
default: 0,
},
});
const entityId = computed(() => $props.id || route.params.id);
const salixUrl = ref();
const invoiceInUrl = ref();
const amountsNotMatch = ref(null);
const vatColumns = ref([
{
name: 'expense',
label: 'invoiceIn.summary.expense',
field: (row) => row.expenseFk,
sortable: true,
align: 'left',
},
{
name: 'landed',
label: 'invoiceIn.summary.taxableBase',
field: (row) => row.taxableBase,
format: (value) => toCurrency(value),
sortable: true,
align: 'left',
},
{
name: 'vat',
label: 'invoiceIn.summary.sageVat',
field: (row) => row.taxTypeSage?.vat,
format: (value) => value,
sortable: true,
align: 'left',
},
{
name: 'transaction',
label: 'invoiceIn.summary.sageTransaction',
field: (row) => row.transactionTypeSage?.transaction,
format: (value) => value,
sortable: true,
align: 'left',
},
{
name: 'rate',
label: 'invoiceIn.summary.rate',
field: (row) => row.taxRate,
format: (value) => value,
sortable: true,
align: 'left',
},
{
name: 'currency',
label: 'invoiceIn.summary.currency',
field: (row) => row.foreignValue,
format: (value) => toCurrency(value),
sortable: true,
align: 'left',
},
]);
const dueDayColumns = ref([
{
name: 'date',
label: 'invoiceIn.summary.dueDay',
field: (row) => toDate(row.dueDated),
sortable: true,
align: 'left',
},
{
name: 'bank',
label: 'invoiceIn.summary.bank',
field: (row) => row.bank.bank,
sortable: true,
align: 'left',
},
{
name: 'amount',
label: 'invoiceIn.summary.amount',
field: (row) => row.amount,
format: (value) => toCurrency(value),
sortable: true,
align: 'left',
},
{
name: 'landed',
label: 'invoiceIn.summary.foreignValue',
field: (row) => row.foreignValue,
format: (value) => value,
sortable: true,
align: 'left',
},
]);
const intrastatColumns = ref([
{
name: 'code',
label: 'invoiceIn.summary.code',
jorgep marked this conversation as resolved
Review

({intrastat}) en vez de row

({intrastat}) en vez de row
field: (row) => {
return `${row.intrastat.id}: ${row.intrastat?.description}`;
},
sortable: true,
align: 'left',
},
{
name: 'amount',
label: 'invoiceIn.summary.amount',
field: (row) => toCurrency(row.amount),
sortable: true,
align: 'left',
},
{
name: 'net',
label: 'invoiceIn.summary.net',
field: (row) => row.net,
sortable: true,
align: 'left',
},
{
name: 'amount',
label: 'invoiceIn.summary.stems',
field: (row) => row.stems,
format: (value) => value,
sortable: true,
align: 'left',
},
{
name: 'landed',
label: 'invoiceIn.summary.country',
field: (row) => row.country?.code,
format: (value) => value,
sortable: true,
align: 'left',
},
]);
function setAmountNotMatch(entity) {
if (!entity) return false;
const total = entity.totals;
amountsNotMatch.value =
total.totalDueDay != total.totalTaxableBase &&
total.totalDueDay != total.totalVat;
}
</script>
<template>
<CardSummary
ref="summary"
:url="`InvoiceIns/${entityId}/summary`"
@on-fetch="(data) => setAmountNotMatch(data)"
>
<template #header="{ entity: invoiceIn }">
<div>{{ invoiceIn.id }} - {{ invoiceIn.supplier.name }}</div>
</template>
<template #body="{ entity: invoiceIn }">
<QCard class="vn-one">
<QCardSection class="q-pa-none">
<a class="header" :href="`#/invoice-in/${entityId}/basic-data`">
{{ t('invoiceIn.pageTitles.basicData') }}
<QIcon name="open_in_new" color="primary" />
</a>
<QBtn
class="q-ml-sm"
padding="xs"
flat
color="primary"
round
jorgep marked this conversation as resolved Outdated

Revisamos si añadiendo valores por defecto podemos quitar líneas

Revisamos si añadiendo valores por defecto podemos quitar líneas
icon="cloud_download"
@click.stop="downloadFile(invoiceIn.dmsFk)"
>
<QTooltip>
{{ t('components.smartCard.downloadFile') }}
</QTooltip>
</QBtn>
</QCardSection>
<VnLv
:label="t('invoiceIn.summary.supplier')"
:value="invoiceIn.supplier.name"
/>
<VnLv
:label="t('invoiceIn.summary.supplierRef')"
:value="invoiceIn.supplierRef"
/>
<VnLv
:label="t('invoiceIn.summary.currency')"
:value="invoiceIn.currency.code"
/>
<VnLv
jorgep marked this conversation as resolved
Review

4 llamadas al mismo formato de URL, podríamos tenerlo en un método con un argumento?

4 llamadas al mismo formato de URL, podríamos tenerlo en un método con un argumento?
:label="t('invoiceIn.summary.docNumber')"
:value="`${invoiceIn.serial}/${invoiceIn.serialNumber}`"
/>
</QCard>
<QCard class="vn-one">
<QCardSection class="q-pa-none">
<a class="header" :href="`#/invoice-in/${entityId}/basic-data`">
{{ t('invoiceIn.pageTitles.basicData') }}
<QIcon name="open_in_new" color="primary" />
</a>
<QBtn
class="q-ml-sm"
padding="xs"
flat
color="primary"
round
icon="cloud_download"
@click.stop="downloadFile(invoiceIn.dmsFk)"
>
<QTooltip>
{{ t('components.smartCard.downloadFile') }}
</QTooltip>
</QBtn>
</QCardSection>
<VnLv
:label="t('invoiceIn.summary.issued')"
:value="toDate(invoiceIn.issued)"
/>
<VnLv
:label="t('invoiceIn.summary.operated')"
:value="toDate(invoiceIn.operated)"
/>
<VnLv
:label="t('invoiceIn.summary.bookEntried')"
:value="toDate(invoiceIn.bookEntried)"
/>
<VnLv
:label="t('invoiceIn.summary.bookedDate')"
:value="toDate(invoiceIn.booked)"
/>
</QCard>
<QCard class="vn-one">
<QCardSection class="q-pa-none">
<a class="header" :href="`#/invoice-in/${entityId}/basic-data`">
{{ t('invoiceIn.pageTitles.basicData') }}
<QIcon name="open_in_new" color="primary" />
</a>
<QBtn
class="q-ml-sm"
padding="xs"
flat
color="primary"
round
icon="cloud_download"
@click.stop="downloadFile(invoiceIn.dmsFk)"
>
<QTooltip>
{{ t('components.smartCard.downloadFile') }}
</QTooltip>
</QBtn>
</QCardSection>
<QCardSection class="q-pa-none">
<div class="bordered q-px-sm">
<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>
</QCardSection>
</QCard>
<QCard class="q-mb-md">
<QCardSection class="q-pa-none">
<a class="header" :href="`#/invoice-in/${entityId}/basic-data`">
{{ t('invoiceIn.pageTitles.basicData') }}
<QIcon name="open_in_new" color="primary" />
</a>
<QBtn
class="q-ml-sm"
padding="xs"
flat
color="primary"
round
icon="cloud_download"
@click.stop="downloadFile(invoiceIn.dmsFk)"
>
<QTooltip>
{{ t('components.smartCard.downloadFile') }}
</QTooltip>
</QBtn>
</QCardSection>
<VnLv
:label="t('invoiceIn.summary.sage')"
:value="invoiceIn.sageWithholding.withholding"
/>
<VnLv
:label="t('invoiceIn.summary.vat')"
:value="invoiceIn.expenseDeductible?.name"
/>
<VnLv
:label="t('invoiceIn.summary.company')"
:value="invoiceIn.company.code"
/>
<VnLv
:label="t('invoiceIn.summary.booked')"
:value="invoiceIn.isBooked"
/>
</QCard>
<QCard v-if="invoiceIn.invoiceInTax.length" class="vn-three">
<a class="header">
{{ t('invoiceIn.card.vat') }}
</a>
<QTable
:columns="vatColumns"
:rows="invoiceIn.invoiceInTax"
flat
hide-pagination
>
<template #header="props">
<QTr :props="props">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }}
</QTh>
</QTr>
</template>
</QTable>
</QCard>
<QCard v-if="invoiceIn.invoiceInDueDay.length" class="vn-two">
<div class="header">
{{ t('invoiceIn.card.dueDay') }}
</div>
<QTable
:columns="dueDayColumns"
:rows="invoiceIn.invoiceInDueDay"
flat
hide-pagination
>
<template #header="props">
<QTr :props="props">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }}
</QTh>
</QTr>
</template>
</QTable>
</QCard>
<QCard v-if="invoiceIn.invoiceInIntrastat.length">
<div class="header">
{{ t('invoiceIn.card.intrastat') }}
</div>
<QTable
:columns="intrastatColumns"
:rows="invoiceIn.invoiceInIntrastat"
flat
hide-pagination
>
<template #header="props">
<QTr :props="props">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }}
</QTh>
</QTr>
</template>
</QTable>
</QCard>
</template>
</CardSummary>
</template>
<style lang="scss" scoped>
.bordered {
border: 1px solid var(--vn-text);
width: 16em;
}
</style>
<i18n>
es:
Search invoice: Buscar factura emitida
You can search by invoice reference: Puedes buscar por referencia de la factura
</i18n>

View File

@ -0,0 +1,29 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import InvoiceInSummary from './InvoiceInSummary.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
</script>
<template>
<QDialog ref="dialogRef" @hide="onDialogHide">
<InvoiceInSummary v-if="$props.id" :id="$props.id" />
</QDialog>
</template>
<style lang="scss">
.q-dialog .summary .header {
position: sticky;
z-index: $z-max;
top: 0;
}
</style>

View File

@ -0,0 +1,500 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import axios from 'axios';
import { useArrayData } from 'src/composables/useArrayData';
import { toCurrency } from 'src/filters';
import FetchData from 'src/components/FetchData.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import CrudModel from 'src/components/CrudModel.vue';
const route = useRoute();
const { t } = useI18n();
const quasar = useQuasar();
const arrayData = useArrayData('InvoiceIn');
const invoiceIn = computed(() => arrayData.store.data);
const expenses = ref([]);
const sageTaxTypes = ref([]);
const sageTransactionTypes = ref([]);
const rowsSelected = ref([]);
const newExpense = ref({
code: undefined,
isWithheld: false,
description: undefined,
});
const invoiceInFormRef = ref();
const expensesRef = ref();
const newExpenseRef = ref();
const columns = computed(() => [
{
name: 'expense',
label: t('Expense'),
field: (row) => row.expenseFk,
options: expenses.value,
model: 'expenseFk',
optionValue: 'id',
optionLabel: 'id',
sortable: true,
tabIndex: 1,
align: 'left',
},
{
name: 'taxablebase',
label: t('Taxable base'),
field: (row) => toCurrency(row.taxableBase),
model: 'taxableBase',
sortable: true,
tabIndex: 2,
align: 'left',
},
{
name: 'sageiva',
label: t('Sage iva'),
field: (row) => row.taxTypeSageFk,
options: sageTaxTypes.value,
model: 'taxTypeSageFk',
optionValue: 'id',
optionLabel: 'vat',
sortable: true,
tabindex: 3,
align: 'left',
},
{
name: 'sagetransaction',
label: t('Sage transaction'),
field: (row) => row.transactionTypeSageFk,
options: sageTransactionTypes.value,
model: 'transactionTypeSageFk',
optionValue: 'id',
optionLabel: 'transaction',
sortable: true,
tabIndex: 4,
align: 'left',
},
{
name: 'rate',
label: t('Rate'),
sortable: true,
tabIndex: 5,
field: (row) => taxRate(row, row.taxTypeSageFk),
align: 'left',
},
{
name: 'foreignvalue',
label: t('Foreign value'),
sortable: true,
tabIndex: 6,
field: (row) => row.foreignValue,
align: 'left',
},
]);
const filter = {
fields: [
'id',
'invoiceInFk',
'taxableBase',
'expenseFk',
'foreignValue',
'taxTypeSageFk',
'transactionTypeSageFk',
],
where: {
invoiceInFk: route.params.id,
},
};
const isNotEuro = (code) => code != 'EUR';
function taxRate(invoiceInTax, sageTaxTypeId) {
jorgep marked this conversation as resolved Outdated

Lo revisamos

Lo revisamos
const taxRateSelection = sageTaxTypes.value.find(
(transaction) => transaction.id == sageTaxTypeId
);
const taxTypeSage = taxRateSelection && taxRateSelection.rate;
const taxableBase = invoiceInTax && invoiceInTax.taxableBase;
if (taxTypeSage && taxableBase) {
return toCurrency((taxTypeSage / 100) * taxableBase);
}
return toCurrency(0);
}
async function addExpense() {
try {
if (!newExpense.value.code) throw new Error(t(`The code can't be empty`));
if (isNaN(newExpense.value.code))
throw new Error(t(`The code have to be a number`));
if (!newExpense.value.description)
throw new Error(t(`The description can't be empty`));
const data = [
{
id: newExpense.value.code,
isWithheld: newExpense.value.isWithheld,
name: newExpense.value.description,
},
];
await axios.post(`Expenses`, data);
await expensesRef.value.fetch();
quasar.notify({
type: 'positive',
message: t('globals.dataSaved'),
});
} catch (error) {
quasar.notify({
type: 'negative',
message: t(`${error}`),
});
}
}
</script>
<template>
<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)"
/>
<CrudModel
ref="invoiceInFormRef"
v-if="invoiceIn"
data-key="InvoiceInTaxes"
url="InvoiceInTaxes"
:filter="filter"
:data-required="{ invoiceInFk: route.params.id }"
auto-load
v-model:selected="rowsSelected"
>
<template #body="{ rows }">
<QTable
v-model:selected="rowsSelected"
selection="multiple"
:columns="columns"
:rows="rows"
row-key="$index"
hide-pagination
:grid="$q.screen.lt.sm"
:pagination="{ rowsPerPage: 0 }"
>
<template #body-cell-expense="{ row, col }">
<QTd auto-width>
<VnSelectFilter
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:filter-options="['id', 'name']"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
{{ `${scope.opt.id}: ${scope.opt.name}` }}
</QItem>
</template>
<template #append>
<QIcon
name="close"
@click.stop="value = null"
class="cursor-pointer"
/>
<QBtn
padding="xs"
round
flat
icon="add_circle"
@click.stop="newExpenseRef.show()"
>
<QTooltip>
{{ t('Create expense') }}
</QTooltip>
</QBtn>
</template>
</VnSelectFilter>
</QTd>
</template>
<template #body-cell-taxablebase="{ row }">
<QTd>
<QInput
:class="{
'no-pointer-events': isNotEuro(invoiceIn.currency.code),
}"
:disable="isNotEuro(invoiceIn.currency.code)"
label=""
clear-icon="close"
v-model="row.taxableBase"
clearable
>
<template #prepend>
<QIcon name="euro" size="xs" flat />
</template>
</QInput>
</QTd>
</template>
<template #body-cell-sageiva="{ row, col }">
<QTd>
<VnSelectFilter
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:filter-options="['id', 'vat']"
:autofocus="col.tabIndex == 1"
input-debounce="0"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt.vat }}</QItemLabel>
<QItemLabel>
{{ `#${scope.opt.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QTd>
</template>
<template #body-cell-sagetransaction="{ row, col }">
<QTd>
<VnSelectFilter
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:filter-options="['id', 'transaction']"
:autofocus="col.tabIndex == 1"
input-debounce="0"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
scope.opt.transaction
}}</QItemLabel>
<QItemLabel>
{{ `#${scope.opt.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QTd>
</template>
<template #body-cell-foreignvalue="{ row }">
<QTd>
<QInput
:class="{
'no-pointer-events': !isNotEuro(invoiceIn.currency.code),
}"
:disable="!isNotEuro(invoiceIn.currency.code)"
v-model="row.foreignValue"
/>
</QTd>
</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">
<QCardSection>
<QCheckbox v-model="props.selected" dense />
</QCardSection>
<QSeparator />
<QList>
<QItem>
<VnSelectFilter
:label="t('Expense')"
class="full-width"
v-model="props.row['expenseFk']"
:options="expenses"
option-value="id"
option-label="name"
:filter-options="['id', 'name']"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
{{ `${scope.opt.id}: ${scope.opt.name}` }}
</QItem>
</template>
</VnSelectFilter>
</QItem>
<QItem>
<QInput
:label="t('Taxable base')"
:class="{
'no-pointer-events': isNotEuro(
invoiceIn.currency.code
jorgep marked this conversation as resolved
Review

En InvoiceInDueDay, la condición estaba al revés, correcto?

En InvoiceInDueDay, la condición estaba al revés, correcto?
),
}"
class="full-width"
:disable="isNotEuro(invoiceIn.currency.code)"
clear-icon="close"
v-model="props.row.taxableBase"
clearable
>
<template #append>
<QIcon name="euro" size="xs" flat />
</template>
</QInput>
</QItem>
<QItem>
<VnSelectFilter
:label="t('Sage iva')"
class="full-width"
v-model="props.row['taxTypeSageFk']"
:options="sageTaxTypes"
option-value="id"
option-label="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>
</VnSelectFilter>
</QItem>
<QItem>
<VnSelectFilter
class="full-width"
v-model="props.row['transactionTypeSageFk']"
:options="sageTransactionTypes"
option-value="id"
option-label="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>
</VnSelectFilter>
</QItem>
<QItem>
{{ taxRate(props.row, props.row.taxTypeSageFk) }}
</QItem>
<QItem>
<QInput
:label="t('Foreign value')"
class="full-width"
:class="{
'no-pointer-events': !isNotEuro(
invoiceIn.currency.code
),
}"
:disable="!isNotEuro(invoiceIn.currency.code)"
v-model="props.row.foreignValue"
/>
</QItem>
</QList>
</QCard>
</div>
</template>
</QTable>
</template>
</CrudModel>
<QDialog ref="newExpenseRef">
<QCard>
<QCardSection class="q-pb-none">
<QItem class="q-pa-none">
<span class="text-primary text-h6 full-width">
<QIcon name="edit" class="q-mr-xs" />
{{ t('New expense') }}
</span>
<QBtn icon="close" flat round dense v-close-popup />
</QItem>
</QCardSection>
<QCardSection class="q-pt-none">
<QItem>
<QInput :label="`${t('Code')}*`" v-model="newExpense.code" />
<QCheckbox
dense
size="sm"
:label="`${t('It\'s a withholding')}`"
v-model="newExpense.isWithheld"
/>
</QItem>
<QItem>
<QInput
:label="`${t('Descripction')}*`"
v-model="newExpense.description"
/>
</QItem>
</QCardSection>
<QCardActions class="justify-end">
<QBtn flat :label="t('globals.close')" color="primary" v-close-popup />
<QBtn :label="t('globals.save')" color="primary" @click="addExpense" />
</QCardActions>
</QCard>
</QDialog>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn
color="primary"
icon="add"
size="lg"
round
@click="invoiceInFormRef.insert()"
/>
</QPageSticky>
</template>
<style lang="scss" scoped>
@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;
}
</style>
<i18n>
es:
Expense: Gasto
Create expense: Crear gasto
Add tax: Crear gasto
Taxable base: Base imp.
Sage tax: Sage iva
Sage transaction: Sage transacción
Rate: Tasa
Foreign value: Divisa
New expense: Nuevo gasto
Code: Código
It's a withholding: Es una retención
Descripction: Descripción
The code can't be empty: El código no puede estar vacío
The description can't be empty: La descripción no puede estar vacía
The code have to be a number: El código debe ser un número.
</i18n>

View File

@ -0,0 +1,307 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import FetchData from 'components/FetchData.vue';
const { t } = useI18n();
const props = 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)"
/>
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params, searchFn }">
<QList dense>
<QItem>
<QItemSection>
<QInput :label="t('Id or Supplier')" v-model="params.search">
<template #prepend>
<QIcon name="badge" size="sm"></QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput
:label="t('params.supplierRef')"
v-model="params.supplierRef"
@input.
lazy-rules
>
<template #prepend>
<QIcon name="vn:client" size="sm"></QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelectFilter
:label="t('params.supplierFk')"
v-model="params.supplierFk"
:options="suppliers"
option-value="id"
option-label="nickname"
@input-value="suppliersRef.fetch()"
>
</VnSelectFilter>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput :label="t('params.fi')" v-model="params.fi" lazy-rules>
<template #prepend>
<QIcon name="badge" size="sm"></QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput
:label="t('params.serialNumber')"
v-model="params.serialNumber"
lazy-rules
>
<template #prepend>
<QIcon name="badge" size="sm"></QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput
:label="t('params.serial')"
v-model="params.serial"
lazy-rules
>
<template #prepend>
<QIcon name="badge" size="sm"></QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput :label="t('Amount')" v-model="params.amount" lazy-rules>
<template #prepend>
<QIcon name="euro" size="sm"></QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
<QItem class="q-mb-md">
<QItemSection>
<QCheckbox
:label="t('params.isBooked')"
v-model="params.isBooked"
@update:model-value="searchFn()"
toggle-indeterminate
/>
</QItemSection>
</QItem>
<QExpansionItem :label="t('More options')" expand-separator>
<QItem>
<QItemSection>
<QInput
:label="t('params.awb')"
v-model="params.awbCode"
lazy-rules
>
<template #prepend>
<QIcon name="badge" size="sm"></QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput
:label="t('params.account')"
v-model="params.account"
lazy-rules
>
<template #prepend>
<QIcon name="person" size="sm" />
</template>
</QInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput :label="t('From')" v-model="params.from" mask="date">
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate v-model="params.from" landscape>
<div
class="row items-center justify-end q-gutter-sm"
>
<QBtn
:label="t('globals.cancel')"
color="primary"
flat
v-close-popup
/>
<QBtn
:label="t('globals.confirm')"
color="primary"
flat
@click="save"
v-close-popup
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput :label="t('To')" v-model="params.to" mask="date">
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate v-model="params.to" landscape>
<div
class="row items-center justify-end q-gutter-sm"
>
<QBtn
:label="t('globals.cancel')"
color="primary"
flat
v-close-popup
/>
<QBtn
:label="t('globals.confirm')"
color="primary"
flat
@click="save"
v-close-popup
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput
:label="t('Issued')"
v-model="params.issued"
mask="date"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate v-model="params.issued" landscape>
<div
class="row items-center justify-end q-gutter-sm"
>
<QBtn
:label="t('globals.cancel')"
color="primary"
flat
v-close-popup
/>
<QBtn
:label="t('globals.confirm')"
color="primary"
flat
@click="save"
v-close-popup
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
</QExpansionItem>
</QList>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
search: ID
supplierRef: Supplier ref.
supplierFk: Supplier
fi: Supplier fiscal id
clientFk: Customer
amount: Amount
created: Created
awb: AWB
dued: Dued
serialNumber: Serial Number
serial: Serial
account: Account
isBooked: is booked
es:
params:
search: Contiene
supplierRef: Ref. proveedor
supplierFk: Proveedor
clientFk: Cliente
fi: CIF proveedor
serialNumber: Num. serie
serial: Serie
awb: AWB
amount: Importe
issued: Emitida
isBooked: Conciliada
account: Cuenta
created: Creada
dued: Vencida
From: Desde
To: Hasta
Amount: Importe
Issued: Fecha factura
Id or supplier: Id o proveedor
More options: Más opciones
</i18n>

View File

@ -0,0 +1,179 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { downloadFile } from 'src/composables/downloadFile';
import { toDate, toCurrency } from 'src/filters/index';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
import InvoiceInFilter from './InvoiceInFilter.vue';
import InvoiceInSummaryDialog from './Card/InvoiceInSummaryDialog.vue';
import { getUrl } from 'src/composables/getUrl';
const stateStore = useStateStore();
const router = useRouter();
const quasar = useQuasar();
let url = ref();
const { t } = useI18n();
onMounted(async () => {
stateStore.rightDrawer = true;
url.value = await getUrl('');
});
onUnmounted(() => (stateStore.rightDrawer = false));
function navigate(id) {
router.push({ path: `/invoice-in/${id}` });
}
function viewSummary(id) {
quasar.dialog({
component: InvoiceInSummaryDialog,
componentProps: {
id,
},
});
}
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="InvoiceInList"
:label="t('Search invoice')"
:info="t('You can search by invoice reference')"
/>
</Teleport>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
</template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<InvoiceInFilter data-key="InvoiceInList" />
</QScrollArea>
</QDrawer>
<QPage class="column items-center q-pa-md">
<div class="card-list">
<VnPaginate
data-key="InvoiceInList"
url="InvoiceIns/filter"
order="issued DESC, id DESC"
auto-load
>
<template #body="{ rows }">
<CardList
v-for="row of rows"
:key="row.id"
:title="row.supplierRef"
@click="navigate(row.id)"
>
<template #list-items>
<VnLv label="ID" :value="row.id" />
<VnLv
:label="t('invoiceIn.list.supplierRef')"
:value="row.supplierRef"
/>
<VnLv
:label="t('invoiceIn.list.supplier')"
:value="row.supplierName"
/>
<VnLv
:label="t('invoiceIn.list.serialNumber')"
:value="row.serialNumber"
/>
<VnLv
:label="t('invoiceIn.list.serial')"
:value="row.serial"
/>
<VnLv
:label="t('invoiceIn.list.issued')"
:value="toDate(row.issued)"
/>
<VnLv :label="t('invoiceIn.list.awb')" :value="row.awbCode" />
<VnLv
:label="t('invoiceIn.list.amount')"
:value="toCurrency(row.amount)"
/>
<VnLv :label="t('invoiceIn.list.isBooked')">
<template #value>
<QCheckbox
class="no-pointer-events"
v-model="row.isBooked"
size="xs"
:true-value="1"
:false-value="0"
/>
</template>
</VnLv>
</template>
<template #actions>
<QBtn
flat
icon="arrow_circle_right"
@click.stop="navigate(row.id)"
>
<QTooltip>
{{ t('components.smartCard.openCard') }}
</QTooltip>
</QBtn>
<QBtn flat icon="preview" @click.stop="viewSummary(row.id)">
<QTooltip>
{{ t('components.smartCard.openSummary') }}
</QTooltip>
</QBtn>
<QBtn
flat
icon="cloud_download"
@click.stop="downloadFile(row.dmsFk)"
>
<QTooltip>
{{ t('components.smartCard.downloadFile') }}
</QTooltip>
</QBtn>
</template>
</CardList>
</template>
</VnPaginate>
</div>
</QPage>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn
color="primary"
icon="add"
size="lg"
round
:href="`${url}invoice-in/create`"
/>
</QPageSticky>
</template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
</style>
<i18n>
es:
Search invoice: Buscar factura emitida
You can search by invoice reference: Puedes buscar por referencia de la factura
</i18n>

View File

@ -0,0 +1,17 @@
<script setup>
import { useStateStore } from 'stores/useStateStore';
import LeftMenu from 'src/components/LeftMenu.vue';
const stateStore = useStateStore();
</script>
<template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit text-grey-8">
<LeftMenu />
</QScrollArea>
</QDrawer>
<QPageContainer>
<RouterView></RouterView>
</QPageContainer>
</template>

View File

@ -2,16 +2,9 @@ import Customer from './customer';
import Ticket from './ticket';
import Claim from './claim';
import InvoiceOut from './invoiceOut';
import invoiceIn from './invoiceIn';
import Worker from './worker';
import Wagon from './wagon';
import Route from './route';
export default [
Customer,
Ticket,
Claim,
InvoiceOut,
Worker,
Wagon,
Route
]
export default [Customer, Ticket, Claim, InvoiceOut, invoiceIn, Worker, Wagon, Route];

View File

@ -0,0 +1,98 @@
import { RouterView } from 'vue-router';
export default {
path: '/invoice-in',
name: 'InvoiceIn',
meta: {
title: 'invoiceIns',
icon: 'vn:invoice-in',
},
component: RouterView,
redirect: { name: 'InvoiceInMain' },
menus: {
main: ['InvoiceInList'],
card: [
'InvoiceInBasicData',
'InvoiceInVat',
'InvoiceInDueDay',
'InvoiceInIntrastat',
],
},
children: [
{
path: '',
name: 'InvoiceInMain',
component: () => import('src/pages/InvoiceIn/InvoiceInMain.vue'),
redirect: { name: 'InvoiceInList' },
children: [
{
path: 'list',
name: 'InvoiceInList',
meta: {
title: 'list',
icon: 'view_list',
},
component: () => import('src/pages/InvoiceIn/InvoiceInList.vue'),
},
],
},
{
name: 'InvoiceInCard',
path: ':id',
component: () => import('src/pages/InvoiceIn/Card/InvoiceInCard.vue'),
redirect: { name: 'InvoiceInSummary' },
children: [
{
name: 'InvoiceInSummary',
path: 'summary',
meta: {
title: 'summary',
icon: 'view_list',
},
component: () =>
import('src/pages/InvoiceIn/Card/InvoiceInSummary.vue'),
},
{
name: 'InvoiceInBasicData',
path: 'basic-data',
meta: {
title: 'basicData',
icon: 'vn:settings',
roles: ['salesPerson'],
},
component: () =>
import('src/pages/InvoiceIn/Card/InvoiceInBasicData.vue'),
},
{
name: 'InvoiceInVat',
path: 'vat',
meta: {
title: 'vat',
icon: 'vn:tax',
},
component: () => import('src/pages/InvoiceIn/Card/InvoiceInVat.vue'),
},
{
name: 'InvoiceInDueDay',
path: 'due-day',
meta: {
title: 'dueDay',
icon: 'vn:calendar',
},
component: () =>
import('src/pages/InvoiceIn/Card/InvoiceInDueDay.vue'),
},
{
name: 'InvoiceInIntrastat',
path: 'intrastat',
meta: {
title: 'intrastat',
icon: 'vn:lines',
},
component: () =>
import('src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue'),
},
],
},
],
};

View File

@ -3,6 +3,7 @@ import ticket from './modules/ticket';
import claim from './modules/claim';
import worker from './modules/worker';
import invoiceOut from './modules/invoiceOut';
import invoiceIn from './modules/invoiceIn';
import wagon from './modules/wagon';
import route from './modules/route';
@ -49,6 +50,7 @@ const routes = [
claim,
worker,
invoiceOut,
invoiceIn,
{
path: '/:catchAll(.*)*',
name: 'NotFound',

View File

@ -6,7 +6,16 @@ import { useRole } from 'src/composables/useRole';
import routes from 'src/router/modules';
export const useNavigationStore = defineStore('navigationStore', () => {
const modules = ['customer', 'claim', 'ticket', 'invoiceOut', 'worker', 'wagon', 'route'];
const modules = [
'customer',
'claim',
'ticket',
'invoiceOut',
'invoiceIn',
'worker',
'wagon',
'route',
];
const pinnedModules = ref([]);
const role = useRole();

View File

@ -0,0 +1,27 @@
/// <reference types="cypress" />
describe('InvoiceInList', () => {
const firstCard = '.q-card:nth-child(1)';
const firstId =
'.q-card:nth-child(1) .list-items > .vn-label-value:first-child > .value > span';
const firstDetailBtn = '.q-card:nth-child(1) .q-btn:nth-child(2)';
const summaryHeaders = '.summaryBody .header';
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/invoice-in`);
});
it('should redirect on clicking a invoice', () => {
cy.get(firstId)
.invoke('text')
.then((id) => {
cy.get(firstCard).click();
cy.url().should('include', `/invoice-in/${id}/summary`);
});
});
it('should open the details', () => {
cy.get(firstDetailBtn).click();
cy.get(summaryHeaders).eq(1).contains('Basic Data');
});
});

View File

@ -0,0 +1,25 @@
import { vi, describe, expect, it } from 'vitest';
import { axios } from 'app/test/vitest/helper';
import { downloadFile } from 'src/composables/downloadFile';
import { useSession } from 'src/composables/useSession';
const session = useSession();
const token = session.getToken();
describe('downloadFile', () => {
it('should open a new window to download the file', async () => {
const url = 'http://localhost:9000';
vi.spyOn(axios, 'get').mockResolvedValueOnce({ data: url });
const mockWindowOpen = vi.spyOn(window, 'open');
await downloadFile(1);
expect(mockWindowOpen).toHaveBeenCalledWith(
`${url}/api/dms/1/downloadFile?access_token=${token}`
);
mockWindowOpen.mockRestore();
});
});