refs #5835 migrateInvoiceIn #110

Merged
jorgep merged 29 commits from 5835-migrateInvoiceIn into dev 2023-12-13 10:25:07 +00:00
50 changed files with 3867 additions and 48 deletions

View File

@ -8,7 +8,7 @@ module.exports = defineConfig({
supportFile: 'test/cypress/support/index.js',
videosFolder: 'test/cypress/videos',
video: false,
specPattern: 'test/cypress/integration/*.spec.js',
specPattern: 'test/cypress/integration/**/*.spec.js',
experimentalRunAllSpecs: true,
component: {
componentFolder: 'src',

View File

@ -136,6 +136,10 @@ async function saveChanges(data) {
hasChanges.value = false;
isLoading.value = false;
emit('saveChanges', data);
quasar.notify({
type: 'positive',
message: t('globals.dataSaved'),
});
}
async function insert() {

View File

@ -24,12 +24,13 @@ const address = ref(props.data.address);
const isLoading = ref(false);
async function confirm() {
const response = { address };
const response = { address: address.value };
if (props.promise) {
isLoading.value = true;
const { address: _address, ...restData } = props.data;
try {
Object.assign(response, props.data);
Object.assign(response, restData);
await props.promise(response);
} finally {
isLoading.value = false;

View File

@ -15,6 +15,10 @@ const $props = defineProps({
type: String,
default: '',
},
filterOptions: {
type: Array,
default: () => [],
},
isClearable: {
type: Boolean,
default: true,
@ -32,18 +36,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);
});
};
@ -85,6 +93,7 @@ const value = computed({
map-options
use-input
@filter="filterHandler"
hide-selected
fill-input
ref="vnSelectRef"
>
@ -92,7 +101,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

@ -40,6 +40,10 @@ const quasar = useQuasar();
const slots = useSlots();
const { t } = useI18n();
const entity = computed(() => useArrayData($props.dataKey).store.data);
defineExpose({
getData,
});
onMounted(async () => {
await getData();
watch(

View File

@ -82,11 +82,13 @@ watch(props, async () => {
.summaryBody {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
gap: 15px;
padding: 15px;
> .q-card.vn-one {
width: 350px;
jorgep marked this conversation as resolved
Review

tamaño fijo? Es correcto?

tamaño fijo? Es correcto?
Review

Según hablé con Juan sí, lo ideal es que tengan el mismo tamaño.

Según hablé con Juan sí, lo ideal es que tengan el mismo tamaño.
flex: 1;
}
> .q-card.vn-two {
@ -123,7 +125,6 @@ watch(props, async () => {
width: max-content;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.header {

View File

@ -4,9 +4,10 @@ import { dashIfEmpty } from 'src/filters';
const $props = defineProps({
label: { type: String, default: null },
value: { type: [Number, String, Boolean], default: null },
titleLabel: { type: String, default: null },
titleValue: { type: [Number, String, Boolean], default: null },
value: {
type: [String, Boolean],
default: null,
},
info: { type: String, default: null },
dash: { type: Boolean, default: true },
});
@ -16,7 +17,7 @@ const isBooleanValue = computed(() => typeof $props.value === 'boolean');
<div class="vn-label-value">
<div v-if="$props.label || $slots.label" class="label">
<slot name="label">
<span :title="$props.titleLabel ?? $props.label">{{ $props.label }}</span>
<span>{{ $props.label }}</span>
</slot>
</div>
<div class="value">

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

@ -33,6 +33,7 @@ body.body--light {
--vn-gray: #f5f5f5;
--vn-label: #5f5f5f;
--vn-dark: white;
--vn-light-gray: #e7e3e3;
}
body.body--dark {
@ -40,6 +41,7 @@ body.body--dark {
--vn-gray: #313131;
--vn-label: #a8a8a8;
--vn-dark: #292929;
--vn-light-gray: #424242;
}
.bg-vn-dark {

View File

@ -9,13 +9,10 @@ export default function (value, symbol = 'EUR', fractionSize = 2) {
style: 'currency',
currency: symbol,
minimumFractionDigits: fractionSize,
maximumFractionDigits: fractionSize
maximumFractionDigits: fractionSize,
};
const lang = locale.value == 'es' ? 'de' : locale.value;
return new Intl.NumberFormat(lang, options)
.format(value);
return new Intl.NumberFormat(lang, options).format(value);
}

View File

@ -357,7 +357,7 @@ export default {
},
invoiceOut: {
pageTitles: {
invoiceOuts: 'Invoices Out',
invoiceOuts: 'Create invoice',
list: 'List',
createInvoiceOut: 'Create invoice out',
summary: 'Summary',
@ -401,6 +401,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',
@ -526,6 +591,7 @@ export default {
openCard: 'View card',
openSummary: 'Open summary',
viewDescription: 'View description',
downloadFile: 'Download file',
},
cardDescriptor: {
mainList: 'Main list',

View File

@ -357,7 +357,7 @@ export default {
},
invoiceOut: {
pageTitles: {
invoiceOuts: 'Fact. emitidas',
invoiceOuts: 'Crear factura',
list: 'Listado',
createInvoiceOut: 'Crear fact. emitida',
summary: 'Resumen',
@ -401,6 +401,69 @@ export default {
totalWithVat: 'Importe',
},
},
invoiceIn: {
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
pageTitles: {
invoiceIns: 'Fact. recibidas',
list: 'Listado',
createInvoiceIn: 'Crear fact. recibida',
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
summary: 'Resumen',
basicData: 'Datos básicos',
vat: 'IVA',
dueDay: 'Vencimiento',
intrastat: 'Intrastat',
log: 'Registros de auditoría',
},
list: {
ref: 'Referencia',
supplier: 'Proveedor',
supplierRef: 'Ref. proveedor',
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: 'Iva',
dueDay: 'Fecha de vencimiento',
},
summary: {
supplier: 'Proveedor',
supplierRef: 'Ref. proveedor',
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',
@ -526,6 +589,7 @@ export default {
openCard: 'Ver ficha',
openSummary: 'Abrir detalles',
viewDescription: 'Ver descripción',
downloadFile: 'Descargar archivo',
},
cardDescriptor: {
mainList: 'Listado principal',

View File

@ -37,7 +37,7 @@ function confirmPickupOrder() {
data: {
address: customer.email,
},
send: sendPickupOrder,
promise: sendPickupOrder,
},
});
}

View File

@ -0,0 +1,710 @@
<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();
const requiredFieldRule = (val) => val || t('Required field');
const dateMask = '####-##-##';
const fillMask = '_';
async function checkFileExists(dmsId) {
if (!dmsId) return;
try {
await axios.get(`Dms/${dmsId}`, { fields: ['id'] });
editDownloadDisabled.value = false;
} catch (e) {
editDownloadDisabled.value = true;
}
}
jorgep marked this conversation as resolved
Review

lo miramos

lo miramos
async function setEditDms(dmsId) {
const { data } = await axios.get(`Dms/${dmsId}`);
dms.value = {
warehouseId: data.warehouseFk,
companyId: data.companyFk,
dmsTypeId: data.dmsTypeFk,
...data,
};
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 upsert() {
try {
const isEdit = !!dms.value.id;
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
const errors = {
companyId: `The company can't be empty`,
warehouseId: `The warehouse can't be empty`,
dmsTypeId: `The DMS Type can't be empty`,
description: `The description can't be empty`,
};
Object.keys(errors).forEach((key) => {
if (!dms.value[key]) throw Error(t(errors[key]));
});
if (!isEdit && !dms.value.files) throw 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 url = isEdit ? `dms/${dms.value.id}/updateFile` : 'Dms/uploadFile';
const { data } = await axios.post(url, formData, {
params: dms.value,
});
if (data.length) invoiceIn.value.dmsFk = data[0].id;
if (!isEdit) {
createDmsRef.value.hide();
} else {
editDmsRef.value.hide();
}
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
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
});
} catch (error) {
quasar.notify({
message: t(`${error.message}`),
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
/>
<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-debounce="100"
@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"
: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>
</div>
<div class="col">
<QInput
: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>
</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
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>
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?
</div>
<div class="col">
<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>
</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">
<div class="col">
<QCheckbox
:label="t('invoiceIn.summary.booked')"
v-model="data.isBooked"
/>
</div>
<div class="col"></div>
</div>
</template>
</FormModel>
<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"
clearable
clear-icon="close"
/>
<VnSelectFilter
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?
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="[requiredFieldRule]"
/>
</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="[requiredFieldRule]"
/>
<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="[requiredFieldRule]"
/>
</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="upsert" />
</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="[requiredFieldRule]"
/>
</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="[requiredFieldRule]"
/>
<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="[requiredFieldRule]"
/>
</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="upsert" />
</QCardActions>
</QCard>
</QDialog>
</template>
<style lang="scss" scoped>
@media (max-width: $breakpoint-xs) {
.column {
.row:not(:last-child) {
flex-direction: column;
}
}
.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,327 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute, useRouter } 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';
import VnLv from 'src/components/ui/VnLv.vue';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import FetchData from 'src/components/FetchData.vue';
import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const route = useRoute();
const router = useRouter();
const quasar = useQuasar();
const { hasAny } = useRole();
const { t } = useI18n();
const { openReport, sendEmail } = usePrintService();
const arrayData = useArrayData('InvoiceIn');
const invoiceIn = computed(() => arrayData.store.data);
const cardDescriptorRef = ref();
const entityId = computed(() => $props.id || route.params.id);
const totalAmount = ref();
const currentAction = ref();
const config = ref();
const actions = {
book: {
title: 'Are you sure you want to book this invoice?',
cb: checkToBook,
action: toBook,
},
delete: {
title: 'Are you sure you want to delete this invoice?',
action: deleteInvoice,
},
clone: {
title: 'Are you sure you want to clone this invoice?',
action: cloneInvoice,
},
showPdf: {
cb: showPdfInvoice,
},
sendPdf: {
cb: sendPdfInvoiceConfirmation,
},
};
const filter = {
include: [
{
relation: 'supplier',
scope: {
include: {
relation: 'contacts',
scope: {
where: {
email: { neq: null },
},
},
},
},
},
{
relation: 'invoiceInDueDay',
},
{
relation: 'company',
},
{
relation: 'currency',
},
],
};
const data = ref(useCardDescription());
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,
componentProps: {
title: currentAction.value.title,
promise: currentAction.value.action,
},
});
}
async function checkToBook() {
let directBooking = true;
const totals = await 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: 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`);
// Pendiente de sincronizar todo con arrayData
quasar.notify({
type: 'positive',
message: t('globals.dataSaved'),
});
await cardDescriptorRef.value.getData();
setTimeout(() => location.reload(), 500);
}
async function deleteInvoice() {
await axios.delete(`InvoiceIns/${entityId.value}`);
quasar.notify({
type: 'positive',
message: t('Invoice deleted'),
});
router.push({ path: '/invoice-in' });
}
async function cloneInvoice() {
const { data } = await axios.post(`InvoiceIns/${entityId.value}/clone`);
quasar.notify({
type: 'positive',
message: t('Invoice cloned'),
});
router.push({ path: `/invoice-in/${data.id}/summary` });
}
const isAdministrative = () => hasAny(['administrative']);
const isAgricultural = () =>
invoiceIn.value.supplier.sageWithholdingFk == config.value[0].sageWithholdingFk;
function showPdfInvoice() {
if (isAgricultural()) openReport(`InvoiceIns/${entityId.value}/invoice-in-pdf`);
}
function sendPdfInvoiceConfirmation() {
quasar.dialog({
component: SendEmailDialog,
componentProps: {
data: {
address: invoiceIn.value.supplier.contacts[0].email,
},
promise: sendPdfInvoice,
},
});
}
function sendPdfInvoice({ address }) {
if (!address)
quasar.notify({
type: 'negative',
message: t(`The email can't be empty`),
});
else
return sendEmail(`InvoiceIns/${entityId.value}/invoice-in-email`, {
recipientId: invoiceIn.value.supplier.id,
recipient: address,
});
}
function triggerMenu(type) {
currentAction.value = actions[type];
if (currentAction.value.cb) currentAction.value.cb();
else openDialog(type);
}
</script>
<template>
<FetchData
url="InvoiceInConfigs"
:where="{ fields: ['sageWithholdingFk'] }"
auto-load
@on-fetch="(data) => (config = data)"
/>
<!--Refactor para añadir en el arrayData-->
<CardDescriptor
ref="cardDescriptorRef"
module="InvoiceIn"
:url="`InvoiceIns/${entityId}`"
:filter="filter"
:title="data.title"
:subtitle="data.subtitle"
@on-fetch="setData"
data-key="invoiceInData"
>
<template #menu="{ entity }">
jorgep marked this conversation as resolved
Review

Igual que has hecho isAgricultural, podrías hacer para isAdministrative?

Igual que has hecho isAgricultural, podrías hacer para isAdministrative?
<QItem
v-if="!entity.isBooked && isAdministrative()"
v-ripple
clickable
@click="triggerMenu('book')"
>
<QItemSection>{{ t('To book') }}</QItemSection>
</QItem>
<QItem
v-if="isAdministrative()"
v-ripple
clickable
@click="triggerMenu('delete')"
>
<QItemSection>{{ t('Delete invoice') }}</QItemSection>
</QItem>
<QItem
v-if="isAdministrative()"
v-ripple
clickable
@click="triggerMenu('clone')"
>
<QItemSection>{{ t('Clone invoice') }}</QItemSection>
</QItem>
<QItem
v-if="isAgricultural()"
v-ripple
clickable
@click="triggerMenu('showPdf')"
>
<QItemSection>{{ t('Show agricultural receipt as PDF') }}</QItemSection>
</QItem>
<QItem
v-if="isAgricultural()"
v-ripple
clickable
@click="triggerMenu('sendPdf')"
>
<QItemSection
>{{ t('Send agricultural receipt as PDF') }}...</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"
/>
</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>
<style lang="scss" scoped>
.q-dialog {
jorgep marked this conversation as resolved
Review

tiene sentido tener un width y un max-width de igual tamaño?
Se puede dar el caso que el valor de la qcard sea mas pequeño¿?

tiene sentido tener un width y un max-width de igual tamaño? Se puede dar el caso que el valor de la qcard sea mas pequeño¿?
.q-card {
width: 35em;
}
}
</style>
<i18n>
es:
To book: Contabilizar
Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura?
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
Show agricultural receipt as PDF: Ver recibo agrícola como PDF
Send agricultural receipt as PDF: Enviar recibo agrícola como PDF
Are you sure you want to send it?: Estás seguro que quieres enviarlo?
Send PDF invoice: Enviar factura a PDF
</i18n>

View File

@ -0,0 +1,294 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
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 invoiceId = route.params.id;
const placeholder = 'yyyy/mm/dd';
const filter = {
where: {
invoiceInFk: invoiceId,
},
};
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',
},
]);
const isNotEuro = (code) => code != 'EUR';
jorgep marked this conversation as resolved Outdated

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

TIP: es igual poner Number(invoiceId) que +invoiceId
async function insert() {
await axios.post('/InvoiceInDueDays/new ', { id: +invoiceId });
await invoiceInFormRef.value.reload();
}
</script>
<template>
<FetchData url="Banks" auto-load limit="30" @on-fetch="(data) => (banks = data)" />
<CrudModel
v-if="invoiceIn"
ref="invoiceInFormRef"
data-key="InvoiceInDueDays"
url="InvoiceInDueDays"
:filter="filter"
auto-load
:data-required="{ invoiceInFk: invoiceId }"
v-model:selected="rowsSelected"
@on-fetch="(data) => (areRows = !!data.length)"
>
<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"
jorgep marked this conversation as resolved
Review

Quizás debería ser una valor constante

Quizás debería ser una valor constante
mask="date"
:placeholder="placeholder"
clearable
clear-icon="close"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
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="placeholder"
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="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,428 @@
<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 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 intrastatTotals = ref({});
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) => taxRate(row.taxableBase, row.taxTypeSage?.rate),
format: (value) => toCurrency(value),
sortable: true,
align: 'left',
},
{
name: 'currency',
label: 'invoiceIn.summary.currency',
field: (row) => row.foreignValue,
format: (value) => 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',
field: (row) => {
jorgep marked this conversation as resolved
Review

({intrastat}) en vez de row

({intrastat}) en vez de 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: 'stems',
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 getAmountNotMatch(totals) {
return (
totals.totalDueDay != totals.totalTaxableBase &&
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),
};
return totals;
}
function getTaxTotal(tax) {
return 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) {
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
return (rate / 100) * taxableBase;
}
function getLink(param) {
return `#/invoice-in/${entityId.value}/${param}`;
}
</script>
<template>
<CardSummary
ref="summary"
:url="`InvoiceIns/${entityId}/summary`"
@on-fetch="(data) => setData(data)"
>
<template #header="{ entity: invoiceIn }">
<div>{{ invoiceIn.id }} - {{ invoiceIn.supplier.name }}</div>
</template>
<template #body="{ entity: invoiceIn }">
<!--Basic Data-->
<QCard class="vn-one">
<QCardSection class="q-pa-none">
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?
<a class="header" :href="getLink('basic-data')">
{{ t('invoiceIn.pageTitles.basicData') }}
<QIcon name="open_in_new" color="primary" />
</a>
</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
:label="t('invoiceIn.summary.docNumber')"
:value="`${invoiceIn.serial}/${invoiceIn.serialNumber}`"
/>
</QCard>
<QCard class="vn-one">
<QCardSection class="q-pa-none">
<a class="header" :href="getLink('basic-data')">
{{ t('invoiceIn.pageTitles.basicData') }}
<QIcon name="open_in_new" color="primary" />
</a>
</QCardSection>
<VnLv
:ellipsis-value="false"
: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="getLink('basic-data')">
{{ t('invoiceIn.pageTitles.basicData') }}
<QIcon name="open_in_new" color="primary" />
</a>
</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 class="vn-one">
<QCardSection class="q-pa-none">
<a class="header" :href="getLink('basic-data')">
{{ t('invoiceIn.pageTitles.basicData') }}
<QIcon name="open_in_new" color="primary" />
</a>
</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>
</QCardSection>
</QCard>
<!--Vat-->
<QCard v-if="invoiceIn.invoiceInTax.length">
<a class="header" :href="getLink('vat')">
{{ t('invoiceIn.card.vat') }}
<QIcon name="open_in_new" color="primary" />
</a>
<QTable
:columns="vatColumns"
:rows="invoiceIn.invoiceInTax"
flat
hide-pagination
>
<template #header="props">
<QTr :props="props" class="bg">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
{{ 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))
}}</QTd>
<QTd></QTd>
</QTr>
</template>
</QTable>
</QCard>
<!--Due Day-->
<QCard v-if="invoiceIn.invoiceInDueDay.length">
<a class="header" :href="getLink('due-day')">
{{ t('invoiceIn.card.dueDay') }}
<QIcon name="open_in_new" color="primary" />
</a>
<QTable
class="full-width"
:columns="dueDayColumns"
:rows="invoiceIn.invoiceInDueDay"
flat
hide-pagination
>
<template #header="props">
<QTr :props="props" class="bg">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }}
</QTh>
</QTr>
</template>
<template #bottom-row>
<QTr class="bg">
<QTd></QTd>
<QTd></QTd>
<QTd>{{ toCurrency(invoiceIn.totals.totalDueDay) }}</QTd>
<QTd></QTd>
</QTr>
</template>
</QTable>
</QCard>
<!--Intrastat-->
<QCard v-if="invoiceIn.invoiceInIntrastat.length">
<a class="header" :href="getUrl('intrastat')">
{{ t('invoiceIn.card.intrastat') }}
<QIcon name="open_in_new" color="primary" />
</a>
<QTable
:columns="intrastatColumns"
:rows="invoiceIn.invoiceInIntrastat"
flat
hide-pagination
>
<template #header="props">
<QTr :props="props" class="bg">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }}
</QTh>
</QTr>
</template>
<template #bottom-row>
<QTr class="bg">
<QTd></QTd>
<QTd>{{ toCurrency(intrastatTotals.amount) }}</QTd>
<QTd>{{ intrastatTotals.net }}</QTd>
<QTd>{{ intrastatTotals.stems }}</QTd>
<QTd></QTd>
</QTr>
</template>
</QTable>
</QCard>
</template>
</CardSummary>
</template>
<style lang="scss" scoped>
.bg {
background-color: var(--vn-light-gray);
}
.bordered {
border: 1px solid var(--vn-text);
max-width: 18em;
}
</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) => toCurrency(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) {
jorgep marked this conversation as resolved Outdated

Lo revisamos

Lo revisamos
const sageTaxTypeId = invoiceInTax.taxTypeSageFk;
const taxRateSelection = sageTaxTypes.value.find(
(transaction) => transaction.id == sageTaxTypeId
);
const taxTypeSage = taxRateSelection?.rate ?? 0;
const taxableBase = invoiceInTax?.taxableBase ?? 0;
return (taxTypeSage / 100) * taxableBase;
}
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'),
});
newExpenseRef.value.hide();
} catch (error) {
quasar.notify({
type: 'negative',
message: t(`${error.message}`),
});
}
}
</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>
{{ toCurrency(taxRate(props.row)) }}
</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,53 @@
/// <reference types="cypress" />
describe('InvoiceInBasicData', () => {
const selects = '.q-form .q-select';
const appendBtns = 'label button';
const dialogAppendBtns = '.q-dialog label button';
const dialogInputs = '.q-dialog input';
const dialogActionBtns = '.q-card__actions button';
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/invoice-in/1/basic-data`);
});
it('should edit the provideer and supplier ref', () => {
cy.get(selects).eq(0).click();
cy.get(selects).eq(0).type('Bros');
cy.get(selects).eq(0).type('{enter}');
cy.get(appendBtns).eq(0).click();
cy.get('input').eq(2).type(4739);
cy.saveCard();
cy.get(`${selects} input`).eq(0).invoke('val').should('eq', 'Bros nick');
cy.get('input').eq(2).invoke('val').should('eq', '4739');
});
it('should edit the dms data', () => {
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(dialogActionBtns).eq(1).click();
cy.get(appendBtns).eq(3).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(dialogActionBtns).eq(1).click();
cy.get('.q-notification__message').should(
'have.text',
"The files can't be empty"
);
});
});

View File

@ -0,0 +1,30 @@
/// <reference types="cypress" />
describe('InvoiceInDueDay', () => {
const inputs = 'label input';
const inputBtns = 'label button';
const addBtn = '.q-page-sticky > div > .q-btn > .q-btn__content';
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/invoice-in/6/due-day`);
});
it('should update the amount', () => {
cy.get(inputBtns).eq(1).click();
cy.get(inputs).eq(3).type(23);
cy.saveCard();
cy.get('.q-notification__message').should('have.text', 'Data saved');
});
it('should remove the first line', () => {
cy.removeRow(1);
});
it('should add a new row ', () => {
cy.waitForElement('thead');
cy.get(addBtn).click();
cy.saveCard();
cy.get('.q-notification__message').should('have.text', 'Data saved');
});
});

View File

@ -0,0 +1,38 @@
/// <reference types="cypress" />
describe('InvoiceInIntrastat', () => {
const inputBtns = 'label button';
const thirdRow = 'tbody > :nth-child(3)';
const firstLineCode = 'tbody > :nth-child(1) > :nth-child(2)';
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/invoice-in/1/intrastat`);
});
it('should edit the first line', () => {
cy.selectOption(firstLineCode, 'Plantas vivas: Esqueje/injerto, Vid');
cy.get(inputBtns).eq(1).click();
cy.saveCard();
cy.visit(`/#/invoice-in/1/intrastat`);
cy.getValue(firstLineCode).should(
'have.value',
'Plantas vivas: Esqueje/injerto, Vid'
);
});
it('should add a new row', () => {
const rowData = [false, 'Plantas vivas: Esqueje/injerto, Vid', 30, 10, 5, 'FR'];
cy.addRow();
cy.fillRow(thirdRow, rowData);
cy.saveCard();
cy.get('.q-notification__message').should('have.text', 'Data saved');
});
it('should remove the first line', () => {
cy.removeRow(1);
});
});

