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

This commit is contained in:
Jose Antonio Tubau 2025-05-19 11:03:19 +00:00
commit cabee31109
29 changed files with 845 additions and 1043 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);
@ -183,9 +189,8 @@ async function onSubmit() {
});
}
isLoading.value = true;
await saveChanges($props.saveFn ? formData.value : null);
}
async function onSubmitAndGo() {
@ -194,10 +199,10 @@ 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) {
$props.saveFn(data, getChanges);
isLoading.value = false;
@ -228,31 +233,29 @@ 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;
if (formData.value.length && isLastRowEmpty) return;
const $index = formData.value.length ? formData.value.at(-1).$index + 1 : 0;
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

@ -18,7 +18,7 @@ const arrayData = defineModel({
function handler(event) {
const clickedElement = event.target.closest('td');
if (!clickedElement) return;
event.preventDefault();
target.value = event.target;
qmenuRef.value.show();
colField.value = clickedElement.getAttribute('data-col-field');

View File

@ -151,6 +151,10 @@ const $props = defineProps({
type: String,
default: 'vnTable',
},
selectionFn: {
type: Function,
default: null,
},
});
const { t } = useI18n();
@ -218,10 +222,7 @@ onBeforeMount(() => {
onMounted(async () => {
if ($props.isEditable) document.addEventListener('click', clickHandler);
document.addEventListener('contextmenu', (event) => {
event.preventDefault();
contextMenuRef.value.handler(event);
});
document.addEventListener('contextmenu', contextMenuRef.value.handler);
mode.value =
quasar.platform.is.mobile && !$props.disableOption?.card
? CARD_MODE
@ -338,10 +339,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 +396,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 +450,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 +484,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 +660,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 +708,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 +735,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

@ -0,0 +1,43 @@
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import VnAccountNumber from 'src/components/common/VnAccountNumber.vue';
describe('VnAccountNumber', () => {
let wrapper;
let input;
let vnInput;
let spyShort;
beforeEach(() => {
wrapper = createWrapper(VnAccountNumber);
wrapper = wrapper.wrapper;
input = wrapper.find('input');
vnInput = wrapper.findComponent({ name: 'VnInput' });
spyShort = vi.spyOn(wrapper.vm, 'useAccountShortToStandard');
});
it('should filter out non-numeric characters on input event', async () => {
await input.setValue('abc123.45!@#');
const emitted = wrapper.emitted('update:modelValue');
expect(emitted.pop()[0]).toBe('123.45');
expect(spyShort).not.toHaveBeenCalled();
});
it('should apply conversion on blur when valid short value is provided', async () => {
await input.setValue('123.45');
await vnInput.trigger('blur');
const emitted = wrapper.emitted('update:modelValue');
expect(emitted.pop()[0]).toBe('1230000045');
expect(spyShort).toHaveBeenCalled();
});
it('should not change value for invalid input values', async () => {
await input.setValue('123');
await vnInput.trigger('blur');
const emitted = wrapper.emitted('update:modelValue');
expect(emitted.pop()[0]).toBe('123');
expect(spyShort).toHaveBeenCalled();
});
});

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

@ -6,13 +6,7 @@ import { useRequired } from 'src/composables/useRequired';
const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const { t } = useI18n();
const emit = defineEmits([
'update:modelValue',
'update:options',
'keyup.enter',
'remove',
'blur',
]);
const emit = defineEmits(['update:modelValue', 'update:options', 'remove']);
const $props = defineProps({
modelValue: {
@ -126,6 +120,14 @@ const handleInsertMode = (e) => {
const handleUppercase = () => {
value.value = value.value?.toUpperCase() || '';
};
const listeners = computed(() =>
Object.fromEntries(
Object.entries($attrs).filter(
([key, val]) => key.startsWith('on') && typeof val === 'function',
),
),
);
</script>
<template>
@ -134,10 +136,9 @@ const handleUppercase = () => {
ref="vnInputRef"
v-model="value"
v-bind="{ ...$attrs, ...styleAttrs }"
v-on="listeners"
:type="$attrs.type"
:class="{ required: isRequired }"
@keyup.enter="emit('keyup.enter')"
@blur="emit('blur')"
@keydown="handleKeydown"
:clearable="false"
:rules="mixinRules"

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

@ -7,6 +7,8 @@ import FetchData from 'components/FetchData.vue';
import VnSelect from 'components/common/VnSelect.vue';
import { tMobile } from 'composables/tMobile';
import VnSelectWorker from 'src/components/common/VnSelectWorker.vue';
import axios from 'axios';
import { useArrayData } from 'composables/useArrayData';
const route = useRoute();
@ -19,6 +21,7 @@ const claimResponsibles = ref([]);
const claimRedeliveries = ref([]);
const selected = ref([]);
const saveButtonRef = ref();
const arrayData = useArrayData('Claim');
const developmentsFilter = computed(() => {
return {
@ -105,6 +108,32 @@ const columns = computed(() => [
align: 'left',
},
]);
const handleWorker = async (row) => {
const { claimResponsibleFk } = row;
if (!claimResponsibleFk) {
row.workerFk = null;
return;
}
const commercialResponsible = claimResponsibles?.value?.find(
(responsible) => responsible.code === 'com',
);
const claim = arrayData.store.data;
if (claimResponsibleFk === commercialResponsible?.id) {
row.workerFk = claim.workerFk;
return;
}
const { data } = await axios.get(
`ClaimDevelopments/${claim.ticketFk}/getResponsible/${claimResponsibleFk}`,
);
row.workerFk = data?.userFk ?? null;
};
</script>
<template>
<FetchData
@ -166,6 +195,20 @@ const columns = computed(() => [
input-debounce="0"
hide-selected
/>
<VnSelect
v-else-if="col.name == 'claimResponsible'"
v-model="row[col.model]"
:url="col.url"
:where="col.where"
:sort-by="col.sortBy"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
@update:modelValue="handleWorker(row)"
:autofocus="col.tabIndex == 1"
input-debounce="0"
hide-selected
/>
<VnSelect
v-else
v-model="row[col.model]"

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

@ -386,6 +386,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

@ -61,6 +61,7 @@ invoiceIn:
isBooked: Is booked
account: Ledger account
correctingFk: Rectificative
issued: Issued
noMatch: No match with the vat({totalTaxableBase})
linkVehicleToInvoiceIn: Link vehicle to invoice
unlinkedVehicle: Unlinked vehicle

View File

@ -60,6 +60,7 @@ invoiceIn:
isBooked: Contabilizada
account: Cuenta contable
correctingFk: Rectificativa
issued: Fecha de emisión
noMatch: No cuadra con el iva({totalTaxableBase})
linkVehicleToInvoiceIn: Vincular vehículo a factura
unlinkedVehicle: Vehículo desvinculado

View File

@ -68,6 +68,19 @@ onMounted(async () => {
<template #menu="{ entity }">
<RouteDescriptorMenu :route="entity" />
</template>
<template #actions="{ entity }">
<QCardActions class="flex justify-center" style="padding-inline: 0">
<QBtn
size="md"
icon="vn:delivery"
color="primary"
:href="`https://grafana.verdnatura.es/d/edkvyi479dbeob/pronostico-de-entregas?orgId=1&var-vRouteFk=${entity.id}`"
target="_blank"
>
<QTooltip>{{ $t('route.deliveryForecast') }}</QTooltip>
</QBtn>
</QCardActions>
</template>
</EntityDescriptor>
</template>
<i18n>

View File

@ -4,7 +4,7 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import { QIcon } from 'quasar';
import { dashIfEmpty, toCurrency, toDate, toHour } from 'src/filters';
import { dashIfEmpty, toCurrency, toDate, toDateHourMinSec, toHour } from 'src/filters';
import { openBuscaman } from 'src/utils/buscaman';
import CardSummary from 'components/ui/CardSummary.vue';
import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue';
@ -80,6 +80,20 @@ const ticketColumns = ref([
sortable: false,
align: 'left',
},
{
name: 'delivered',
label: t('route.delivered'),
field: (row) => dashIfEmpty(toDateHourMinSec(row?.delivered)),
sortable: false,
align: 'center',
},
{
name: 'estimated',
label: t('route.estimated'),
field: (row) => dashIfEmpty(toDateHourMinSec(row?.estimated)),
sortable: false,
align: 'center',
},
{
name: 'packages',
label: t('route.summary.packages'),
@ -89,7 +103,7 @@ const ticketColumns = ref([
},
{
name: 'volume',
label: t('route.summary.m3'),
label: 'm³',
field: (row) => row?.volume,
sortable: false,
align: 'center',
@ -267,61 +281,3 @@ const ticketColumns = ref([
</CardSummary>
</div>
</template>
<i18n>
en:
route:
summary:
date: Date
agency: Agency
vehicle: Vehicle
driver: Driver
cost: Cost
started: Started time
finished: Finished time
kmStart: Km start
kmEnd: Km end
volume: Volume
packages: Packages
description: Description
tickets: Tickets
order: Order
street: Street
city: City
pc: PC
client: Client
state: State
m3:
packaging: Packaging
ticket: Ticket
closed: Closed
open: Open
yes: Yes
no: No
es:
route:
summary:
date: Fecha
agency: Agencia
vehicle: Vehículo
driver: Conductor
cost: Costo
started: Hora inicio
finished: Hora fin
kmStart: Km inicio
kmEnd: Km fin
volume: Volumen
packages: Bultos
description: Descripción
tickets: Tickets
order: Orden
street: Dirección fiscal
city: Población
pc: CP
client: Cliente
state: Estado
packaging: Encajado
closed: Cerrada
open: Abierta
yes:
no: No
</i18n>

View File

@ -2,7 +2,7 @@
import VnPaginate from 'components/ui/VnPaginate.vue';
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import { dashIfEmpty } from 'src/filters';
import { dashIfEmpty, toDateHourMinSec } from 'src/filters';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'components/common/VnInput.vue';
import axios from 'axios';
@ -24,49 +24,63 @@ const selectedRows = ref([]);
const columns = computed(() => [
{
name: 'order',
label: t('Order'),
label: t('route.ticket.order'),
field: (row) => dashIfEmpty(row?.priority),
sortable: false,
align: 'center',
},
{
name: 'client',
label: t('Client'),
label: t('route.ticket.client'),
field: (row) => row?.nickname,
sortable: false,
align: 'left',
},
{
name: 'street',
label: t('Street'),
label: t('route.ticket.street'),
field: (row) => row?.street,
sortable: false,
align: 'left',
},
{
name: 'pc',
label: t('PC'),
label: t('route.ticket.PC'),
field: (row) => row?.postalCode,
sortable: false,
align: 'center',
},
{
name: 'city',
label: t('City'),
label: t('route.ticket.city'),
field: (row) => row?.city,
sortable: false,
align: 'left',
},
{
name: 'warehouse',
label: t('Warehouse'),
label: t('route.ticket.warehouse'),
field: (row) => row?.warehouseName,
sortable: false,
align: 'left',
},
{
name: 'delivered',
label: t('route.delivered'),
field: (row) => dashIfEmpty(toDateHourMinSec(row?.delivered)),
sortable: false,
align: 'left',
},
{
name: 'estimated',
label: t('route.estimated'),
field: (row) => dashIfEmpty(toDateHourMinSec(row?.estimated)),
sortable: false,
align: 'left',
},
{
name: 'packages',
label: t('Packages'),
label: t('route.ticket.packages'),
field: (row) => row?.packages,
sortable: false,
align: 'center',
@ -80,14 +94,14 @@ const columns = computed(() => [
},
{
name: 'packaging',
label: t('Packaging'),
label: t('route.ticket.packaging'),
field: (row) => row?.ipt,
sortable: false,
align: 'center',
},
{
name: 'ticket',
label: t('Ticket'),
label: t('route.ticket.ticket'),
field: (row) => row?.id,
sortable: false,
align: 'center',
@ -188,8 +202,8 @@ const confirmRemove = (ticket) => {
.dialog({
component: VnConfirm,
componentProps: {
title: t('Confirm removal from route'),
message: t('Are you sure you want to remove this ticket from the route?'),
title: t('route.ticket.confirmRemoval'),
message: t('route.ticket.confirmRemovalConfirmation'),
promise: () => removeTicket(ticket),
},
})
@ -219,7 +233,7 @@ const openSmsDialog = async () => {
quasar.dialog({
component: SendSmsDialog,
componentProps: {
title: t('Send SMS to the selected tickets'),
title: t('route.ticket.sendSmsTickets'),
url: 'Routes/sendSms',
destinationFk: clientsId.toString(),
destination: clientsPhone.toString(),
@ -240,18 +254,25 @@ const openSmsDialog = async () => {
<QDialog v-model="confirmationDialog">
<QCard style="min-width: 350px">
<QCardSection>
<p class="text-h6 q-ma-none">{{ t('Select the starting date') }}</p>
<p class="text-h6 q-ma-none">
{{ t('route.ticket.selectStartingDate') }}
</p>
</QCardSection>
<QCardSection class="q-pt-none">
<VnInputDate
:label="t('Stating date')"
:label="t('route.ticket.startingDate')"
v-model="startingDate"
autofocus
/>
</QCardSection>
<QCardActions align="right">
<QBtn flat :label="t('Cancel')" v-close-popup class="text-primary" />
<QBtn
flat
:label="t('globals.cancel')"
v-close-popup
class="text-primary"
/>
<QBtn color="primary" v-close-popup @click="cloneRoutes">
{{ t('globals.clone') }}
</QBtn>
@ -262,7 +283,7 @@ const openSmsDialog = async () => {
<QToolbar class="justify-end">
<div id="st-actions" class="q-pa-sm">
<QBtn icon="vn:wand" color="primary" class="q-mr-sm" @click="sortRoutes">
<QTooltip>{{ t('Sort routes') }}</QTooltip>
<QTooltip>{{ t('route.ticket.sortRoutes') }}</QTooltip>
</QBtn>
<QBtn
icon="vn:buscaman"
@ -271,7 +292,7 @@ const openSmsDialog = async () => {
:disable="!selectedRows?.length"
@click="goToBuscaman()"
>
<QTooltip>{{ t('Open buscaman') }}</QTooltip>
<QTooltip>{{ t('route.ticket.openBuscaman') }}</QTooltip>
</QBtn>
<QBtn
icon="filter_alt"
@ -280,7 +301,7 @@ const openSmsDialog = async () => {
:disable="!selectedRows?.length"
@click="deletePriorities"
>
<QTooltip>{{ t('Delete priority') }}</QTooltip>
<QTooltip>{{ t('route.ticket.deletePriority') }}</QTooltip>
</QBtn>
<QBtn
icon="format_list_numbered"
@ -288,11 +309,7 @@ const openSmsDialog = async () => {
class="q-mr-sm"
@click="setOrderedPriority"
>
<QTooltip
>{{
t('Renumber all tickets in the order you see on the screen')
}}
</QTooltip>
<QTooltip>{{ t('route.ticket.renumberAllTickets') }} </QTooltip>
</QBtn>
<QBtn
icon="sms"
@ -301,7 +318,7 @@ const openSmsDialog = async () => {
:disable="!selectedRows?.length"
@click="openSmsDialog"
>
<QTooltip>{{ t('Send SMS to all clients') }}</QTooltip>
<QTooltip>{{ t('route.ticket.sendSmsClients') }}</QTooltip>
</QBtn>
</div>
</QToolbar>
@ -339,7 +356,11 @@ const openSmsDialog = async () => {
@click="setHighestPriority(row, rows)"
>
<QTooltip>
{{ t('Assign highest priority') }}
{{
t(
'route.ticket.assignHighestPriority',
)
}}
</QTooltip>
</QIcon>
<VnInput
@ -354,7 +375,9 @@ const openSmsDialog = async () => {
<QTd>
<span class="link" @click="goToBuscaman(row)">
{{ value }}
<QTooltip>{{ t('Open buscaman') }}</QTooltip>
<QTooltip>{{
t('route.ticket.openBuscaman')
}}</QTooltip>
</span>
</QTd>
</template>
@ -411,7 +434,7 @@ const openSmsDialog = async () => {
@click="openTicketsDialog"
>
<QTooltip>
{{ t('Add ticket') }}
{{ t('route.ticket.addTicket') }}
</QTooltip>
</QBtn>
</QPageSticky>
@ -432,24 +455,3 @@ const openSmsDialog = async () => {
gap: 12px;
}
</style>
<i18n>
es:
Order: Orden
Street: Dirección fiscal
City: Población
PC: CP
Client: Cliente
Warehouse: Almacén
Packages: Bultos
Packaging: Encajado
Confirm removal from route: Quitar de la ruta
Are you sure you want to remove this ticket from the route?: ¿Seguro que quieres quitar este ticket de la ruta?
Sort routes: Ordenar rutas
Open buscaman: Abrir buscaman
Delete priority: Borrar orden
Renumber all tickets in the order you see on the screen: Renumerar todos los tickets con el orden que ves por pantalla
Assign highest priority: Asignar máxima prioridad
Send SMS to all clients: Mandar sms a todos los clientes de las rutas
Send SMS to the selected tickets: Enviar SMS a los tickets seleccionados
Add ticket: Añadir ticket
</i18n>

View File

@ -1,6 +1,33 @@
route:
filter:
Served: Served
summary:
date: Date
agency: Agency
vehicle: Vehicle
driver: Driver
cost: Cost
started: Started time
finished: Finished time
kmStart: Km start
kmEnd: Km end
volume: Volume
packages: Packages
description: Description
tickets: Tickets
order: Order
street: Street
city: City
pc: PC
client: Client
state: State
m3:
packaging: Packaging
ticket: Ticket
closed: Closed
open: Open
yes: Yes
no: No
extendedList:
selectStartingDate: Select the starting date
startingDate: Starting date
@ -75,3 +102,45 @@ route:
searchInfo: You can search by route reference
dated: Dated
preview: Preview
delivered: Delivered
estimated: Estimated
cmr:
search: Search Cmr
searchInfo: You can search Cmr by Id
params:
results: results
cmrFk: CMR id
hasCmrDms: Attached in gestdoc
true: Yes
false: No
ticketFk: Ticketd id
routeFk: Route id
countryFk: Country
clientFk: Client id
warehouseFk: Warehouse
shipped: Preparation date
viewCmr: View CMR
downloadCmrs: Download CMRs
search: General search
ticket:
order: Order
street: Street
city: City
PC: PC
client: Client
warehouse: Warehouse
packages: Packages
packaging: Packaging
ticket: Ticket
confirmRemoval: Confirm removal from route
confirmRemovalConfirmation: Are you sure you want to remove this ticket from the route?
selectStartingDate: Select the starting date
startingDate: Starting date
sortRoutes: Sort routes
openBuscaman: Open buscaman
deletePriority: Delete priority
renumberAllTickets: Renumber all tickets in the order you see on the screen
assignHighest: Assign highest priority
sendSmsTickets: Send SMS to the selected tickets
sendSmsClients: Send SMS to all clients
addTicket: Add ticket

View File

@ -1,6 +1,31 @@
route:
filter:
Served: Servida
summary:
date: Fecha
agency: Agencia
vehicle: Vehículo
driver: Conductor
cost: Costo
started: Hora inicio
finished: Hora fin
kmStart: Km inicio
kmEnd: Km fin
volume: Volumen
packages: Bultos
description: Descripción
tickets: Tickets
order: Orden
street: Dirección fiscal
city: Población
pc: CP
client: Cliente
state: Estado
packaging: Encajado
closed: Cerrada
open: Abierta
yes:
no: No
extendedList:
selectStartingDate: Seleccione la fecha de inicio
statingDate: Fecha de inicio
@ -76,13 +101,15 @@ route:
searchInfo: Puedes buscar por referencia de la ruta
dated: Fecha
preview: Vista previa
delivered: Entregado
estimated: Pronóstico
cmr:
list:
results: resultados
cmrFk: Id CMR
hasCmrDms: Gestdoc
'true':
'false': 'No'
true:
false: No
ticketFk: Id ticket
routeFk: Id ruta
country: País
@ -90,3 +117,25 @@ route:
shipped: Fecha preparación
viewCmr: Ver CMR
downloadCmrs: Descargar CMRs
ticket:
order: Orden
street: Dirección fiscal
city: Población
PC: CP
client: Cliente
warehouse: Almacén
packages: Bultos
packaging: Encajado
ticket: Ticket
confirmRemoval: Quitar de la ruta
confirmRemovalConfirmation: ¿Seguro que quieres quitar este ticket de la ruta?
selectStartingDate: Seleccionar fecha de inicio
startingDate: F. Inicio
sortRoutes: Ordenar rutas
openBuscaman: Abrir buscaman
deletePriority: Borrar orden
renumberAllTickets: Renumerar todos los tickets con el orden que ves por pantalla
assignHighest: Asignar máxima prioridad
sendSmsTickets: Enviar SMS a los tickets seleccionados
sendSmsClients: Mandar sms a todos los clientes de las rutas
addTicket: Añadir ticket

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