#8277 createEntryControl #1370

Open
jorgep wants to merge 66 commits from 8277-createEntryControl into dev
10 changed files with 683 additions and 21 deletions

View File

@ -374,7 +374,7 @@ function getCaption(opt) {
>
<template #append>
<QIcon
v-show="isClearable && value"
v-show="isClearable && value != null && value !== ''"
Review

Así admite valor 0 también, valor que se comprueba en el tipo de cuenta.

Así admite valor 0 también, valor que se comprueba en el tipo de cuenta.
name="close"
@click="
() => {
@ -389,7 +389,7 @@ function getCaption(opt) {
<template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">
<div v-if="slotName == 'append'">
<QIcon
v-show="isClearable && value"
v-show="isClearable && value != null && value !== ''"
name="close"
@click.stop="
() => {
@ -414,7 +414,7 @@ function getCaption(opt) {
<QItemLabel>
{{ opt[optionLabel] }}
</QItemLabel>
<QItemLabel caption v-if="getCaption(opt)">
<QItemLabel caption v-if="getCaption(opt) !== false">
{{ `#${getCaption(opt)}` }}
</QItemLabel>
</QItemSection>

View File

@ -89,24 +89,26 @@ function cancel() {
<slot name="customHTML"></slot>
</QCardSection>
<QCardActions align="right">
<QBtn
:label="t('globals.cancel')"
color="primary"
:disable="isLoading"
flat
@click="cancel()"
data-cy="VnConfirm_cancel"
/>
<QBtn
:label="t('globals.confirm')"
:title="t('globals.confirm')"
color="primary"
:loading="isLoading"
@click="confirm()"
unelevated
autofocus
data-cy="VnConfirm_confirm"
/>
<slot name="actions" :actions="{ confirm, cancel }">
<QBtn
:label="t('globals.cancel')"
color="primary"
:disable="isLoading"
flat
@click="cancel()"
data-cy="VnConfirm_cancel"
/>
<QBtn
:label="t('globals.confirm')"
:title="t('globals.confirm')"
color="primary"
:loading="isLoading"
@click="confirm()"
unelevated
autofocus
data-cy="VnConfirm_confirm"
/>
</slot>
</QCardActions>
</QCard>
</QDialog>

View File

@ -342,6 +342,7 @@ globals:
parking: Parking
vehicleList: Vehicles
vehicle: Vehicle
entryPreAccount: Pre-account
unsavedPopup:
title: Unsaved changes will be lost
subtitle: Are you sure exit without saving?

View File

@ -345,6 +345,7 @@ globals:
parking: Parking
vehicleList: Vehículos
vehicle: Vehículo
entryPreAccount: Precontabilizar
unsavedPopup:
title: Los cambios que no haya guardado se perderán
subtitle: ¿Seguro que quiere salir sin guardar?

View File

@ -0,0 +1,474 @@
<script setup>
import { ref, computed, markRaw, useTemplateRef, onBeforeMount, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { toDate, toCurrency } from 'src/filters';
import { useArrayData } from 'src/composables/useArrayData';
import VnTable from 'src/components/VnTable/VnTable.vue';
import FetchData from 'src/components/FetchData.vue';
import VnSelectSupplier from 'src/components/common/VnSelectSupplier.vue';
import EntryDescriptorProxy from './Card/EntryDescriptorProxy.vue';
import SupplierDescriptorProxy from '../Supplier/Card/SupplierDescriptorProxy.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import VnDms from 'src/components/common/VnDms.vue';
import { useState } from 'src/composables/useState';
import { useQuasar } from 'quasar';
import InvoiceInDescriptorProxy from '../InvoiceIn/Card/InvoiceInDescriptorProxy.vue';
import { useStateStore } from 'src/stores/useStateStore';
import { downloadFile } from 'src/composables/downloadFile';
const { t } = useI18n();
const quasar = useQuasar();
const { notify } = useNotify();
const user = useState().getUser();
const stateStore = useStateStore();
const updateDialog = ref();
const uploadDialog = ref();
const MAXDAYS = 365;
const DEFAULTDAYS = 60;
const dataKey = 'entryPreaccountingFilter';
const url = 'Entries/preAccountingFilter';
const arrayData = useArrayData(dataKey);
const daysAgo = ref();
const isBooked = ref();
const dmsData = ref();
const table = useTemplateRef('table');
const companies = ref([]);
const countries = ref([]);
const entryTypes = ref([]);
const entryAccounts = ref([]);
const warehouses = ref([]);
const defaultDmsDescription = ref();
const dmsTypeId = ref();
const selectedRows = ref([]);
const totalAmount = ref();
const totalSelectedAmount = computed(() => {
if (!selectedRows.value.length) return 0;
return selectedRows.value.reduce((acc, entry) => acc + entry.amount, 0);
});
let supplierRef;
let dmsFk;
const columns = computed(() => [
{
name: 'id',
label: t('entry.preAccount.id'),
isId: true,
chip: {
condition: () => true,
},
},
{
name: 'invoiceNumber',
label: t('entry.preAccount.invoiceNumber'),
},
{
name: 'company',
label: t('globals.company'),
columnFilter: {
component: 'select',
name: 'companyFk',
optionLabel: 'code',
options: companies.value,
},
},
{
name: 'warehouse',
label: t('globals.warehouse'),
columnFilter: {
component: 'select',
name: 'warehouseInFk',
options: warehouses.value,
},
},
{
name: 'gestDocFk',
label: t('entry.preAccount.gestDocFk'),
},
{
name: 'dmsType',
label: t('entry.preAccount.dmsType'),
columnFilter: {
component: 'select',
label: null,
name: 'dmsTypeFk',
url: 'DmsTypes',
fields: ['id', 'name'],
},
},
{
name: 'reference',
label: t('entry.preAccount.reference'),
},
{
name: 'shipped',
label: t('entry.preAccount.shipped'),
format: ({ shipped }, dashIfEmpty) => dashIfEmpty(toDate(shipped)),
columnFilter: {
component: 'date',
name: 'shipped',
},
},
{
name: 'landed',
label: t('entry.preAccount.landed'),
format: ({ landed }, dashIfEmpty) => dashIfEmpty(toDate(landed)),
columnFilter: {
component: 'date',
name: 'landed',
},
},
{
name: 'invoiceInFk',
label: t('entry.preAccount.invoiceInFk'),
},
{
name: 'supplier',
label: t('globals.supplier'),
format: (row) => row.supplier,
columnFilter: {
component: markRaw(VnSelectSupplier),
label: null,
name: 'supplierFk',
class: 'fit',
},
},
{
name: 'country',
label: t('globals.country'),
columnFilter: {
component: 'select',
name: 'countryFk',
options: countries.value,
},
},
{
name: 'description',
label: t('entry.preAccount.entryType'),
columnFilter: {
component: 'select',
label: null,
name: 'typeFk',
options: entryTypes.value,
optionLabel: 'description',
optionValue: 'code',
},
},
{
name: 'payDem',
label: t('entry.preAccount.payDem'),
columnFilter: {
component: 'number',
name: 'payDem',
},
},
{
name: 'accountType',
label: t('entry.preAccount.accountType'),
columnFilter: {
component: 'select',
name: 'accountType',
options: entryAccounts.value,
optionLabel: 'type',
optionValue: 'code',
sortBy: 'code',
},
},
{
name: 'amount',
label: t('globals.amount'),
format: ({ amount }) => toCurrency(amount),
columnFilter: {
component: 'number',
name: 'amount',
},
},
{
name: 'isAgricultural',
label: t('entry.preAccount.isAgricultural'),
component: 'checkbox',
isEditable: false,
},
{
name: 'isBooked',
label: t('entry.preAccount.isBooked'),
component: 'checkbox',
},
{
name: 'received',
label: t('entry.preAccount.received'),
component: 'checkbox',
isEditable: false,
},
]);
onBeforeMount(() => {
daysAgo.value = arrayData.store.userParams.daysAgo || DEFAULTDAYS;
isBooked.value = arrayData.store.userParams.isBooked || false;
stateStore.leftDrawer = false;
});
watch(selectedRows, (nVal, oVal) => {
const lastRow = nVal.at(-1);
if (lastRow?.isBooked) selectedRows.value.pop();
if (nVal.length > oVal.length && lastRow.invoiceInFk)
quasar.dialog({
component: VnConfirm,
componentProps: { title: t('entry.preAccount.hasInvoice') },
});
});
function filterByDaysAgo(val) {
if (!val) val = DEFAULTDAYS;
else if (val > MAXDAYS) val = MAXDAYS;
daysAgo.value = val;
arrayData.store.userParams.daysAgo = daysAgo.value;
table.value.reload();
}
async function preAccount() {
const entries = selectedRows.value;
const { companyFk, isAgricultural, landed } = entries.at(0);
try {
await axios.get('Entries/canAddInvoiceIn', {
params: { entryIds: entries.map(({ id }) => id) },
});
jorgep marked this conversation as resolved Outdated

Cambiar para que de fallo si hay mas de un dms diferente.

Cambiar para que de fallo si hay mas de un dms diferente.
dmsFk = entries.find(({ gestDocFk }) => gestDocFk)?.gestDocFk;
if (isAgricultural) {
jorgep marked this conversation as resolved Outdated

Esta seria mas correcta tenerla en el back.

Esta seria mas correcta tenerla en el back.
const year = new Date(landed).getFullYear();
supplierRef = (
await axios.get('InvoiceIns/getMaxRef', { params: { companyFk, year } })
).data;
return createInvoice();
} else if (dmsFk) {
supplierRef = (
await axios.get(`Dms/${dmsFk}`, {
params: { filter: JSON.stringify({ fields: { reference: true } }) },
})
).data?.reference;
updateDialog.value.show();
} else {
uploadFile();
}
} catch (e) {
throw e;
}
}
async function updateFile() {
await axios.post(`Dms/${dmsFk}/updateFile`, { dmsTypeId: dmsTypeId.value });
await createInvoice();
}
async function uploadFile() {
const firstSelectedEntry = selectedRows.value.at(0);
const { supplier, companyFk, invoiceNumber } = firstSelectedEntry;
dmsData.value = {
dmsTypeFk: dmsTypeId.value,
warehouseFk: user.value.warehouseFk,
companyFk: companyFk,
description: supplier + defaultDmsDescription.value + invoiceNumber,
hasFile: false,
};
uploadDialog.value.show();
}
async function afterUploadFile({ reference }, res) {
supplierRef = reference;
dmsFk = res.data[0].id;
await createInvoice();
}
async function createInvoice() {
try {
await axios.post(`Entries/addInvoiceIn`, {
ids: selectedRows.value.map((entry) => entry.id),
supplierRef,
dmsFk,
});
notify(t('entry.preAccount.success'), 'positive');
} catch (e) {
throw e;
} finally {
table.value.reload();
supplierRef = null;
dmsFk = undefined;
selectedRows.value.length = 0;
}
}
</script>
<template>
<FetchData
url="Countries"
:filter="{ fields: ['id', 'name'] }"
@on-fetch="(data) => (countries = data)"
auto-load
/>
<FetchData
url="Companies"
:filter="{ fields: ['id', 'code'] }"
@on-fetch="(data) => (companies = data)"
auto-load
/>
<FetchData
url="Warehouses"
:filter="{ fields: ['id', 'name'] }"
@on-fetch="(data) => (warehouses = data)"
auto-load
/>
<FetchData
url="EntryTypes"
:filter="{ fields: ['code', 'description'] }"
@on-fetch="(data) => (entryTypes = data)"
auto-load
/>
<FetchData
url="EntryAccounts"
:filter="{ fields: ['code', 'type'] }"
@on-fetch="
(data) => (entryAccounts = data.map((x) => ({ ...x, type: t(x.type) })))
"
auto-load
/>
<FetchData
url="InvoiceInConfigs/findOne"
:filter="{ fields: ['defaultDmsDescription'] }"
@on-fetch="(data) => (defaultDmsDescription = data?.defaultDmsDescription)"
auto-load
/>
<FetchData
url="DmsTypes/findOne"
:filter="{ fields: ['id'] }"
:where="{ code: 'invoiceIn' }"
@on-fetch="(data) => (dmsTypeId = data?.id)"
auto-load
/>
<VnSearchbar
:data-key
:url
:label="t('entry.preAccount.search')"
:info="t('entry.preAccount.searchInfo')"
:search-remove-params="false"
/>
<VnTable
v-model:selected="selectedRows"
:data-key
:columns
:url
:search-url="dataKey"
ref="table"
:disable-option="{ card: true }"
redirect="Entry"
:order="['landed DESC']"
:right-search="false"
:user-params="{ daysAgo, isBooked }"
:row-click="false"
:table="{ selection: 'multiple' }"
:limit="0"
:footer="true"
@on-fetch="
(data) => (totalAmount = data?.reduce((acc, entry) => acc + entry.amount, 0))
"
auto-load
>
<template #top-left>
<QBtn
data-cy="preAccount_btn"
icon="account_balance"
color="primary"
class="q-mr-sm"
:disable="!selectedRows.length"
@click="preAccount"
>
<QTooltip>{{ t('entry.preAccount.btn') }}</QTooltip>
</QBtn>
<VnInputNumber
v-model="daysAgo"
:label="$t('globals.daysAgo')"
dense
:step="1"
:decimal-places="0"
@update:model-value="filterByDaysAgo"
debounce="500"
:title="t('entry.preAccount.daysAgo')"
/>
</template>
<template #column-id="{ row }">
<span class="link" @click.stop>
{{ row.id }}
<EntryDescriptorProxy :id="row.id" />
</span>
</template>
<template #column-company="{ row }">
<QBadge :color="row.color ?? 'transparent'" :label="row.company" />
</template>
<template #column-gestDocFk="{ row }">
<span class="link" @click.stop="downloadFile(row.gestDocFk)">
{{ row.gestDocFk }}
</span>
</template>
<template #column-supplier="{ row }">
<span class="link" @click.stop>
{{ row.supplier }}
<SupplierDescriptorProxy :id="row.supplierFk" />
</span>
</template>
<template #column-invoiceInFk="{ row }">
<span class="link" @click.stop>
{{ row.invoiceInFk }}
<InvoiceInDescriptorProxy :id="row.invoiceInFk" />
</span>
</template>
<template #column-footer-amount>
<div v-text="toCurrency(totalSelectedAmount)" />
<div v-text="toCurrency(totalAmount)" />
</template>
</VnTable>
<VnConfirm
ref="updateDialog"
:title="t('entry.preAccount.dialog.title')"
:message="t('entry.preAccount.dialog.message')"
>
<template #actions>
<QBtn
data-cy="updateFile_yes"
:label="t('globals.yes')"
color="primary"
@click="updateFile"
autofocus
v-close-popup
/>
<QBtn
data-cy="updateFile_no"
:label="t('globals.no')"
color="primary"
flat
@click="uploadFile"
v-close-popup
/>
<QBtn :label="t('globals.cancel')" color="primary" flat v-close-popup />
</template>
</VnConfirm>
<QDialog ref="uploadDialog">
<VnDms
model="dms"
:form-initial-data="dmsData"
url="Dms/uploadFile"
@on-data-saved="afterUploadFile"
/>
</QDialog>
</template>
<i18n>
en:
IntraCommunity: Intra-community
NonCommunity: Non-community
CanaryIsland: Canary Islands
es:
IntraCommunity: Intracomunitario
NonCommunity: Extracomunitario
CanaryIsland: Islas Canarias
National: Nacional
</i18n>

View File

@ -0,0 +1,59 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import EntryPreAccount from '../EntryPreAccount.vue';
import axios from 'axios';
describe('EntryPreAccount', () => {
let wrapper;
let vm;
beforeAll(() => {
vi.spyOn(axios, 'get').mockImplementation(() => ({ data: [] }));
wrapper = createWrapper(EntryPreAccount);
vm = wrapper.vm;
});
afterEach(() => {
vi.clearAllMocks();
});
describe('filterByDaysAgo()', () => {
it('should set daysAgo to DEFAULTDAYS if no value is provided', () => {
vm.filterByDaysAgo();
expect(vm.daysAgo).toBe(vm.DEFAULTDAYS);
expect(vm.arrayData.store.userParams.daysAgo).toBe(vm.DEFAULTDAYS);
});
it('should set daysAgo to MAXDAYS if the value exceeds MAXDAYS', () => {
vm.filterByDaysAgo(500);
expect(vm.daysAgo).toBe(vm.MAXDAYS);
expect(vm.arrayData.store.userParams.daysAgo).toBe(vm.MAXDAYS);
});
it('should set daysAgo to the provided value if it is valid', () => {
vm.filterByDaysAgo(30);
expect(vm.daysAgo).toBe(30);
expect(vm.arrayData.store.userParams.daysAgo).toBe(30);
});
});
describe('Dialog behavior when adding a new row', () => {
it('should open the dialog if the new row has invoiceInFk', async () => {
const dialogSpy = vi.spyOn(vm.quasar, 'dialog');
const selectedRows = [{ id: 1, invoiceInFk: 123 }];
vm.selectedRows = selectedRows;
await vm.$nextTick();
expect(dialogSpy).toHaveBeenCalledWith({
component: vm.VnConfirm,
componentProps: { title: vm.t('entry.preAccount.hasInvoice') },
});
});
it('should not open the dialog if the new row does not have invoiceInFk', async () => {
const dialogSpy = vi.spyOn(vm.quasar, 'dialog');
vm.selectedRows = [{ id: 1, invoiceInFk: null }];
await vm.$nextTick();
expect(dialogSpy).not.toHaveBeenCalled();
});
});
});

View File

@ -118,6 +118,33 @@ entry:
searchInfo: You can search by entry reference
descriptorMenu:
showEntryReport: Show entry report
preAccount:
gestDocFk: Gestdoc
dmsType: Gestdoc type
invoiceNumber: Entry ref.
reference: Gestdoc ref.
shipped: Shipped
landed: Landed
id: Entry
invoiceInFk: Invoice in
supplierFk: Supplier
country: Country
description: Entry type
payDem: Payment term
isBooked: B
received: R
entryType: Entry type
isAgricultural: Agricultural
accountType: Account type
daysAgo: Max 365 days
search: Search
searchInfo: You can search by supplier name or nickname
btn: Pre-account
hasInvoice: This entry has already an invoice in
success: It has been successfully pre-accounted
dialog:
title: Pre-account entries
message: Do you want the invoice to inherit the entry document?
entryFilter:
params:
isExcludedFromAvailable: Excluded from available

View File

@ -69,6 +69,33 @@ entry:
observationType: Tipo de observación
search: Buscar entradas
searchInfo: Puedes buscar por referencia de entrada
preAccount:
gestDocFk: Gestdoc
dmsType: Tipo gestdoc
invoiceNumber: Ref. Entrada
reference: Ref. GestDoc
shipped: F. envío
landed: F. llegada
id: Entrada
invoiceInFk: Recibida
supplierFk: Proveedor
country: País
description: Tipo de Entrada
payDem: Plazo de pago
isBooked: C
received: R
entryType: Tipo de entrada
isAgricultural: Agricultural
accountType: Tipo de cuenta
daysAgo: Máximo 365 días
search: Buscar
searchInfo: Puedes buscar por nombre o alias de proveedor
btn: Precontabilizar
hasInvoice: Esta entrada ya tiene una f. recibida
success: Se ha precontabilizado correctamente
dialog:
title: Precontabilizar entradas
message: ¿Desea que la factura herede el documento de la entrada?
params:
entryFk: Entrada
observationTypeFk: Tipo de observación

View File

@ -85,6 +85,7 @@ export default {
'EntryLatestBuys',
'EntryStockBought',
'EntryWasteRecalc',
'EntryPreAccount',
],
},
component: RouterView,
@ -94,6 +95,7 @@ export default {
name: 'EntryMain',
path: '',
component: () => import('src/components/common/VnModule.vue'),
props: (route) => ({ leftDrawer: route.name !== 'EntryPreAccount' }),
redirect: { name: 'EntryIndexMain' },
children: [
{
@ -150,6 +152,15 @@ export default {
},
component: () => import('src/pages/Entry/EntryWasteRecalc.vue'),
},
{
path: 'pre-account',
name: 'EntryPreAccount',
meta: {
title: 'entryPreAccount',
icon: 'account_balance',
},
component: () => import('src/pages/Entry/EntryPreAccount.vue'),
},
],
},
],

View File

@ -0,0 +1,60 @@
/// <reference types="cypress" />
describe('Entry PreAccount Functionality', () => {
beforeEach(() => {
cy.login('administrative');
cy.visit('/#/entry/pre-account');
});
it('should handle entries with different suppliers or companies', () => {
selectRowsByCol('id', [3, 2]);
cy.dataCy('preAccount_btn').click();
cy.checkNotification('Entries must have the same supplier and company');
});
it('should handle entries with different gestdoc files', () => {
selectRowsByCol('id', [3, 101]);
cy.dataCy('preAccount_btn').click();
cy.checkNotification('Entries have different gestdoc file');
});
it("should pre-account without questions if it's agricultural", () => {
selectRowsByCol('id', [2]);
cy.dataCy('preAccount_btn').click();
cy.checkNotification('It has been successfully pre-accounted');
});
it("should ask to upload a doc. if it's not agricultural and doesn't have doc. ", () => {
selectRowsByCol('id', [3]);
cy.dataCy('preAccount_btn').click();
cy.dataCy('Reference_input').type('{selectall}234343fh', { delay: 0 });
cy.dataCy('VnDms_inputFile').selectFile('test/cypress/fixtures/image.jpg', {
force: true,
});
cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('It has been successfully pre-accounted');
});
it('should ask to inherit the doc. and open VnDms popup if user choose "no"', () => {
selectRowsByCol('id', [101]);
cy.dataCy('preAccount_btn').click();
cy.dataCy('updateFile_no').click();
cy.get('#formModel').should('be.visible');
});
it('should ask to inherit the doc. and open VnDms popup if user choose "yes" and pre-account', () => {
selectRowsByCol('id', [101]);
cy.dataCy('preAccount_btn').click();
cy.dataCy('updateFile_yes').click();
cy.checkNotification('It has been successfully pre-accounted');
});
});
function selectRowsByCol(col = 'id', vals = []) {
for (const val of vals) {
const regex = new RegExp(`^\\s*(${val})\\s*$`);
cy.contains(`[data-col-field="${col}"]`, regex)
.parent()
.find('td > .q-checkbox')
.click();
}
}