View File

@ -0,0 +1,28 @@
/// <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');
cy.get(summaryHeaders).eq(4).contains('Vat');
});
});

View File

@ -0,0 +1,55 @@
/// <reference types="cypress" />
describe('InvoiceInVat', () => {
const inputs = 'label input';
const inputBtns = 'label button';
const thirdRow = 'tbody > :nth-child(3)';
const firstLineVat = 'tbody > :nth-child(1) > :nth-child(4)';
const dialogInputs = '.q-dialog label input';
const dialogBtns = '.q-dialog button';
const randomInt = Math.floor(Math.random() * 100);
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/invoice-in/1/vat`);
});
it('should edit the first line', () => {
cy.get(inputBtns).eq(1).click();
cy.get(inputs).eq(2).type(23);
cy.selectOption(firstLineVat, 'H.P. IVA 21% CEE');
cy.saveCard();
cy.visit(`/#/invoice-in/1/vat`);
cy.getValue(firstLineVat).should('have.value', 'H.P. IVA 21% CEE');
});
it('should add a new row', () => {
cy.addRow();
cy.fillRow(thirdRow, [true, 2000000001, 30, 'H.P. IVA 10']);
cy.saveCard();
cy.get('.q-notification__message').should('have.text', 'Data saved');
});
it('should remove the first line', () => {
cy.removeRow(1);
});
it('should throw an error if there are fields undefined', () => {
cy.get(inputBtns).eq(0).click();
cy.get(dialogBtns).eq(2).click();
cy.get('.q-notification__message').should('have.text', "The code can't be empty");
});
it('should correctly handle expense addition', () => {
cy.get(inputBtns).eq(0).click();
cy.get(dialogInputs).eq(0).click();
cy.get(dialogInputs).eq(0).type(randomInt);
cy.get(dialogInputs).eq(1).click();
cy.get(dialogInputs).eq(1).type('This is a dummy expense');
cy.get(dialogBtns).eq(2).click();
cy.get('.q-notification__message').should('have.text', 'Data saved');
});
});

