Compare commits

..

35 Commits

Author SHA1 Message Date
Jon Elias d4f4f03485 Merge branch '8193-AddFilterInClientNotes' of https: refs #8193//gitea.verdnatura.es/verdnatura/salix-front into 8193-AddFilterInClientNotes
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-05-12 11:08:22 +02:00
Jon Elias 68860c272c fix: refs #8193 conflicts 2025-05-12 11:08:07 +02:00
Jon Elias f01fd66c85 Merge branch 'dev' of https: refs #8193//gitea.verdnatura.es/verdnatura/salix-front into 8193-AddFilterInClientNotes 2025-05-12 11:07:49 +02:00
Alex Moreno 484559e06c refactor: refs #8193 update worker references to user in CustomerNotes component
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-05-12 09:54:22 +02:00
Alex Moreno ab9bca2327 Merge branch 'dev' of https: refs #8193//gitea.verdnatura.es/verdnatura/salix-front into 8193-AddFilterInClientNotes
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-05-12 08:22:10 +02:00
Jon Elias bb67bdbc29 Merge branch 'dev' of https: refs #8193//gitea.verdnatura.es/verdnatura/salix-front into 8193-AddFilterInClientNotes
gitea/salix-front/pipeline/pr-dev This commit looks good Details
2025-05-08 09:22:50 +02:00
Jon Elias 431147a025 refactor: refs #8193 make VnNotes fetch data to avoid double request
gitea/salix-front/pipeline/pr-dev This commit looks good Details
2025-04-30 15:07:27 +02:00
Jon Elias e636ddda0d Merge branch '8193-AddFilterInClientNotes' of https://gitea.verdnatura.es/verdnatura/salix-front into 8193-AddFilterInClientNotes
gitea/salix-front/pipeline/pr-dev This commit looks good Details
2025-04-25 13:19:43 +02:00
Jon Elias 360340111c refactor: refs #8193 deleted unused code 2025-04-25 13:19:42 +02:00
Jon Elias 3e864264b0 Merge branch 'dev' into 8193-AddFilterInClientNotes 2025-04-24 14:19:56 +00:00
Jon Elias 200aed4ccc feat: refs #8193 add default observationType rely on user department
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-04-24 12:53:16 +02:00
Jon Elias bb8699f026 feat: refs #8193 customer notes e2e
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-04-22 13:42:08 +02:00
Jon Elias 0d5539796c feat: refs #8193 undo code until #7248 is done
gitea/salix-front/pipeline/pr-dev Build queued... Details
2025-04-22 13:40:19 +02:00
Jon Elias 636b10088e Merge branch '8193-AddFilterInClientNotes' of https://gitea.verdnatura.es/verdnatura/salix-front into 8193-AddFilterInClientNotes
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-04-22 13:38:59 +02:00
Jon Elias 897d6455ec feat: refs #8193 customer filter 2025-04-22 13:38:57 +02:00
Jon Elias 3270da5bfd Merge branch 'dev' into 8193-AddFilterInClientNotes
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-04-17 14:14:07 +00:00
Jon Elias 9c0334349b Merge branch 'dev' into 8193-AddFilterInClientNotes
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-04-17 12:17:10 +00:00
Jon Elias ad3edab617 feat: refs #8193 wip use arrayData in notes filter
gitea/salix-front/pipeline/pr-dev There was a failure building this commit Details
2025-04-14 16:09:01 +02:00
Jon Elias d3828fc068 refactor: refs #8193 noteFilter
gitea/salix-front/pipeline/pr-dev There was a failure building this commit Details
2025-04-14 15:52:10 +02:00
Jon Elias b548f34c81 feat: refs #8193 filtering working except worker module
gitea/salix-front/pipeline/pr-dev There was a failure building this commit Details
2025-04-14 15:48:53 +02:00
Jon Elias db021194c2 refactor: refs #8193 modified default observation type to adapt it to user
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-04-14 11:56:20 +02:00
Jon Elias d1eab07b76 Merge branch '8193-AddFilterInClientNotes' of https://gitea.verdnatura.es/verdnatura/salix-front into 8193-AddFilterInClientNotes
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-04-14 11:26:44 +02:00
Jon Elias 8ecda754bc Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into 8193-AddFilterInClientNotes 2025-04-14 11:25:34 +02:00
Alex Moreno cc05f25c3b Merge branch 'dev' into 8193-AddFilterInClientNotes
gitea/salix-front/pipeline/pr-dev There was a failure building this commit Details
2025-04-07 12:55:04 +00:00
Jon Elias ce722c56ca Merge branch 'dev' into 8193-AddFilterInClientNotes
gitea/salix-front/pipeline/pr-dev Something is wrong with the build of this commit Details
2025-04-07 07:09:49 +00:00
Jon Elias 94a66098c6 feat: refs #8193 added user filter e2e in VnNotes
gitea/salix-front/pipeline/pr-dev Something is wrong with the build of this commit Details
2025-04-04 12:05:50 +02:00
Jon Elias 0d0433bec4 feat: refs #8193 make VnNotes use VnNotesFilter
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-04-04 12:01:03 +02:00
Jon Elias 60fc6bee45 Merge branch 'dev' of https: refs #8193//gitea.verdnatura.es/verdnatura/salix-front into 8193-AddFilterInClientNotes
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-04-01 14:23:25 +02:00
Jon Elias 33ec70a637 Merge branch '8193-AddFilterInClientNotes' of https://gitea.verdnatura.es/verdnatura/salix-front into 8193-AddFilterInClientNotes
gitea/salix-front/pipeline/pr-dev This commit looks good Details
2025-03-25 08:28:13 +01:00
Jon Elias 490e5cbf97 Merge branch 'dev' of https: refs #8193//gitea.verdnatura.es/verdnatura/salix-front into 8193-AddFilterInClientNotes 2025-03-25 08:28:11 +01:00
Jon Elias 921af7abda Merge branch 'dev' into 8193-AddFilterInClientNotes
gitea/salix-front/pipeline/pr-dev This commit looks good Details
2025-03-20 06:57:56 +00:00
Jon Elias 6840c8f053 Merge branch 'dev' into 8193-AddFilterInClientNotes
gitea/salix-front/pipeline/pr-dev There was a failure building this commit Details
2025-03-18 12:50:01 +00:00
Jon Elias c1964dc16e perf: refs #8193 e2e
gitea/salix-front/pipeline/pr-dev There was a failure building this commit Details
2025-03-18 13:48:44 +01:00
Jon Elias 9bf1f74061 feat: refs #8193 added notes e2e
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-03-18 10:40:41 +01:00
Jon Elias 76b793b559 feat: refs #8193 added filter in client notes
gitea/salix-front/pipeline/pr-dev This commit is unstable Details
2025-03-18 10:15:46 +01:00
14 changed files with 643 additions and 242 deletions

