Merge branch 'dev' into 7385-fixColumnNameReference
gitea/salix-front/pipeline/pr-dev This commit looks good Details

This commit is contained in:
Jose Antonio Tubau 2025-05-19 10:20:00 +00:00
commit acd9c97324
20 changed files with 547 additions and 918 deletions

View File

@ -131,11 +131,10 @@ async function fetch(data) {
const rows = keyData ? data[keyData] : data;
resetData(rows);
emit('onFetch', rows);
$props.insertOnLoad && await insert();
$props.insertOnLoad && (await insert());
return rows;
}
function resetData(data) {
if (!data) return;
if (data && Array.isArray(data)) {
@ -146,15 +145,22 @@ function resetData(data) {
formData.value = JSON.parse(JSON.stringify(data));
if (watchChanges.value) watchChanges.value(); //destroy watcher
watchChanges.value = watch(formData, (nVal) => {
hasChanges.value = false;
const filteredNewData = nVal.filter(row => !isRowEmpty(row) || row[$props.primaryKey]);
const filteredOriginal = originalData.value.filter(row => row[$props.primaryKey]);
watchChanges.value = watch(
formData,
(nVal) => {
hasChanges.value = false;
const filteredNewData = nVal.filter(
(row) => !isRowEmpty(row) || row[$props.primaryKey],
);
const filteredOriginal = originalData.value.filter(
(row) => row[$props.primaryKey],
);
const changes = getDifferences(filteredOriginal, filteredNewData);
hasChanges.value = !isEmpty(changes);
}, { deep: true });
const changes = getDifferences(filteredOriginal, filteredNewData);
hasChanges.value = !isEmpty(changes);
},
{ deep: true },
);
}
async function reset() {
await fetch(originalData.value);
@ -185,7 +191,6 @@ async function onSubmit() {
isLoading.value = true;
await saveChanges($props.saveFn ? formData.value : null);
}
async function onSubmitAndGo() {
@ -194,8 +199,8 @@ async function onSubmitAndGo() {
}
async function saveChanges(data) {
formData.value = formData.value.filter(row =>
row[$props.primaryKey] || !isRowEmpty(row)
formData.value = formData.value.filter(
(row) => row[$props.primaryKey] || !isRowEmpty(row),
);
if ($props.saveFn) {
@ -228,7 +233,7 @@ async function saveChanges(data) {
}
async function insert(pushData = { ...$props.dataRequired, ...$props.dataDefault }) {
formData.value = formData.value.filter(row => !isRowEmpty(row));
formData.value = formData.value.filter((row) => !isRowEmpty(row));
const lastRow = formData.value.at(-1);
const isLastRowEmpty = lastRow ? isRowEmpty(lastRow) : false;
@ -239,20 +244,18 @@ async function insert(pushData = { ...$props.dataRequired, ...$props.dataDefault
const nRow = Object.assign({ $index }, pushData);
formData.value.push(nRow);
const hasChange = Object.keys(nRow).some(key => !isChange(nRow, key));
const hasChange = Object.keys(nRow).some((key) => !isChange(nRow, key));
if (hasChange) hasChanges.value = true;
}
function isRowEmpty(row) {
return Object.keys(row).every(key => isChange(row, key));
return Object.keys(row).every((key) => isChange(row, key));
}
function isChange(row,key){
function isChange(row, key) {
return !row[key] || key == '$index' || Object.hasOwn($props.dataRequired || {}, key);
}
async function remove(data) {
if (!data.length)
return quasar.notify({
@ -270,7 +273,9 @@ async function remove(data) {
(form) => !preRemove.some((index) => index == form.$index),
);
formData.value = newData;
hasChanges.value = JSON.stringify(removeIndexField(formData.value)) !== JSON.stringify(removeIndexField(originalData.value));
hasChanges.value =
JSON.stringify(removeIndexField(formData.value)) !==
JSON.stringify(removeIndexField(originalData.value));
}
if (ids.length) {
quasar
@ -286,7 +291,7 @@ async function remove(data) {
})
.onOk(async () => {
newData = newData.filter((form) => !ids.some((id) => id == form[pk]));
fetch(newData);
await reload();
});
}

View File

@ -254,17 +254,13 @@ async function save() {
old: originalData.value,
});
if ($props.reload) await arrayData.fetch({});
if ($props.goTo) push({ path: $props.goTo });
hasChanges.value = false;
} finally {
isLoading.value = false;
}
}
async function saveAndGo() {
await save();
push({ path: $props.goTo });
}
function reset() {
formData.value = JSON.parse(JSON.stringify(originalData.value));
updateAndEmit('onFetch', { val: originalData.value });
@ -385,7 +381,7 @@ defineExpose({
<QBtnDropdown
data-cy="saveAndContinueDefaultBtn"
v-if="$props.goTo"
@click="saveAndGo"
@click="submitForm"
:label="
tMobile('globals.saveAndContinue') +
' ' +
@ -405,7 +401,7 @@ defineExpose({
<QItem
clickable
v-close-popup
@click="save"
@click="submitForm"
:title="t('globals.save')"
>
<QItemSection>

View File

@ -185,6 +185,7 @@ const col = computed(() => {
newColumn.attrs = { ...newColumn.component?.attrs, autofocus: $props.autofocus };
newColumn.event = { ...newColumn.component?.event, ...$props?.eventHandlers };
}
return newColumn;
});

View File

@ -151,6 +151,10 @@ const $props = defineProps({
type: String,
default: 'vnTable',
},
selectionFn: {
type: Function,
default: null,
},
});
const { t } = useI18n();
@ -338,10 +342,10 @@ function stopEventPropagation(event) {
event.stopPropagation();
}
function reload(params) {
async function reload(params) {
selected.value = [];
selectAll.value = false;
CrudModelRef.value.reload(params);
await CrudModelRef.value.reload(params);
}
function columnName(col) {
@ -395,12 +399,14 @@ function hasEditableFormat(column) {
}
const clickHandler = async (event) => {
const clickedElement = event.target.closest('td');
const isDateElement = event.target.closest('.q-date');
const isTimeElement = event.target.closest('.q-time');
const isQSelectDropDown = event.target.closest('.q-select__dropdown-icon');
const el = event.target;
const clickedElement = el.closest('td');
const isDateElement = el.closest('.q-date');
const isTimeElement = el.closest('.q-time');
const isQSelectDropDown = el.closest('.q-select__dropdown-icon');
const isDialog = el.closest('.q-dialog');
if (isDateElement || isTimeElement || isQSelectDropDown) return;
if (isDateElement || isTimeElement || isQSelectDropDown || isDialog) return;
if (clickedElement === null) {
await destroyInput(editingRow.value, editingField.value);
@ -447,6 +453,7 @@ async function renderInput(rowId, field, clickedElement) {
const row = CrudModelRef.value.formData[rowId];
const oldValue = CrudModelRef.value.formData[rowId][column?.name];
if (column.disable) return;
if (!clickedElement)
clickedElement = document.querySelector(
`[data-row-index="${rowId}"][data-col-field="${field}"]`,
@ -480,6 +487,7 @@ async function renderInput(rowId, field, clickedElement) {
await destroyInput(rowId, field, clickedElement);
},
keydown: async (event) => {
await column?.cellEvent?.['keydown']?.(event, row);
switch (event.key) {
case 'Tab':
await handleTabKey(event, rowId, field);
@ -655,7 +663,9 @@ const rowCtrlClickFunction = computed(() => {
});
const handleHeaderSelection = (evt, data) => {
if (evt === 'updateSelected' && selectAll.value) {
selected.value = tableRef.value.rows;
const fn = $props.selectionFn;
const rows = tableRef.value.rows;
selected.value = fn ? fn(rows) : rows;
} else if (evt === 'selectAll') {
selected.value = data;
} else {
@ -701,7 +711,6 @@ const handleHeaderSelection = (evt, data) => {
:search-url="searchUrl"
:disable-infinite-scroll="isTableMode"
:before-save-fn="removeTextValue"
@save-changes="reload"
:has-sub-toolbar="$props.hasSubToolbar ?? isEditable"
:auto-load="hasParams || $attrs['auto-load']"
>
@ -729,7 +738,15 @@ const handleHeaderSelection = (evt, data) => {
:virtual-scroll="isTableMode"
@virtual-scroll="onVirtualScroll"
@row-click="(event, row) => handleRowClick(event, row)"
@update:selected="emit('update:selected', $event)"
@update:selected="
(evt) => {
if ($props.selectionFn) selected = $props.selectionFn(evt);
emit(
'update:selected',
selectionFn ? selectionFn(selected) : selected,
);
}
"
@selection="(details) => handleSelection(details, rows)"
:hide-selected-banner="true"
:data-cy

View File

@ -36,8 +36,6 @@ const validate = async () => {
isLoading.value = true;
await props.submitFn(newPassword, oldPassword);
emit('onSubmit');
} catch (e) {
notify('errors.writeRequest', 'negative');
} finally {
changePassDialog.value.hide();
isLoading.value = false;

View File

@ -445,7 +445,12 @@ function getOptionLabel(property) {
</template>
<template #option="{ opt, itemProps }">
<QItem v-bind="itemProps">
<QItemSection v-if="typeof opt !== 'object'"> {{ opt }}</QItemSection>
<QItemSection v-if="typeof optionLabel === 'function'">{{
optionLabel(opt)
}}</QItemSection>
<QItemSection v-else-if="typeof opt !== 'object'">
{{ opt }}</QItemSection
>
<QItemSection v-else-if="opt[optionValue] == opt[optionLabel]">
<QItemLabel>{{ opt[optionLabel] }}</QItemLabel>
</QItemSection>

View File

@ -1,22 +1,20 @@
<script setup>
import { ref, useTemplateRef } from 'vue';
import { computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAccountShortToStandard } from 'src/composables/useAccountShortToStandard';
import VnSelectDialog from './VnSelectDialog.vue';
import CreateNewExpenseForm from '../CreateNewExpenseForm.vue';
import FetchData from '../FetchData.vue';
import { useArrayData } from 'src/composables/useArrayData';
const model = defineModel({ type: [String, Number, Object] });
const { t } = useI18n();
const expenses = ref([]);
const selectDialogRef = useTemplateRef('selectDialogRef');
const arrayData = useArrayData('expenses', { url: 'Expenses' });
const expenses = computed(() => arrayData.store.data);
async function autocompleteExpense(evt) {
const val = evt.target.value;
if (!val || isNaN(val)) return;
const lookup = expenses.value.find(({ id }) => id == useAccountShortToStandard(val));
if (selectDialogRef.value)
selectDialogRef.value.vnSelectDialogRef.vnSelectRef.toggleOption(lookup);
onMounted(async () => await arrayData.fetch({}));
async function updateModel(evt) {
await arrayData.fetch({});
model.value = evt.id;
}
</script>
<template>
@ -30,18 +28,11 @@ async function autocompleteExpense(evt) {
:filter-options="['id', 'name']"
:tooltip="t('Create a new expense')"
:acls="[{ model: 'Expense', props: '*', accessType: 'WRITE' }]"
@keydown.tab.prevent="autocompleteExpense"
>
<template #form>
<CreateNewExpenseForm @on-data-saved="$refs.expensesRef.fetch()" />
<CreateNewExpenseForm @on-data-saved="updateModel" />
</template>
</VnSelectDialog>
<FetchData
ref="expensesRef"
url="Expenses"
auto-load
@on-fetch="(data) => (expenses = data)"
/>
</template>
<i18n>
es:

View File

@ -1,5 +1,13 @@
<script setup>
import { ref, computed, markRaw, useTemplateRef, onBeforeMount, watch } from 'vue';
import {
ref,
computed,
markRaw,
useTemplateRef,
onBeforeMount,
watch,
onUnmounted,
} from 'vue';
import { useI18n } from 'vue-i18n';
import { toDate, toCurrency } from 'src/filters';
import { useArrayData } from 'src/composables/useArrayData';
@ -19,7 +27,10 @@ import { useQuasar } from 'quasar';
import InvoiceInDescriptorProxy from '../InvoiceIn/Card/InvoiceInDescriptorProxy.vue';
import { useStateStore } from 'src/stores/useStateStore';
import { downloadFile } from 'src/composables/downloadFile';
import { useRoute } from 'vue-router';
import { useAcl } from 'src/composables/useAcl';
const route = useRoute();
const { t } = useI18n();
const quasar = useQuasar();
const { notify } = useNotify();
@ -27,8 +38,6 @@ const user = useState().getUser();
const stateStore = useStateStore();
const updateDialog = ref();
const uploadDialog = ref();
let maxDays;
let defaultDays;
const dataKey = 'entryPreaccountingFilter';
const url = 'Entries/preAccountingFilter';
const arrayData = useArrayData(dataKey);
@ -45,12 +54,16 @@ const defaultDmsDescription = ref();
const dmsTypeId = ref();
const selectedRows = ref([]);
const totalAmount = ref();
const hasDiferentDms = ref(false);
let maxDays;
let defaultDays;
let supplierRef;
let dmsFk;
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',
@ -63,6 +76,7 @@ const columns = computed(() => [
{
name: 'invoiceNumber',
label: t('entry.preAccount.invoiceNumber'),
width: 'max-content',
},
{
name: 'company',
@ -73,7 +87,7 @@ const columns = computed(() => [
optionLabel: 'code',
options: companies.value,
},
orderBy: false,
class: 'shrink',
},
{
name: 'warehouse',
@ -102,6 +116,7 @@ const columns = computed(() => [
{
name: 'reference',
label: t('entry.preAccount.reference'),
width: 'max-content',
},
{
name: 'shipped',
@ -136,6 +151,7 @@ const columns = computed(() => [
class: 'fit',
event: 'update',
},
width: 'max-content',
},
{
name: 'country',
@ -145,6 +161,7 @@ const columns = computed(() => [
name: 'countryFk',
options: countries.value,
},
class: 'shrink',
},
{
name: 'description',
@ -165,11 +182,12 @@ const columns = computed(() => [
component: 'number',
name: 'payDem',
},
class: 'shrink',
},
{
name: 'fiscalCode',
label: t('entry.preAccount.fiscalCode'),
format: ({ fiscalCode }) => t(fiscalCode),
format: ({ fiscalCode }, dashIfEmpty) => t(dashIfEmpty(fiscalCode)),
columnFilter: {
component: 'select',
name: 'fiscalCode',
@ -208,20 +226,21 @@ const columns = computed(() => [
]);
onBeforeMount(async () => {
const filter = JSON.parse(route.query.entryPreaccountingFilter ?? '{}');
const { data } = await axios.get('EntryConfigs/findOne', {
params: { filter: JSON.stringify({ fields: ['maxDays', 'defaultDays'] }) },
});
maxDays = data.maxDays;
defaultDays = data.defaultDays;
daysAgo.value = arrayData.store.userParams.daysAgo || defaultDays;
isBooked.value = arrayData.store.userParams.isBooked || false;
daysAgo.value = filter.daysAgo || defaultDays;
isBooked.value = filter.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)
onUnmounted(() => (stateStore.leftDrawer = true));
watch(selectedRows, async (nVal, oVal) => {
if (nVal.length > oVal.length && nVal.at(-1).invoiceInFk)
quasar.dialog({
component: VnConfirm,
componentProps: { title: t('entry.preAccount.hasInvoice') },
@ -232,15 +251,22 @@ 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();
table.value.reload({
userParams: { ...arrayData.store.userParams, daysAgo: daysAgo.value },
});
}
async function preAccount() {
const entries = selectedRows.value;
const { companyFk, isAgricultural, landed } = entries.at(0);
supplierRef = null;
dmsFk = undefined;
hasDiferentDms.value = false;
try {
dmsFk = entries.find(({ gestDocFk }) => gestDocFk)?.gestDocFk;
entries.forEach(({ gestDocFk }) => {
if (!dmsFk && gestDocFk) dmsFk = gestDocFk;
if (dmsFk && gestDocFk && dmsFk != gestDocFk) hasDiferentDms.value = true;
});
if (isAgricultural) {
const year = new Date(landed).getFullYear();
supplierRef = (
@ -297,8 +323,6 @@ async function createInvoice() {
} catch (e) {
throw e;
} finally {
supplierRef = null;
dmsFk = undefined;
selectedRows.value.length = 0;
table.value.reload();
}
@ -359,6 +383,7 @@ async function createInvoice() {
:search-remove-params="false"
/>
<VnTable
v-if="isBooked !== undefined && daysAgo !== undefined"
v-model:selected="selectedRows"
:data-key
:columns
@ -377,10 +402,12 @@ async function createInvoice() {
@on-fetch="
(data) => (totalAmount = data?.reduce((acc, entry) => acc + entry.amount, 0))
"
:selection-fn="(rows) => rows.filter((row) => !row.isBooked)"
auto-load
>
<template #top-left>
<QBtn
v-if="useAcl().hasAcl('Entry', 'addInvoiceIn', 'WRITE')"
data-cy="preAccount_btn"
icon="account_balance"
color="primary"
@ -437,6 +464,11 @@ async function createInvoice() {
:title="t('entry.preAccount.dialog.title')"
:message="t('entry.preAccount.dialog.message')"
>
<template #customHTML v-if="hasDiferentDms">
<p class="text-negative">
{{ t('differentGestdoc') }}
</p>
</template>
<template #actions>
<QBtn
data-cy="updateFileYes"
@ -471,9 +503,11 @@ en:
IntraCommunity: Intra-community
NonCommunity: Non-community
CanaryIslands: Canary Islands
differentGestdoc: The entries have different gestdoc
es:
IntraCommunity: Intracomunitaria
NonCommunity: Extracomunitaria
CanaryIslands: Islas Canarias
National: Nacional
differentGestdoc: Las entradas tienen diferentes gestdoc
</i18n>

View File

@ -130,7 +130,7 @@ entry:
supplierFk: Supplier
country: Country
description: Entry type
payDem: Payment term
payDem: P. term
isBooked: B
isReceived: R
entryType: Entry type
@ -138,7 +138,7 @@ entry:
fiscalCode: Account type
daysAgo: Max 365 days
search: Search
searchInfo: You can search by supplier name or nickname
searchInfo: You can search by supplier name, nickname or tax number
btn: Pre-account
hasInvoice: This entry has already an invoice in
success: It has been successfully pre-accounted

View File

@ -81,7 +81,7 @@ entry:
supplierFk: Proveedor
country: País
description: Tipo de Entrada
payDem: Plazo de pago
payDem: P. pago
isBooked: C
isReceived: R
entryType: Tipo de entrada
@ -89,7 +89,7 @@ entry:
fiscalCode: Tipo de cuenta
daysAgo: Máximo 365 días
search: Buscar
searchInfo: Puedes buscar por nombre o alias de proveedor
searchInfo: Puedes buscar por nombre, alias o cif de proveedor
btn: Precontabilizar
hasInvoice: Esta entrada ya tiene una f. recibida
success: Se ha precontabilizado correctamente

View File

@ -3,15 +3,11 @@ import { ref, computed, onBeforeMount } 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 { getTotal } from 'src/composables/getTotal';
import CrudModel from 'src/components/CrudModel.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import useNotify from 'src/composables/useNotify.js';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import { toCurrency } from 'filters/index';
import VnTable from 'src/components/VnTable/VnTable.vue';
const route = useRoute();
const { notify } = useNotify();
@ -19,256 +15,187 @@ const { t } = useI18n();
const arrayData = useArrayData();
const invoiceIn = computed(() => arrayData.store.data);
const currency = computed(() => invoiceIn.value?.currency?.code);
const rowsSelected = ref([]);
const invoiceInFormRef = ref();
const invoiceInDueDayTableRef = ref();
const invoiceId = +route.params.id;
const filter = { where: { invoiceInFk: invoiceId } };
const areRows = ref(false);
const totalTaxableBase = ref();
const noMatch = computed(() => totalAmount.value != totalTaxableBase.value);
const totalVat = ref();
const tableRows = computed(
() => invoiceInDueDayTableRef.value?.CrudModelRef?.formData || [],
);
const totalAmount = computed(() => getTotal(tableRows.value, 'amount'));
const noMatch = computed(
() =>
totalAmount.value != totalTaxableBase.value &&
totalAmount.value != totalVat.value,
);
const columns = computed(() => [
{
name: 'duedate',
name: 'dueDated',
label: t('Date'),
field: (row) => toDate(row.dueDated),
sortable: true,
tabIndex: 1,
align: 'left',
component: 'date',
isEditable: true,
create: true,
},
{
name: 'bank',
name: 'bankFk',
label: t('Bank'),
field: (row) => row.bankFk,
model: 'bankFk',
optionLabel: 'bank',
url: 'Accountings',
sortable: true,
tabIndex: 2,
align: 'left',
component: 'select',
attrs: {
url: 'Accountings',
optionLabel: 'bank',
optionValue: 'id',
fields: ['id', 'bank'],
'emit-value': true,
},
format: ({ bank }) => bank?.bank,
isEditable: true,
create: true,
},
{
name: 'amount',
label: t('Amount'),
field: (row) => row.amount,
sortable: true,
tabIndex: 3,
align: 'left',
component: 'number',
attrs: {
clearable: true,
'clear-icon': 'close',
},
isEditable: true,
create: true,
},
{
name: 'foreignvalue',
name: 'foreignValue',
label: t('Foreign value'),
field: (row) => row.foreignValue,
sortable: true,
tabIndex: 4,
align: 'left',
component: 'number',
isEditable: isNotEuro(currency.value),
create: true,
},
]);
const totalAmount = computed(() => getTotal(invoiceInFormRef.value.formData, 'amount'));
const isNotEuro = (code) => code != 'EUR';
async function insert() {
await axios.post('/InvoiceInDueDays/new', { id: +invoiceId });
await invoiceInFormRef.value.reload();
await invoiceInDueDayTableRef.value.reload();
notify(t('globals.dataSaved'), 'positive');
}
async function setTaxableBase() {
const ref = invoiceInDueDayTableRef.value;
if (ref) ref.create = null;
const { data } = await axios.get(`InvoiceIns/${invoiceId}/getTotals`);
totalTaxableBase.value = data.totalTaxableBase;
totalVat.value = data.totalVat;
}
onBeforeMount(async () => await setTaxableBase());
</script>
<template>
<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)"
@save-changes="setTaxableBase"
>
<template #body="{ rows }">
<QTable
v-model:selected="rowsSelected"
selection="multiple"
:columns
:rows
row-key="$index"
:grid="$q.screen.lt.sm"
>
<template #body-cell-duedate="{ row }">
<QTd>
<VnInputDate v-model="row.dueDated" />
</QTd>
<div class="invoice-in-due-day">
<VnTable
v-if="invoiceIn"
ref="invoiceInDueDayTableRef"
data-key="InvoiceInDueDays"
url="InvoiceInDueDays"
save-url="InvoiceInDueDays/crud"
:filter="filter"
:user-filter="{
include: { relation: 'bank', scope: { fields: ['id', 'bank'] } },
}"
auto-load
:data-required="{ invoiceInFk: invoiceId }"
v-model:selected="rowsSelected"
@on-fetch="(data) => (areRows = !!data.length)"
@save-changes="setTaxableBase"
:columns="columns"
:is-editable="true"
:table="{ selection: 'multiple', 'row-key': '$index' }"
footer
:right-search="false"
:column-search="false"
:disable-option="{ card: true }"
class="q-pa-none"
>
<template #column-footer-amount>
<QChip
dense
:color="noMatch ? 'negative' : 'transparent'"
class="q-pa-xs"
:title="noMatch ? t('invoiceIn.noMatch', { totalTaxableBase }) : ''"
>
{{ toCurrency(totalAmount) }}
</QChip>
</template>
<template #column-footer-foreignValue>
<template v-if="isNotEuro(currency)">
{{
getTotal(tableRows, 'foreignValue', {
currency: currency,
})
}}
</template>
<template #body-cell-bank="{ row, col }">
<QTd>
<VnSelect
v-model="row[col.model]"
:url="col.url"
:option-label="col.optionLabel"
:option-value="col.optionValue"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt.id}: ${scope.opt.bank}`
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QTd>
</template>
<template #body-cell-amount="{ row }">
<QTd>
<VnInputNumber
v-model="row.amount"
:is-outlined="false"
clearable
clear-icon="close"
/>
</QTd>
</template>
<template #body-cell-foreignvalue="{ row }">
<QTd>
<VnInputNumber
:class="{
'no-pointer-events': !isNotEuro(currency),
}"
:disable="!isNotEuro(currency)"
v-model="row.foreignValue"
/>
</QTd>
</template>
<template #bottom-row>
<QTr class="bg">
<QTd />
<QTd />
<QTd />
<QTd>
<QChip
dense
:color="noMatch ? 'negative' : 'transparent'"
class="q-pa-xs"
:title="
noMatch
? t('invoiceIn.noMatch', { totalTaxableBase })
: ''
"
>
{{ toCurrency(totalAmount) }}
</QChip>
</QTd>
<QTd>
<template v-if="isNotEuro(invoiceIn.currency.code)">
{{
getTotal(rows, 'foreignValue', {
currency: invoiceIn.currency.code,
})
}}
</template>
</QTd>
</QTr>
</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>
<VnInputDate
class="full-width"
:label="t('Date')"
v-model="props.row.dueDated"
/>
</QItem>
<QItem>
<VnSelect
:label="t('Bank')"
class="full-width"
v-model="props.row['bankFk']"
url="Accountings"
option-label="bank"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt.id}: ${scope.opt.bank}`
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItem>
<QItem>
<VnInputNumber
:label="t('Amount')"
class="full-width"
v-model="props.row.amount"
/>
</QItem>
<QItem>
<VnInputNumber
:label="t('Foreign value')"
class="full-width"
:class="{
'no-pointer-events': !isNotEuro(currency),
}"
:disable="!isNotEuro(currency)"
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"
v-shortcut="'+'"
size="lg"
round
@click="
() => {
if (!areRows) insert();
else
invoiceInFormRef.insert({
amount: (totalTaxableBase - totalAmount).toFixed(2),
invoiceInFk: invoiceId,
});
}
"
/>
</QPageSticky>
</template>
</VnTable>
<QPageSticky :offset="[20, 20]" style="z-index: 2">
<QBtn
@click="
() => {
if (!areRows) return insert();
invoiceInDueDayTableRef.create = {
urlCreate: 'InvoiceInDueDays',
onDataSaved: () => invoiceInDueDayTableRef.reload(),
title: t('Create due day'),
formInitialData: {
invoiceInFk: invoiceId,
dueDated: Date.vnNew(),
amount: (totalTaxableBase - totalAmount).toFixed(2),
},
};
invoiceInDueDayTableRef.showForm = true;
}
"
color="primary"
fab
icon="add"
v-shortcut="'+'"
data-cy="invoiceInDueDayAdd"
/>
<QTooltip class="text-no-wrap">
{{ t('Create due day') }}
</QTooltip>
</QPageSticky>
</div>
</template>
<style lang="scss" scoped>
.bg {
background-color: var(--vn-light-gray);
}
.q-chip {
color: var(--vn-text-color);
}
.invoice-in-due-day {
display: flex;
flex-direction: column;
align-items: center;
}
:deep(.full-width) {
max-width: 900px;
}
</style>
<i18n>
es:
@ -276,4 +203,6 @@ onBeforeMount(async () => await setTaxableBase());
Bank: Caja
Amount: Importe
Foreign value: Divisa
invoiceIn.noMatch: El total {totalTaxableBase} no coincide con el importe
Create due day: Crear Vencimiento
</i18n>

View File

@ -3,71 +3,83 @@ import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { getTotal } from 'src/composables/getTotal';
import CrudModel from 'src/components/CrudModel.vue';
import FetchData from 'src/components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import VnTable from 'src/components/VnTable/VnTable.vue';
const { t } = useI18n();
const route = useRoute();
const invoceInIntrastat = ref([]);
const rowsSelected = ref([]);
const countries = ref([]);
const intrastats = ref([]);
const invoiceInFormRef = ref();
const invoiceInIntrastatRef = ref();
const invoiceInId = computed(() => +route.params.id);
const filter = { where: { invoiceInFk: invoiceInId.value } };
const filter = computed(() => ({ where: { invoiceInFk: invoiceInId.value } }));
const columns = computed(() => [
{
name: 'code',
name: 'intrastatFk',
label: t('Code'),
field: (row) => row.intrastatFk,
options: intrastats.value,
model: 'intrastatFk',
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.description}`,
sortable: true,
tabIndex: 1,
align: 'left',
component: 'select',
columnFilter: false,
attrs: {
options: intrastats.value,
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.description}`,
'data-cy': 'intrastat-code',
sortBy: 'id',
fields: ['id', 'description'],
filterOptions: ['id', 'description']
},
format: (row, dashIfEmpty) => dashIfEmpty(getCode(row)),
create: true,
isEditable: true,
width: 'max-content',
},
{
name: 'amount',
label: t('amount'),
field: (row) => row.amount,
sortable: true,
tabIndex: 2,
align: 'left',
component: 'number',
create: true,
isEditable: true,
columnFilter: false,
},
{
name: 'net',
label: t('net'),
field: (row) => row.net,
sortable: true,
tabIndex: 3,
align: 'left',
component: 'number',
create: true,
isEditable: true,
columnFilter: false,
},
{
name: 'stems',
label: t('stems'),
field: (row) => row.stems,
sortable: true,
tabIndex: 4,
align: 'left',
component: 'number',
create: true,
isEditable: true,
columnFilter: false,
},
{
name: 'country',
name: 'countryFk',
label: t('country'),
field: (row) => row.countryFk,
options: countries.value,
model: 'countryFk',
optionValue: 'id',
optionLabel: 'code',
sortable: true,
tabIndex: 5,
align: 'left',
component: 'select',
attrs: {
options: countries.value,
optionLabel: 'code',
},
create: true,
isEditable: true,
columnFilter: false,
},
]);
const tableRows = computed(
() => invoiceInIntrastatRef.value?.CrudModelRef?.formData || [],
);
function getCode(row) {
const code = intrastats.value.find(({ id }) => id === row.intrastatFk);
return code ? `${code.id}: ${code.description}` : null;
}
</script>
<template>
<FetchData
@ -82,165 +94,51 @@ const columns = computed(() => [
auto-load
@on-fetch="(data) => (intrastats = data)"
/>
<div class="invoiceIn-intrastat">
<CrudModel
ref="invoiceInFormRef"
<div class="invoice-in-intrastat">
<VnTable
ref="invoiceInIntrastatRef"
data-key="InvoiceInIntrastats"
url="InvoiceInIntrastats"
save-url="InvoiceInIntrastats/crud"
search-url="InvoiceInIntrastats"
auto-load
:data-required="{ invoiceInFk: invoiceInId }"
:filter="filter"
:insert-on-load="true"
:filter
:user-filter
v-model:selected="rowsSelected"
@on-fetch="(data) => (invoceInIntrastat = data)"
:columns
:is-editable="true"
:table="{ selection: 'multiple', 'row-key': '$index' }"
:create="{
urlCreate: 'InvoiceInIntrastats',
title: t('Create Intrastat'),
formInitialData: { invoiceInFk: invoiceInId },
onDataSaved: () => invoiceInIntrastatRef.reload(),
}"
footer
data-cy="invoice-in-intrastat-table"
:right-search="false"
:disable-option="{ card: true }"
>
<template #body="{ rows }">
<QTable
v-model:selected="rowsSelected"
selection="multiple"
:columns="columns"
:rows="rows"
row-key="$index"
:grid="$q.screen.lt.sm"
>
<template #body-cell="{ row, col }">
<QTd>
<VnInputNumber v-model="row[col.name]" />
</QTd>
</template>
<template #body-cell-code="{ row, col }">
<QTd>
<VnSelect
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:filter-options="['id', 'description']"
data-cy="intrastat-code"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
{{ `${scope.opt.id}: ${scope.opt.description}` }}
</QItem>
</template>
</VnSelect>
</QTd>
</template>
<template #body-cell-country="{ row, col }">
<QTd>
<VnSelect
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
/>
</QTd>
</template>
<template #bottom-row>
<QTr class="bg">
<QTd />
<QTd />
<QTd>
{{ getTotal(rows, 'amount', { currency: 'default' }) }}
</QTd>
<QTd>
{{ getTotal(rows, 'net') }}
</QTd>
<QTd>
{{ getTotal(rows, 'stems', { decimalPlaces: 0 }) }}
</QTd>
<QTd />
</QTr>
</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>
<VnSelect
:label="t('Code')"
class="full-width"
v-model="props.row['intrastatFk']"
:options="intrastats"
option-value="id"
:option-label="
(row) => `${row.id}:${row.description}`
"
:filter-options="['id', 'description']"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
{{
`${scope.opt.id}: ${scope.opt.description}`
}}
</QItem>
</template>
</VnSelect>
</QItem>
<QItem
v-for="(value, index) of [
'amount',
'net',
'stems',
]"
:key="index"
>
<VnInputNumber
:label="t(value)"
class="full-width"
v-model="props.row[value]"
clearable
clear-icon="close"
/>
</QItem>
<QItem>
<VnSelect
: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 #column-footer-amount>
{{ getTotal(tableRows, 'amount', { currency: 'default' }) }}
</template>
</CrudModel>
<template #column-footer-net>
{{ getTotal(tableRows, 'net') }}
</template>
<template #column-footer-stems>
{{ getTotal(tableRows, 'stems', { decimalPlaces: 0 }) }}
</template>
</VnTable>
</div>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn
color="primary"
icon="add"
v-shortcut="'+'"
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 lang="scss" scoped>
.invoice-in-intrastat {
display: flex;
flex-direction: column;
align-items: center;
}
:deep(.full-width) {
max-width: 900px;
}
</style>
<i18n>
@ -258,4 +156,5 @@ const columns = computed(() => [
Total amount: Total importe
Total net: Total neto
Total stems: Total tallos
Create Intrastat: Crear Intrastat
</i18n>

View File

@ -363,6 +363,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
<QTd>{{ toCurrency(entity.totals.totalTaxableBase) }}</QTd>
<QTd></QTd>
<QTd></QTd>
<QTd></QTd>
<QTd>{{ toCurrency(getTotalTax(entity.invoiceInTax)) }}</QTd>
<QTd>{{
entity.totals.totalTaxableBaseForeignValue &&

View File

@ -1,123 +1,178 @@
<script setup>
import { ref, computed, nextTick } from 'vue';
import { ref, computed, markRaw, useTemplateRef, onBeforeMount } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useArrayData } from 'src/composables/useArrayData';
import { getTotal } from 'src/composables/getTotal';
import { toCurrency } from 'src/filters';
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 VnTable from 'src/components/VnTable/VnTable.vue';
import VnSelectExpense from 'src/components/common/VnSelectExpense.vue';
import axios from 'axios';
import { useAccountShortToStandard } from 'src/composables/useAccountShortToStandard';
const { t } = useI18n();
const arrayData = useArrayData();
const expensesArrayData = useArrayData('expenses', { url: 'Expenses' });
const route = useRoute();
const invoiceIn = computed(() => arrayData.store.data);
const currency = computed(() => invoiceIn.value?.currency?.code);
const expenses = ref([]);
const expenses = computed(() => expensesArrayData.store.data);
const sageTaxTypes = ref([]);
const sageTransactionTypes = ref([]);
const rowsSelected = ref([]);
const invoiceInFormRef = ref();
const invoiceInVatTableRef = useTemplateRef('invoiceInVatTableRef');
defineProps({ actionIcon: { type: String, default: 'add' } });
defineProps({
actionIcon: {
type: String,
default: 'add',
},
});
function taxRate(invoiceInTax) {
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;
}
const columns = computed(() => [
{
name: 'expense',
name: 'expenseFk',
label: t('Expense'),
field: (row) => row.expenseFk,
options: expenses.value,
model: 'expenseFk',
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.name}`,
component: markRaw(VnSelectExpense),
format: (row, dashIfEmpty) => {
const expense = expenses.value?.find((e) => e.id === row.expenseFk);
return expense ? `${expense.id}: ${expense.name}` : dashIfEmpty(null);
},
sortable: true,
align: 'left',
isEditable: true,
create: true,
width: 'max-content',
cellEvent: {
keydown: async (evt, row) => {
if (evt.key !== 'Tab') return;
const val = evt.target.value;
if (!val || isNaN(val)) return;
row.expenseFk = expenses.value.find(
(e) => e.id === useAccountShortToStandard(val),
)?.id;
},
},
},
{
name: 'taxablebase',
name: 'taxableBase',
label: t('Taxable base'),
field: (row) => row.taxableBase,
model: 'taxableBase',
component: 'number',
attrs: {
clearable: true,
'clear-icon': 'close',
},
sortable: true,
align: 'left',
isEditable: true,
create: true,
},
{
name: 'isDeductible',
label: t('invoiceIn.isDeductible'),
field: (row) => row.isDeductible,
model: 'isDeductible',
component: 'checkbox',
align: 'center',
isEditable: true,
create: true,
createAttrs: {
defaultValue: true,
},
},
{
name: 'sageiva',
name: 'taxTypeSageFk',
label: t('Sage iva'),
field: (row) => row.taxTypeSageFk,
options: sageTaxTypes.value,
model: 'taxTypeSageFk',
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.vat}`,
component: 'select',
attrs: {
options: sageTaxTypes.value,
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.vat}`,
filterOptions: ['id', 'vat'],
'data-cy': 'vat-sageiva',
},
format: ({ taxTypeSageFk }, dashIfEmpty) => {
const taxType = sageTaxTypes.value.find((t) => t.id === taxTypeSageFk);
return taxType ? `${taxType.id}: ${taxType.vat}` : dashIfEmpty(taxTypeSageFk);
},
sortable: true,
align: 'left',
isEditable: true,
create: true,
width: 'max-content',
},
{
name: 'sagetransaction',
name: 'transactionTypeSageFk',
label: t('Sage transaction'),
field: (row) => row.transactionTypeSageFk,
options: sageTransactionTypes.value,
model: 'transactionTypeSageFk',
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.transaction}`,
component: 'select',
attrs: {
options: sageTransactionTypes.value,
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.transaction}`,
filterOptions: ['id', 'transaction'],
},
format: ({ transactionTypeSageFk }, dashIfEmpty) => {
const transType = sageTransactionTypes.value.find(
(t) => t.id === transactionTypeSageFk,
);
return transType
? `${transType.id}: ${transType.transaction}`
: dashIfEmpty(transactionTypeSageFk);
},
sortable: true,
align: 'left',
isEditable: true,
create: true,
width: 'max-content',
},
{
name: 'rate',
label: t('Rate'),
sortable: true,
field: (row) => taxRate(row, row.taxTypeSageFk),
sortable: false,
format: (row) => taxRate(row).toFixed(2),
align: 'left',
},
{
name: 'foreignvalue',
name: 'foreignValue',
label: t('Foreign value'),
component: 'number',
sortable: true,
field: (row) => row.foreignValue,
align: 'left',
create: true,
isEditable: isNotEuro(currency.value),
format: (row, dashIfEmpty) => dashIfEmpty(row.foreignValue),
cellEvent: {
'update:modelValue': async (value, oldValue, row) =>
await handleForeignValueUpdate(value, row),
},
},
{
name: 'total',
label: 'Total',
label: t('Total'),
align: 'left',
format: (row) => (Number(row.taxableBase || 0) + Number(taxRate(row))).toFixed(2),
},
]);
const tableRows = computed(
() => invoiceInVatTableRef.value?.CrudModelRef?.formData || [],
);
const taxableBaseTotal = computed(() => {
return getTotal(invoiceInFormRef.value.formData, 'taxableBase');
return getTotal(tableRows.value, 'taxableBase');
});
const taxRateTotal = computed(() => {
return getTotal(invoiceInFormRef.value.formData, null, {
cb: taxRate,
});
return tableRows.value.reduce((sum, row) => sum + Number(taxRate(row)), 0);
});
const combinedTotal = computed(() => {
return +taxableBaseTotal.value + +taxRateTotal.value;
});
const filter = {
const filter = computed(() => ({
fields: [
'id',
'invoiceInFk',
@ -131,389 +186,75 @@ const filter = {
where: {
invoiceInFk: route.params.id,
},
};
}));
onBeforeMount(async () => await expensesArrayData.fetch({}));
const isNotEuro = (code) => code != 'EUR';
function taxRate(invoiceInTax) {
const sageTaxTypeId = invoiceInTax.taxTypeSageFk;
const taxRateSelection = sageTaxTypes.value.find(
(transaction) => transaction.id == sageTaxTypeId,
async function handleForeignValueUpdate(val, row) {
if (!isNotEuro(currency.value)) return;
row.taxableBase = await getExchange(
val,
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>
<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"
<VnTable
v-if="invoiceIn"
ref="invoiceInVatTableRef"
data-key="InvoiceInTaxes"
url="InvoiceInTaxes"
save-url="InvoiceInTaxes/crud"
:filter="filter"
:data-required="{ invoiceInFk: $route.params.id }"
:insert-on-load="true"
auto-load
v-model:selected="rowsSelected"
:columns="columns"
: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(),
}"
:go-to="`/invoice-in/${$route.params.id}/due-day`"
>
<template #body="{ rows }">
<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 #column-footer-taxableBase>
{{ toCurrency(taxableBaseTotal) }}
</template>
</CrudModel>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn
color="primary"
icon="add"
size="lg"
v-shortcut="'+'"
round
@click="invoiceInFormRef.insert()"
>
<QTooltip>{{ t('Add tax') }}</QTooltip>
</QBtn>
</QPageSticky>
<template #column-footer-rate>
{{ toCurrency(taxRateTotal) }}
</template>
<template #column-footer-total>
{{ toCurrency(combinedTotal) }}
</template>
</VnTable>
</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>
es:
Expense: Gasto
Create a new expense: Crear nuevo gasto
Add tax: Crear gasto
Add tax: Añadir Gasto/IVA # Changed label slightly
Taxable base: Base imp.
Sage tax: Sage iva
Sage iva: Sage iva # Kept original label
Sage transaction: Sage transacción
Rate: Tasa
Rate: Cuota # Changed label
Foreign value: Divisa
Total: Total
invoiceIn.isDeductible: Deducible
</i18n>

View File

@ -41,7 +41,7 @@ async function checkToBook(id) {
params: {
where: JSON.stringify({
invoiceInFk: id,
dueDated: { gte: Date.vnNew() },
dueDated: { lte: Date.vnNew() },
}),
},
})

View File

@ -69,4 +69,5 @@ invoiceIn:
isBooked: Is booked
account: Ledger account
correctingFk: Rectificative
issued: Issued
noMatch: No match with the vat({totalTaxableBase})

View File

@ -67,4 +67,5 @@ invoiceIn:
isBooked: Contabilizada
account: Cuenta contable
correctingFk: Rectificativa
issued: Fecha de emisión
noMatch: No cuadra con el iva({totalTaxableBase})

View File

@ -13,6 +13,7 @@ export default {
'daysInForward',
'availabled',
'awbFk',
'isOrdered',
'isDelivered',
'isReceived',
],

View File

@ -1,33 +1,38 @@
/// <reference types="cypress" />
describe('InvoiceInIntrastat', () => {
const firstRow = 'tbody > :nth-child(1)';
const thirdRow = 'tbody > :nth-child(3)';
const codes = `[data-cy="intrastat-code"]`;
const firstRowAmount = `${firstRow} > :nth-child(3)`;
const firstInstrastat = 'td[data-col-field="intrastatFk"][data-row-index="0"]';
const firstAmount = 'td[data-col-field="amount"][data-row-index="0"]';
const intrastat = 'Plantas vivas: Esqueje/injerto, Vid';
beforeEach(() => {
before(() => {
cy.login('administrative');
cy.visit(`/#/invoice-in/1/intrastat`);
});
it('should edit the first line', () => {
cy.selectOption(`${firstRow} ${codes}`, 'Plantas vivas: Esqueje/injerto, Vid');
cy.get(firstInstrastat).click().type(`{selectall}{backspace}${intrastat}{enter}`);
cy.get(firstAmount).click().type('10{selectall}{backspace}20{enter}');
cy.saveCard();
cy.get(codes).eq(0).contains('6021010: Plantas vivas: Esqueje/injerto, Vid');
cy.get(firstInstrastat).should('have.text', `6021010: ${intrastat}`);
});
it('should add a new row', () => {
cy.addRow();
cy.fillRow(thirdRow, [
false,
'Plantas vivas: Esqueje/injerto, Vid',
30,
10,
5,
'FR',
]);
cy.saveCard();
cy.checkNotification('Data saved');
cy.dataCy('vnTableCreateBtn').click();
cy.fillInForm(
{
'intrastat-code': {
val: 'Plantas vivas: Esqueje/injerto, Vid',
type: 'select',
},
Amount_input: '30',
Net_input: '10',
Stems_input: '5',
Country_select: { val: 'FR', type: 'select' },
},
{ attr: 'data-cy' },
);
cy.dataCy('FormModelPopup_save').click();
cy.get('.q-notification__message').should('have.text', 'Data created');
});
it('should remove the first line', () => {

View File

@ -1,37 +1,41 @@
/// <reference types="cypress" />
describe('InvoiceInVat', () => {
const thirdRow = 'tbody > :nth-child(3)';
const firstLineVat = 'tbody > :nth-child(1) ';
const vats = '[data-cy="vat-sageiva"]';
const dialogInputs = '.q-dialog label input';
const addBtn = 'tbody tr:nth-child(1) td:nth-child(2) .--add-icon';
const randomInt = Math.floor(Math.random() * 100);
const firstTaxType = 'td[data-col-field="taxTypeSageFk"][data-row-index="0"]';
const firstExpense = 'td[data-col-field="expenseFk"][data-row-index="0"]';
const firstDeductible = 'td[data-col-field="isDeductible"][data-row-index="0"]';
const taxType = 'H.P. IVA 21% CEE';
beforeEach(() => {
before(() => {
cy.login('administrative');
cy.visit(`/#/invoice-in/1/vat`);
});
it('should edit the sage iva', () => {
cy.selectOption(`${firstLineVat} ${vats}`, 'H.P. IVA 21% CEE');
cy.get(firstTaxType).click().type(`{selectall}{backspace}${taxType}{enter}`);
cy.saveCard();
cy.get(vats).eq(0).contains('8: H.P. IVA 21% CEE');
cy.get(firstTaxType).should('have.text', `8: ${taxType}`);
});
it('should mark the line as deductible VAT', () => {
cy.get(`${firstLineVat} [data-cy="isDeductible_checkbox"]`).click();
cy.saveCard();
cy.get(`${firstLineVat} [data-cy="isDeductible_checkbox"]`).click();
cy.get(firstDeductible).click().click();
cy.saveCard();
cy.get('.q-notification__message').should('have.text', 'Data saved');
});
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');
cy.dataCy('vnTableCreateBtn').click();
cy.fillInForm(
{
Expense_select: { val: '2000000001', type: 'select' },
'Taxable base_input': '30',
'vat-sageiva': { val: 'H.P. IVA 10', type: 'select' },
},
{ attr: 'data-cy' },
);
cy.dataCy('FormModelPopup_save').click();
cy.get('.q-notification__message').should('have.text', 'Data created');
});
it('should remove the first line', () => {
@ -39,10 +43,10 @@ describe('InvoiceInVat', () => {
});
it('should correctly handle expense addition', () => {
cy.get(addBtn).click();
cy.get(firstExpense).click();
cy.get('.--add-icon').click();
cy.get(dialogInputs).eq(0).type(randomInt);
cy.get(dialogInputs).eq(1).type('This is a dummy expense');
cy.get('[data-cy="FormModelPopup_save"]').click();
cy.get('.q-notification__message').should('have.text', 'Data created');
});