View File

@ -108,14 +108,19 @@ Cypress.Commands.add('fillRow', (rowSelector, data) => {
.then((td) => {
if (td.find('.q-select__dropdown-icon').length) {
cy.selectOption(td, value);
}
if (td.find('.q-checkbox__inner').length && value) {
} else if (td.find('.q-checkbox__inner').length && value) {
cy.checkOption(td);
}
} else if (td.find('input[type="text"]') && value)
cy.get(td).find('input').type(value);
});
});
});
Cypress.Commands.add('addRow', () => {
cy.waitForElement('tbody');
cy.get('.q-page-sticky > div > .q-btn > .q-btn__content').click();
});
Cypress.Commands.add('validateRow', (rowSelector, expectedValues) => {
cy.waitForElement('tbody');
cy.get(rowSelector).within(() => {
@ -131,6 +136,27 @@ Cypress.Commands.add('validateRow', (rowSelector, expectedValues) => {
});
});
Cypress.Commands.add('removeRow', (rowIndex) => {
let rowsBefore;
let rowsAfter;
cy.get('tr')
.its('length')
.then((length) => {
rowsBefore = length;
cy.get('.q-checkbox').eq(rowIndex).click();
cy.removeCard();
cy.get('.q-dialog button').eq(2).click();
})
.then(() => {
cy.get('tr')
.its('length')
.then((length) => {
rowsAfter = length;
expect(rowsBefore).to.eq(rowsAfter + 1);
});
});
});
Cypress.Commands.add('openListSummary', (row) => {
cy.get('.card-list-body .actions .q-btn:nth-child(2)').eq(row).click();
});

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();
});
});