View File

@ -7,7 +7,7 @@ import { QLayout } from 'quasar';
import mainShortcutMixin from './mainShortcutMixin';
import { useCau } from 'src/composables/useCau';
export default boot(({ app, router }) => {
export default boot(({ app }) => {
QForm.mixins = [qFormMixin];
QLayout.mixins = [mainShortcutMixin];
@ -22,14 +22,6 @@ export default boot(({ app, router }) => {
}
switch (response?.status) {
case 401:
if (!router.currentRoute.value.name.toLowerCase().includes('login')) {
message = 'errors.sessionExpired';
} else message = 'login.loginError';
break;
case 403:
if (!message || message.toLowerCase() === 'access denied')
message = 'errors.accessDenied';
case 422:
if (error.name == 'ValidationError')
message += ` "${responseError.details.context}.${Object.keys(

View File

@ -180,11 +180,7 @@ const col = computed(() => {
)
newColumn.component = 'checkbox';
if ($props.default && !newColumn.component) newColumn.component = $props.default;
if (typeof newColumn.component !== 'string') {
newColumn.attrs = { ...newColumn.component.attrs, autofocus: $props.autofocus };
newColumn.event = { ...newColumn.component.event, ...$props?.eventHandlers };
}
return newColumn;
});

View File

@ -1,49 +0,0 @@
<script setup>
import { ref, useTemplateRef } 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';
const model = defineModel({ type: [String, Number, Object] });
const { t } = useI18n();
const expenses = ref([]);
const selectDialogRef = useTemplateRef('selectDialogRef');
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);
}
</script>
<template>
<VnSelectDialog
v-bind="$attrs"
ref="selectDialogRef"
v-model="model"
:options="expenses"
option-value="id"
:option-label="(x) => `${x.id}: ${x.name}`"
: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()" />
</template>
</VnSelectDialog>
<FetchData
ref="expensesRef"
url="Expenses"
auto-load
@on-fetch="(data) => (expenses = data)"
/>
</template>
<i18n>
es:
Create a new expense: Crear nuevo gasto
</i18n>

View File

@ -6,6 +6,7 @@ import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { tMobile } from 'src/composables/tMobile';
import { useState } from 'src/composables/useState';
import { toDateHourMin } from 'src/filters';
import VnPaginate from 'components/ui/VnPaginate.vue';
@ -24,9 +25,9 @@ const $attrs = computed(() => {
const { required, deletable, ...rest } = originalAttrs;
return rest;
});
const $props = defineProps({
url: { type: String, default: null },
dataKey: { type: String, default: null },
saveUrl: { type: String, default: null },
userFilter: { type: Object, default: () => {} },
filter: { type: Object, default: () => {} },
@ -36,8 +37,8 @@ const $props = defineProps({
justInput: { type: Boolean, default: false },
goTo: { type: String, default: '' },
useUserRelation: { type: Boolean, default: true },
filterColumns: { type: Array, default: () => [] },
});
const { t } = useI18n();
const quasar = useQuasar();
const stateStore = useStateStore();
@ -47,7 +48,8 @@ const componentIsRendered = ref(false);
const newNote = reactive({ text: null, observationTypeFk: null });
const observationTypes = ref([]);
const vnPaginateRef = ref();
const state = useState();
const user = state.getUser();
const defaultObservationType = computed(
() => observationTypes.value.find((ot) => ot.code === 'salesPerson')?.id,
);
@ -135,13 +137,42 @@ function fetchData([data]) {
emit('onFetch', data);
}
const handleObservationTypes = (data) => {
const handleObservationTypes = async (data) => {
observationTypes.value = data;
if (defaultObservationType.value) {
newNote.observationTypeFk = defaultObservationType.value;
}
const { data: departments } = await axios.get('Departments');
const userDepartment = departments.find((ot) => ot.id === user.value.departmentFk);
const hasObservationType =
observationTypes.value.find(
(ot) => parseInt(ot.departmentFk) === userDepartment.id,
) ||
observationTypes.value.find(
(ot) => parseInt(ot.departmentFk) === userDepartment.parentFk,
);
newNote.observationTypeFk = hasObservationType
? hasObservationType.code
: defaultObservationType.value;
};
function exprBuilder(param, value) {
switch (param) {
case 'observationTypeFk':
case 'userFk':
return { [param]: value };
}
}
function setData(data) {
newNote.text = '';
emit('onFetch', data);
}
onMounted(() => {
nextTick(() => (componentIsRendered.value = true));
});
async function saveAndGo() {
savedNote = false;
await insert();
@ -205,7 +236,7 @@ function getUserFilter() {
<FetchData
v-if="selectType"
url="ObservationTypes"
:filter="{ fields: ['id', 'description', 'code'] }"
:filter="{ fields: ['id', 'description', 'code', 'departmentFk'] }"
auto-load
@on-fetch="handleObservationTypes"
/>
@ -227,6 +258,7 @@ function getUserFilter() {
<QCardSection class="q-px-xs q-my-none q-py-none">
<VnRow class="full-width">
<VnSelect
class="q-mr-xs"
:label="t('Observation type')"
v-if="selectType"
url="ObservationTypes"
@ -235,6 +267,8 @@ function getUserFilter() {
style="flex: 0.15"
:required="'required' in originalAttrs"
@keyup.enter.stop="insert"
data-cy="VnNotes-observation-type"
filled
/>
<VnInput
v-model.trim="newNote.text"
@ -242,10 +276,10 @@ function getUserFilter() {
:label="$props.justInput && newNote.text ? '' : t('Add note here...')"
filled
autogrow
autofocus
@keyup.enter.stop="handleClick"
:required="'required' in originalAttrs"
clearable
data-cy="VnNotes-text-input"
>
<template #append v-if="!$props.goTo">
<QBtn
@ -275,8 +309,9 @@ function getUserFilter() {
ref="vnPaginateRef"
class="show"
v-bind="$attrs"
:search-url="false"
@on-fetch="newNote.text = ''"
:search-url="$props.url"
@on-fetch="setData"
:exprBuilder
>
<template #body="{ rows }">
<TransitionGroup name="list" tag="div" class="column items-center full-width">
@ -303,6 +338,7 @@ function getUserFilter() {
outline
color="grey"
v-if="selectType && note.observationTypeFk"
data-cy="VnNotes-observation-type-badge"
>
{{
observationTypes.find(

View File

@ -400,8 +400,6 @@ errors:
updateUserConfig: Error updating user config
tokenConfig: Error fetching token config
writeRequest: The requested operation could not be completed
sessionExpired: Your session has expired. Please log in again
accessDenied: Access denied
claimBeginningQuantity: Cannot import a line with a claimed quantity of 0
login:
title: Login

View File

@ -396,8 +396,6 @@ errors:
updateUserConfig: Error al actualizar la configuración de usuario
tokenConfig: Error al obtener configuración de token
writeRequest: No se pudo completar la operación solicitada
sessionExpired: Tu sesión ha expirado, por favor vuelve a iniciar sesión
accessDenied: Acceso denegado
claimBeginningQuantity: No se puede importar una linea sin una cantidad reclamada
login:
title: Inicio de sesión

View File

@ -1,12 +1,117 @@
<script setup>
import VnNotes from 'src/components/ui/VnNotes.vue';
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import { useState } from 'src/composables/useState';
import RightMenu from 'src/components/common/RightMenu.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnNotes from 'src/components/ui/VnNotes.vue';
import VnTableFilter from 'src/components/VnTable/VnTableFilter.vue';
import VnAvatar from 'src/components/ui/VnAvatar.vue';
import { useState } from 'src/composables/useState';
const state = useState();
const user = state.getUser();
const { t } = useI18n();
const url = 'clientObservations';
const stateStore = useStateStore();
const filteredWorkers = ref([]);
const columns = computed(() => [
{
name: 'observationTypeFk',
},
{
name: 'userFk',
},
]);
const setWorkerObservations = (data) => {
const seen = new Set();
filteredWorkers.value = data;
filteredWorkers.value = data.filter((observation) => {
if (!seen.has(observation.userFk)) {
seen.add(observation.userFk);
return observation.user;
}
return false;
});
};
function exprBuilder(param, value) {
switch (param) {
case 'observationTypeFk':
case 'userFk':
return { [param]: value };
}
}
onMounted(() => {
stateStore.rightDrawerChangeValue(true);
});
onUnmounted(() => {
stateStore.rightDrawer = false;
});
</script>
<template>
<RightMenu>
<template #right-panel>
<VnTableFilter
data-key="clientObservations"
:columns="columns"
:redirect="false"
:exprBuilder
:search-url="url"
:showTagChips="false"
>
<template #filter-observationTypeFk="{ params, columnName, searchFn }">
<VnSelect
:label="t('Observation type')"
url="ObservationTypes"
v-model="params[columnName]"
option-label="description"
option-value="id"
@keyup.enter="searchFn"
@update:modelValue="() => searchFn()"
dense
filled
data-cy="VnNotes-observation-type-filter"
/>
</template>
<template #filter-userFk="{ params, columnName, searchFn }">
<VnSelect
:label="t('globals.user')"
v-model="params[columnName]"
option-label="name"
option-value="userFk"
:options="filteredWorkers"
@keyup.enter="searchFn"
@update:modelValue="() => searchFn()"
dense
filled
data-cy="VnNotes-user-filter"
>
<template #option="{ opt, itemProps }">
<QItem v-bind="itemProps" class="q-pa-xs row items-center">
<QItemSection class="col-3 items-center">
<VnAvatar :worker-id="opt?.user?.id" size="md" />
</QItemSection>
<QItemSection class="col-9 justify-center">
<span>{{ opt?.user?.name }}</span>
<span class="text-grey">
{{ `#${opt?.user?.id}` }}
</span>
</QItemSection>
</QItem>
</template>
</VnSelect>
</template>
</VnTableFilter>
</template>
</RightMenu>
<VnNotes
url="clientObservations"
:url="url"
data-key="clientObservations"
:add-note="true"
:filter="{ where: { clientFk: $route.params.id } }"
:body="{ clientFk: $route.params.id, userFk: user.id }"
@ -14,5 +119,10 @@ const user = state.getUser();
:select-type="true"
required
order="created DESC"
@on-fetch="(data) => setWorkerObservations(data)"
/>
</template>
<i18n>
es:
Observation type: Tipo de observación
</i18n>

View File

@ -73,7 +73,6 @@ const columns = computed(() => [
optionLabel: 'code',
options: companies.value,
},
orderBy: false,
},
{
name: 'warehouse',

View File

@ -1,16 +1,21 @@
<script setup>
import { ref, computed, markRaw } from 'vue';
import { ref, computed, nextTick } 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 { useAccountShortToStandard } from 'src/composables/useAccountShortToStandard';
const { t } = useI18n();
const arrayData = useArrayData();
const route = useRoute();
const invoiceIn = computed(() => arrayData.store.data);
@ -19,142 +24,100 @@ const expenses = ref([]);
const sageTaxTypes = ref([]);
const sageTransactionTypes = ref([]);
const rowsSelected = ref([]);
const invoiceInVatTableRef = ref();
const invoiceInFormRef = ref();
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;
}
defineProps({
actionIcon: {
type: String,
default: 'add',
},
});
const columns = computed(() => [
{
name: 'expenseFk',
name: 'expense',
label: t('Expense'),
component: markRaw(VnSelectExpense),
format: (row) => {
const expense = expenses.value.find((e) => e.id === row.expenseFk);
return expense ? `${expense.id}: ${expense.name}` : row.expenseFk;
},
field: (row) => row.expenseFk,
options: expenses.value,
model: 'expenseFk',
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.name}`,
sortable: true,
align: 'left',
isEditable: true,
create: true,
width: '250px',
},
{
name: 'taxableBase',
name: 'taxablebase',
label: t('Taxable base'),
component: 'number',
attrs: {
clearable: true,
'clear-icon': 'close',
},
field: (row) => row.taxableBase,
model: 'taxableBase',
sortable: true,
align: 'left',
isEditable: true,
create: true,
},
{
name: 'isDeductible',
label: t('invoiceIn.isDeductible'),
component: 'checkbox',
field: (row) => row.isDeductible,
model: 'isDeductible',
align: 'center',
isEditable: true,
create: true,
createAttrs: {
defaultValue: true,
},
width: '100px',
},
{
name: 'taxTypeSageFk',
name: 'sageiva',
label: t('Sage iva'),
component: 'select',
attrs: {
options: sageTaxTypes.value,
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.vat}`,
filterOptions: ['id', 'vat'],
'data-cy': 'vat-sageiva',
},
format: (row) => {
const taxType = sageTaxTypes.value.find((t) => t.id === row.taxTypeSageFk);
return taxType ? `${taxType.id}: ${taxType.vat}` : row.taxTypeSageFk;
},
field: (row) => row.taxTypeSageFk,
options: sageTaxTypes.value,
model: 'taxTypeSageFk',
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.vat}`,
sortable: true,
align: 'left',
isEditable: true,
create: true,
},
{
name: 'transactionTypeSageFk',
name: 'sagetransaction',
label: t('Sage transaction'),
component: 'select',
attrs: {
options: sageTransactionTypes.value,
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.transaction}`,
filterOptions: ['id', 'transaction'],
},
format: (row) => {
const transType = sageTransactionTypes.value.find(
(t) => t.id === row.transactionTypeSageFk,
);
return transType
? `${transType.id}: ${transType.transaction}`
: row.transactionTypeSageFk;
},
field: (row) => row.transactionTypeSageFk,
options: sageTransactionTypes.value,
model: 'transactionTypeSageFk',
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.transaction}`,
sortable: true,
align: 'left',
isEditable: true,
create: true,
},
{
name: 'rate',
label: t('Rate'),
sortable: false,
format: (row) => taxRate(row).toFixed(2),
sortable: true,
field: (row) => taxRate(row, row.taxTypeSageFk),
align: 'left',
},
{
name: 'foreignValue',
name: 'foreignvalue',
label: t('Foreign value'),
component: 'number',
sortable: true,
field: (row) => row.foreignValue,
align: 'left',
create: true,
disable: !isNotEuro(currency.value),
},
{
name: 'total',
label: t('Total'),
label: '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(tableRows.value, 'taxableBase');
return getTotal(invoiceInFormRef.value.formData, 'taxableBase');
});
const taxRateTotal = computed(() => {
return tableRows.value.reduce((sum, row) => sum + Number(taxRate(row)), 0);
return getTotal(invoiceInFormRef.value.formData, null, {
cb: taxRate,
});
});
const combinedTotal = computed(() => {
return +taxableBaseTotal.value + +taxRateTotal.value;
});
const filter = computed(() => ({
const filter = {
fields: [
'id',
'invoiceInFk',
@ -168,75 +131,389 @@ const filter = computed(() => ({
where: {
invoiceInFk: route.params.id,
},
}));
};
const isNotEuro = (code) => code != 'EUR';
async function handleForeignValueUpdate(val, row) {
if (!isNotEuro(currency.value)) return;
row.taxableBase = await getExchange(
val,
invoiceIn.value?.currencyFk,
invoiceIn.value?.issued,
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).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 url="Expenses" auto-load @on-fetch="(data) => (expenses = data)" />
<FetchData
ref="expensesRef"
url="Expenses"
auto-load
@on-fetch="(data) => (expenses = data)"
/>
<FetchData url="SageTaxTypes" auto-load @on-fetch="(data) => (sageTaxTypes = data)" />
<FetchData
url="sageTransactionTypes"
auto-load
@on-fetch="(data) => (sageTransactionTypes = data)"
/>
<VnTable
<CrudModel
ref="invoiceInFormRef"
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(),
}"
:crud-model="{ goTo: `/invoice-in/${$route.params.id}/due-day` }"
:go-to="`/invoice-in/${$route.params.id}/due-day`"
>
<template #column-footer-taxableBase>
{{ toCurrency(taxableBaseTotal) }}
<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>
<template #column-footer-rate>
{{ toCurrency(taxRateTotal) }}
</template>
<template #column-footer-total>
{{ toCurrency(combinedTotal) }}
</template>
</VnTable>
</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>
<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: Añadir Gasto/IVA # Changed label slightly
Add tax: Crear gasto
Taxable base: Base imp.
Sage iva: Sage iva # Kept original label
Sage tax: Sage iva
Sage transaction: Sage transacción
Rate: Cuota # Changed label
Rate: Tasa
Foreign value: Divisa
Total: Total
invoiceIn.isDeductible: Deducible
</i18n>

View File

@ -1,11 +1,12 @@
<script setup>
import { ref } from 'vue';
import { Notify } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnInputPassword from 'src/components/common/VnInputPassword.vue';
import { useSession } from 'src/composables/useSession';
import { useLogin } from 'src/composables/useLogin';
import useNotify from 'src/composables/useNotify';
import VnLogo from 'components/ui/VnLogo.vue';
import VnInput from 'src/components/common/VnInput.vue';
import axios from 'axios';
@ -14,14 +15,16 @@ const session = useSession();
const loginCache = useLogin();
const router = useRouter();
const { t } = useI18n();
const { notify } = useNotify();
const username = ref('');
const password = ref('');
const keepLogin = ref(true);
async function onSubmit() {
const params = { user: username.value, password: password.value };
const params = {
user: username.value,
password: password.value,
};
try {
const { data } = await axios.post('Accounts/login', params);
if (!data) return;
@ -30,7 +33,11 @@ async function onSubmit() {
await session.setLogin(data);
} catch (res) {
if (res.response?.data?.error?.code === 'REQUIRES_2FA') {
notify(t('login.twoFactorRequired'), 'warning', 'phoneLink_lock');
Notify.create({
message: t('login.twoFactorRequired'),
icon: 'phoneLink_lock',
type: 'warning',
});
params.keepLogin = keepLogin.value;
loginCache.setUser(params);
return router.push({
@ -38,7 +45,10 @@ async function onSubmit() {
query: router.currentRoute.value?.query,
});
}
throw res;
Notify.create({
message: t('login.loginError'),
type: 'negative',
});
}
}
</script>

View File

@ -24,8 +24,13 @@ export CI=true
export TZ=Europe/Madrid
# IMAGES
docker-compose -f test/cypress/docker-compose.yml --project-directory . pull db
docker-compose -f test/cypress/docker-compose.yml --project-directory . pull back
docker build -t registry.verdnatura.es/salix-back:dev -f "$salix_dir/back/Dockerfile" "$salix_dir"
cd "$salix_dir" && npx myt run -t
docker exec vn-database sh -c "rm -rf /mysql-template"
docker exec vn-database sh -c "cp -a /var/lib/mysql /mysql-template"
docker commit vn-database registry.verdnatura.es/salix-db:dev
docker rm -f vn-database
cd "$current_dir"
docker build -f ./docs/Dockerfile.dev -t lilium-dev .
# END IMAGES

View File

@ -1,13 +1,37 @@
/// <reference types="cypress" />
describe('Client notes', () => {
const obervationType = 'Packager';
const user = 'developer';
beforeEach(() => {
cy.viewport(1280, 720);
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit('#/customer/1110/notes', {
timeout: 5000,
});
cy.visit('#/customer/1102/notes');
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
it('should add and filter notes by observation type', () => {
cy.selectOption("[data-cy='VnNotes-observation-type']", obervationType);
cy.dataCy('VnNotes-text-input').type('Test note {enter}');
cy.selectOption("[data-cy='VnNotes-observation-type-filter']", obervationType);
cy.get('.column.full-width')
.children()
.each(($el) => {
cy.dataCy('VnNotes-observation-type-badge').should(
'include.text',
obervationType,
);
});
});
it('should filter notes by user', () => {
cy.selectOption("[data-cy='VnNotes-user-filter']", user);
cy.get('.column.full-width')
.children()
.each(($el) => {
cy.dataCy('VnNotes-observation-type-badge').should(
'include.text',
obervationType,
);
});
});
});

View File

@ -5,28 +5,33 @@ describe('Logout', { testIsolation: true }, () => {
cy.visit(`/#/dashboard`);
cy.waitForElement('.q-page', 6000);
});
it('should logout', () => {
cy.get('#user').click();
cy.get('#logout').click();
describe('by user', () => {
it('should logout', () => {
cy.get('#user').click();
cy.get('#logout').click();
});
});
it('should throw session expired error if token has expired or is not valid during navigation', () => {
cy.intercept('GET', '**StarredModules**', {
statusCode: 401,
body: {
error: {
statusCode: 401,
name: 'Error',
message: 'Authorization Required',
code: 'AUTHORIZATION_REQUIRED',
describe('not user', () => {
beforeEach(() => {
cy.intercept('GET', '**StarredModules**', {
statusCode: 401,
body: {
error: {
statusCode: 401,
name: 'Error',
message: 'Authorization Required',
code: 'AUTHORIZATION_REQUIRED',
},
},
},
statusMessage: 'AUTHORIZATION_REQUIRED',
}).as('badRequest');
cy.get('.q-list').should('be.visible').first().should('be.visible').click();
cy.wait('@badRequest');
statusMessage: 'AUTHORIZATION_REQUIRED',
}).as('badRequest');
});
cy.checkNotification('Your session has expired. Please log in again');
it('when token not exists', () => {
cy.get('.q-list').should('be.visible').first().should('be.visible').click();
cy.wait('@badRequest');
cy.checkNotification('Authorization Required');
});
});
});

View File

@ -1,6 +1,6 @@
describe('Vehicle Notes', () => {
const selectors = {
addNoteInput: 'Add note here..._input',
addNoteInput: 'VnNotes-text-input',
saveNoteBtn: 'saveNote',
deleteNoteBtn: 'notesRemoveNoteBtn',
noteCard: '.column.full-width > :nth-child(1) > .q-card__section--vert',