Merge pull request 'fix: refs #7939 rollback' (!1803) from 7939-warmfix-rollbackVat into dev
gitea/salix-front/pipeline/head This commit looks good Details

Reviewed-on: #1803
Reviewed-by: Jon Elias <jon@verdnatura.es>
This commit is contained in:
Jorge Penadés 2025-05-15 11:02:11 +00:00
commit b46e3a0307
1 changed files with 404 additions and 127 deletions

View File

@ -1,16 +1,21 @@
<script setup> <script setup>
import { ref, computed, markRaw } from 'vue'; import { ref, computed, nextTick } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { getTotal } from 'src/composables/getTotal'; import { getTotal } from 'src/composables/getTotal';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import CrudModel from 'src/components/CrudModel.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
import CreateNewExpenseForm from 'src/components/CreateNewExpenseForm.vue';
import { getExchange } from 'src/composables/getExchange'; import { getExchange } from 'src/composables/getExchange';
import VnTable from 'src/components/VnTable/VnTable.vue'; import { useAccountShortToStandard } from 'src/composables/useAccountShortToStandard';
import VnSelectExpense from 'src/components/common/VnSelectExpense.vue';
const { t } = useI18n(); const { t } = useI18n();
const arrayData = useArrayData(); const arrayData = useArrayData();
const route = useRoute(); const route = useRoute();
const invoiceIn = computed(() => arrayData.store.data); const invoiceIn = computed(() => arrayData.store.data);
@ -19,142 +24,100 @@ const expenses = ref([]);
const sageTaxTypes = ref([]); const sageTaxTypes = ref([]);
const sageTransactionTypes = ref([]); const sageTransactionTypes = ref([]);
const rowsSelected = ref([]); const rowsSelected = ref([]);
const invoiceInVatTableRef = ref(); const invoiceInFormRef = ref();
defineProps({ actionIcon: { type: String, default: 'add' } }); defineProps({
actionIcon: {
function taxRate(invoiceInTax) { type: String,
const sageTaxTypeId = invoiceInTax.taxTypeSageFk; default: 'add',
const taxRateSelection = sageTaxTypes.value.find( },
(transaction) => transaction.id == sageTaxTypeId, });
);
const taxTypeSage = taxRateSelection?.rate ?? 0;
const taxableBase = invoiceInTax?.taxableBase ?? 0;
return (taxTypeSage / 100) * taxableBase;
}
const columns = computed(() => [ const columns = computed(() => [
{ {
name: 'expenseFk', name: 'expense',
label: t('Expense'), label: t('Expense'),
component: markRaw(VnSelectExpense), field: (row) => row.expenseFk,
format: (row) => { options: expenses.value,
const expense = expenses.value.find((e) => e.id === row.expenseFk); model: 'expenseFk',
return expense ? `${expense.id}: ${expense.name}` : row.expenseFk; optionValue: 'id',
}, optionLabel: (row) => `${row.id}: ${row.name}`,
sortable: true, sortable: true,
align: 'left', align: 'left',
isEditable: true,
create: true,
width: '250px',
}, },
{ {
name: 'taxableBase', name: 'taxablebase',
label: t('Taxable base'), label: t('Taxable base'),
component: 'number', field: (row) => row.taxableBase,
attrs: { model: 'taxableBase',
clearable: true,
'clear-icon': 'close',
},
sortable: true, sortable: true,
align: 'left', align: 'left',
isEditable: true,
create: true,
}, },
{ {
name: 'isDeductible', name: 'isDeductible',
label: t('invoiceIn.isDeductible'), label: t('invoiceIn.isDeductible'),
component: 'checkbox', field: (row) => row.isDeductible,
model: 'isDeductible',
align: 'center', align: 'center',
isEditable: true,
create: true,
createAttrs: {
defaultValue: true,
},
width: '100px',
}, },
{ {
name: 'taxTypeSageFk', name: 'sageiva',
label: t('Sage iva'), label: t('Sage iva'),
component: 'select', field: (row) => row.taxTypeSageFk,
attrs: { options: sageTaxTypes.value,
options: sageTaxTypes.value, model: 'taxTypeSageFk',
optionValue: 'id', optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.vat}`, optionLabel: (row) => `${row.id}: ${row.vat}`,
filterOptions: ['id', 'vat'],
'data-cy': 'vat-sageiva',
},
format: (row) => {
const taxType = sageTaxTypes.value.find((t) => t.id === row.taxTypeSageFk);
return taxType ? `${taxType.id}: ${taxType.vat}` : row.taxTypeSageFk;
},
sortable: true, sortable: true,
align: 'left', align: 'left',
isEditable: true,
create: true,
}, },
{ {
name: 'transactionTypeSageFk', name: 'sagetransaction',
label: t('Sage transaction'), label: t('Sage transaction'),
component: 'select', field: (row) => row.transactionTypeSageFk,
attrs: { options: sageTransactionTypes.value,
options: sageTransactionTypes.value, model: 'transactionTypeSageFk',
optionValue: 'id', optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.transaction}`, optionLabel: (row) => `${row.id}: ${row.transaction}`,
filterOptions: ['id', 'transaction'],
},
format: (row) => {
const transType = sageTransactionTypes.value.find(
(t) => t.id === row.transactionTypeSageFk,
);
return transType
? `${transType.id}: ${transType.transaction}`
: row.transactionTypeSageFk;
},
sortable: true, sortable: true,
align: 'left', align: 'left',
isEditable: true,
create: true,
}, },
{ {
name: 'rate', name: 'rate',
label: t('Rate'), label: t('Rate'),
sortable: false, sortable: true,
format: (row) => taxRate(row).toFixed(2), field: (row) => taxRate(row, row.taxTypeSageFk),
align: 'left', align: 'left',
}, },
{ {
name: 'foreignValue', name: 'foreignvalue',
label: t('Foreign value'), label: t('Foreign value'),
component: 'number',
sortable: true, sortable: true,
field: (row) => row.foreignValue,
align: 'left', align: 'left',
create: true,
disable: !isNotEuro(currency.value),
}, },
{ {
name: 'total', name: 'total',
label: t('Total'), label: 'Total',
align: 'left', align: 'left',
format: (row) => (Number(row.taxableBase || 0) + Number(taxRate(row))).toFixed(2),
}, },
]); ]);
const tableRows = computed(
() => invoiceInVatTableRef.value?.CrudModelRef?.formData || [],
);
const taxableBaseTotal = computed(() => { const taxableBaseTotal = computed(() => {
return getTotal(tableRows.value, 'taxableBase'); return getTotal(invoiceInFormRef.value.formData, 'taxableBase');
}); });
const taxRateTotal = computed(() => { const taxRateTotal = computed(() => {
return tableRows.value.reduce((sum, row) => sum + Number(taxRate(row)), 0); return getTotal(invoiceInFormRef.value.formData, null, {
cb: taxRate,
});
}); });
const combinedTotal = computed(() => { const combinedTotal = computed(() => {
return +taxableBaseTotal.value + +taxRateTotal.value; return +taxableBaseTotal.value + +taxRateTotal.value;
}); });
const filter = computed(() => ({ const filter = {
fields: [ fields: [
'id', 'id',
'invoiceInFk', 'invoiceInFk',
@ -168,75 +131,389 @@ const filter = computed(() => ({
where: { where: {
invoiceInFk: route.params.id, invoiceInFk: route.params.id,
}, },
})); };
const isNotEuro = (code) => code != 'EUR'; const isNotEuro = (code) => code != 'EUR';
async function handleForeignValueUpdate(val, row) { function taxRate(invoiceInTax) {
if (!isNotEuro(currency.value)) return; const sageTaxTypeId = invoiceInTax.taxTypeSageFk;
row.taxableBase = await getExchange( const taxRateSelection = sageTaxTypes.value.find(
val, (transaction) => transaction.id == sageTaxTypeId,
invoiceIn.value?.currencyFk,
invoiceIn.value?.issued,
); );
const taxTypeSage = taxRateSelection?.rate ?? 0;
const taxableBase = invoiceInTax?.taxableBase ?? 0;
return ((taxTypeSage / 100) * taxableBase).toFixed(2);
}
function autocompleteExpense(evt, row, col, ref) {
const val = evt.target.value;
if (!val) return;
const param = isNaN(val) ? row[col.model] : val;
const lookup = expenses.value.find(
({ id }) => id == useAccountShortToStandard(param),
);
ref.vnSelectDialogRef.vnSelectRef.toggleOption(lookup);
}
function setCursor(ref) {
nextTick(() => {
const select = ref.vnSelectDialogRef
? ref.vnSelectDialogRef.vnSelectRef
: ref.vnSelectRef;
select.$el.querySelector('input').setSelectionRange(0, 0);
});
} }
</script> </script>
<template> <template>
<FetchData url="Expenses" auto-load @on-fetch="(data) => (expenses = data)" /> <FetchData
ref="expensesRef"
url="Expenses"
auto-load
@on-fetch="(data) => (expenses = data)"
/>
<FetchData url="SageTaxTypes" auto-load @on-fetch="(data) => (sageTaxTypes = data)" /> <FetchData url="SageTaxTypes" auto-load @on-fetch="(data) => (sageTaxTypes = data)" />
<FetchData <FetchData
url="sageTransactionTypes" url="sageTransactionTypes"
auto-load auto-load
@on-fetch="(data) => (sageTransactionTypes = data)" @on-fetch="(data) => (sageTransactionTypes = data)"
/> />
<VnTable <CrudModel
ref="invoiceInFormRef"
v-if="invoiceIn" v-if="invoiceIn"
ref="invoiceInVatTableRef"
data-key="InvoiceInTaxes" data-key="InvoiceInTaxes"
url="InvoiceInTaxes" url="InvoiceInTaxes"
save-url="InvoiceInTaxes/crud"
:filter="filter" :filter="filter"
:data-required="{ invoiceInFk: $route.params.id }" :data-required="{ invoiceInFk: $route.params.id }"
:insert-on-load="true" :insert-on-load="true"
auto-load auto-load
v-model:selected="rowsSelected" v-model:selected="rowsSelected"
:columns="columns" :go-to="`/invoice-in/${$route.params.id}/due-day`"
:is-editable="true"
:table="{ selection: 'multiple', 'row-key': '$index' }"
footer
:right-search="false"
:column-search="false"
:disable-option="{ card: true }"
class="q-pa-none"
:create="{
urlCreate: 'InvoiceInTaxes',
title: t('Add tax'),
formInitialData: { invoiceInFk: $route.params.id, isDeductible: true },
onDataSaved: () => invoiceInVatTableRef.reload(),
}"
:crud-model="{ goTo: `/invoice-in/${$route.params.id}/due-day` }"
> >
<template #column-footer-taxableBase> <template #body="{ rows }">
{{ toCurrency(taxableBaseTotal) }} <QTable
v-model:selected="rowsSelected"
selection="multiple"
:columns="columns"
:rows="rows"
row-key="$index"
:grid="$q.screen.lt.sm"
>
<template #body-cell-expense="{ row, col }">
<QTd>
<VnSelectDialog
:ref="`expenseRef-${row.$index}`"
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:filter-options="['id', 'name']"
:tooltip="t('Create a new expense')"
:acls="[
{ model: 'Expense', props: '*', accessType: 'WRITE' },
]"
@keydown.tab.prevent="
autocompleteExpense(
$event,
row,
col,
$refs[`expenseRef-${row.$index}`],
)
"
@update:model-value="
setCursor($refs[`expenseRef-${row.$index}`])
"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
{{ `${scope.opt.id}: ${scope.opt.name}` }}
</QItem>
</template>
<template #form>
<CreateNewExpenseForm
@on-data-saved="$refs.expensesRef.fetch()"
/>
</template>
</VnSelectDialog>
</QTd>
</template>
<template #body-cell-isDeductible="{ row }">
<QTd align="center">
<QCheckbox
v-model="row.isDeductible"
data-cy="isDeductible_checkbox"
/>
</QTd>
</template>
<template #body-cell-taxablebase="{ row }">
<QTd shrink>
<VnInputNumber
clear-icon="close"
v-model="row.taxableBase"
clearable
/>
</QTd>
</template>
<template #body-cell-sageiva="{ row, col }">
<QTd>
<VnSelect
:ref="`sageivaRef-${row.$index}`"
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:filter-options="['id', 'vat']"
data-cy="vat-sageiva"
@update:model-value="
setCursor($refs[`sageivaRef-${row.$index}`])
"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt.vat }}</QItemLabel>
<QItemLabel>
{{ `#${scope.opt.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QTd>
</template>
<template #body-cell-sagetransaction="{ row, col }">
<QTd>
<VnSelect
:ref="`sagetransactionRef-${row.$index}`"
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:filter-options="['id', 'transaction']"
@update:model-value="
setCursor($refs[`sagetransactionRef-${row.$index}`])
"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
scope.opt.transaction
}}</QItemLabel>
<QItemLabel>
{{ `#${scope.opt.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QTd>
</template>
<template #body-cell-foreignvalue="{ row }">
<QTd shrink>
<VnInputNumber
:class="{
'no-pointer-events': !isNotEuro(currency),
}"
:disable="!isNotEuro(currency)"
v-model="row.foreignValue"
@update:model-value="
async (val) => {
if (!isNotEuro(currency)) return;
row.taxableBase = await getExchange(
val,
row.currencyFk,
invoiceIn.issued,
);
}
"
/>
</QTd>
</template>
<template #bottom-row>
<QTr class="bg">
<QTd />
<QTd />
<QTd>
{{ toCurrency(taxableBaseTotal) }}
</QTd>
<QTd />
<QTd />
<QTd />
<QTd>
{{ toCurrency(taxRateTotal) }}
</QTd>
<QTd />
<QTd>
{{ toCurrency(combinedTotal) }}
</QTd>
</QTr>
</template>
<template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
<QCard bordered flat class="q-my-xs">
<QCardSection>
<QCheckbox v-model="props.selected" dense />
</QCardSection>
<QSeparator />
<QList>
<QItem>
<VnSelectDialog
:label="t('Expense')"
class="full-width"
v-model="props.row['expenseFk']"
:options="expenses"
option-value="id"
:option-label="(row) => `${row.id}:${row.name}`"
:filter-options="['id', 'name']"
:tooltip="t('Create a new expense')"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
{{ `${scope.opt.id}: ${scope.opt.name}` }}
</QItem>
</template>
<template #form>
<CreateNewExpenseForm />
</template>
</VnSelectDialog>
</QItem>
<QItem>
<VnInputNumber
:label="t('Taxable base')"
:class="{
'no-pointer-events': isNotEuro(currency),
}"
class="full-width"
:disable="isNotEuro(currency)"
clear-icon="close"
v-model="props.row.taxableBase"
clearable
/>
</QItem>
<QItem>
<VnSelect
:label="t('Sage iva')"
class="full-width"
v-model="props.row['taxTypeSageFk']"
:options="sageTaxTypes"
option-value="id"
:option-label="(row) => `${row.id}:${row.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>
</VnSelect>
</QItem>
<QItem>
<VnSelect
class="full-width"
v-model="props.row['transactionTypeSageFk']"
:options="sageTransactionTypes"
option-value="id"
:option-label="
(row) => `${row.id}:${row.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>
</VnSelect>
</QItem>
<QItem>
{{ toCurrency(taxRate(props.row), currency) }}
</QItem>
<QItem>
<VnInputNumber
:label="t('Foreign value')"
class="full-width"
:class="{
'no-pointer-events': !isNotEuro(currency),
}"
:disable="!isNotEuro(currency)"
v-model="props.row.foreignValue"
/>
</QItem>
</QList>
</QCard>
</div>
</template>
</QTable>
</template> </template>
<template #column-footer-rate> </CrudModel>
{{ toCurrency(taxRateTotal) }} <QPageSticky position="bottom-right" :offset="[25, 25]">
</template> <QBtn
<template #column-footer-total> color="primary"
{{ toCurrency(combinedTotal) }} icon="add"
</template> size="lg"
</VnTable> v-shortcut="'+'"
round
@click="invoiceInFormRef.insert()"
>
<QTooltip>{{ t('Add tax') }}</QTooltip>
</QBtn>
</QPageSticky>
</template> </template>
<style lang="scss" scoped>
.bg {
background-color: var(--vn-light-gray);
}
@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;
}
.default-icon {
cursor: pointer;
border-radius: 50px;
background-color: $primary;
}
</style>
<i18n> <i18n>
es: es:
Expense: Gasto Expense: Gasto
Create a new expense: Crear nuevo gasto Create a new expense: Crear nuevo gasto
Add tax: Añadir Gasto/IVA # Changed label slightly Add tax: Crear gasto
Taxable base: Base imp. Taxable base: Base imp.
Sage iva: Sage iva # Kept original label Sage tax: Sage iva
Sage transaction: Sage transacción Sage transaction: Sage transacción
Rate: Cuota # Changed label Rate: Tasa
Foreign value: Divisa Foreign value: Divisa
Total: Total
invoiceIn.isDeductible: Deducible
</i18n> </i18n>