diff --git a/src/components/VnTable/filters/tableFooter.js b/src/components/VnTable/filters/tableFooter.js new file mode 100644 index 000000000..9c7d080f6 --- /dev/null +++ b/src/components/VnTable/filters/tableFooter.js @@ -0,0 +1,12 @@ +export default function (initialFooter, data) { + const footer = data.reduce( + (acc, row) => { + Object.entries(initialFooter).forEach(([key, initialValue]) => { + acc[key] += row?.[key] !== undefined ? row[key] : initialValue; + }); + return acc; + }, + { ...initialFooter } + ); + return footer; +} diff --git a/src/pages/Department/Card/DepartmentCard.vue b/src/pages/Department/Card/DepartmentCard.vue index 8597e37cf..4b9fe419c 100644 --- a/src/pages/Department/Card/DepartmentCard.vue +++ b/src/pages/Department/Card/DepartmentCard.vue @@ -10,4 +10,4 @@ import DepartmentDescriptor from 'pages/Department/Card/DepartmentDescriptor.vue base-url="Departments" :descriptor="DepartmentDescriptor" /> -</template> \ No newline at end of file +</template> diff --git a/src/pages/Supplier/Card/SupplierBalance.vue b/src/pages/Supplier/Card/SupplierBalance.vue new file mode 100644 index 000000000..87eac2abf --- /dev/null +++ b/src/pages/Supplier/Card/SupplierBalance.vue @@ -0,0 +1,191 @@ +<script setup> +import { computed, onBeforeMount, ref } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useRoute } from 'vue-router'; +import tableFooter from 'src/components/VnTable/filters/tableFooter'; +import { dashIfEmpty, toCurrency, toDateHourMin } from 'src/filters'; +import { useState } from 'src/composables/useState'; +import { useStateStore } from 'src/stores/useStateStore'; + +import VnTable from 'src/components/VnTable/VnTable.vue'; + +import InvoiceInDescriptorProxy from 'src/pages/InvoiceIn/Card/InvoiceInDescriptorProxy.vue'; +import SupplierBalanceFilter from './SupplierBalanceFilter.vue'; +import { onMounted } from 'vue'; +import FetchData from 'src/components/FetchData.vue'; + +const { t } = useI18n(); + +const route = useRoute(); +const state = useState(); +const stateStore = useStateStore(); +const user = state.getUser(); + +const tableRef = ref(); +const companyId = ref(); +const companyUser = ref(user.value.companyFk); +const balances = ref([]); +const userParams = ref({ + supplierId: route.params.id, + companyId: companyId.value ?? companyUser.value, + isBooked: false, +}); + +const columns = computed(() => [ + { + align: 'left', + name: 'dated', + label: t('Creation date'), + format: ({ dated }) => toDateHourMin(dated), + cardVisible: true, + }, + { + align: 'left', + name: 'sref', + label: t('Reference'), + isTitle: true, + class: 'extend', + format: ({ sref }) => dashIfEmpty(sref), + }, + { + align: 'left', + name: 'bank', + label: t('Bank'), + cardVisible: true, + }, + { + align: 'left', + name: 'invoiceEuros', + label: t('Debit'), + format: ({ invoiceEuros }) => toCurrency(invoiceEuros), + isId: true, + }, + { + align: 'left', + name: 'paymentEuros', + label: t('Havings'), + format: ({ paymentEuros }) => toCurrency(paymentEuros), + cardVisible: true, + }, + { + align: 'left', + name: 'euroBalance', + label: t('Balance'), + format: ({ euroBalance }) => toCurrency(euroBalance), + cardVisible: true, + }, + { + align: 'left', + name: 'isBooked', + label: t('Conciliated'), + cardVisible: true, + }, +]); + +onBeforeMount(() => { + stateStore.rightDrawer = true; + companyId.value = companyUser.value; +}); + +onMounted(async () => { + Object.assign(userParams, { + supplierId: route.params.id, + companyId: companyId.value ?? companyUser.value, + isConciliated: false, + }); +}); + +function setFooter(data) { + const initialFooter = { + invoiceEuros: 0, + paymentEuros: 0, + euroBalance: 0, + }; + + tableRef.value.footer = tableFooter(initialFooter, data); +} +async function onFetch(data) { + setFooter(data); + return; +} + +function round(value) { + return Math.round(value * 100) / 100; +} +const onFetchCurrencies = ([currency]) => { + userParams.value.currencyFk = currency?.id; +}; +</script> + +<template> + <QDrawer side="right" :width="265" v-model="stateStore.rightDrawer"> + <SupplierBalanceFilter data-key="SupplierBalance" /> + </QDrawer> + <FetchData + url="Currencies" + :filter="{ fields: ['id', 'code'], where: { code: 'EUR' } }" + sort-by="code" + @on-fetch="onFetchCurrencies" + auto-load + /> + <VnTable + v-if="userParams.currencyFk" + ref="tableRef" + data-key="SupplierBalance" + url="Suppliers/receipts" + search-url="balance" + :user-params="userParams" + :columns="columns" + :right-search="false" + :is-editable="false" + :column-search="false" + @on-fetch="onFetch" + :disable-option="{ card: true }" + :footer="true" + :order="['dated ASC']" + data-cy="supplierBalanceTable" + auto-load + :map-key="false" + > + <template #column-balance="{ rowIndex }"> + {{ toCurrency(balances[rowIndex]?.balance) }} + </template> + <template #column-sref="{ row }"> + <span class="link" v-if="row.statementType === 'invoiceIn'"> + {{ dashIfEmpty(row.sref) }} + <InvoiceInDescriptorProxy :id="row.id" /> + </span> + <span v-else> {{ dashIfEmpty(row.sref) }}</span> + </template> + + <template #column-footer-invoiceEuros> + <span> + {{ toCurrency(round(tableRef.footer.invoiceEuros)) }} + </span> + </template> + <template #column-footer-paymentEuros> + <span> + {{ toCurrency(round(tableRef.footer.paymentEuros)) }} + </span> + </template> + <template #column-footer-euroBalance> + <span> + {{ toCurrency(round(tableRef.footer.euroBalance)) }} + </span> + </template> + </VnTable> +</template> + +<i18n> +es: + Company: Empresa + Total by company: Total por empresa + Date: Fecha + Creation date: Fecha de creación + Reference: Referencia + Bank: Caja + Debit: Debe + Havings: Haber + Balance: Balance + Conciliated: Conciliado + </i18n> diff --git a/src/pages/Supplier/Card/SupplierBalanceFilter.vue b/src/pages/Supplier/Card/SupplierBalanceFilter.vue new file mode 100644 index 000000000..c4b63d9c8 --- /dev/null +++ b/src/pages/Supplier/Card/SupplierBalanceFilter.vue @@ -0,0 +1,121 @@ +<script setup> +import { useI18n } from 'vue-i18n'; +import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import VnInputDate from 'src/components/common/VnInputDate.vue'; + +const { t } = useI18n(); +defineProps({ + dataKey: { + type: String, + required: true, + }, +}); +</script> + +<template> + <VnFilterPanel + :data-key="dataKey" + :search-button="true" + :redirect="false" + :unremovable-params="['supplierId', 'companyId']" + > + <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 }"> + <QItem> + <QItemSection> + <VnInputDate + :label="t('params.from')" + v-model="params.from" + @update:model-value="searchFn()" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + :label="t('params.bankFk')" + v-model="params.bankFk" + url="Accountings" + option-label="bank" + :include="{ relation: 'accountingType' }" + sort-by="id" + dense + outlined + rounded + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt.id }} {{ scope.opt.bank }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + :label="t('params.currencyFk')" + url="Currencies" + :filter="{ fields: ['id', 'name'] }" + order="code" + v-model="params.currencyFk" + option-value="id" + option-label="name" + hide-selected + dense + outlined + rounded + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <QCheckbox + v-model="params.isConciliated" + :label="t('params.isConciliated')" + /></QItemSection> + </QItem> + </template> + </VnFilterPanel> +</template> + +<i18n> +en: + params: + search: General search + supplierId: Supplier + categoryId: Category + from: From + to: To + isConciliated: Is conciliated + currencyFk: Currency + bankFk: Bank + companyId: Comapany + isBooked: Is booked +es: + params: + supplierId: Proveedor + isConciliated: Conciliado + currencyFk: Moneda + New payment: Añadir pago + Date: Fecha + from: Desde + to: Hasta + companyId: Empresa + isBooked: Contabilizado + bankFk: Caja + Amount: Importe + Reference: Referencia + Cash: Efectivo +</i18n> diff --git a/src/router/modules/supplier.js b/src/router/modules/supplier.js index 647f4bdd3..4ece4c784 100644 --- a/src/router/modules/supplier.js +++ b/src/router/modules/supplier.js @@ -21,6 +21,7 @@ export default { 'SupplierAccounts', 'SupplierContacts', 'SupplierAddresses', + 'SupplierBalance', 'SupplierConsumption', 'SupplierAgencyTerm', 'SupplierDms', @@ -144,6 +145,16 @@ export default { component: () => import('src/pages/Supplier/Card/SupplierAddressesCreate.vue'), }, + { + path: 'balance', + name: 'SupplierBalance', + meta: { + title: 'balance', + icon: 'balance', + }, + component: () => + import('src/pages/Supplier/Card/SupplierBalance.vue'), + }, { path: 'consumption', name: 'SupplierConsumption', diff --git a/test/cypress/integration/Supplier/SupplierBalance.spec.js b/test/cypress/integration/Supplier/SupplierBalance.spec.js new file mode 100644 index 000000000..e4a3ee65c --- /dev/null +++ b/test/cypress/integration/Supplier/SupplierBalance.spec.js @@ -0,0 +1,11 @@ +describe('Supplier Balance', () => { + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('developer'); + cy.visit(`/#/supplier/1/balance`); + }); + + it('Should load layout', () => { + cy.get('.q-page').should('be.visible'); + }); +}); diff --git a/test/cypress/integration/client/clientBalance.spec.js b/test/cypress/integration/client/clientBalance.spec.js index dfba56b16..abfa74cec 100644 --- a/test/cypress/integration/client/clientBalance.spec.js +++ b/test/cypress/integration/client/clientBalance.spec.js @@ -3,9 +3,7 @@ describe('Client balance', () => { beforeEach(() => { cy.viewport(1280, 720); cy.login('developer'); - cy.visit('#/customer/1101/balance', { - timeout: 5000, - }); + cy.visit('#/customer/1101/balance'); }); it('Should load layout', () => { cy.get('.q-page').should('be.visible');