View File

@ -0,0 +1,34 @@
import { vi, describe, expect, it, beforeAll } from 'vitest';
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

@ -0,0 +1,34 @@
import { vi, describe, expect, it, beforeAll } from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper';
import InvoiceInIntrastat from 'src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue';
describe('InvoiceInIntrastat', () => {
let vm;
beforeAll(() => {
vm = createWrapper(InvoiceInIntrastat, {
global: {
stubs: ['vnPaginate'],
mocks: {
fetch: vi.fn(),
},
},
}).vm;
vi.spyOn(axios, 'get').mockResolvedValue({ data: [{}] });
});
describe('getTotal()', () => {
it('should correctly handle the sum', () => {
vm.invoceInIntrastat = [
{ amount: 10, stems: 162 },
{ amount: 20, stems: 21 },
];
const totalAmount = vm.getTotal('amount');
const totalStems = vm.getTotal('stems');
expect(totalAmount).toBe(10 + 20);
expect(totalStems).toBe(162 + 21);
});
});
});

View File

@ -0,0 +1,73 @@
import { vi, describe, expect, it, beforeAll } from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper';
import InvoiceInVat from 'src/pages/InvoiceIn/Card/InvoiceInVat.vue';
describe('InvoiceInVat', () => {
let vm;
beforeAll(() => {
vm = createWrapper(InvoiceInVat, {
global: {
stubs: [],
mocks: {
fetch: vi.fn(),
},
},
}).vm;
});
describe('addExpense()', () => {
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 };
vm.sageTaxTypes = [
{ id: 1, rate: 10 },
{ id: 2, rate: 20 },
];
const result = vm.taxRate(invoiceInTax);
expect(result).toBe((10 / 100) * 100);
});
it('should return 0 if there is not tax rate', () => {
const invoiceInTax = { taxableBase: 100, taxTypeSageFk: 1 };
vm.sageTaxTypes = [];
const result = vm.taxRate(invoiceInTax);
expect(result).toBe(0);
});
});
});