8448-devToTest #1254

Merged
alexm merged 365 commits from 8448-devToTest into test 2025-01-21 10:44:46 +00:00
192 changed files with 3953 additions and 2654 deletions

View File

@ -11,6 +11,7 @@ module.exports = defineConfig({
screenshotsFolder: 'test/cypress/screenshots', screenshotsFolder: 'test/cypress/screenshots',
supportFile: 'test/cypress/support/index.js', supportFile: 'test/cypress/support/index.js',
videosFolder: 'test/cypress/videos', videosFolder: 'test/cypress/videos',
downloadsFolder: 'test/cypress/downloads',
video: false, video: false,
specPattern: 'test/cypress/integration/**/*.spec.js', specPattern: 'test/cypress/integration/**/*.spec.js',
experimentalRunAllSpecs: true, experimentalRunAllSpecs: true,
@ -33,5 +34,7 @@ module.exports = defineConfig({
require('cypress-mochawesome-reporter/plugin')(on); require('cypress-mochawesome-reporter/plugin')(on);
// implement node event listeners here // implement node event listeners here
}, },
viewportWidth: 1280,
viewportHeight: 720,
}, },
}); });

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "25.02.0", "version": "25.04.0",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",

View File

@ -62,6 +62,7 @@ async function setProvince(id, data) {
postcodeFormData.provinceFk = id; postcodeFormData.provinceFk = id;
await fetchTowns(); await fetchTowns();
} }
async function onProvinceCreated(data) { async function onProvinceCreated(data) {
postcodeFormData.provinceFk = data.id; postcodeFormData.provinceFk = data.id;
} }

View File

@ -198,6 +198,7 @@ async function fetch() {
} catch (e) { } catch (e) {
state.set(modelValue, {}); state.set(modelValue, {});
originalData.value = {}; originalData.value = {};
throw e;
} }
} }

View File

@ -44,7 +44,7 @@ const onDataSaved = (data) => {
<FormModelPopup <FormModelPopup
url-create="Items/regularize" url-create="Items/regularize"
model="Items" model="Items"
:title="t('Regularize stock')" :title="t('item.regularizeStock')"
:form-initial-data="regularizeFormData" :form-initial-data="regularizeFormData"
@on-data-saved="onDataSaved($event)" @on-data-saved="onDataSaved($event)"
> >
@ -55,6 +55,7 @@ const onDataSaved = (data) => {
v-model.number="data.quantity" v-model.number="data.quantity"
type="number" type="number"
autofocus autofocus
data-cy="regularizeStockInput"
/> />
</VnRow> </VnRow>
<VnRow> <VnRow>

View File

@ -1,6 +1,5 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
import VnFilter from 'components/VnTable/VnFilter.vue'; import VnFilter from 'components/VnTable/VnFilter.vue';
@ -11,16 +10,11 @@ defineProps({
type: Array, type: Array,
required: true, required: true,
}, },
chipLocale: {
type: String,
default: null,
},
searchUrl: { searchUrl: {
type: [String, Boolean], type: [String, Boolean],
default: 'table', default: 'table',
}, },
}); });
const { t } = useI18n();
const tableFilterRef = ref([]); const tableFilterRef = ref([]);
@ -62,9 +56,9 @@ function columnName(col) {
:columns="columns" :columns="columns"
/> />
</template> </template>
<template #tags="{ tag, formatFn }" v-if="chipLocale"> <template #tags="{ tag, formatFn, getLocale }">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
<strong>{{ t(`${chipLocale}.${tag.label}`) }}: </strong> <strong>{{ getLocale(`${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span> <span>{{ formatFn(tag.value) }}</span>
</div> </div>
</template> </template>

View File

@ -0,0 +1,82 @@
import { createWrapper, axios } from 'app/test/vitest/helper';
import FilterItemForm from 'src/components/FilterItemForm.vue';
import { vi, beforeAll, describe, expect, it } from 'vitest';
describe('FilterItemForm', () => {
let vm;
let wrapper;
beforeAll(() => {
wrapper = createWrapper(FilterItemForm, {
props: {
url: 'Items/withName',
},
});
vm = wrapper.vm;
wrapper = wrapper.wrapper;
vi.spyOn(axios, 'get').mockResolvedValue({
data: [
{
id: 999996,
name: 'bolas de madera',
size: 2,
inkFk: null,
producerFk: null,
},
],
});
});
it('should filter data and populate tableRows for table display', async () => {
vm.itemFilterParams.name = 'bolas de madera';
await vm.onSubmit();
const expectedFilter = {
include: [
{ relation: 'producer', scope: { fields: ['name'] } },
{ relation: 'ink', scope: { fields: ['name'] } },
],
where: {"name":{"like":"%bolas de madera%"}},
};
expect(axios.get).toHaveBeenCalledWith('Items/withName', {
params: { filter: JSON.stringify(expectedFilter) },
});
expect(vm.tableRows).toEqual([
{
id: 999996,
name: 'bolas de madera',
size: 2,
inkFk: null,
producerFk: null,
},
]);
});
it('should handle an empty itemFilterParams correctly', async () => {
vm.itemFilterParams.name = null;
vm.itemFilterParams = {};
await vm.onSubmit();
const expectedFilter = {
include: [
{ relation: 'producer', scope: { fields: ['name'] } },
{ relation: 'ink', scope: { fields: ['name'] } },
],
where: {},
};
expect(axios.get).toHaveBeenCalledWith('Items/withName', {
params: { filter: JSON.stringify(expectedFilter) },
});
});
it('should emit "itemSelected" with the correct id and close the form', () => {
vm.selectItem({ id: 12345 });
expect(wrapper.emitted('itemSelected')[0]).toEqual([12345]);
});
});

View File

@ -2,9 +2,9 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnRow from '../ui/VnRow.vue'; import VnRow from '../ui/VnRow.vue';
import VnInput from './VnInput.vue';
import FetchData from '../FetchData.vue'; import FetchData from '../FetchData.vue';
import useNotify from 'src/composables/useNotify'; import useNotify from 'src/composables/useNotify';
import VnInputPassword from 'src/components/common/VnInputPassword.vue';
const props = defineProps({ const props = defineProps({
submitFn: { type: Function, default: () => {} }, submitFn: { type: Function, default: () => {} },
@ -70,19 +70,19 @@ defineExpose({ show: () => changePassDialog.value.show() });
</QCardSection> </QCardSection>
<QForm ref="form"> <QForm ref="form">
<QCardSection> <QCardSection>
<VnInput <VnInputPassword
v-if="props.askOldPass" v-if="props.askOldPass"
:label="t('Old password')" :label="t('Old password')"
v-model="passwords.oldPassword" v-model="passwords.oldPassword"
type="password"
:required="true" :required="true"
:toggle-visibility="true"
autofocus autofocus
/> />
<VnInput <VnInputPassword
:label="t('New password')" :label="t('New password')"
v-model="passwords.newPassword" v-model="passwords.newPassword"
type="password"
:required="true" :required="true"
:toggle-visibility="true"
:info=" :info="
t('passwordRequirements', { t('passwordRequirements', {
length: requirements.length, length: requirements.length,
@ -95,10 +95,10 @@ defineExpose({ show: () => changePassDialog.value.show() });
autofocus autofocus
/> />
<VnInput <VnInputPassword
:label="t('Repeat password')" :label="t('Repeat password')"
v-model="passwords.repeatPassword" v-model="passwords.repeatPassword"
type="password" :toggle-visibility="true"
/> />
</QCardSection> </QCardSection>
</QForm> </QForm>

View File

@ -42,6 +42,10 @@ const $props = defineProps({
type: Number, type: Number,
default: null, default: null,
}, },
uppercase: {
type: Boolean,
default: false,
},
}); });
const vnInputRef = ref(null); const vnInputRef = ref(null);
@ -116,6 +120,10 @@ const handleInsertMode = (e) => {
input.setSelectionRange(cursorPos + 1, cursorPos + 1); input.setSelectionRange(cursorPos + 1, cursorPos + 1);
}); });
}; };
const handleUppercase = () => {
value.value = value.value?.toUpperCase() || '';
};
</script> </script>
<template> <template>
@ -134,20 +142,23 @@ const handleInsertMode = (e) => {
hide-bottom-space hide-bottom-space
:data-cy="$attrs.dataCy ?? $attrs.label + '_input'" :data-cy="$attrs.dataCy ?? $attrs.label + '_input'"
> >
<template v-if="$slots.prepend" #prepend> <template #prepend>
<slot name="prepend" /> <slot name="prepend" />
</template> </template>
<template #append> <template #append>
<QIcon <QIcon
name="close" name="close"
size="xs" size="xs"
v-if=" :style="{
hover && visibility:
value && hover &&
!$attrs.disabled && value &&
!$attrs.readonly && !$attrs.disabled &&
$props.clearable !$attrs.readonly &&
" $props.clearable
? 'visible'
: 'hidden',
}"
@click=" @click="
() => { () => {
value = null; value = null;
@ -156,6 +167,15 @@ const handleInsertMode = (e) => {
} }
" "
></QIcon> ></QIcon>
<QIcon
name="match_case"
size="xs"
v-if="!$attrs.disabled && !($attrs.readonly) && $props.uppercase"
@click="handleUppercase"
class="uppercase-icon"
/>
<slot name="append" v-if="$slots.append && !$attrs.disabled" /> <slot name="append" v-if="$slots.append && !$attrs.disabled" />
<QIcon v-if="info" name="info"> <QIcon v-if="info" name="info">
<QTooltip max-width="350px"> <QTooltip max-width="350px">
@ -166,6 +186,7 @@ const handleInsertMode = (e) => {
</QInput> </QInput>
</div> </div>
</template> </template>
<i18n> <i18n>
en: en:
inputMin: Must be more than {value} inputMin: Must be more than {value}
@ -175,9 +196,4 @@ const handleInsertMode = (e) => {
inputMin: Debe ser mayor a {value} inputMin: Debe ser mayor a {value}
maxLength: El valor excede los {value} carácteres maxLength: El valor excede los {value} carácteres
inputMax: Debe ser menor a {value} inputMax: Debe ser menor a {value}
</i18n> </i18n>
<style lang="scss">
.q-field__append {
padding-inline: 0;
}
</style>

View File

@ -1,14 +1,12 @@
<script setup> <script setup>
import { onMounted, watch, computed, ref, useAttrs } from 'vue'; import { onMounted, watch, computed, ref, useAttrs } from 'vue';
import { date } from 'quasar'; import { date } from 'quasar';
import { useI18n } from 'vue-i18n';
import VnDate from './VnDate.vue'; import VnDate from './VnDate.vue';
import { useRequired } from 'src/composables/useRequired'; import { useRequired } from 'src/composables/useRequired';
const $attrs = useAttrs(); const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs); const { isRequired, requiredFieldRule } = useRequired($attrs);
const model = defineModel({ type: [String, Date] }); const model = defineModel({ type: [String, Date] });
const { t } = useI18n();
const $props = defineProps({ const $props = defineProps({
isOutlined: { isOutlined: {

View File

@ -0,0 +1,31 @@
<script setup>
import VnInput from 'src/components/common/VnInput.vue';
import { ref } from 'vue';
const model = defineModel({ type: [Number, String] });
const $props = defineProps({
toggleVisibility: {
type: Boolean,
default: false,
},
});
const showPassword = ref(false);
</script>
<template>
<VnInput
v-bind="{ ...$attrs }"
v-model="model"
:type="
$props.toggleVisibility ? (showPassword ? 'text' : 'password') : $attrs.type
"
>
<template #append v-if="toggleVisibility">
<QIcon
:name="showPassword ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showPassword = !showPassword"
/>
</template>
</VnInput>
</template>

View File

@ -1,13 +1,11 @@
<script setup> <script setup>
import { computed, ref, useAttrs } from 'vue'; import { computed, ref, useAttrs } from 'vue';
import { useI18n } from 'vue-i18n';
import { date } from 'quasar'; import { date } from 'quasar';
import VnTime from './VnTime.vue'; import VnTime from './VnTime.vue';
import { useRequired } from 'src/composables/useRequired'; import { useRequired } from 'src/composables/useRequired';
const $attrs = useAttrs(); const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs); const { isRequired, requiredFieldRule } = useRequired($attrs);
const { t } = useI18n();
const model = defineModel({ type: String }); const model = defineModel({ type: String });
const props = defineProps({ const props = defineProps({
timeOnly: { timeOnly: {

View File

@ -80,7 +80,6 @@ onBeforeMount(() => {
/> />
<div :id="searchbarId"></div> <div :id="searchbarId"></div>
</slot> </slot>
<RightMenu> <RightMenu>
<template #right-panel v-if="$slots['rightMenu'] || rightFilter"> <template #right-panel v-if="$slots['rightMenu'] || rightFilter">
<slot name="rightMenu"> <slot name="rightMenu">

View File

@ -205,10 +205,10 @@ function filter(val, options) {
} }
if (!row) return; if (!row) return;
const id = row[$props.optionValue]; const id = String(row[$props.optionValue]);
const optionLabel = String(row[$props.optionLabel]).toLowerCase(); const optionLabel = String(row[$props.optionLabel]).toLowerCase();
return id == search || optionLabel.includes(search); return id.includes(search) || optionLabel.includes(search);
}); });
} }

View File

@ -51,6 +51,7 @@ const url = computed(() => {
option-value="id" option-value="id"
option-label="nickname" option-label="nickname"
:fields="['id', 'name', 'nickname', 'code']" :fields="['id', 'name', 'nickname', 'code']"
:filter-options="['id', 'name', 'nickname', 'code']"
sort-by="nickname ASC" sort-by="nickname ASC"
> >
<template #prepend v-if="$props.hasAvatar"> <template #prepend v-if="$props.hasAvatar">
@ -71,7 +72,7 @@ const url = computed(() => {
{{ scope.opt.nickname }} {{ scope.opt.nickname }}
</QItemLabel> </QItemLabel>
<QItemLabel caption v-else> <QItemLabel caption v-else>
{{ scope.opt.nickname }}, {{ scope.opt.code }} #{{ scope.opt.id }}, {{ scope.opt.nickname }}, {{ scope.opt.code }}
</QItemLabel> </QItemLabel>
</QItemSection> </QItemSection>
</QItem> </QItem>

View File

@ -0,0 +1,72 @@
import { createWrapper } from 'app/test/vitest/helper.js';
import { describe, it, expect } from 'vitest';
import VnInputDate from 'components/common/VnInputDate.vue';
let vm;
let wrapper;
function generateWrapper(date, outlined, required) {
wrapper = createWrapper(VnInputDate, {
props: {
modelValue: date,
},
attrs: {
isOutlined: outlined,
required: required
},
});
wrapper = wrapper.wrapper;
vm = wrapper.vm;
};
describe('VnInputDate', () => {
describe('formattedDate', () => {
it('formats a valid date correctly', async () => {
generateWrapper('2023-12-25', false, false);
await vm.$nextTick();
expect(vm.formattedDate).toBe('25/12/2023');
});
it('updates the model value when a new date is set', async () => {
const input = wrapper.find('input');
await input.setValue('31/12/2023');
expect(wrapper.emitted()['update:modelValue']).toBeTruthy();
expect(wrapper.emitted()['update:modelValue'][0][0]).toBe('2023-12-31T00:00:00.000Z');
});
it('should not update the model value when an invalid date is set', async () => {
const input = wrapper.find('input');
await input.setValue('invalid-date');
expect(wrapper.emitted()['update:modelValue'][0][0]).toBe('2023-12-31T00:00:00.000Z');
});
});
describe('styleAttrs', () => {
it('should return empty styleAttrs when isOutlined is false', async () => {
generateWrapper('2023-12-25', false, false);
await vm.$nextTick();
expect(vm.styleAttrs).toEqual({});
});
it('should set styleAttrs when isOutlined is true', async () => {
generateWrapper('2023-12-25', true, false);
await vm.$nextTick();
expect(vm.styleAttrs.outlined).toBe(true);
});
});
describe('required', () => {
it('should not applies required class when isRequired is false', async () => {
generateWrapper('2023-12-25', false, false);
await vm.$nextTick();
expect(wrapper.find('.vn-input-date').classes()).not.toContain('required');
});
it('should applies required class when isRequired is true', async () => {
generateWrapper('2023-12-25', false, true);
await vm.$nextTick();
expect(wrapper.find('.vn-input-date').classes()).toContain('required');
});
});
});

View File

@ -0,0 +1,63 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import VnInputTime from 'components/common/VnInputTime.vue';
describe('VnInputTime', () => {
let wrapper;
let vm;
beforeAll(() => {
wrapper = createWrapper(VnInputTime, {
props: {
isOutlined: true,
timeOnly: false,
},
});
vm = wrapper.vm;
wrapper = wrapper.wrapper;
});
afterEach(() => {
vi.clearAllMocks();
});
it('should return the correct data if isOutlined is true', () => {
expect(vm.isOutlined).toBe(true);
expect(vm.styleAttrs).toEqual({ dense: true, outlined: true, rounded: true });
});
it('should return the formatted data', () => {
expect(vm.dateToTime('2022-01-01T03:23:43')).toBe('03:23');
});
describe('formattedTime', () => {
it('should return the formatted time for a valid ISO date', () => {
vm.model = '2025-01-02T15:45:00';
expect(vm.formattedTime).toBe('15:45');
});
it('should handle null model value gracefully', () => {
vm.model = null;
expect(vm.formattedTime).toBe(null);
});
it('should handle time-only input correctly', async () => {
await wrapper.setProps({ timeOnly: true });
vm.formattedTime = '14:30';
expect(vm.model).toBe('14:30');
});
it('should pad short time values correctly', async () => {
await wrapper.setProps({ timeOnly: true });
vm.formattedTime = '9';
expect(vm.model).toBe('09:00');
});
it('should not update the model if the value is unchanged', () => {
vm.model = '14:30';
const previousModel = vm.model;
vm.formattedTime = '14:30';
expect(vm.model).toBe(previousModel);
});
});
});

View File

@ -0,0 +1,95 @@
import { describe, it, expect } from 'vitest';
import VnJsonValue from 'src/components/common/VnJsonValue.vue';
import { createWrapper } from 'app/test/vitest/helper';
const buildComponent = (props) => {
return createWrapper(VnJsonValue, {
props,
}).wrapper;
};
describe('VnJsonValue', () => {
it('renders null value correctly', async () => {
const wrapper = buildComponent({ value: null });
const span = wrapper.find('span');
expect(span.text()).toBe('∅');
expect(span.classes()).toContain('json-null');
});
it('renders boolean true correctly', async () => {
const wrapper = buildComponent({ value: true });
const span = wrapper.find('span');
expect(span.text()).toBe('✓');
expect(span.classes()).toContain('json-true');
});
it('renders boolean false correctly', async () => {
const wrapper = buildComponent({ value: false });
const span = wrapper.find('span');
expect(span.text()).toBe('✗');
expect(span.classes()).toContain('json-false');
});
it('renders a short string correctly', async () => {
const wrapper = buildComponent({ value: 'Hello' });
const span = wrapper.find('span');
expect(span.text()).toBe('Hello');
expect(span.classes()).toContain('json-string');
});
it('renders a long string correctly with ellipsis', async () => {
const longString = 'a'.repeat(600);
const wrapper = buildComponent({ value: longString });
const span = wrapper.find('span');
expect(span.text()).toContain('...');
expect(span.text().length).toBeLessThanOrEqual(515);
expect(span.attributes('title')).toBe(longString);
expect(span.classes()).toContain('json-string');
});
it('renders a number correctly', async () => {
const wrapper = buildComponent({ value: 123.4567 });
const span = wrapper.find('span');
expect(span.text()).toBe('123.457');
expect(span.classes()).toContain('json-number');
});
it('renders an integer correctly', async () => {
const wrapper = buildComponent({ value: 42 });
const span = wrapper.find('span');
expect(span.text()).toBe('42');
expect(span.classes()).toContain('json-number');
});
it('renders a date correctly', async () => {
const date = new Date('2023-01-01');
const wrapper = buildComponent({ value: date });
const span = wrapper.find('span');
expect(span.text()).toBe('2023-01-01');
expect(span.classes()).toContain('json-object');
});
it('renders an object correctly', async () => {
const obj = { key: 'value' };
const wrapper = buildComponent({ value: obj });
const span = wrapper.find('span');
expect(span.text()).toBe(obj.toString());
expect(span.classes()).toContain('json-object');
});
it('renders an array correctly', async () => {
const arr = [1, 2, 3];
const wrapper = buildComponent({ value: arr });
const span = wrapper.find('span');
expect(span.text()).toBe(arr.toString());
expect(span.classes()).toContain('json-object');
});
it('updates value when prop changes', async () => {
const wrapper = buildComponent({ value: true });
await wrapper.setProps({ value: 123 });
const span = wrapper.find('span');
expect(span.text()).toBe('123');
expect(span.classes()).toContain('json-number');
});
});

View File

@ -0,0 +1,107 @@
import { describe, it, expect, vi, beforeAll, afterEach, beforeEach } from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper';
import VnNotes from 'src/components/ui/VnNotes.vue';
describe('VnNotes', () => {
let vm;
let wrapper;
let spyFetch;
let postMock;
let expectedBody;
const mockData= {name: 'Tony', lastName: 'Stark', text: 'Test Note', observationTypeFk: 1};
function generateExpectedBody() {
expectedBody = {...vm.$props.body, ...{ text: vm.newNote.text, observationTypeFk: vm.newNote.observationTypeFk }};
}
async function setTestParams(text, observationType, type){
vm.newNote.text = text;
vm.newNote.observationTypeFk = observationType;
wrapper.setProps({ selectType: type });
}
beforeAll(async () => {
vi.spyOn(axios, 'get').mockReturnValue({ data: [] });
wrapper = createWrapper(VnNotes, {
propsData: {
url: '/test',
body: { name: 'Tony', lastName: 'Stark' },
}
});
wrapper = wrapper.wrapper;
vm = wrapper.vm;
});
beforeEach(() => {
postMock = vi.spyOn(axios, 'post').mockResolvedValue(mockData);
spyFetch = vi.spyOn(vm.vnPaginateRef, 'fetch').mockImplementation(() => vi.fn());
});
afterEach(() => {
vi.clearAllMocks();
expectedBody = {};
});
describe('insert', () => {
it('should not call axios.post and vnPaginateRef.fetch if newNote.text is null', async () => {
await setTestParams( null, null, true );
await vm.insert();
expect(postMock).not.toHaveBeenCalled();
expect(spyFetch).not.toHaveBeenCalled();
});
it('should not call axios.post and vnPaginateRef.fetch if newNote.text is empty', async () => {
await setTestParams( "", null, false );
await vm.insert();
expect(postMock).not.toHaveBeenCalled();
expect(spyFetch).not.toHaveBeenCalled();
});
it('should not call axios.post and vnPaginateRef.fetch if observationTypeFk is missing and selectType is true', async () => {
await setTestParams( "Test Note", null, true );
await vm.insert();
expect(postMock).not.toHaveBeenCalled();
expect(spyFetch).not.toHaveBeenCalled();
});
it('should call axios.post and vnPaginateRef.fetch if observationTypeFk is missing and selectType is false', async () => {
await setTestParams( "Test Note", null, false );
generateExpectedBody();
await vm.insert();
expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody);
expect(spyFetch).toHaveBeenCalled();
});
it('should call axios.post and vnPaginateRef.fetch if observationTypeFk is setted and selectType is false', async () => {
await setTestParams( "Test Note", 1, false );
generateExpectedBody();
await vm.insert();
expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody);
expect(spyFetch).toHaveBeenCalled();
});
it('should call axios.post and vnPaginateRef.fetch when newNote is valid', async () => {
await setTestParams( "Test Note", 1, true );
generateExpectedBody();
await vm.insert();
expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody);
expect(spyFetch).toHaveBeenCalled();
});
});
});

View File

@ -6,6 +6,7 @@ import { useArrayData } from 'composables/useArrayData';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import VnMoreOptions from './VnMoreOptions.vue';
const $props = defineProps({ const $props = defineProps({
url: { url: {
@ -47,7 +48,6 @@ let store;
let entity; let entity;
const isLoading = ref(false); const isLoading = ref(false);
const isSameDataKey = computed(() => $props.dataKey === route.meta.moduleName); const isSameDataKey = computed(() => $props.dataKey === route.meta.moduleName);
const menuRef = ref();
defineExpose({ getData }); defineExpose({ getData });
onBeforeMount(async () => { onBeforeMount(async () => {
@ -159,25 +159,11 @@ const toModule = computed(() =>
</QTooltip> </QTooltip>
</QBtn> </QBtn>
</RouterLink> </RouterLink>
<QBtn <VnMoreOptions v-if="$slots.menu">
v-if="$slots.menu" <template #menu="{ menuRef }">
color="white" <slot name="menu" :entity="entity" :menu-ref="menuRef" />
dense </template>
flat </VnMoreOptions>
icon="more_vert"
round
size="md"
data-cy="descriptor-more-opts"
>
<QTooltip>
{{ t('components.cardDescriptor.moreOptions') }}
</QTooltip>
<QMenu :ref="menuRef">
<QList>
<slot name="menu" :entity="entity" :menu-ref="menuRef" />
</QList>
</QMenu>
</QBtn>
</div> </div>
<slot name="before" /> <slot name="before" />
<div class="body q-py-sm"> <div class="body q-py-sm">
@ -222,8 +208,8 @@ const toModule = computed(() =>
/> />
</template> </template>
<style lang="scss" scoped> <style lang="scss">
:deep(.body) { .body {
background-color: var(--vn-section-color); background-color: var(--vn-section-color);
.text-h5 { .text-h5 {
font-size: 20px; font-size: 20px;
@ -262,7 +248,9 @@ const toModule = computed(() =>
} }
} }
} }
</style>
<style lang="scss" scoped>
.title { .title {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@ -1,10 +1,10 @@
<script setup> <script setup>
import { ref, computed, watch, onBeforeMount, onMounted } from 'vue'; import { ref, computed, watch, onBeforeMount } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue'; import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { isDialogOpened } from 'src/filters'; import { isDialogOpened } from 'src/filters';
import { useStateStore } from 'src/stores/useStateStore'; import VnMoreOptions from './VnMoreOptions.vue';
const props = defineProps({ const props = defineProps({
url: { url: {
@ -40,7 +40,6 @@ const { store } = arrayData;
const entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data)); const entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data));
const isLoading = ref(false); const isLoading = ref(false);
const stateStore = useStateStore();
defineExpose({ defineExpose({
entity, entity,
fetch, fetch,
@ -52,9 +51,6 @@ onBeforeMount(async () => {
watch(props, async () => await fetch()); watch(props, async () => await fetch());
}); });
onMounted(() => {
stateStore.rightDrawerChangeValue(false);
});
async function fetch() { async function fetch() {
store.url = props.url; store.url = props.url;
store.filter = props.filter ?? {}; store.filter = props.filter ?? {};
@ -64,6 +60,7 @@ async function fetch() {
isLoading.value = false; isLoading.value = false;
} }
</script> </script>
<template> <template>
<div class="summary container"> <div class="summary container">
<QCard class="cardSummary"> <QCard class="cardSummary">
@ -86,9 +83,14 @@ async function fetch() {
<slot name="header" :entity="entity" dense> <slot name="header" :entity="entity" dense>
{{ entity.id + ' - ' + entity.name }} {{ entity.id + ' - ' + entity.name }}
</slot> </slot>
<slot name="header-right" :entity="entity"> <span class="row no-wrap">
<span></span> <slot name="header-right" :entity="entity" />
</slot> <VnMoreOptions v-if="$slots.menu && isDialogOpened()">
<template #menu="{ menuRef }">
<slot name="menu" :entity="entity" :menu-ref="menuRef" />
</template>
</VnMoreOptions>
</span>
</div> </div>
<div class="summaryBody row q-mb-md"> <div class="summaryBody row q-mb-md">
<slot name="body" :entity="entity" /> <slot name="body" :entity="entity" />
@ -97,6 +99,7 @@ async function fetch() {
</QCard> </QCard>
</div> </div>
</template> </template>
<style lang="scss"> <style lang="scss">
.summary.container { .summary.container {
display: flex; display: flex;

View File

@ -18,8 +18,7 @@ const $props = defineProps({
}, },
columns: { columns: {
type: Number, type: Number,
required: false, default: 3,
default: null,
}, },
}); });

View File

@ -1,38 +1,49 @@
<template> <template>
<div class="header bg-primary q-pa-sm q-mb-md"> <div class="header bg-primary q-pa-sm q-mb-md">
<QSkeleton type="rect" square /> <QSkeleton type="rect" square />
<QSkeleton type="rect" square />
</div> </div>
<div class="row q-pa-md q-col-gutter-md q-mb-md"> <div class="row q-pa-md q-col-gutter-md q-mb-md">
<QSkeleton type="rect" class="q-mb-md" square /> <div class="col">
<QSkeleton type="text" square /> <QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="rect" class="q-mb-md" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> </div>
<QSkeleton type="text" square /> <div class="col">
<QSkeleton type="text" square /> <QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="rect" class="q-mb-md" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> </div>
<QSkeleton type="text" square /> <div class="col">
<QSkeleton type="text" square /> <QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="rect" class="q-mb-md" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> </div>
<QSkeleton type="rect" class="q-mb-md" square /> <div class="col">
<QSkeleton type="text" square /> <QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square />
</div>
<div class="col">
<QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
</div>
</div> </div>
</template> </template>

View File

@ -190,7 +190,7 @@ const getLocale = (label) => {
const globalLocale = `globals.params.${param}`; const globalLocale = `globals.params.${param}`;
if (te(globalLocale)) return t(globalLocale); if (te(globalLocale)) return t(globalLocale);
else if (te(t(`params.${param}`))); else if (te(t(`params.${param}`)));
else return t(`${route.meta.moduleName}.params.${param}`); else return t(`${route.meta.moduleName.toLowerCase()}.params.${param}`);
}; };
</script> </script>

View File

@ -0,0 +1,20 @@
<template>
<QBtn
color="white"
dense
flat
icon="more_vert"
round
size="md"
data-cy="descriptor-more-opts"
>
<QTooltip>
{{ $t('components.cardDescriptor.moreOptions') }}
</QTooltip>
<QMenu ref="menuRef">
<QList>
<slot name="menu" :menu-ref="$refs.menuRef" />
</QList>
</QMenu>
</QBtn>
</template>

View File

@ -1,14 +1,16 @@
<script setup> <script setup>
import { onMounted, ref, watch } from 'vue'; import { onMounted, ref, computed, watch } from 'vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useStateStore } from 'src/stores/useStateStore'; import { useStateStore } from 'src/stores/useStateStore';
import { useRoute } from 'vue-router';
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const state = useStateStore(); const state = useStateStore();
const route = useRoute();
const props = defineProps({ const props = defineProps({
dataKey: { dataKey: {
@ -83,6 +85,17 @@ if (props.redirect)
}; };
let arrayData = useArrayData(props.dataKey, arrayDataProps); let arrayData = useArrayData(props.dataKey, arrayDataProps);
let store = arrayData.store; let store = arrayData.store;
const to = computed(() => {
const url = { path: route.path, query: { ...(route.query ?? {}) } };
const searchUrl = arrayData.store.searchUrl;
const currentFilter = {
...arrayData.store.currentFilter,
search: searchText.value || undefined,
};
if (searchUrl) url.query[searchUrl] = JSON.stringify(currentFilter);
return url;
});
watch( watch(
() => props.dataKey, () => props.dataKey,
@ -132,23 +145,32 @@ async function search() {
<template> <template>
<Teleport to="#searchbar" v-if="state.isHeaderMounted()"> <Teleport to="#searchbar" v-if="state.isHeaderMounted()">
<QForm @submit="search" id="searchbarForm"> <QForm @submit="search" id="searchbarForm">
<RouterLink
:to="to"
@click="
!$event.shiftKey && !$event.ctrlKey && search();
$refs.input.focus();
"
>
<QIcon
v-if="!quasar.platform.is.mobile"
class="cursor-pointer"
name="search"
size="sm"
>
<QTooltip>{{ t('link') }}</QTooltip>
</QIcon>
</RouterLink>
<VnInput <VnInput
id="searchbar" id="searchbar"
ref="input"
v-model.trim="searchText" v-model.trim="searchText"
:placeholder="t(props.label)" :placeholder="t(props.label)"
dense dense
standout
autofocus autofocus
data-cy="vnSearchBar" data-cy="vn-searchbar"
borderless
> >
<template #prepend>
<QIcon
v-if="!quasar.platform.is.mobile"
class="cursor-pointer"
name="search"
@click="search"
/>
</template>
<template #append> <template #append>
<QIcon <QIcon
v-if="props.info && $q.screen.gt.xs" v-if="props.info && $q.screen.gt.xs"
@ -173,20 +195,52 @@ async function search() {
.q-field { .q-field {
transition: width 0.36s; transition: width 0.36s;
} }
</style>
<style lang="scss"> :deep(.q-field__native) {
padding-top: 10px;
padding-left: 5px;
}
:deep(.q-field--dark .q-field__native:focus) {
color: black;
}
:deep(.q-field--focused) {
.q-icon {
color: black;
}
}
.cursor-info { .cursor-info {
cursor: help; cursor: help;
} }
#searchbar {
.q-field--standout.q-field--highlighted .q-field__control { .q-form {
display: flex;
align-items: center;
border-radius: 4px;
padding: 0 5px;
background-color: var(--vn-search-color);
&:hover {
background-color: var(--vn-search-color-hover);
}
&:focus-within {
background-color: white; background-color: white;
color: black;
.q-field__native,
.q-icon { .q-icon {
color: black !important; color: black;
} }
} }
} }
.q-icon {
color: var(--vn-label-color);
}
</style> </style>
<i18n>
en:
link: click to search, ctrl + click to open in a new tab, shift + click to open in a new window
es:
link: clic para buscar, ctrl + clic para abrir en una nueva pestaña, shift + clic para abrir en una nueva ventana
</i18n>

View File

@ -0,0 +1,78 @@
import { vi, describe, expect, it, beforeAll, afterEach, beforeEach } from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper';
import CardSummary from 'src/components/ui/CardSummary.vue';
import * as vueRouter from 'vue-router';
describe('CardSummary', () => {
let vm;
let wrapper;
beforeAll(() => {
vi.spyOn(axios, 'get').mockResolvedValue({ data: [] });
});
vi.spyOn(vueRouter, 'useRoute').mockReturnValue({
query: {},
params: {},
meta: { moduleName: 'mockName' },
path: 'mockName/1/summary',
name: 'CardSummary',
});
beforeEach(() => {
wrapper = createWrapper(CardSummary, {
propsData: {
dataKey: 'cardSummaryKey',
url: 'cardSummaryUrl',
filter: 'cardFilter',
},
});
vm = wrapper.vm;
wrapper = wrapper.wrapper;
});
afterEach(() => {
vi.clearAllMocks();
});
it('should fetch data correctly', async () => {
const fetchSpy = vi
.spyOn(vm.arrayData, 'fetch')
.mockResolvedValue({ data: [{ id: 1, name: 'Test Entity' }] });
await vm.fetch();
expect(fetchSpy).toHaveBeenCalledWith({ append: false, updateRouter: false });
expect(wrapper.emitted('onFetch')).toBeTruthy();
expect(vm.isLoading).toBe(false);
});
it('should set correct props to the store', () => {
expect(vm.store.url).toEqual('cardSummaryUrl');
expect(vm.store.filter).toEqual('cardFilter');
});
it('should compute entity correctly from store data', () => {
vm.store.data = [{ id: 1, name: 'Entity 1' }];
expect(vm.entity).toEqual({ id: 1, name: 'Entity 1' });
});
it('should handle empty data gracefully', () => {
vm.store.data = [];
expect(vm.entity).toBeUndefined();
});
it('should respond to prop changes and refetch data', async () => {
const newUrl = 'CardSummary/35';
const newKey = 'cardSummaryKey/35';
const fetchSpy = vi.spyOn(vm.arrayData, 'fetch');
await wrapper.setProps({ url: newUrl, filter: { key: newKey } });
expect(fetchSpy).toHaveBeenCalled();
expect(vm.store.url).toBe(newUrl);
expect(vm.store.filter).toEqual({ key: newKey });
});
it('should return true if route path ends with /summary' , () => {
expect(vm.isSummary).toBe(true);
});
});

View File

@ -0,0 +1,81 @@
import { describe, expect, it } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import FetchedTags from 'src/components/ui/FetchedTags.vue';
describe('tags computed property', () => {
it('returns an object with the correct keys and values', () => {
const vm = createWrapper(FetchedTags, {
props: {
item: {
tag1: 'JavaScript',
value1: 'Programming Language',
tag2: 'Vue',
value2: 'Framework',
tag3: 'EmptyTag',
},
tag: 'tag',
value: 'value',
columns: 2,
},
}).vm;
expect(vm.tags).toEqual({
JavaScript: 'Programming Language',
Vue: 'Framework',
EmptyTag: '',
});
});
it('returns an empty object if the item prop is an empty object', () => {
const vm = createWrapper(FetchedTags, {
props: {
item: {},
tag: 'tag',
value: 'value',
},
}).vm;
expect(vm.tags).toEqual({});
});
it('should calculate the correct columnStyle when columns prop is defined', () => {
const vm = createWrapper(FetchedTags, {
props: {
item: {
tag1: 'JavaScript',
value1: 'Programming Language',
tag2: 'Vue',
value2: 'Framework',
tag3: 'EmptyTag',
},
tag: 'tag',
value: 'value',
columns: 2,
},
}).vm;
const expectedStyle = {
'grid-template-columns': 'repeat(2, 1fr)',
'max-width': '8rem',
};
expect(vm.columnStyle).toEqual(expectedStyle);
});
it('should return an empty object for columnStyle when columns prop is not defined', () => {
const vm = createWrapper(FetchedTags, {
props: {
item: {
tag1: 'JavaScript',
value1: 'Programming Language',
tag2: 'Vue',
value2: 'Framework',
tag3: 'EmptyTag',
},
tag: 'tag',
value: 'value',
columns: null,
},
}).vm;
expect(vm.columnStyle).toEqual({});
});
});

View File

@ -7,7 +7,9 @@ import { isDialogOpened } from 'src/filters';
const arrayDataStore = useArrayDataStore(); const arrayDataStore = useArrayDataStore();
export function useArrayData(key = useRoute().meta.moduleName, userOptions) { export function useArrayData(key, userOptions) {
key ??= useRoute().meta.moduleName;
if (!key) throw new Error('ArrayData: A key is required to use this composable'); if (!key) throw new Error('ArrayData: A key is required to use this composable');
if (!arrayDataStore.get(key)) arrayDataStore.set(key); if (!arrayDataStore.get(key)) arrayDataStore.set(key);
@ -76,26 +78,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
cancelRequest(); cancelRequest();
canceller = new AbortController(); canceller = new AbortController();
const { params, limit } = getCurrentFilter();
const filter = {
limit: store.limit,
};
let userParams = { ...store.userParams };
Object.assign(filter, store.userFilter);
let where;
if (filter?.where || store.filter?.where)
where = Object.assign(filter?.where ?? {}, store.filter?.where ?? {});
Object.assign(filter, store.filter);
filter.where = where;
const params = { filter };
Object.assign(params, userParams);
if (params.filter) params.filter.skip = store.skip;
if (store?.order && typeof store?.order == 'string') store.order = [store.order];
if (store.order?.length) params.filter.order = [...store.order];
else delete params.filter.order;
store.currentFilter = JSON.parse(JSON.stringify(params)); store.currentFilter = JSON.parse(JSON.stringify(params));
delete store.currentFilter.filter.include; delete store.currentFilter.filter.include;
@ -121,7 +104,6 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
params, params,
}); });
const { limit } = filter;
store.hasMoreData = limit && response.data.length >= limit; store.hasMoreData = limit && response.data.length >= limit;
if (!append && !isDialogOpened() && updateRouter) { if (!append && !isDialogOpened() && updateRouter) {
@ -293,6 +275,31 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
router.replace(newUrl); router.replace(newUrl);
} }
function getCurrentFilter() {
const filter = {
limit: store.limit,
};
let userParams = { ...store.userParams };
Object.assign(filter, store.userFilter);
let where;
if (filter?.where || store.filter?.where)
where = Object.assign(filter?.where ?? {}, store.filter?.where ?? {});
Object.assign(filter, store.filter);
filter.where = where;
const params = { filter };
Object.assign(params, userParams);
if (params.filter) params.filter.skip = store.skip;
if (store?.order && typeof store?.order == 'string') store.order = [store.order];
if (store.order?.length) params.filter.order = [...store.order];
else delete params.filter.order;
return { filter, params, limit: filter.limit };
}
function processData(data, { map = true, append = true }) { function processData(data, { map = true, append = true }) {
if (!append) { if (!append) {
store.data = []; store.data = [];
@ -325,6 +332,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
fetch, fetch,
applyFilter, applyFilter,
addFilter, addFilter,
getCurrentFilter,
addFilterWhere, addFilterWhere,
addOrder, addOrder,
deleteOrder, deleteOrder,

View File

@ -0,0 +1,6 @@
import axios from 'axios';
export default async (id) => {
const { data } = await axios.get(`Accounts/${id}/exists`);
return data.exists;
};

View File

@ -10,6 +10,8 @@ body.body--light {
--vn-text-color: black; --vn-text-color: black;
--vn-label-color: #5f5f5f; --vn-label-color: #5f5f5f;
--vn-accent-color: #e7e3e3; --vn-accent-color: #e7e3e3;
--vn-search-color: #d4d4d4;
--vn-search-color-hover: #cfcfcf;
--vn-empty-tag: #acacac; --vn-empty-tag: #acacac;
--vn-black-text-color: black; --vn-black-text-color: black;
--vn-text-color-contrast: white; --vn-text-color-contrast: white;
@ -28,6 +30,8 @@ body.body--dark {
--vn-text-color: white; --vn-text-color: white;
--vn-label-color: #a8a8a8; --vn-label-color: #a8a8a8;
--vn-accent-color: #424242; --vn-accent-color: #424242;
--vn-search-color: #4b4b4b;
--vn-search-color-hover: #5b5b5b;
--vn-empty-tag: #2d2d2d; --vn-empty-tag: #2d2d2d;
--vn-black-text-color: black; --vn-black-text-color: black;
--vn-text-color-contrast: black; --vn-text-color-contrast: black;

View File

@ -16,6 +16,7 @@ import getUpdatedValues from './getUpdatedValues';
import getParamWhere from './getParamWhere'; import getParamWhere from './getParamWhere';
import parsePhone from './parsePhone'; import parsePhone from './parsePhone';
import isDialogOpened from './isDialogOpened'; import isDialogOpened from './isDialogOpened';
import toCelsius from './toCelsius';
export { export {
getUpdatedValues, getUpdatedValues,
@ -36,4 +37,5 @@ export {
dashIfEmpty, dashIfEmpty,
dateRange, dateRange,
getParamWhere, getParamWhere,
toCelsius,
}; };

3
src/filters/toCelsius.js Normal file
View File

@ -0,0 +1,3 @@
export default function toCelsius(value) {
return value ? `${value}°C` : '';
}

View File

@ -346,6 +346,7 @@ globals:
countryFk: Country countryFk: Country
companyFk: Company companyFk: Company
changePass: Change password changePass: Change password
setPass: Set password
deleteConfirmTitle: Delete selected elements deleteConfirmTitle: Delete selected elements
changeState: Change state changeState: Change state
raid: 'Raid {daysInForward} days' raid: 'Raid {daysInForward} days'
@ -388,74 +389,19 @@ cau:
subtitle: By sending this ticket, all the data related to the error, the section, the user, etc., are already sent. subtitle: By sending this ticket, all the data related to the error, the section, the user, etc., are already sent.
inputLabel: Explain why this error should not appear inputLabel: Explain why this error should not appear
askPrivileges: Ask for privileges askPrivileges: Ask for privileges
entry:
list:
newEntry: New entry
tableVisibleColumns:
created: Creation
supplierFk: Supplier
isBooked: Booked
isConfirmed: Confirmed
isOrdered: Ordered
companyFk: Company
travelFk: Travel
isExcludedFromAvailable: Inventory
invoiceAmount: Import
summary:
commission: Commission
currency: Currency
invoiceNumber: Invoice number
ordered: Ordered
booked: Booked
excludedFromAvailable: Inventory
travelReference: Reference
travelAgency: Agency
travelShipped: Shipped
travelDelivered: Delivered
travelLanded: Landed
travelReceived: Received
buys: Buys
stickers: Stickers
package: Package
packing: Pack.
grouping: Group.
buyingValue: Buying value
import: Import
pvp: PVP
basicData:
travel: Travel
currency: Currency
commission: Commission
observation: Observation
booked: Booked
excludedFromAvailable: Inventory
buys:
observations: Observations
packagingFk: Box
color: Color
printedStickers: Printed stickers
notes:
observationType: Observation type
latestBuys:
tableVisibleColumns:
image: Picture
itemFk: Item ID
weightByPiece: Weight/Piece
isActive: Active
family: Family
entryFk: Entry
freightValue: Freight value
comissionValue: Commission value
packageValue: Package value
isIgnored: Is ignored
price2: Grouping
price3: Packing
minPrice: Min
ektFk: Ekt
packingOut: Package out
landing: Landing
isExcludedFromAvailable: Es inventory
ticket: ticket:
params:
ticketFk: Ticket ID
weekDay: Weekday
agencyModeFk: Agency
id: Worker
state: State
created: Created
externalId: External ID
counter: Counter
freightItemName: Freight item name
packageItemName: Package item name
longName: Long name
card: card:
customerId: Customer ID customerId: Customer ID
customerCard: Customer card customerCard: Customer card
@ -506,6 +452,7 @@ invoiceOut:
card: card:
issued: Issued issued: Issued
customerCard: Customer card customerCard: Customer card
ticketList: Ticket List
summary: summary:
issued: Issued issued: Issued
dued: Due dued: Due
@ -558,27 +505,6 @@ parking:
searchBar: searchBar:
info: You can search by parking code info: You can search by parking code
label: Search parking... label: Search parking...
order:
field:
salesPersonFk: Sales Person
form:
clientFk: Client
addressFk: Address
agencyModeFk: Agency
list:
newOrder: New Order
summary:
basket: Basket
notConfirmed: Not confirmed
created: Created
createdFrom: Created From
address: Address
total: Total
items: Items
orderTicketList: Order Ticket List
amount: Amount
confirm: Confirm
confirmLines: Confirm lines
department: department:
chat: Chat chat: Chat
bossDepartment: Boss Department bossDepartment: Boss Department
@ -697,6 +623,11 @@ wagon:
minHeightBetweenTrays: 'The minimum height between trays is ' minHeightBetweenTrays: 'The minimum height between trays is '
maxWagonHeight: 'The maximum height of the wagon is ' maxWagonHeight: 'The maximum height of the wagon is '
uncompleteTrays: There are incomplete trays uncompleteTrays: There are incomplete trays
params:
label: Label
plate: Plate
volume: Volume
name: Name
supplier: supplier:
list: list:
@ -865,7 +796,10 @@ components:
hasMinPrice: Minimum price hasMinPrice: Minimum price
# LatestBuysFilter # LatestBuysFilter
salesPersonFk: Buyer salesPersonFk: Buyer
supplierFk: Supplier
from: From from: From
to: To
visible: Is visible
active: Is active active: Is active
floramondo: Is floramondo floramondo: Is floramondo
showBadDates: Show future items showBadDates: Show future items

View File

@ -348,6 +348,7 @@ globals:
countryFk: País countryFk: País
companyFk: Empresa companyFk: Empresa
changePass: Cambiar contraseña changePass: Cambiar contraseña
setPass: Establecer contraseña
deleteConfirmTitle: Eliminar los elementos seleccionados deleteConfirmTitle: Eliminar los elementos seleccionados
changeState: Cambiar estado changeState: Cambiar estado
raid: 'Redada {daysInForward} días' raid: 'Redada {daysInForward} días'
@ -388,75 +389,19 @@ cau:
subtitle: Al enviar este cau ya se envían todos los datos relacionados con el error, la sección, el usuario, etc subtitle: Al enviar este cau ya se envían todos los datos relacionados con el error, la sección, el usuario, etc
inputLabel: Explique el motivo por el que no deberia aparecer este fallo inputLabel: Explique el motivo por el que no deberia aparecer este fallo
askPrivileges: Solicitar permisos askPrivileges: Solicitar permisos
entry:
list:
newEntry: Nueva entrada
tableVisibleColumns:
created: Creación
supplierFk: Proveedor
isBooked: Asentado
isConfirmed: Confirmado
isOrdered: Pedida
companyFk: Empresa
travelFk: Envio
isExcludedFromAvailable: Inventario
invoiceAmount: Importe
summary:
commission: Comisión
currency: Moneda
invoiceNumber: Núm. factura
ordered: Pedida
booked: Contabilizada
excludedFromAvailable: Inventario
travelReference: Referencia
travelAgency: Agencia
travelShipped: F. envio
travelWarehouseOut: Alm. salida
travelDelivered: Enviada
travelLanded: F. entrega
travelReceived: Recibida
buys: Compras
stickers: Etiquetas
package: Embalaje
packing: Pack.
grouping: Group.
buyingValue: Coste
import: Importe
pvp: PVP
basicData:
travel: Envío
currency: Moneda
observation: Observación
commission: Comisión
booked: Asentado
excludedFromAvailable: Inventario
buys:
observations: Observaciónes
packagingFk: Embalaje
color: Color
printedStickers: Etiquetas impresas
notes:
observationType: Tipo de observación
latestBuys:
tableVisibleColumns:
image: Foto
itemFk: Id Artículo
weightByPiece: Peso (gramos)/tallo
isActive: Activo
family: Familia
entryFk: Entrada
freightValue: Porte
comissionValue: Comisión
packageValue: Embalaje
isIgnored: Ignorado
price2: Grouping
price3: Packing
minPrice: Min
ektFk: Ekt
packingOut: Embalaje envíos
landing: Llegada
isExcludedFromAvailable: Es inventario
ticket: ticket:
params:
ticketFk: ID de ticket
weekDay: Salida
agencyModeFk: Agencia
id: Comercial
created: Creado
state: Estado
externalId: ID externo
counter: Contador
freightItemName: Nombre
packageItemName: Embalaje
longName: Descripción
card: card:
customerId: ID cliente customerId: ID cliente
customerCard: Ficha del cliente customerCard: Ficha del cliente
@ -544,30 +489,6 @@ invoiceOut:
comercial: Comercial comercial: Comercial
errors: errors:
downloadCsvFailed: Error al descargar CSV downloadCsvFailed: Error al descargar CSV
order:
field:
salesPersonFk: Comercial
form:
clientFk: Cliente
addressFk: Dirección
agencyModeFk: Agencia
list:
newOrder: Nuevo Pedido
summary:
basket: Cesta
notConfirmed: No confirmada
created: Creado
createdFrom: Creado desde
address: Dirección
total: Total
vat: IVA
state: Estado
alias: Alias
items: Artículos
orderTicketList: Tickets del pedido
amount: Monto
confirm: Confirmar
confirmLines: Confirmar lineas
shelving: shelving:
list: list:
parking: Parking parking: Parking
@ -700,6 +621,11 @@ wagon:
minHeightBetweenTrays: 'La distancia mínima entre bandejas es ' minHeightBetweenTrays: 'La distancia mínima entre bandejas es '
maxWagonHeight: 'La altura máxima del vagón es ' maxWagonHeight: 'La altura máxima del vagón es '
uncompleteTrays: Hay bandejas sin completar uncompleteTrays: Hay bandejas sin completar
params:
label: Etiqueta
plate: Matrícula
volume: Volumen
name: Nombre
supplier: supplier:
list: list:
payMethod: Método de pago payMethod: Método de pago
@ -867,7 +793,11 @@ components:
wareHouseFk: Almacén wareHouseFk: Almacén
# LatestBuysFilter # LatestBuysFilter
salesPersonFk: Comprador salesPersonFk: Comprador
supplierFk: Proveedor
visible: Visible
active: Activo active: Activo
from: Desde
to: Hasta
floramondo: Floramondo floramondo: Floramondo
showBadDates: Ver items a futuro showBadDates: Ver items a futuro
userPanel: userPanel:

View File

@ -6,6 +6,7 @@ import FormModelPopup from 'components/FormModelPopup.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnInputPassword from 'src/components/common/VnInputPassword.vue';
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
@ -61,10 +62,10 @@ const redirectToAccountBasicData = (_, { id }) => {
hide-selected hide-selected
:rules="validate('VnUser.roleFk')" :rules="validate('VnUser.roleFk')"
/> />
<VnInput <VnInputPassword
v-model="data.password" v-model="data.password"
:label="t('ldap.password')" :label="t('ldap.password')"
type="password" :toggle-visibility="true"
:rules="validate('VnUser.password')" :rules="validate('VnUser.password')"
/> />
<QCheckbox <QCheckbox

View File

@ -8,6 +8,7 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import axios from 'axios'; import axios from 'axios';
import VnInputPassword from 'src/components/common/VnInputPassword.vue';
const { t } = useI18n(); const { t } = useI18n();
const { notify } = useNotify(); const { notify } = useNotify();
@ -128,10 +129,9 @@ onMounted(async () => await getInitialLdapConfig());
:required="true" :required="true"
:rules="validate('LdapConfig.rdn')" :rules="validate('LdapConfig.rdn')"
/> />
<VnInput <VnInputPassword
:label="t('ldap.password')" :label="t('ldap.password')"
clearable clearable
type="password"
v-model="data.password" v-model="data.password"
:required="true" :required="true"
:rules="validate('LdapConfig.password')" :rules="validate('LdapConfig.password')"

View File

@ -4,9 +4,9 @@ import { computed, ref } from 'vue';
import VnTable from 'components/VnTable/VnTable.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import AccountSummary from './Card/AccountSummary.vue'; import AccountSummary from './Card/AccountSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import VnInput from 'src/components/common/VnInput.vue';
import VnSection from 'src/components/common/VnSection.vue'; import VnSection from 'src/components/common/VnSection.vue';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
import VnInputPassword from 'src/components/common/VnInputPassword.vue';
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
@ -168,10 +168,9 @@ function exprBuilder(param, value) {
> >
<template #more-create-dialog="{ data }"> <template #more-create-dialog="{ data }">
<QCardSection> <QCardSection>
<VnInput <VnInputPassword
:label="t('Password')" :label="t('Password')"
v-model="data.password" v-model="data.password"
type="password"
:required="true" :required="true"
autocomplete="new-password" autocomplete="new-password"
/> />

View File

@ -8,6 +8,7 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import axios from 'axios'; import axios from 'axios';
import VnInputPassword from 'src/components/common/VnInputPassword.vue';
const { t } = useI18n(); const { t } = useI18n();
const { notify } = useNotify(); const { notify } = useNotify();
@ -143,10 +144,9 @@ onMounted(async () => await getInitialSambaConfig());
v-model="data.adUser" v-model="data.adUser"
:rules="validate('SambaConfigs.adUser')" :rules="validate('SambaConfigs.adUser')"
/> />
<VnInput <VnInputPassword
:label="t('samba.passwordAD')" :label="t('samba.passwordAD')"
clearable clearable
type="password"
v-model="data.adPassword" v-model="data.adPassword"
/> />
<VnInput <VnInput

View File

@ -1,13 +1,13 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import CardDescriptor from 'components/ui/CardDescriptor.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription'; import useCardDescription from 'src/composables/useCardDescription';
import AccountDescriptorMenu from './AccountDescriptorMenu.vue'; import AccountDescriptorMenu from './AccountDescriptorMenu.vue';
import FetchData from 'src/components/FetchData.vue';
import VnImg from 'src/components/ui/VnImg.vue'; import VnImg from 'src/components/ui/VnImg.vue';
import useHasAccount from 'src/composables/useHasAccount.js';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -23,6 +23,7 @@ const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
const data = ref(useCardDescription()); const data = ref(useCardDescription());
const hasAccount = ref();
const setData = (entity) => (data.value = useCardDescription(entity.nickname, entity.id)); const setData = (entity) => (data.value = useCardDescription(entity.nickname, entity.id));
const filter = { const filter = {
@ -30,18 +31,16 @@ const filter = {
fields: ['id', 'nickname', 'name', 'role'], fields: ['id', 'nickname', 'name', 'role'],
include: { relation: 'role', scope: { fields: ['id', 'name'] } }, include: { relation: 'role', scope: { fields: ['id', 'name'] } },
}; };
const hasAccount = ref(false);
onMounted(async () => {
hasAccount.value = await useHasAccount(entityId.value);
});
</script> </script>
<template> <template>
<FetchData
:url="`Accounts/${entityId}/exists`"
auto-load
@on-fetch="(data) => (hasAccount = data.exists)"
/>
<CardDescriptor <CardDescriptor
ref="descriptor" ref="descriptor"
url="VnUsers/preview" :url="`VnUsers/preview`"
:filter="filter" :filter="filter"
module="Account" module="Account"
@on-fetch="setData" @on-fetch="setData"
@ -50,7 +49,7 @@ const hasAccount = ref(false);
:subtitle="data.subtitle" :subtitle="data.subtitle"
> >
<template #menu> <template #menu>
<AccountDescriptorMenu :has-account="hasAccount" /> <AccountDescriptorMenu :entity-id="entityId" />
</template> </template>
<template #before> <template #before>
<VnImg :id="entityId" collection="user" resolution="520x520" class="photo"> <VnImg :id="entityId" collection="user" resolution="520x520" class="photo">
@ -74,7 +73,7 @@ const hasAccount = ref(false);
<VnLv :label="t('account.card.nickname')" :value="entity.name" /> <VnLv :label="t('account.card.nickname')" :value="entity.name" />
<VnLv :label="t('account.card.role')" :value="entity.role.name" /> <VnLv :label="t('account.card.role')" :value="entity.role.name" />
</template> </template>
<template #icons="{ entity }"> <template #actions="{ entity }">
<QCardActions class="q-gutter-x-md"> <QCardActions class="q-gutter-x-md">
<QIcon <QIcon
v-if="!entity.active" v-if="!entity.active"
@ -82,7 +81,7 @@ const hasAccount = ref(false);
name="vn:disabled" name="vn:disabled"
flat flat
round round
size="xs" size="sm"
class="fill-icon" class="fill-icon"
> >
<QTooltip>{{ t('account.card.deactivated') }}</QTooltip> <QTooltip>{{ t('account.card.deactivated') }}</QTooltip>
@ -93,7 +92,7 @@ const hasAccount = ref(false);
v-if="hasAccount" v-if="hasAccount"
flat flat
round round
size="xs" size="sm"
class="fill-icon" class="fill-icon"
> >
<QTooltip>{{ t('account.card.enabled') }}</QTooltip> <QTooltip>{{ t('account.card.enabled') }}</QTooltip>

View File

@ -1,14 +1,17 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { computed, ref, toRefs } from 'vue'; import { computed, onMounted, ref, toRefs } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useVnConfirm } from 'composables/useVnConfirm'; import { useVnConfirm } from 'composables/useVnConfirm';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useAcl } from 'src/composables/useAcl'; import { useAcl } from 'src/composables/useAcl';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { useState } from 'src/composables/useState';
import VnConfirm from 'src/components/ui/VnConfirm.vue'; import VnConfirm from 'src/components/ui/VnConfirm.vue';
import VnInputPassword from 'src/components/common/VnInputPassword.vue';
import VnChangePassword from 'src/components/common/VnChangePassword.vue'; import VnChangePassword from 'src/components/common/VnChangePassword.vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useRouter } from 'vue-router';
const $props = defineProps({ const $props = defineProps({
hasAccount: { hasAccount: {
@ -17,14 +20,20 @@ const $props = defineProps({
required: true, required: true,
}, },
}); });
const { t } = useI18n(); const { t } = useI18n();
const { hasAccount } = toRefs($props); const { hasAccount } = toRefs($props);
const { openConfirmationModal } = useVnConfirm(); const { openConfirmationModal } = useVnConfirm();
const route = useRoute(); const route = useRoute();
const router = useRouter();
const state = useState();
const user = state.getUser();
const { notify } = useQuasar(); const { notify } = useQuasar();
const account = computed(() => useArrayData('AccountId').store.data[0]); const account = computed(() => useArrayData('AccountId').store.data[0]);
account.value.hasAccount = hasAccount.value; account.value.hasAccount = hasAccount.value;
const entityId = computed(() => +route.params.id); const entityId = computed(() => +route.params.id);
const hasitManagementAccess = ref();
const hasSysadminAccess = ref();
async function updateStatusAccount(active) { async function updateStatusAccount(active) {
if (active) { if (active) {
@ -36,7 +45,7 @@ async function updateStatusAccount(active) {
account.value.hasAccount = active; account.value.hasAccount = active;
const status = active ? 'enable' : 'disable'; const status = active ? 'enable' : 'disable';
notify({ notify({
message: t(`account.card.${status}Account.success`), message: t(`account.card.actions.${status}Account.success`),
type: 'positive', type: 'positive',
}); });
} }
@ -49,6 +58,17 @@ async function updateStatusUser(active) {
type: 'positive', type: 'positive',
}); });
} }
async function deleteAccount() {
const { data } = await axios.delete(`VnUsers/${entityId.value}`);
if (data) {
notify({
message: t('account.card.actions.delete.success'),
type: 'positive',
});
router.push({ name: 'AccountList' });
}
}
const showSyncDialog = ref(false); const showSyncDialog = ref(false);
const syncPassword = ref(null); const syncPassword = ref(null);
const shouldSyncPassword = ref(false); const shouldSyncPassword = ref(false);
@ -63,11 +83,27 @@ async function sync() {
type: 'positive', type: 'positive',
}); });
} }
const askOldPass = ref(false);
const changePassRef = ref();
const onChangePass = (oldPass) => {
askOldPass.value = oldPass;
changePassRef.value.show();
};
onMounted(() => {
hasitManagementAccess.value = useAcl().hasAny([
{ model: 'VnUser', props: 'higherPrivileges', accessType: 'WRITE' },
]);
hasSysadminAccess.value = useAcl().hasAny([
{ model: 'VnUser', props: 'adminUser', accessType: 'WRITE' },
]);
});
</script> </script>
<template> <template>
<VnChangePassword <VnChangePassword
ref="changePassRef" ref="changePassRef"
:ask-old-pass="true" :ask-old-pass="askOldPass"
:submit-fn=" :submit-fn="
async (newPassword, oldPassword) => { async (newPassword, oldPassword) => {
await axios.patch(`Accounts/change-password`, { await axios.patch(`Accounts/change-password`, {
@ -97,7 +133,7 @@ async function sync() {
<QTooltip>{{ t('account.card.actions.sync.tooltip') }}</QTooltip> <QTooltip>{{ t('account.card.actions.sync.tooltip') }}</QTooltip>
</QIcon></QCheckbox </QIcon></QCheckbox
> >
<QInput <VnInputPassword
v-if="shouldSyncPassword" v-if="shouldSyncPassword"
:label="t('login.password')" :label="t('login.password')"
v-model="syncPassword" v-model="syncPassword"
@ -109,18 +145,46 @@ async function sync() {
</template> </template>
</VnConfirm> </VnConfirm>
<QItem <QItem
v-if=" v-if="hasitManagementAccess"
entityId == account.id &&
useAcl().hasAny([{ model: 'Account', props: '*', accessType: 'WRITE' }])
"
v-ripple v-ripple
clickable clickable
@click="$refs.changePassRef.show()" @click="
openConfirmationModal(
t('account.card.actions.disableAccount.title'),
t('account.card.actions.disableAccount.subtitle'),
() => deleteAccount()
)
"
> >
<QItemSection>{{ t('globals.changePass') }}</QItemSection> <QItemSection>{{ t('globals.delete') }}</QItemSection>
</QItem> </QItem>
<QItem <QItem
v-if="account.hasAccount" v-if="hasSysadminAccess"
v-ripple
clickable
@click="user.id === account.id ? onChangePass(true) : onChangePass(false)"
>
<QItemSection v-if="user.id === account.id">
{{ t('globals.changePass') }}
</QItemSection>
<QItemSection v-else>{{ t('globals.setPass') }}</QItemSection>
</QItem>
<QItem
v-if="!account.hasAccount && hasSysadminAccess"
v-ripple
clickable
@click="
openConfirmationModal(
t('account.card.actions.enableAccount.title'),
t('account.card.actions.enableAccount.subtitle'),
() => updateStatusAccount(true)
)
"
>
<QItemSection>{{ t('account.card.actions.enableAccount.name') }}</QItemSection>
</QItem>
<QItem
v-if="account.hasAccount && hasSysadminAccess"
v-ripple v-ripple
clickable clickable
@click=" @click="
@ -135,7 +199,7 @@ async function sync() {
</QItem> </QItem>
<QItem <QItem
v-if="!account.active" v-if="!account.active && hasitManagementAccess"
v-ripple v-ripple
clickable clickable
@click=" @click="
@ -149,7 +213,7 @@ async function sync() {
<QItemSection>{{ t('account.card.actions.activateUser.name') }}</QItemSection> <QItemSection>{{ t('account.card.actions.activateUser.name') }}</QItemSection>
</QItem> </QItem>
<QItem <QItem
v-if="account.active" v-if="account.active && hasitManagementAccess"
v-ripple v-ripple
clickable clickable
@click=" @click="
@ -162,7 +226,12 @@ async function sync() {
> >
<QItemSection>{{ t('account.card.actions.deactivateUser.name') }}</QItemSection> <QItemSection>{{ t('account.card.actions.deactivateUser.name') }}</QItemSection>
</QItem> </QItem>
<QItem v-ripple clickable @click="showSyncDialog = true"> <QItem
v-if="useAcl().hasAny([{ model: 'VnRole', props: '*', accessType: 'WRITE' }])"
v-ripple
clickable
@click="showSyncDialog = true"
>
<QItemSection>{{ t('account.card.actions.sync.name') }}</QItemSection> <QItemSection>{{ t('account.card.actions.sync.name') }}</QItemSection>
</QItem> </QItem>
<QSeparator /> <QSeparator />

View File

@ -9,6 +9,7 @@ import AccountMailAliasCreateForm from './AccountMailAliasCreateForm.vue';
import { useVnConfirm } from 'composables/useVnConfirm'; import { useVnConfirm } from 'composables/useVnConfirm';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import useHasAccount from 'src/composables/useHasAccount.js';
import axios from 'axios'; import axios from 'axios';
const { t } = useI18n(); const { t } = useI18n();
@ -50,16 +51,6 @@ const columns = computed(() => [
}, },
]); ]);
const fetchAccountExistence = async () => {
try {
const { data } = await axios.get(`Accounts/${route.params.id}/exists`);
return data.exists;
} catch (error) {
console.error('Error fetching account existence', error);
return false;
}
};
const deleteMailAlias = async (row) => { const deleteMailAlias = async (row) => {
await axios.delete(`${urlPath}/${row.id}`); await axios.delete(`${urlPath}/${row.id}`);
fetchMailAliases(); fetchMailAliases();
@ -79,7 +70,7 @@ const fetchMailAliases = async () => {
const getAccountData = async (reload = true) => { const getAccountData = async (reload = true) => {
loading.value = true; loading.value = true;
hasAccount.value = await fetchAccountExistence(); hasAccount.value = await useHasAccount(route.params.id);
if (!hasAccount.value) { if (!hasAccount.value) {
loading.value = false; loading.value = false;
store.data = []; store.data = [];

View File

@ -9,6 +9,7 @@ import VnRow from 'components/ui/VnRow.vue';
import axios from 'axios'; import axios from 'axios';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import useHasAccount from 'src/composables/useHasAccount';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@ -30,23 +31,9 @@ const hasDataChanged = computed(
initialData.value.hasData !== hasData.value initialData.value.hasData !== hasData.value
); );
const fetchAccountExistence = async () => {
try {
const { data } = await axios.get(`Accounts/${route.params.id}/exists`);
return data.exists;
} catch (error) {
console.error('Error fetching account existence', error);
return false;
}
};
const fetchMailForwards = async () => { const fetchMailForwards = async () => {
try { const response = await axios.get(`MailForwards/${route.params.id}`);
const response = await axios.get(`MailForwards/${route.params.id}`); return response.data;
return response.data;
} catch {
return null;
}
}; };
const deleteMailForward = async () => { const deleteMailForward = async () => {
@ -72,7 +59,7 @@ const setInitialData = async () => {
loading.value = true; loading.value = true;
initialData.value.account = route.params.id; initialData.value.account = route.params.id;
formData.value.account = route.params.id; formData.value.account = route.params.id;
hasAccount.value = await fetchAccountExistence(route.params.id); hasAccount.value = await useHasAccount(route.params.id);
if (!hasAccount.value) { if (!hasAccount.value) {
loading.value = false; loading.value = false;
return; return;

View File

@ -7,6 +7,7 @@ import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import AccountDescriptorMenu from './AccountDescriptorMenu.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -31,12 +32,14 @@ const filter = {
<template> <template>
<CardSummary <CardSummary
data-key="AccountId" data-key="AccountId"
ref="AccountSummary"
url="VnUsers/preview" url="VnUsers/preview"
:filter="filter" :filter="filter"
@on-fetch="(data) => (account = data)" @on-fetch="(data) => (account = data)"
> >
<template #header>{{ account.id }} - {{ account.nickname }}</template> <template #header>{{ account.id }} - {{ account.nickname }}</template>
<template #menu="">
<AccountDescriptorMenu :entity-id="entityId" />
</template>
<template #body> <template #body>
<QCard class="vn-one"> <QCard class="vn-one">
<QCardSection class="q-pa-none"> <QCardSection class="q-pa-none">

View File

@ -1,4 +1,18 @@
account: account:
params:
id: Id
name: Name
roleFk: Role
nickname: Nickname
password: Password
active: Active
search: Id
description: Description
alias: Alias
model: Model
principalId: Role
property: Property
accessType: Access
card: card:
nickname: User nickname: User
role: Role role: Role

View File

@ -1,4 +1,20 @@
accessType: Acceso
property: Propiedad
account: account:
params:
id: Id
name: Nombre
roleFk: Rol
nickname: Nickname
password: Contraseña
active: Activo
search: Id
description: Descripción
alias: Alias
model: Modelo
principalId: Rol
property: Propiedad
accessType: Acceso
card: card:
nickname: Usuario nickname: Usuario
role: Rol role: Rol

View File

@ -1,21 +1,13 @@
<script setup> <script setup>
import VnCard from 'components/common/VnCard.vue'; import VnCardBeta from 'components/common/VnCardBeta.vue';
import ClaimDescriptor from './ClaimDescriptor.vue'; import ClaimDescriptor from './ClaimDescriptor.vue';
import ClaimFilter from '../ClaimFilter.vue';
import filter from './ClaimFilter.js'; import filter from './ClaimFilter.js';
</script> </script>
<template> <template>
<VnCard <VnCardBeta
data-key="Claim" data-key="Claim"
base-url="Claims" base-url="Claims"
:descriptor="ClaimDescriptor" :descriptor="ClaimDescriptor"
:filter-panel="ClaimFilter"
search-data-key="ClaimList"
:filter="filter" :filter="filter"
:searchbar-props="{
url: 'Claims/filter',
label: 'Search claim',
info: 'You can search by claim id or customer name',
}"
/> />
</template> </template>

View File

@ -19,6 +19,7 @@ import ClaimNotes from 'src/pages/Claim/Card/ClaimNotes.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import ClaimDescriptorMenu from './ClaimDescriptorMenu.vue';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -228,6 +229,9 @@ function claimUrl(section) {
</QList> </QList>
</QBtnDropdown> </QBtnDropdown>
</template> </template>
<template #menu="{ entity }">
<ClaimDescriptorMenu :claim="entity.claim" />
</template>
<template #body="{ entity: { claim, salesClaimed, developments } }"> <template #body="{ entity: { claim, salesClaimed, developments } }">
<QCard class="vn-one" v-if="$route.name != 'ClaimSummary'"> <QCard class="vn-one" v-if="$route.name != 'ClaimSummary'">
<VnTitle <VnTitle

View File

@ -93,16 +93,7 @@ defineExpose({ states });
outlined outlined
rounded rounded
dense dense
> />
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel> #{{ scope.opt?.id }} </QItemLabel>
<QItemLabel caption>{{ scope.opt?.name }}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect <VnSelect
:label="t('claim.responsible')" :label="t('claim.responsible')"
v-model="params.claimResponsibleFk" v-model="params.claimResponsibleFk"

View File

@ -2,18 +2,18 @@
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toDate } from 'filters/index'; import { toDate } from 'filters/index';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import ClaimFilter from './ClaimFilter.vue'; import ClaimFilter from './ClaimFilter.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import ClaimSummary from './Card/ClaimSummary.vue'; import ClaimSummary from './Card/ClaimSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import RightMenu from 'src/components/common/RightMenu.vue';
import VnTable from 'src/components/VnTable/VnTable.vue'; import VnTable from 'src/components/VnTable/VnTable.vue';
import ZoneDescriptorProxy from '../Zone/Card/ZoneDescriptorProxy.vue'; import ZoneDescriptorProxy from '../Zone/Card/ZoneDescriptorProxy.vue';
import VnSection from 'src/components/common/VnSection.vue';
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const dataKey = 'ClaimList';
const claimFilterRef = ref(); const claimFilterRef = ref();
const columns = computed(() => [ const columns = computed(() => [
@ -125,49 +125,50 @@ const STATE_COLOR = {
</script> </script>
<template> <template>
<VnSearchbar <VnSection
data-key="ClaimList" :data-key="dataKey"
:label="t('Search claim')" :columns="columns"
:info="t('You can search by claim id or customer name')" prefix="claim"
/> :array-data-props="{
<RightMenu> url: 'Claims/filter',
<template #right-panel> order: ['cs.priority ASC', 'created ASC'],
}"
>
<template #rightMenu>
<ClaimFilter data-key="ClaimList" ref="claimFilterRef" /> <ClaimFilter data-key="ClaimList" ref="claimFilterRef" />
</template> </template>
</RightMenu> <template #body>
<VnTable <VnTable
data-key="ClaimList" :data-key="dataKey"
url="Claims/filter" :columns="columns"
:order="['cs.priority ASC', 'created ASC']" redirect="claim"
:columns="columns" :right-search="false"
redirect="claim" auto-load
:right-search="false" >
auto-load <template #column-clientFk="{ row }">
> <span class="link" @click.stop>
<template #column-clientFk="{ row }"> {{ row.clientName }}
<span class="link" @click.stop> <CustomerDescriptorProxy :id="row.clientFk" />
{{ row.clientName }} </span>
<CustomerDescriptorProxy :id="row.clientFk" /> </template>
</span> <template #column-attendedBy="{ row }">
<span @click.stop>
<VnUserLink :name="row.workerName" :worker-id="row.workerFk" />
</span>
</template>
<template #column-zoneFk="{ row }">
<span class="link" @click.stop>
{{ row.zoneName }}
<ZoneDescriptorProxy :id="row.zoneId" />
</span>
</template>
</VnTable>
</template> </template>
<template #column-attendedBy="{ row }"> </VnSection>
<span @click.stop>
<VnUserLink :name="row.workerName" :worker-id="row.workerFk" />
</span>
</template>
<template #column-zoneFk="{ row }">
<span class="link" @click.stop>
{{ row.zoneName }}
<ZoneDescriptorProxy :id="row.zoneId" />
</span>
</template>
</VnTable>
</template> </template>
<i18n> <i18n>
es: es:
Search claim: Buscar reclamación
You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente
params: params:
stateCode: Estado stateCode: Estado
en: en:

View File

@ -44,3 +44,5 @@ claim:
fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}' fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}'
noData: 'There are no images/videos, click here or drag and drop the file' noData: 'There are no images/videos, click here or drag and drop the file'
dragDrop: Drag and drop it here dragDrop: Drag and drop it here
search: Search claims
searchInfo: You can search by claim id or customer name

View File

@ -1,5 +1,3 @@
Search claim: Buscar reclamación
You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente
claim: claim:
customer: Cliente customer: Cliente
code: Código code: Código
@ -46,3 +44,5 @@ claim:
fileDescription: 'ID de reclamación {claimId} del cliente {clientName} con ID {clientId}' fileDescription: 'ID de reclamación {claimId} del cliente {clientName} con ID {clientId}'
noData: 'No hay imágenes/videos, haz clic aquí o arrastra y suelta el archivo' noData: 'No hay imágenes/videos, haz clic aquí o arrastra y suelta el archivo'
dragDrop: Arrastra y suelta aquí dragDrop: Arrastra y suelta aquí
search: Buscar reclamación
searchInfo: Puedes buscar por ID de la reclamación o nombre del cliente

View File

@ -14,7 +14,6 @@ import CustomerDescriptorMenu from './CustomerDescriptorMenu.vue';
const customerDebt = ref(); const customerDebt = ref();
const customerCredit = ref(); const customerCredit = ref();
const $props = defineProps({ const $props = defineProps({
id: { id: {
type: Number, type: Number,

View File

@ -44,6 +44,7 @@ function handleLocation(data, location) {
:required="true" :required="true"
:rules="validate('client.socialName')" :rules="validate('client.socialName')"
clearable clearable
uppercase="true"
v-model="data.socialName" v-model="data.socialName"
> >
<template #append> <template #append>

View File

@ -12,6 +12,7 @@ import VnLinkMail from 'src/components/ui/VnLinkMail.vue';
import CustomerSummaryTable from 'src/pages/Customer/components/CustomerSummaryTable.vue'; import CustomerSummaryTable from 'src/pages/Customer/components/CustomerSummaryTable.vue';
import VnTitle from 'src/components/common/VnTitle.vue'; import VnTitle from 'src/components/common/VnTitle.vue';
import VnRow from 'src/components/ui/VnRow.vue'; import VnRow from 'src/components/ui/VnRow.vue';
import CustomerDescriptorMenu from './CustomerDescriptorMenu.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const grafanaUrl = 'https://grafana.verdnatura.es'; const grafanaUrl = 'https://grafana.verdnatura.es';
@ -70,6 +71,9 @@ const sumRisk = ({ clientRisks }) => {
data-key="CustomerSummary" data-key="CustomerSummary"
module-name="Customer" module-name="Customer"
> >
<template #menu="{ entity }">
<CustomerDescriptorMenu :customer="entity" />
</template>
<template #body="{ entity }"> <template #body="{ entity }">
<QCard class="vn-one"> <QCard class="vn-one">
<VnTitle <VnTitle
@ -94,14 +98,13 @@ const sumRisk = ({ clientRisks }) => {
:phone-number="entity.mobile" :phone-number="entity.mobile"
:channel="entity.country?.saySimpleCountry?.channel" :channel="entity.country?.saySimpleCountry?.channel"
class="q-ml-xs" class="q-ml-xs"
:country="entity.country?.code"
/> />
</template> </template>
</VnLv> </VnLv>
<VnLv :value="entity.email" copy <VnLv :value="entity.email" copy
><template #label> ><template #label>
{{ t('globals.params.email') }} {{ t('globals.params.email') }}
<VnLinkMail :email="entity.email"></VnLinkMail> </template <VnLinkMail email="entity.email"></VnLinkMail> </template
></VnLv> ></VnLv>
<VnLv <VnLv
:label="t('customer.summary.salesPerson')" :label="t('customer.summary.salesPerson')"
@ -173,7 +176,7 @@ const sumRisk = ({ clientRisks }) => {
:label="t('customer.summary.notifyByEmail')" :label="t('customer.summary.notifyByEmail')"
:value="entity.isToBeMailed" :value="entity.isToBeMailed"
/> />
<VnLv :label="t('globals.isVies')" :value="entity.isVies" /> <VnLv :label="t('customer.summary.vies')" :value="entity.isVies" />
</VnRow> </VnRow>
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">

View File

@ -50,6 +50,14 @@ const columns = computed(() => [
isTitle: true, isTitle: true,
create: true, create: true,
columnClass: 'expand', columnClass: 'expand',
attrs: {
uppercase: true,
},
columnFilter: {
attrs: {
uppercase: false,
},
},
}, },
{ {
align: 'left', align: 'left',
@ -423,7 +431,7 @@ function handleLocation(data, location) {
:label="t('customer.summary.salesPerson')" :label="t('customer.summary.salesPerson')"
v-model="data.salesPersonFk" v-model="data.salesPersonFk"
:params="{ :params="{
departmentCodes: ['VT', 'shopping'], departmentCodes: ['VT'],
}" }"
:has-avatar="true" :has-avatar="true"
:id-value="data.salesPersonFk" :id-value="data.salesPersonFk"

View File

@ -11,6 +11,7 @@ import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnSelectDialog from 'src/components/common/VnSelectDialog.vue'; import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
import CustomerNewCustomsAgent from 'src/pages/Customer/components/CustomerNewCustomsAgent.vue'; import CustomerNewCustomsAgent from 'src/pages/Customer/components/CustomerNewCustomsAgent.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@ -150,6 +151,22 @@ function onAgentCreated({ id, fiscalName }, data) {
</template> </template>
</VnSelectDialog> </VnSelectDialog>
</VnRow> </VnRow>
<VnRow>
<VnInputNumber
:label="t('Longitude')"
clearable
v-model="data.longitude"
:decimal-places="7"
:positive="false"
/>
<VnInputNumber
:label="t('Latitude')"
clearable
v-model="data.latitude"
:decimal-places="7"
:positive="false"
/>
</VnRow>
</template> </template>
</FormModel> </FormModel>
</template> </template>
@ -175,4 +192,6 @@ es:
Mobile: Movíl Mobile: Movíl
Incoterms: Incoterms Incoterms: Incoterms
Customs agent: Agente de aduanas Customs agent: Agente de aduanas
Longitude: Longitud
Latitude: Latitud
</i18n> </i18n>

View File

@ -94,3 +94,24 @@ customer:
hasToInvoiceByAddress: Invoice by address hasToInvoiceByAddress: Invoice by address
isToBeMailed: Mailing isToBeMailed: Mailing
hasSepaVnl: VNL B2B received hasSepaVnl: VNL B2B received
params:
id: Id
isWorker: Is Worker
payMethod: Payment Method
workerFk: Author
observation: Last Observation
created: Last Update Date
creditInsurance: Credit Insurance
defaulterSinced: Defaulted Since
hasRecovery: Has Recovery
socialName: Social name
city: City
phone: Phone
postcode: Postcode
campaign: Campaign
grouped: Grouped
search: Contains
itemId: Item Id
ticketFk: Ticket Id
description: Description
quantity: Quantity

View File

@ -96,3 +96,24 @@ customer:
hasToInvoiceByAddress: Factura por consigna hasToInvoiceByAddress: Factura por consigna
isToBeMailed: Env. emails isToBeMailed: Env. emails
hasSepaVnl: Recibido B2B VNL hasSepaVnl: Recibido B2B VNL
params:
id: ID
isWorker: Es trabajador
payMethod: F. Pago
workerFk: Autor
observation: Última observación
created: Fecha Ú. O.
creditInsurance: Crédito A.
defaulterSinced: Desde
hasRecovery: Tiene recobro
socialName: Razón social
campaign: Campaña
city: Ciudad
phone: Teléfono
postcode: Código postal
grouped: Agrupado
search: Contiene
itemId: Id Artículo
ticketFk: Id Ticket
description: Descripción
quantity: Cantidad

View File

@ -1,13 +1,13 @@
<script setup> <script setup>
import VnCard from 'components/common/VnCard.vue'; import VnCardBeta from 'components/common/VnCardBeta.vue';
import DepartmentDescriptor from 'pages/Department/Card/DepartmentDescriptor.vue'; import DepartmentDescriptor from 'pages/Department/Card/DepartmentDescriptor.vue';
</script> </script>
<template> <template>
<VnCard <VnCardBeta
class="q-pa-md column items-center" class="q-pa-md column items-center"
v-bind="{ ...$attrs }" v-bind="{ ...$attrs }"
data-key="Department" data-key="Department"
base-url="Departments" base-url="Departments"
:descriptor="DepartmentDescriptor" :descriptor="DepartmentDescriptor"
/> />
</template> </template>

View File

@ -83,7 +83,11 @@ const { openConfirmationModal } = useVnConfirm();
</template> </template>
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv :label="t('department.chat')" :value="entity.chatName" /> <VnLv :label="t('department.chat')" :value="entity.chatName" />
<VnLv :label="t('globals.email')" :value="entity.notificationEmail" copy /> <VnLv
:label="t('globals.params.email')"
:value="entity.notificationEmail"
copy
/>
<VnLv <VnLv
:label="t('department.selfConsumptionCustomer')" :label="t('department.selfConsumptionCustomer')"
:value="entity.client?.name" :value="entity.client?.name"
@ -102,7 +106,7 @@ const { openConfirmationModal } = useVnConfirm();
:to="{ :to="{
name: 'WorkerList', name: 'WorkerList',
query: { query: {
params: JSON.stringify({ departmentFk: entityId }), table: JSON.stringify({ departmentFk: entityId }),
}, },
}" }"
> >

View File

@ -40,7 +40,7 @@ onMounted(async () => {
<template #body="{ entity: department }"> <template #body="{ entity: department }">
<QCard class="column"> <QCard class="column">
<VnTitle <VnTitle
:url="`#/department/department/${entityId}/basic-data`" :url="`#/worker/department/${entityId}/basic-data`"
:text="t('Basic data')" :text="t('Basic data')"
/> />
<div class="full-width row wrap justify-between content-between"> <div class="full-width row wrap justify-between content-between">
@ -58,7 +58,7 @@ onMounted(async () => {
dash dash
/> />
<VnLv <VnLv
:label="t('globals.email')" :label="t('globals.params.email')"
:value="department.notificationEmail" :value="department.notificationEmail"
dash dash
/> />

View File

@ -3,7 +3,6 @@ import { ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRole } from 'src/composables/useRole'; import { useRole } from 'src/composables/useRole';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
@ -11,7 +10,7 @@ import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnSelectDialog from 'src/components/common/VnSelectDialog.vue'; import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
import FilterTravelForm from 'src/components/FilterTravelForm.vue'; import FilterTravelForm from 'src/components/FilterTravelForm.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
const route = useRoute(); const route = useRoute();
@ -26,6 +25,7 @@ const onFilterTravelSelected = (formData, id) => {
formData.travelFk = id; formData.travelFk = id;
}; };
</script> </script>
<template> <template>
<FetchData <FetchData
ref="companiesRef" ref="companiesRef"
@ -93,14 +93,13 @@ const onFilterTravelSelected = (formData, id) => {
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
<QItemSection> <QItemSection>
<QItemLabel <QItemLabel>
>{{ scope.opt?.agencyModeName }} - {{ scope.opt?.agencyModeName }} -
{{ scope.opt?.warehouseInName }} ({{ {{ scope.opt?.warehouseInName }}
toDate(scope.opt?.shipped) ({{ toDate(scope.opt?.shipped) }})
}}) &#x2192; {{ scope.opt?.warehouseOutName }} ({{ {{ scope.opt?.warehouseOutName }}
toDate(scope.opt?.landed) ({{ toDate(scope.opt?.landed) }})
}})</QItemLabel </QItemLabel>
>
</QItemSection> </QItemSection>
</QItem> </QItem>
</template> </template>
@ -126,6 +125,13 @@ const onFilterTravelSelected = (formData, id) => {
/> />
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnInputNumber
:label="t('entry.summary.commission')"
v-model="data.commission"
step="1"
autofocus
:positive="false"
/>
<VnSelect <VnSelect
:label="t('entry.summary.currency')" :label="t('entry.summary.currency')"
v-model="data.currencyFk" v-model="data.currencyFk"
@ -133,12 +139,23 @@ const onFilterTravelSelected = (formData, id) => {
option-value="id" option-value="id"
option-label="code" option-label="code"
/> />
<QInput </VnRow>
:label="t('entry.summary.commission')" <VnRow>
v-model="data.commission" <VnInputNumber
type="number" v-model="data.initialTemperature"
autofocus name="initialTemperature"
min="0" :label="t('entry.basicData.initialTemperature')"
:step="0.5"
:decimal-places="2"
:positive="false"
/>
<VnInputNumber
v-model="data.finalTemperature"
name="finalTemperature"
:label="t('entry.basicData.finalTemperature')"
:step="0.5"
:decimal-places="2"
:positive="false"
/> />
</VnRow> </VnRow>
<VnRow> <VnRow>

View File

@ -1,21 +1,13 @@
<script setup> <script setup>
import VnCard from 'components/common/VnCard.vue'; import VnCardBeta from 'components/common/VnCardBeta.vue';
import EntryDescriptor from './EntryDescriptor.vue'; import EntryDescriptor from './EntryDescriptor.vue';
import EntryFilter from '../EntryFilter.vue'; import filter from './EntryFilter.js'
import filter from './EntryFilter.js';
</script> </script>
<template> <template>
<VnCard <VnCardBeta
data-key="Entry" data-key="Entry"
base-url="Entries" base-url="Entries"
:filter="filter"
:descriptor="EntryDescriptor" :descriptor="EntryDescriptor"
:filter-panel="EntryFilter" :user-filter="filter"
search-data-key="EntryList"
:searchbar-props="{
url: 'Entries/filter',
label: 'Search entries',
info: 'You can search by entry reference',
}"
/> />
</template> </template>

View File

@ -7,9 +7,9 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
import { usePrintService } from 'composables/usePrintService';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
import filter from './EntryFilter.js'; import filter from './EntryFilter.js';
import EntryDescriptorMenu from './EntryDescriptorMenu.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -21,7 +21,6 @@ const $props = defineProps({
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { openReport } = usePrintService();
const entryDescriptorRef = ref(null); const entryDescriptorRef = ref(null);
const url = ref(); const url = ref();
@ -52,10 +51,6 @@ const getEntryRedirectionFilter = (entry) => {
to, to,
}); });
}; };
const showEntryReport = () => {
openReport(`Entries/${route.params.id}/entry-order-pdf`);
};
</script> </script>
<template> <template>
@ -68,14 +63,12 @@ const showEntryReport = () => {
data-key="Entry" data-key="Entry"
> >
<template #menu="{ entity }"> <template #menu="{ entity }">
<QItem v-ripple clickable @click="showEntryReport(entity)"> <EntryDescriptorMenu :id="entity.id" />
<QItemSection>{{ t('Show entry report') }}</QItemSection>
</QItem>
</template> </template>
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv :label="t('globals.agency')" :value="entity.travel?.agency?.name" /> <VnLv :label="t('globals.agency')" :value="entity.travel?.agency?.name" />
<VnLv :label="t('globals.shipped')" :value="toDate(entity.travel?.shipped)" /> <VnLv :label="t('shipped')" :value="toDate(entity.travel?.shipped)" />
<VnLv :label="t('globals.landed')" :value="toDate(entity.travel?.landed)" /> <VnLv :label="t('landed')" :value="toDate(entity.travel?.landed)" />
<VnLv <VnLv
:label="t('globals.warehouseOut')" :label="t('globals.warehouseOut')"
:value="entity.travel?.warehouseOut?.name" :value="entity.travel?.warehouseOut?.name"
@ -154,7 +147,6 @@ es:
Supplier card: Ficha del proveedor Supplier card: Ficha del proveedor
All travels with current agency: Todos los envíos con la agencia actual All travels with current agency: Todos los envíos con la agencia actual
All entries with current supplier: Todas las entradas con el proveedor actual All entries with current supplier: Todas las entradas con el proveedor actual
Show entry report: Ver informe del pedido
Go to module index: Ir al índice del modulo Go to module index: Ir al índice del modulo
Inventory entry: Es inventario Inventory entry: Es inventario
Virtual entry: Es una redada Virtual entry: Es una redada

View File

@ -0,0 +1,22 @@
<script setup>
import { usePrintService } from 'composables/usePrintService';
const { openReport } = usePrintService();
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
function showEntryReport() {
openReport(`Entries/${$props.id}/entry-order-pdf`);
}
</script>
<template>
<QItem v-ripple clickable @click="showEntryReport">
<QItemSection>{{ $t('entryList.list.showEntryReport') }}</QItemSection>
</QItem>
</template>

View File

@ -7,11 +7,14 @@ import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue'; import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue';
import { toDate, toCurrency } from 'src/filters'; import { toDate, toCurrency, toCelsius } from 'src/filters';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
import axios from 'axios'; import axios from 'axios';
import FetchedTags from 'src/components/ui/FetchedTags.vue'; import FetchedTags from 'src/components/ui/FetchedTags.vue';
import VnToSummary from 'src/components/ui/VnToSummary.vue'; import VnToSummary from 'src/components/ui/VnToSummary.vue';
import EntryDescriptorMenu from './EntryDescriptorMenu.vue';
import VnRow from 'src/components/ui/VnRow.vue';
import VnTitle from 'src/components/common/VnTitle.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -147,9 +150,8 @@ async function setEntryData(data) {
} }
const fetchEntryBuys = async () => { const fetchEntryBuys = async () => {
const { data } = await axios.get(`Entries/${entry.value.id}/getBuys`); const { data } = await axios.get(`Entries/${entry.value.id}/getBuys`);
if (data) entryBuys.value = data; if (data) entryBuys.value = data;
}; };
</script> </script>
@ -171,15 +173,15 @@ const fetchEntryBuys = async () => {
<template #header> <template #header>
<span>{{ entry.id }} - {{ entry.supplier.nickname }}</span> <span>{{ entry.id }} - {{ entry.supplier.nickname }}</span>
</template> </template>
<template #menu="{ entity }">
<EntryDescriptorMenu :id="entity.id" />
</template>
<template #body> <template #body>
<QCard class="vn-one"> <QCard class="vn-one">
<router-link <VnTitle
:to="{ name: 'EntryBasicData', params: { id: entityId } }" :url="`#/entry/${entityId}/basic-data`"
class="header header-link" :text="t('globals.summary.basicData')"
> />
{{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" />
</router-link>
<VnLv :label="t('entry.summary.commission')" :value="entry.commission" /> <VnLv :label="t('entry.summary.commission')" :value="entry.commission" />
<VnLv <VnLv
:label="t('entry.summary.currency')" :label="t('entry.summary.currency')"
@ -191,15 +193,20 @@ const fetchEntryBuys = async () => {
:label="t('entry.summary.invoiceNumber')" :label="t('entry.summary.invoiceNumber')"
:value="entry.invoiceNumber" :value="entry.invoiceNumber"
/> />
<VnLv
:label="t('entry.basicData.initialTemperature')"
:value="toCelsius(entry.initialTemperature)"
/>
<VnLv
:label="t('entry.basicData.finalTemperature')"
:value="toCelsius(entry.finalTemperature)"
/>
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<router-link <VnTitle
:to="{ name: 'EntryBasicData', params: { id: entityId } }" :url="`#/entry/${entityId}/basic-data`"
class="header header-link" :text="t('globals.summary.basicData')"
> />
{{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" />
</router-link>
<VnLv :label="t('entry.summary.travelReference')"> <VnLv :label="t('entry.summary.travelReference')">
<template #value> <template #value>
<span class="link"> <span class="link">
@ -212,61 +219,45 @@ const fetchEntryBuys = async () => {
:label="t('entry.summary.travelAgency')" :label="t('entry.summary.travelAgency')"
:value="entry.travel.agency?.name" :value="entry.travel.agency?.name"
/> />
<VnLv :label="t('globals.shipped')" :value="toDate(entry.travel.shipped)" /> <VnLv
:label="t('globals.shipped')"
:value="toDate(entry.travel.shipped)"
/>
<VnLv <VnLv
:label="t('globals.warehouseOut')" :label="t('globals.warehouseOut')"
:value="entry.travel.warehouseOut?.name" :value="entry.travel.warehouseOut?.name"
/> />
<QCheckbox <VnLv
:label="t('entry.summary.travelDelivered')" :label="t('entry.summary.travelDelivered')"
v-model="entry.travel.isDelivered" :value="entry.travel.isDelivered"
:disable="true"
/> />
<VnLv :label="t('globals.landed')" :value="toDate(entry.travel.landed)" /> <VnLv :label="t('globals.landed')" :value="toDate(entry.travel.landed)" />
<VnLv <VnLv
:label="t('globals.warehouseIn')" :label="t('globals.warehouseIn')"
:value="entry.travel.warehouseIn?.name" :value="entry.travel.warehouseIn?.name"
/> />
<QCheckbox <VnLv
:label="t('entry.summary.travelReceived')" :label="t('entry.summary.travelReceived')"
v-model="entry.travel.isReceived" :value="entry.travel.isReceived"
:disable="true"
/> />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<router-link <VnTitle :url="`#/travel/${entityId}/summary`" :text="t('Travel data')" />
:to="{ name: 'TravelSummary', params: { id: entry.travel.id } }" <VnRow class="block">
class="header header-link" <VnLv :label="t('entry.summary.ordered')" :value="entry.isOrdered" />
> <VnLv :label="t('globals.confirmed')" :value="entry.isConfirmed" />
{{ t('Travel data') }} <VnLv :label="t('entry.summary.booked')" :value="entry.isBooked" />
<QIcon name="open_in_new" /> <VnLv
</router-link> :label="t('entry.summary.excludedFromAvailable')"
<QCheckbox :value="entry.isExcludedFromAvailable"
:label="t('entry.summary.ordered')" />
v-model="entry.isOrdered" </VnRow>
:disable="true"
/>
<QCheckbox
:label="t('globals.confirmed')"
v-model="entry.isConfirmed"
:disable="true"
/>
<QCheckbox
:label="t('entry.summary.booked')"
v-model="entry.isBooked"
:disable="true"
/>
<QCheckbox
:label="t('entry.summary.excludedFromAvailable')"
v-model="entry.isExcludedFromAvailable"
:disable="true"
/>
</QCard> </QCard>
<QCard class="vn-two" style="min-width: 100%"> <QCard class="vn-max">
<a class="header header-link"> <VnTitle
{{ t('entry.summary.buys') }} :url="`#/entry/${entityId}/buys`"
<QIcon name="open_in_new" /> :text="t('entry.summary.buys')"
</a> />
<QTable <QTable
:rows="entryBuys" :rows="entryBuys"
:columns="entriesTableColumns" :columns="entriesTableColumns"

View File

@ -40,7 +40,7 @@ const companiesOptions = ref([]);
<VnFilterPanel :data-key="props.dataKey" :search-button="true"> <VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong> <strong>{{ t(`entryFilter.params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span> <span>{{ formatFn(tag.value) }}</span>
</div> </div>
</template> </template>
@ -49,7 +49,7 @@ const companiesOptions = ref([]);
<QItemSection> <QItemSection>
<VnInput <VnInput
v-model="params.search" v-model="params.search"
:label="t('entryFilter.filter.search')" :label="t('entryFilter.params.search')"
is-outlined is-outlined
/> />
</QItemSection> </QItemSection>
@ -58,7 +58,7 @@ const companiesOptions = ref([]);
<QItemSection> <QItemSection>
<VnInput <VnInput
v-model="params.reference" v-model="params.reference"
:label="t('entryFilter.filter.reference')" :label="t('entryFilter.params.reference')"
is-outlined is-outlined
/> />
</QItemSection> </QItemSection>
@ -67,7 +67,7 @@ const companiesOptions = ref([]);
<QItemSection> <QItemSection>
<VnInput <VnInput
v-model="params.invoiceNumber" v-model="params.invoiceNumber"
:label="t('params.invoiceNumber')" :label="t('entryFilter.params.invoiceNumber')"
is-outlined is-outlined
/> />
</QItemSection> </QItemSection>
@ -76,7 +76,7 @@ const companiesOptions = ref([]);
<QItemSection> <QItemSection>
<VnInput <VnInput
v-model="params.travelFk" v-model="params.travelFk"
:label="t('params.travelFk')" :label="t('entryFilter.params.travelFk')"
is-outlined is-outlined
/> />
</QItemSection> </QItemSection>
@ -84,7 +84,7 @@ const companiesOptions = ref([]);
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnSelect <VnSelect
:label="t('params.companyFk')" :label="t('entryFilter.params.companyFk')"
v-model="params.companyFk" v-model="params.companyFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
:options="companiesOptions" :options="companiesOptions"
@ -100,7 +100,7 @@ const companiesOptions = ref([]);
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnSelect <VnSelect
:label="t('params.currencyFk')" :label="t('entryFilter.params.currencyFk')"
v-model="params.currencyFk" v-model="params.currencyFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
:options="currenciesOptions" :options="currenciesOptions"
@ -116,13 +116,14 @@ const companiesOptions = ref([]);
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnSelect <VnSelect
:label="t('params.supplierFk')" :label="t('entryFilter.params.supplierFk')"
v-model="params.supplierFk" v-model="params.supplierFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
url="Suppliers" url="Suppliers"
option-value="id" option-value="id"
option-label="name" option-label="name"
:fields="['id', 'name', 'nickname']" :fields="['id', 'name', 'nickname']"
:filter-options="['id', 'name', 'nickname']"
sort-by="nickname" sort-by="nickname"
hide-selected hide-selected
dense dense
@ -132,9 +133,12 @@ const companiesOptions = ref([]);
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
<QItemSection> <QItemSection>
<QItemLabel>{{ <QItemLabel>
scope.opt?.name + ': ' + scope.opt?.nickname {{ scope.opt?.name}}
}}</QItemLabel> </QItemLabel>
<QItemLabel caption>
{{ `#${scope.opt?.id } , ${ scope.opt?.nickname}` }}
</QItemLabel>
</QItemSection> </QItemSection>
</QItem> </QItem>
</template> </template>
@ -144,7 +148,7 @@ const companiesOptions = ref([]);
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate
:label="t('params.created')" :label="t('entryFilter.params.created')"
v-model="params.created" v-model="params.created"
@update:model-value="searchFn()" @update:model-value="searchFn()"
is-outlined is-outlined
@ -154,7 +158,7 @@ const companiesOptions = ref([]);
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate
:label="t('params.from')" :label="t('entryFilter.params.from')"
v-model="params.from" v-model="params.from"
@update:model-value="searchFn()" @update:model-value="searchFn()"
is-outlined is-outlined
@ -164,7 +168,7 @@ const companiesOptions = ref([]);
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate
:label="t('params.to')" :label="t('entryFilter.params.to')"
v-model="params.to" v-model="params.to"
@update:model-value="searchFn()" @update:model-value="searchFn()"
is-outlined is-outlined
@ -174,14 +178,14 @@ const companiesOptions = ref([]);
<QItem> <QItem>
<QItemSection> <QItemSection>
<QCheckbox <QCheckbox
:label="t('params.isBooked')" :label="t('entryFilter.params.isBooked')"
v-model="params.isBooked" v-model="params.isBooked"
toggle-indeterminate toggle-indeterminate
/> />
</QItemSection> </QItemSection>
<QItemSection> <QItemSection>
<QCheckbox <QCheckbox
:label="t('params.isConfirmed')" :label="t('entryFilter.params.isConfirmed')"
v-model="params.isConfirmed" v-model="params.isConfirmed"
toggle-indeterminate toggle-indeterminate
/> />
@ -190,7 +194,7 @@ const companiesOptions = ref([]);
<QItem> <QItem>
<QItemSection> <QItemSection>
<QCheckbox <QCheckbox
:label="t('params.isOrdered')" :label="t('entryFilter.params.isOrdered')"
v-model="params.isOrdered" v-model="params.isOrdered"
toggle-indeterminate toggle-indeterminate
/> />
@ -198,35 +202,4 @@ const companiesOptions = ref([]);
</QItem> </QItem>
</template> </template>
</VnFilterPanel> </VnFilterPanel>
</template> </template>
<i18n>
en:
params:
invoiceNumber: Invoice number
travelFk: Travel
companyFk: Company
currencyFk: Currency
supplierFk: Supplier
from: From
to: To
created: Created
isBooked: Booked
isConfirmed: Confirmed
isOrdered: Ordered
es:
params:
invoiceNumber: Núm. factura
travelFk: Envío
companyFk: Empresa
currencyFk: Moneda
supplierFk: Proveedor
from: Desde
to: Hasta
created: Fecha creación
isBooked: Asentado
isConfirmed: Confirmado
isOrdered: Pedida
</i18n>

View File

@ -102,7 +102,7 @@ const columns = [
}, },
{ {
align: 'left', align: 'left',
label: t('globals.weightByPiece'), label: t('entry.latestBuys.tableVisibleColumns.weightByPiece'),
name: 'weightByPiece', name: 'weightByPiece',
columnFilter: { columnFilter: {
component: 'number', component: 'number',
@ -157,7 +157,7 @@ const columns = [
}, },
{ {
align: 'left', align: 'left',
label: t('entry.buys.packageValue'), label: t('entry.latestBuys.tableVisibleColumns.packageValue'),
name: 'packageValue', name: 'packageValue',
columnFilter: { columnFilter: {
component: 'number', component: 'number',
@ -262,8 +262,3 @@ onUnmounted(() => (stateStore.rightDrawer = false));
:right-search="false" :right-search="false"
/> />
</template> </template>
<i18n>
es:
Edit buy(s): Editar compra(s)
</i18n>

View File

@ -69,12 +69,14 @@ const tagValues = ref([]);
use-input use-input
@update:model-value="searchFn()" @update:model-value="searchFn()"
> >
<template #option="{ itemProps, opt }"> <template #option="scope">
<QItem v-bind="itemProps"> <QItem v-bind="scope.itemProps">
<QItemSection> <QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel> <QItemLabel>
{{ scope.opt?.name}}
</QItemLabel>
<QItemLabel caption> <QItemLabel caption>
{{ opt.nickname }} {{ `#${scope.opt?.id } , ${ scope.opt?.nickname}` }}
</QItemLabel> </QItemLabel>
</QItemSection> </QItemSection>
</QItem> </QItem>

View File

@ -1,20 +1,18 @@
<script setup> <script setup>
import { onMounted, ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import EntryFilter from './EntryFilter.vue'; import EntryFilter from './EntryFilter.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { useStateStore } from 'stores/useStateStore';
import VnTable from 'components/VnTable/VnTable.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import RightMenu from 'src/components/common/RightMenu.vue'; import { toCelsius, toDate } from 'src/filters';
import { toDate } from 'src/filters';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import EntrySummary from './Card/EntrySummary.vue'; import EntrySummary from './Card/EntrySummary.vue';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue'; import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue';
import VnSection from 'src/components/common/VnSection.vue';
const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const tableRef = ref(); const tableRef = ref();
const dataKey = 'EntryList';
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const entryFilter = { const entryFilter = {
@ -159,6 +157,20 @@ const columns = computed(() => [
name: 'invoiceAmount', name: 'invoiceAmount',
cardVisible: true, cardVisible: true,
}, },
{
align: 'left',
name: 'initialTemperature',
label: t('entry.basicData.initialTemperature'),
field: 'initialTemperature',
format: (row) => toCelsius(row.initialTemperature),
},
{
align: 'left',
name: 'finalTemperature',
label: t('entry.basicData.finalTemperature'),
field: 'finalTemperature',
format: (row) => toCelsius(row.finalTemperature),
},
{ {
label: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'), label: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'),
name: 'isExcludedFromAvailable', name: 'isExcludedFromAvailable',
@ -180,74 +192,73 @@ const columns = computed(() => [
}, },
]); ]);
</script> </script>
<template> <template>
<VnSearchbar <VnSection
data-key="EntryList" :data-key="dataKey"
:columns="columns"
prefix="entry"
url="Entries/filter" url="Entries/filter"
:label="t('Search entries')" :array-data-props="{
:info="t('You can search by entry reference')" url: 'Entries/filter',
/> order: 'id DESC',
<RightMenu> userFilter: entryFilter,
<template #right-panel> }"
>
<template #rightMenu>
<EntryFilter data-key="EntryList" /> <EntryFilter data-key="EntryList" />
</template> </template>
</RightMenu> <template #body>
<VnTable <VnTable
ref="tableRef" ref="tableRef"
data-key="EntryList" :data-key="dataKey"
url="Entries/filter" :create="{
:filter="entryFilter" urlCreate: 'Entries',
:create="{ title: t('entry.list.newEntry'),
urlCreate: 'Entries', onDataSaved: ({ id }) => tableRef.redirect(id),
title: t('Create entry'), formInitialData: {},
onDataSaved: ({ id }) => tableRef.redirect(id), }"
formInitialData: {}, :columns="columns"
}" redirect="entry"
order="id DESC" :right-search="false"
:columns="columns" >
redirect="entry" <template #column-status="{ row }">
:right-search="false" <div class="row q-gutter-xs">
> <QIcon
<template #column-status="{ row }"> v-if="!!row.isExcludedFromAvailable"
<div class="row q-gutter-xs"> name="vn:inventory"
<QIcon color="primary"
v-if="!!row.isExcludedFromAvailable" >
name="vn:inventory" <QTooltip>{{
color="primary" t(
> 'entry.list.tableVisibleColumns.isExcludedFromAvailable'
<QTooltip>{{ )
t('entry.list.tableVisibleColumns.isExcludedFromAvailable') }}</QTooltip>
}}</QTooltip> </QIcon>
</QIcon> <QIcon v-if="!!row.isRaid" name="vn:net" color="primary">
<QIcon v-if="!!row.isRaid" name="vn:net" color="primary"> <QTooltip>
<QTooltip> {{
{{ t('globals.raid', {
t('globals.raid', { daysInForward: row.daysInForward }) daysInForward: row.daysInForward,
}}</QTooltip })
> }}</QTooltip
</QIcon> >
</div> </QIcon>
</div>
</template>
<template #column-supplierFk="{ row }">
<span class="link" @click.stop>
{{ row.supplierName }}
<SupplierDescriptorProxy :id="row.supplierFk" />
</span>
</template>
<template #column-travelFk="{ row }">
<span class="link" @click.stop>
{{ row.travelRef }}
<TravelDescriptorProxy :id="row.travelFk" />
</span>
</template>
</VnTable>
</template> </template>
<template #column-supplierFk="{ row }"> </VnSection>
<span class="link" @click.stop>
{{ row.supplierName }}
<SupplierDescriptorProxy :id="row.supplierFk" />
</span>
</template>
<template #column-travelFk="{ row }">
<span class="link" @click.stop>
{{ row.travelRef }}
<TravelDescriptorProxy :id="row.travelFk" />
</span>
</template>
</VnTable>
</template> </template>
<i18n>
es:
Inventory entry: Es inventario
Virtual entry: Es una redada
Search entries: Buscar entradas
You can search by entry reference: Puedes buscar por referencia de la entrada
Create entry: Crear entrada
</i18n>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
@ -19,7 +19,7 @@ const { t } = useI18n();
const quasar = useQuasar(); const quasar = useQuasar();
const state = useState(); const state = useState();
const user = state.getUser(); const user = state.getUser();
const columns = [ const columns = computed(() => [
{ {
align: 'left', align: 'left',
label: 'Id', label: 'Id',
@ -31,7 +31,7 @@ const columns = [
{ {
align: 'left', align: 'left',
name: 'workerFk', name: 'workerFk',
label: t('Buyer'), label: t('entryStockBought.buyer'),
isTitle: true, isTitle: true,
component: 'select', component: 'select',
cardVisible: true, cardVisible: true,
@ -49,7 +49,7 @@ const columns = [
}, },
{ {
align: 'center', align: 'center',
label: t('Reserve'), label: t('entryStockBought.reserve'),
name: 'reserve', name: 'reserve',
columnFilter: false, columnFilter: false,
create: true, create: true,
@ -58,7 +58,7 @@ const columns = [
}, },
{ {
align: 'center', align: 'center',
label: t('Bought'), label: t('entryStockBought.bought'),
name: 'bought', name: 'bought',
summation: true, summation: true,
cardVisible: true, cardVisible: true,
@ -66,7 +66,7 @@ const columns = [
}, },
{ {
align: 'left', align: 'left',
label: t('Date'), label: t('entryStockBought.date'),
name: 'dated', name: 'dated',
component: 'date', component: 'date',
visible: false, visible: false,
@ -77,7 +77,7 @@ const columns = [
name: 'tableActions', name: 'tableActions',
actions: [ actions: [
{ {
title: t('View more details'), title: t('entryStockBought.viewMoreDetails'),
icon: 'search', icon: 'search',
isPrimary: true, isPrimary: true,
action: (row) => { action: (row) => {
@ -92,7 +92,7 @@ const columns = [
}, },
], ],
}, },
]; ]);
const fetchDataRef = ref(); const fetchDataRef = ref();
const travelDialogRef = ref(false); const travelDialogRef = ref(false);
@ -166,7 +166,7 @@ function round(value) {
<VnRow class="travel"> <VnRow class="travel">
<div v-if="travel"> <div v-if="travel">
<span style="color: var(--vn-label-color)"> <span style="color: var(--vn-label-color)">
{{ t('Purchase Spaces') }}: {{ t('entryStockBought.purchaseSpaces') }}:
</span> </span>
<span> <span>
{{ travel?.m3 }} {{ travel?.m3 }}
@ -177,7 +177,7 @@ function round(value) {
flat flat
icon="edit" icon="edit"
@click="openDialog()" @click="openDialog()"
:title="t('Edit travel')" :title="t('entryStockBought.editTravel')"
color="primary" color="primary"
/> />
</div> </div>
@ -226,7 +226,7 @@ function round(value) {
@on-fetch="(data) => setFooter(data)" @on-fetch="(data) => setFooter(data)"
:create="{ :create="{
urlCreate: 'StockBoughts', urlCreate: 'StockBoughts',
title: t('Reserve some space'), title: t('entryStockBought.reserveSomeSpace'),
onDataSaved: () => tableRef.reload(), onDataSaved: () => tableRef.reload(),
formInitialData: { formInitialData: {
workerFk: user.id, workerFk: user.id,
@ -288,16 +288,3 @@ function round(value) {
color: $negative !important; color: $negative !important;
} }
</style> </style>
<i18n>
es:
Edit travel: Editar envío
Travel: Envíos
Purchase Spaces: Espacios de compra
Buyer: Comprador
Reserve: Reservado
Bought: Comprado
Date: Fecha
View more details: Ver más detalles
Reserve some space: Reservar espacio
This buyer has already made a reservation for this date: Este comprador ya ha hecho una reserva para esta fecha
</i18n>

View File

@ -123,8 +123,8 @@ const printBuys = (rowId) => {
<VnSearchbar <VnSearchbar
data-key="myEntriesList" data-key="myEntriesList"
url="Entries/filter" url="Entries/filter"
:label="t('Search entries')" :label="t('myEntries.search')"
:info="t('You can search by entry reference')" :info="t('myEntries.searchInfo')"
/> />
<VnTable <VnTable
data-key="myEntriesList" data-key="myEntriesList"
@ -137,7 +137,3 @@ const printBuys = (rowId) => {
chip-locale="myEntries" chip-locale="myEntries"
/> />
</template> </template>
<i18n>
You can search by entry reference: Puedes buscar por referencia de la entrada
</i18n>

View File

@ -1,8 +1,94 @@
entryList: entry:
list: list:
newEntry: New entry
tableVisibleColumns:
created: Creation
supplierFk: Supplier
isBooked: Booked
isConfirmed: Confirmed
isOrdered: Ordered
companyFk: Company
travelFk: Travel
isExcludedFromAvailable: Inventory
invoiceAmount: Import
inventoryEntry: Inventory entry inventoryEntry: Inventory entry
summary:
commission: Commission
currency: Currency
invoiceNumber: Invoice number
ordered: Ordered
booked: Booked
excludedFromAvailable: Inventory
travelReference: Reference
travelAgency: Agency
travelShipped: Shipped
travelDelivered: Delivered
travelLanded: Landed
travelReceived: Received
buys: Buys
stickers: Stickers
package: Package
packing: Pack.
grouping: Group.
buyingValue: Buying value
import: Import
pvp: PVP
basicData:
travel: Travel
currency: Currency
commission: Commission
observation: Observation
booked: Booked
excludedFromAvailable: Inventory
initialTemperature: Ini °C
finalTemperature: Fin °C
buys:
observations: Observations
packagingFk: Box
color: Color
printedStickers: Printed stickers
notes:
observationType: Observation type
latestBuys:
tableVisibleColumns:
image: Picture
itemFk: Item ID
weightByPiece: Weight/Piece
isActive: Active
family: Family
entryFk: Entry
freightValue: Freight value
comissionValue: Commission value
packageValue: Package value
isIgnored: Is ignored
price2: Grouping
price3: Packing
minPrice: Min
ektFk: Ekt
packingOut: Package out
landing: Landing
isExcludedFromAvailable: Es inventory
params:
toShipped: To
fromShipped: From
daysOnward: Days onward
daysAgo: Days ago
warehouseInFk: Warehouse in
search: Search entries
searchInfo: You can search by entry reference
entryFilter: entryFilter:
filter: params:
invoiceNumber: Invoice number
travelFk: Travel
companyFk: Company
currencyFk: Currency
supplierFk: Supplier
from: From
to: To
created: Created
isBooked: Booked
isConfirmed: Confirmed
isOrdered: Ordered
search: General search search: General search
reference: Reference reference: Reference
myEntries: myEntries:
@ -18,5 +104,18 @@ myEntries:
daysOnward: Days onward daysOnward: Days onward
daysAgo: Days ago daysAgo: Days ago
downloadCsv: Download CSV downloadCsv: Download CSV
search: Search entries
searchInfo: You can search by entry reference
entryStockBought:
travel: Travel
editTravel: Edit travel
purchaseSpaces: Purchase spaces
buyer: Buyer
reserve: Reserve
bought: Bought
date: Date
viewMoreDetails: View more details
reserveSomeSpace: Reserve some space
thisBuyerHasReservationThisDate: This buyer has already made a reservation for this date
wasteRecalc: wasteRecalc:
recalcOk: The wastes were successfully recalculated recalcOk: The wastes were successfully recalculated

View File

@ -1,11 +1,95 @@
Search entries: Buscar entradas entry:
You can search by entry reference: Puedes buscar por referencia de la entrada
entryList:
list: list:
newEntry: Nueva entrada
tableVisibleColumns:
created: Creación
supplierFk: Proveedor
isBooked: Asentado
isConfirmed: Confirmado
isOrdered: Pedida
companyFk: Empresa
travelFk: Envio
isExcludedFromAvailable: Inventario
invoiceAmount: Importe
inventoryEntry: Es inventario inventoryEntry: Es inventario
summary:
commission: Comisión
currency: Moneda
invoiceNumber: Núm. factura
ordered: Pedida
booked: Contabilizada
excludedFromAvailable: Inventario
travelReference: Referencia
travelAgency: Agencia
travelShipped: F. envio
travelWarehouseOut: Alm. salida
travelDelivered: Enviada
travelLanded: F. entrega
travelReceived: Recibida
buys: Compras
stickers: Etiquetas
package: Embalaje
packing: Pack.
grouping: Group.
buyingValue: Coste
import: Importe
pvp: PVP
basicData:
travel: Envío
currency: Moneda
observation: Observación
commission: Comisión
booked: Asentado
excludedFromAvailable: Inventario
initialTemperature: Ini °C
finalTemperature: Fin °C
buys:
observations: Observaciónes
packagingFk: Embalaje
color: Color
printedStickers: Etiquetas impresas
notes:
observationType: Tipo de observación
latestBuys:
tableVisibleColumns:
image: Foto
itemFk: Id Artículo
weightByPiece: Peso (gramos)/tallo
isActive: Activo
family: Familia
entryFk: Entrada
freightValue: Porte
comissionValue: Comisión
packageValue: Embalaje
isIgnored: Ignorado
price2: Grouping
price3: Packing
minPrice: Min
ektFk: Ekt
packingOut: Embalaje envíos
landing: Llegada
isExcludedFromAvailable: Es inventario
params:
toShipped: Hasta
fromShipped: Desde
warehouseInFk: Alm. entrada
daysOnward: Días adelante
daysAgo: Días atras
search: Buscar entradas
searchInfo: Puedes buscar por referencia de entrada
entryFilter: entryFilter:
filter: params:
invoiceNumber: Núm. factura
travelFk: Envío
companyFk: Empresa
currencyFk: Moneda
supplierFk: Proveedor
from: Desde
to: Hasta
created: Fecha creación
isBooked: Asentado
isConfirmed: Confirmado
isOrdered: Pedida
search: Búsqueda general search: Búsqueda general
reference: Referencia reference: Referencia
myEntries: myEntries:
@ -21,5 +105,18 @@ myEntries:
daysOnward: Días adelante daysOnward: Días adelante
daysAgo: Días atras daysAgo: Días atras
downloadCsv: Descargar CSV downloadCsv: Descargar CSV
search: Buscar entradas
searchInfo: Puedes buscar por referencia de la entrada
entryStockBought:
travel: Envío
editTravel: Editar envío
purchaseSpaces: Espacios de compra
buyer: Comprador
reserve: Reservado
bought: Comprado
date: Fecha
viewMoreDetails: Ver más detalles
reserveSomeSpace: Reservar espacio
thisBuyerHasReservationThisDate: Este comprador ya ha hecho una reserva para esta fecha
wasteRecalc: wasteRecalc:
recalcOk: Se han recalculado las mermas correctamente recalcOk: Se han recalculado las mermas correctamente

View File

@ -268,7 +268,7 @@ function deleteFile(dmsFk) {
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnSelect <VnSelect
:label="t('InvoiceIn.summary.sage')" :label="t('invoicein.summary.sage')"
v-model="data.withholdingSageFk" v-model="data.withholdingSageFk"
:options="sageWithholdings" :options="sageWithholdings"
option-value="id" option-value="id"

View File

@ -1,60 +1,30 @@
<script setup> <script setup>
import { ref, reactive, computed, onBeforeMount, capitalize } from 'vue'; import { ref, reactive, computed, onBeforeMount } from 'vue';
import { useRouter, onBeforeRouteUpdate } from 'vue-router'; import { useRouter, onBeforeRouteUpdate } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import axios from 'axios'; import axios from 'axios';
import { toCurrency, toDate } from 'src/filters'; import { toCurrency, toDate } from 'src/filters';
import { useAcl } from 'src/composables/useAcl';
import { downloadFile } from 'src/composables/downloadFile';
import { useArrayData } from 'src/composables/useArrayData';
import { usePrintService } from 'composables/usePrintService';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import CardDescriptor from 'components/ui/CardDescriptor.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import { useCapitalize } from 'src/composables/useCapitalize';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
import InvoiceInToBook from '../InvoiceInToBook.vue'; import InvoiceInDescriptorMenu from './InvoiceInDescriptorMenu.vue';
const $props = defineProps({ id: { type: Number, default: null } }); const $props = defineProps({ id: { type: Number, default: null } });
const { push, currentRoute } = useRouter(); const { push, currentRoute } = useRouter();
const quasar = useQuasar();
const { hasAny } = useAcl();
const { t } = useI18n(); const { t } = useI18n();
const { openReport, sendEmail } = usePrintService();
const arrayData = useArrayData();
const invoiceIn = computed(() => arrayData.store.data);
const cardDescriptorRef = ref(); const cardDescriptorRef = ref();
const correctionDialogRef = ref(); const correctionDialogRef = ref();
const entityId = computed(() => $props.id || +currentRoute.value.params.id); const entityId = computed(() => $props.id || +currentRoute.value.params.id);
const totalAmount = ref(); const totalAmount = ref();
const currentAction = ref();
const config = ref(); const config = ref();
const cplusRectificationTypes = ref([]); const cplusRectificationTypes = ref([]);
const siiTypeInvoiceIns = ref([]); const siiTypeInvoiceIns = ref([]);
const invoiceCorrectionTypes = ref([]); const invoiceCorrectionTypes = ref([]);
const actions = {
unbook: {
title: t('assertAction', { action: t('unbook') }),
action: toUnbook,
},
delete: {
title: t('assertAction', { action: t('delete') }),
action: deleteInvoice,
},
clone: {
title: t('assertAction', { action: t('clone') }),
action: cloneInvoice,
},
showPdf: { cb: showPdfInvoice },
sendPdf: { cb: sendPdfInvoiceConfirmation },
correct: { cb: () => correctionDialogRef.value.show() },
};
const filter = { const filter = {
include: [ include: [
{ {
@ -90,7 +60,7 @@ const routes = reactive({
return { return {
name: 'InvoiceInList', name: 'InvoiceInList',
query: { query: {
table: JSON.stringify({ supplierFk: id }), params: JSON.stringify({ supplierFk: id }),
}, },
}; };
}, },
@ -99,7 +69,7 @@ const routes = reactive({
return { return {
name: 'InvoiceInList', name: 'InvoiceInList',
query: { query: {
table: JSON.stringify({ correctedFk: entityId.value }), params: JSON.stringify({ correctedFk: entityId.value }),
}, },
}; };
} }
@ -118,21 +88,21 @@ const routes = reactive({
const correctionFormData = reactive({ const correctionFormData = reactive({
invoiceReason: 2, invoiceReason: 2,
invoiceType: 2, invoiceType: 2,
invoiceClass: 8, invoiceClass: 6,
}); });
const isNotFilled = computed(() => Object.values(correctionFormData).includes(null)); const isNotFilled = computed(() => Object.values(correctionFormData).includes(null));
onBeforeMount(async () => { onBeforeMount(async () => {
await setInvoiceCorrection(entityId.value); await setInvoiceCorrection(entityId.value);
const { data } = await axios.get(`InvoiceIns/${entityId.value}/getTotals`); const { data } = await axios.get(`InvoiceIns/${entityId.value}/getTotals`);
totalAmount.value = data.totalTaxableBase; totalAmount.value = data.totalDueDay;
}); });
onBeforeRouteUpdate(async (to, from) => { onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) { if (to.params.id !== from.params.id) {
await setInvoiceCorrection(to.params.id); await setInvoiceCorrection(to.params.id);
const { data } = await axios.get(`InvoiceIns/${to.params.id}/getTotals`); const { data } = await axios.get(`InvoiceIns/${to.params.id}/getTotals`);
totalAmount.value = data.totalTaxableBase; totalAmount.value = data.totalDueDay;
} }
}); });
@ -153,94 +123,6 @@ async function setInvoiceCorrection(id) {
); );
} }
function openDialog() {
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t(currentAction.value.title),
promise: currentAction.value.action,
},
});
}
async function toUnbook() {
const { data } = await axios.post(`InvoiceIns/${entityId.value}/toUnbook`);
const { isLinked, bookEntry, accountingEntries } = data;
const type = isLinked ? 'warning' : 'positive';
const message = isLinked
? t('isLinked', { bookEntry, accountingEntries })
: t('isNotLinked', { bookEntry });
quasar.notify({ type, message });
if (!isLinked) arrayData.store.data.isBooked = false;
}
async function deleteInvoice() {
await axios.delete(`InvoiceIns/${entityId.value}`);
quasar.notify({
type: 'positive',
message: t('Invoice deleted'),
});
push({ path: '/invoice-in' });
}
async function cloneInvoice() {
const { data } = await axios.post(`InvoiceIns/${entityId.value}/clone`);
quasar.notify({
type: 'positive',
message: t('Invoice cloned'),
});
push({ path: `/invoice-in/${data.id}/summary` });
}
const canEditProp = (props) =>
hasAny([{ model: 'InvoiceIn', props, accessType: 'WRITE' }]);
const isAgricultural = () => {
if (!config.value) return false;
return (
invoiceIn.value?.supplier?.sageFarmerWithholdingFk ===
config?.value[0]?.sageWithholdingFk
);
};
function showPdfInvoice() {
if (isAgricultural())
openReport(`InvoiceIns/${entityId.value}/invoice-in-pdf`, null, '_blank');
}
function sendPdfInvoiceConfirmation() {
quasar.dialog({
component: SendEmailDialog,
componentProps: {
data: {
address: invoiceIn.value.supplier.contacts[0].email,
},
promise: sendPdfInvoice,
},
});
}
function sendPdfInvoice({ address }) {
if (!address)
quasar.notify({
type: 'negative',
message: t(`The email can't be empty`),
});
else
return sendEmail(`InvoiceIns/${entityId.value}/invoice-in-email`, {
recipientId: invoiceIn.value.supplier.id,
recipient: address,
});
}
function triggerMenu(type) {
currentAction.value = actions[type];
if (currentAction.value.cb) currentAction.value.cb();
else openDialog(type);
}
const createInvoiceInCorrection = async () => { const createInvoiceInCorrection = async () => {
const { data: correctingId } = await axios.post( const { data: correctingId } = await axios.post(
'InvoiceIns/corrective', 'InvoiceIns/corrective',
@ -262,7 +144,7 @@ const createInvoiceInCorrection = async () => {
auto-load auto-load
/> />
<FetchData <FetchData
url="siiTypeInvoiceIns" url="SiiTypeInvoiceIns"
:where="{ code: { like: 'R%' } }" :where="{ code: { like: 'R%' } }"
@on-fetch="(data) => (siiTypeInvoiceIns = data)" @on-fetch="(data) => (siiTypeInvoiceIns = data)"
auto-load auto-load
@ -281,87 +163,13 @@ const createInvoiceInCorrection = async () => {
title="supplierRef" title="supplierRef"
> >
<template #menu="{ entity }"> <template #menu="{ entity }">
<InvoiceInToBook> <InvoiceInDescriptorMenu :invoice="entity" />
<template #content="{ book }">
<QItem
v-if="!entity?.isBooked && canEditProp('toBook')"
v-ripple
clickable
@click="book(entityId)"
>
<QItemSection>{{ t('To book') }}</QItemSection>
</QItem>
</template>
</InvoiceInToBook>
<QItem
v-if="entity?.isBooked && canEditProp('toUnbook')"
v-ripple
clickable
@click="triggerMenu('unbook')"
>
<QItemSection>
{{ t('To unbook') }}
</QItemSection>
</QItem>
<QItem
v-if="canEditProp('deleteById')"
v-ripple
clickable
@click="triggerMenu('delete')"
>
<QItemSection>{{ t('Delete invoice') }}</QItemSection>
</QItem>
<QItem
v-if="canEditProp('clone')"
v-ripple
clickable
@click="triggerMenu('clone')"
>
<QItemSection>{{ t('Clone invoice') }}</QItemSection>
</QItem>
<QItem
v-if="isAgricultural()"
v-ripple
clickable
@click="triggerMenu('showPdf')"
>
<QItemSection>{{ t('Show agricultural receipt as PDF') }}</QItemSection>
</QItem>
<QItem
v-if="isAgricultural()"
v-ripple
clickable
@click="triggerMenu('sendPdf')"
>
<QItemSection
>{{ t('Send agricultural receipt as PDF') }}...</QItemSection
>
</QItem>
<QItem
v-if="!invoiceInCorrection.corrected"
v-ripple
clickable
@click="triggerMenu('correct')"
>
<QItemSection>{{ t('Create rectificative invoice') }}...</QItemSection>
</QItem>
<QItem
v-if="entity.dmsFk"
v-ripple
clickable
@click="downloadFile(entity.dmsFk)"
>
<QItemSection>{{ t('components.smartCard.downloadFile') }}</QItemSection>
</QItem>
</template> </template>
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv :label="t('InvoiceIn.list.issued')" :value="toDate(entity.issued)" /> <VnLv :label="t('invoicein.list.issued')" :value="toDate(entity.issued)" />
<VnLv <VnLv :label="t('invoicein.summary.bookedDate')" :value="toDate(entity.booked)" />
:label="t('InvoiceIn.summary.bookedDate')" <VnLv :label="t('invoicein.list.amount')" :value="toCurrency(totalAmount)" />
:value="toDate(entity.booked)" <VnLv :label="t('invoicein.list.supplier')">
/>
<VnLv :label="t('InvoiceIn.list.amount')" :value="toCurrency(totalAmount)" />
<VnLv :label="t('InvoiceIn.list.supplier')">
<template #value> <template #value>
<span class="link"> <span class="link">
{{ entity?.supplier?.nickname }} {{ entity?.supplier?.nickname }}
@ -378,7 +186,7 @@ const createInvoiceInCorrection = async () => {
color="primary" color="primary"
:to="routes.getSupplier(entity.supplierFk)" :to="routes.getSupplier(entity.supplierFk)"
> >
<QTooltip>{{ t('InvoiceIn.list.supplier') }}</QTooltip> <QTooltip>{{ t('invoicein.list.supplier') }}</QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
size="md" size="md"
@ -394,7 +202,7 @@ const createInvoiceInCorrection = async () => {
color="primary" color="primary"
:to="routes.getTickets(entity.supplierFk)" :to="routes.getTickets(entity.supplierFk)"
> >
<QTooltip>{{ t('InvoiceIn.descriptor.ticketList') }}</QTooltip> <QTooltip>{{ t('InvoiceOut.card.ticketList') }}</QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
v-if=" v-if="
@ -438,7 +246,7 @@ const createInvoiceInCorrection = async () => {
readonly readonly
/> />
<VnSelect <VnSelect
:label="`${capitalize(t('globals.class'))}`" :label="`${useCapitalize(t('globals.class'))}`"
v-model="correctionFormData.invoiceClass" v-model="correctionFormData.invoiceClass"
:options="siiTypeInvoiceIns" :options="siiTypeInvoiceIns"
option-value="id" option-value="id"
@ -448,27 +256,15 @@ const createInvoiceInCorrection = async () => {
</QItemSection> </QItemSection>
<QItemSection> <QItemSection>
<VnSelect <VnSelect
:label="`${capitalize(t('globals.type'))}`" :label="`${useCapitalize(t('globals.type'))}`"
v-model="correctionFormData.invoiceType" v-model="correctionFormData.invoiceType"
:options="cplusRectificationTypes" :options="cplusRectificationTypes"
option-value="id" option-value="id"
option-label="description" option-label="description"
:required="true" :required="true"
> />
<template #option="{ opt }">
<QItem>
<QItemSection>
<QItemLabel
>{{ opt.code }} -
{{ opt.description }}</QItemLabel
>
</QItemSection>
</QItem>
<div></div>
</template>
</VnSelect>
<VnSelect <VnSelect
:label="`${capitalize(t('globals.reason'))}`" :label="`${useCapitalize(t('globals.reason'))}`"
v-model="correctionFormData.invoiceReason" v-model="correctionFormData.invoiceReason"
:options="invoiceCorrectionTypes" :options="invoiceCorrectionTypes"
option-value="id" option-value="id"
@ -512,31 +308,3 @@ const createInvoiceInCorrection = async () => {
} }
} }
</style> </style>
<i18n>
en:
isNotLinked: The entry {bookEntry} has been deleted with {accountingEntries} entries
isLinked: The entry {bookEntry} has been linked to Sage. Please contact administration for further information
assertAction: Are you sure you want to {action} this invoice?
es:
book: asentar
unbook: desasentar
delete: eliminar
clone: clonar
To book: Contabilizar
To unbook: Descontabilizar
Delete invoice: Eliminar factura
Invoice deleted: Factura eliminada
Clone invoice: Clonar factura
Invoice cloned: Factura clonada
Show agricultural receipt as PDF: Ver recibo agrícola como PDF
Send agricultural receipt as PDF: Enviar recibo agrícola como PDF
Are you sure you want to send it?: Estás seguro que quieres enviarlo?
Send PDF invoice: Enviar factura a PDF
Create rectificative invoice: Crear factura rectificativa
Rectificative invoice: Factura rectificativa
Original invoice: Factura origen
Entry: entrada
isNotLinked: Se ha eliminado el asiento {bookEntry} con {accountingEntries} apuntes
isLinked: El asiento {bookEntry} fue enlazado a Sage, por favor contacta con administración
assertAction: Estas seguro de querer {action} esta factura?
</i18n>

View File

@ -0,0 +1,206 @@
<script setup>
import { ref, computed, toRefs, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import axios from 'axios';
import { useAcl } from 'src/composables/useAcl';
import { downloadFile } from 'src/composables/downloadFile';
import { useArrayData } from 'src/composables/useArrayData';
import { usePrintService } from 'composables/usePrintService';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import InvoiceInToBook from '../InvoiceInToBook.vue';
const { hasAny } = useAcl();
const { t } = useI18n();
const { openReport, sendEmail } = usePrintService();
const { push, currentRoute } = useRouter();
const $props = defineProps({
invoice: {
type: Object,
required: true,
},
});
const { invoice } = toRefs($props);
const quasar = useQuasar();
const arrayData = useArrayData();
const currentAction = ref();
const config = ref();
const correctionDialogRef = ref();
const invoiceInCorrection = reactive({ correcting: [], corrected: null });
const entityId = computed(() => $props.invoice.id || +currentRoute.value.params.id);
const invoiceIn = computed(() => arrayData.store.data);
const actions = {
unbook: {
title: t('assertAction', { action: t('invoicein.descriptorMenu.unbook') }),
action: toUnbook,
},
delete: {
title: t('assertAction', { action: t('invoicein.descriptorMenu.delete') }),
action: deleteInvoice,
},
clone: {
title: t('assertAction', { action: t('invoicein.descriptorMenu.clone') }),
action: cloneInvoice,
},
showPdf: { cb: showPdfInvoice },
sendPdf: { cb: sendPdfInvoiceConfirmation },
correct: { cb: () => correctionDialogRef.value.show() },
};
const canEditProp = (props) =>
hasAny([{ model: 'InvoiceIn', props, accessType: 'WRITE' }]);
function triggerMenu(type) {
currentAction.value = actions[type];
if (currentAction.value.cb) currentAction.value.cb();
else openDialog(type);
}
function openDialog() {
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t(currentAction.value.title),
promise: currentAction.value.action,
},
});
}
async function toUnbook() {
const { data } = await axios.post(`InvoiceIns/${entityId.value}/toUnbook`);
const { isLinked, bookEntry, accountingEntries } = data;
const type = isLinked ? 'warning' : 'positive';
const message = isLinked
? t('isLinked', { bookEntry })
: t('isNotLinked', { bookEntry, accountingEntries });
quasar.notify({ type, message });
if (!isLinked) arrayData.store.data.isBooked = false;
}
async function deleteInvoice() {
await axios.delete(`InvoiceIns/${entityId.value}`);
quasar.notify({
type: 'positive',
message: t('invoicein.descriptorMenu.invoiceDeleted'),
});
push({ path: '/invoice-in' });
}
async function cloneInvoice() {
const { data } = await axios.post(`InvoiceIns/${entityId.value}/clone`);
quasar.notify({
type: 'positive',
message: t('invoicein.descriptorMenu.invoiceCloned'),
});
push({ path: `/invoice-in/${data.id}/summary` });
}
const isAgricultural = () => {
if (!config.value) return false;
return (
invoiceIn.value?.supplier?.sageFarmerWithholdingFk ===
config?.value[0]?.sageWithholdingFk
);
};
function showPdfInvoice() {
if (isAgricultural()) openReport(`InvoiceIns/${entityId.value}/invoice-in-pdf`);
}
function sendPdfInvoiceConfirmation() {
quasar.dialog({
component: SendEmailDialog,
componentProps: {
data: {
address: invoiceIn.value.supplier.contacts[0].email,
},
promise: sendPdfInvoice,
},
});
}
function sendPdfInvoice({ address }) {
if (!address)
quasar.notify({
type: 'negative',
message: t(`The email can't be empty`),
});
else
return sendEmail(`InvoiceIns/${entityId.value}/invoice-in-email`, {
recipientId: invoiceIn.value.supplier.id,
recipient: address,
});
}
</script>
<template>
<InvoiceInToBook>
<template #content="{ book }">
<QItem
v-if="!invoice?.isBooked && canEditProp('toBook')"
v-ripple
clickable
@click="book(entityId)"
>
<QItemSection>{{ t('invoicein.descriptorMenu.toBook') }}</QItemSection>
</QItem>
</template>
</InvoiceInToBook>
<QItem
v-if="invoice?.isBooked && canEditProp('toUnbook')"
v-ripple
clickable
@click="triggerMenu('unbook')"
>
<QItemSection>
{{ t('invoicein.descriptorMenu.toUnbook') }}
</QItemSection>
</QItem>
<QItem
v-if="canEditProp('deleteById')"
v-ripple
clickable
@click="triggerMenu('invoicein.descriptorMenu.delete')"
>
<QItemSection>{{ t('invoicein.descriptorMenu.deleteInvoice') }}</QItemSection>
</QItem>
<QItem v-if="canEditProp('clone')" v-ripple clickable @click="triggerMenu('clone')">
<QItemSection>{{ t('invoicein.descriptorMenu.cloneInvoice') }}</QItemSection>
</QItem>
<QItem v-if="isAgricultural()" v-ripple clickable @click="triggerMenu('showPdf')">
<QItemSection>{{
t('invoicein.descriptorMenu.showAgriculturalPdf')
}}</QItemSection>
</QItem>
<QItem v-if="isAgricultural()" v-ripple clickable @click="triggerMenu('sendPdf')">
<QItemSection
>{{ t('invoicein.descriptorMenu.sendAgriculturalPdf') }}...</QItemSection
>
</QItem>
<QItem
v-if="!invoiceInCorrection.corrected"
v-ripple
clickable
@click="triggerMenu('correct')"
>
<QItemSection
>{{ t('invoicein.descriptorMenu.createCorrective') }}...</QItemSection
>
</QItem>
<QItem v-if="invoice.dmsFk" v-ripple clickable @click="downloadFile(invoice.dmsFk)">
<QItemSection>{{ t('components.smartCard.downloadFile') }}</QItemSection>
</QItem>
</template>
<i18n>
en:
isNotLinked: The entry {bookEntry} has been deleted with {accountingEntries} entries
isLinked: The entry has been linked to Sage. Please contact administration for further information
assertAction: Are you sure you want to {action} this invoice?
es:
isNotLinked: Se ha eliminado el asiento {bookEntry} con {accountingEntries} apuntes
isLinked: El asiento fue enlazado a Sage, por favor contacta con administración
assertAction: Estas seguro de querer {action} esta factura?
</i18n>

View File

@ -10,6 +10,7 @@ import VnLv from 'src/components/ui/VnLv.vue';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
import InvoiceIntoBook from '../InvoiceInToBook.vue'; import InvoiceIntoBook from '../InvoiceInToBook.vue';
import VnTitle from 'src/components/common/VnTitle.vue'; import VnTitle from 'src/components/common/VnTitle.vue';
import InvoiceInDescriptorMenu from './InvoiceInDescriptorMenu.vue';
const props = defineProps({ id: { type: [Number, String], default: 0 } }); const props = defineProps({ id: { type: [Number, String], default: 0 } });
const { t } = useI18n(); const { t } = useI18n();
@ -26,14 +27,14 @@ const intrastatTotals = ref({ amount: 0, net: 0, stems: 0 });
const vatColumns = ref([ const vatColumns = ref([
{ {
name: 'expense', name: 'expense',
label: 'InvoiceIn.summary.expense', label: 'invoicein.summary.expense',
field: (row) => row.expenseFk, field: (row) => row.expenseFk,
sortable: true, sortable: true,
align: 'left', align: 'left',
}, },
{ {
name: 'landed', name: 'landed',
label: 'InvoiceIn.summary.taxableBase', label: 'invoicein.summary.taxableBase',
field: (row) => row.taxableBase, field: (row) => row.taxableBase,
format: (value) => toCurrency(value), format: (value) => toCurrency(value),
sortable: true, sortable: true,
@ -41,7 +42,7 @@ const vatColumns = ref([
}, },
{ {
name: 'vat', name: 'vat',
label: 'InvoiceIn.summary.sageVat', label: 'invoicein.summary.sageVat',
field: (row) => { field: (row) => {
if (row.taxTypeSage) return `#${row.taxTypeSage.id} : ${row.taxTypeSage.vat}`; if (row.taxTypeSage) return `#${row.taxTypeSage.id} : ${row.taxTypeSage.vat}`;
}, },
@ -51,7 +52,7 @@ const vatColumns = ref([
}, },
{ {
name: 'transaction', name: 'transaction',
label: 'InvoiceIn.summary.sageTransaction', label: 'invoicein.summary.sageTransaction',
field: (row) => { field: (row) => {
if (row.transactionTypeSage) if (row.transactionTypeSage)
return `#${row.transactionTypeSage.id} : ${row.transactionTypeSage?.transaction}`; return `#${row.transactionTypeSage.id} : ${row.transactionTypeSage?.transaction}`;
@ -62,7 +63,7 @@ const vatColumns = ref([
}, },
{ {
name: 'rate', name: 'rate',
label: 'InvoiceIn.summary.rate', label: 'invoicein.summary.rate',
field: (row) => taxRate(row.taxableBase, row.taxTypeSage?.rate), field: (row) => taxRate(row.taxableBase, row.taxTypeSage?.rate),
format: (value) => toCurrency(value), format: (value) => toCurrency(value),
sortable: true, sortable: true,
@ -70,7 +71,7 @@ const vatColumns = ref([
}, },
{ {
name: 'currency', name: 'currency',
label: 'InvoiceIn.summary.currency', label: 'invoicein.summary.currency',
field: (row) => row.foreignValue, field: (row) => row.foreignValue,
format: (val) => val && toCurrency(val, currency.value), format: (val) => val && toCurrency(val, currency.value),
sortable: true, sortable: true,
@ -81,21 +82,21 @@ const vatColumns = ref([
const dueDayColumns = ref([ const dueDayColumns = ref([
{ {
name: 'date', name: 'date',
label: 'InvoiceIn.summary.dueDay', label: 'invoicein.summary.dueDay',
field: (row) => toDate(row.dueDated), field: (row) => toDate(row.dueDated),
sortable: true, sortable: true,
align: 'left', align: 'left',
}, },
{ {
name: 'bank', name: 'bank',
label: 'InvoiceIn.summary.bank', label: 'invoicein.summary.bank',
field: (row) => row.bank.bank, field: (row) => row.bank.bank,
sortable: true, sortable: true,
align: 'left', align: 'left',
}, },
{ {
name: 'amount', name: 'amount',
label: 'InvoiceIn.list.amount', label: 'invoicein.list.amount',
field: (row) => row.amount, field: (row) => row.amount,
format: (value) => toCurrency(value), format: (value) => toCurrency(value),
sortable: true, sortable: true,
@ -103,7 +104,7 @@ const dueDayColumns = ref([
}, },
{ {
name: 'landed', name: 'landed',
label: 'InvoiceIn.summary.foreignValue', label: 'invoicein.summary.foreignValue',
field: (row) => row.foreignValue, field: (row) => row.foreignValue,
format: (val) => val && toCurrency(val, currency.value), format: (val) => val && toCurrency(val, currency.value),
sortable: true, sortable: true,
@ -114,7 +115,7 @@ const dueDayColumns = ref([
const intrastatColumns = ref([ const intrastatColumns = ref([
{ {
name: 'code', name: 'code',
label: 'InvoiceIn.summary.code', label: 'invoicein.summary.code',
field: (row) => { field: (row) => {
return `${row.intrastat.id}: ${row.intrastat?.description}`; return `${row.intrastat.id}: ${row.intrastat?.description}`;
}, },
@ -123,21 +124,21 @@ const intrastatColumns = ref([
}, },
{ {
name: 'amount', name: 'amount',
label: 'InvoiceIn.list.amount', label: 'invoicein.list.amount',
field: (row) => toCurrency(row.amount), field: (row) => toCurrency(row.amount),
sortable: true, sortable: true,
align: 'left', align: 'left',
}, },
{ {
name: 'net', name: 'net',
label: 'InvoiceIn.summary.net', label: 'invoicein.summary.net',
field: (row) => row.net, field: (row) => row.net,
sortable: true, sortable: true,
align: 'left', align: 'left',
}, },
{ {
name: 'stems', name: 'stems',
label: 'InvoiceIn.summary.stems', label: 'invoicein.summary.stems',
field: (row) => row.stems, field: (row) => row.stems,
format: (value) => value, format: (value) => value,
sortable: true, sortable: true,
@ -145,7 +146,7 @@ const intrastatColumns = ref([
}, },
{ {
name: 'landed', name: 'landed',
label: 'InvoiceIn.summary.country', label: 'invoicein.summary.country',
field: (row) => row.country?.code, field: (row) => row.country?.code,
format: (value) => value, format: (value) => value,
sortable: true, sortable: true,
@ -200,6 +201,9 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
</template> </template>
</InvoiceIntoBook> </InvoiceIntoBook>
</template> </template>
<template #menu="{ entity }">
<InvoiceInDescriptorMenu :invoice="entity" />
</template>
<template #body="{ entity }"> <template #body="{ entity }">
<!--Basic Data--> <!--Basic Data-->
<QCard class="vn-one"> <QCard class="vn-one">
@ -210,7 +214,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
/> />
</QCardSection> </QCardSection>
<VnLv <VnLv
:label="t('InvoiceIn.list.supplier')" :label="t('invoicein.list.supplier')"
:value="entity.supplier?.name" :value="entity.supplier?.name"
> >
<template #value> <template #value>
@ -221,14 +225,14 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
</template> </template>
</VnLv> </VnLv>
<VnLv <VnLv
:label="t('InvoiceIn.list.supplierRef')" :label="t('invoicein.list.supplierRef')"
:value="entity.supplierRef" :value="entity.supplierRef"
/> />
<VnLv <VnLv
:label="t('InvoiceIn.summary.currency')" :label="t('invoicein.summary.currency')"
:value="entity.currency?.code" :value="entity.currency?.code"
/> />
<VnLv :label="t('InvoiceIn.serial')" :value="`${entity.serial}`" /> <VnLv :label="t('invoicein.serial')" :value="`${entity.serial}`" />
<VnLv <VnLv
:label="t('globals.country')" :label="t('globals.country')"
:value="entity.supplier?.country?.code" :value="entity.supplier?.country?.code"
@ -243,19 +247,19 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
</QCardSection> </QCardSection>
<VnLv <VnLv
:ellipsis-value="false" :ellipsis-value="false"
:label="t('InvoiceIn.summary.issued')" :label="t('invoicein.summary.issued')"
:value="toDate(entity.issued)" :value="toDate(entity.issued)"
/> />
<VnLv <VnLv
:label="t('InvoiceIn.summary.operated')" :label="t('invoicein.summary.operated')"
:value="toDate(entity.operated)" :value="toDate(entity.operated)"
/> />
<VnLv <VnLv
:label="t('InvoiceIn.summary.bookEntried')" :label="t('invoicein.summary.bookEntried')"
:value="toDate(entity.bookEntried)" :value="toDate(entity.bookEntried)"
/> />
<VnLv <VnLv
:label="t('InvoiceIn.summary.bookedDate')" :label="t('invoicein.summary.bookedDate')"
:value="toDate(entity.booked)" :value="toDate(entity.booked)"
/> />
<VnLv :label="t('globals.isVies')" :value="entity.supplier?.isVies" /> <VnLv :label="t('globals.isVies')" :value="entity.supplier?.isVies" />
@ -268,18 +272,18 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
/> />
</QCardSection> </QCardSection>
<VnLv <VnLv
:label="t('InvoiceIn.summary.sage')" :label="t('invoicein.summary.sage')"
:value="entity.sageWithholding?.withholding" :value="entity.sageWithholding?.withholding"
/> />
<VnLv <VnLv
:label="t('InvoiceIn.summary.vat')" :label="t('invoicein.summary.vat')"
:value="entity.expenseDeductible?.name" :value="entity.expenseDeductible?.name"
/> />
<VnLv <VnLv
:label="t('InvoiceIn.card.company')" :label="t('invoicein.card.company')"
:value="entity.company?.code" :value="entity.company?.code"
/> />
<VnLv :label="t('InvoiceIn.isBooked')" :value="invoiceIn?.isBooked" /> <VnLv :label="t('invoicein.isBooked')" :value="invoiceIn?.isBooked" />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<QCardSection class="q-pa-none"> <QCardSection class="q-pa-none">
@ -290,11 +294,11 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
</QCardSection> </QCardSection>
<QCardSection class="q-pa-none"> <QCardSection class="q-pa-none">
<VnLv <VnLv
:label="t('InvoiceIn.summary.taxableBase')" :label="t('invoicein.summary.taxableBase')"
:value="toCurrency(entity.totals.totalTaxableBase)" :value="toCurrency(entity.totals.totalTaxableBase)"
/> />
<VnLv label="Total" :value="toCurrency(entity.totals.totalVat)" /> <VnLv label="Total" :value="toCurrency(entity.totals.totalVat)" />
<VnLv :label="t('InvoiceIn.summary.dueTotal')"> <VnLv :label="t('invoicein.summary.dueTotal')">
<template #value> <template #value>
<QChip <QChip
dense dense
@ -302,8 +306,8 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
:color="amountsNotMatch ? 'negative' : 'transparent'" :color="amountsNotMatch ? 'negative' : 'transparent'"
:title=" :title="
amountsNotMatch amountsNotMatch
? t('InvoiceIn.summary.noMatch') ? t('invoicein.summary.noMatch')
: t('InvoiceIn.summary.dueTotal') : t('invoicein.summary.dueTotal')
" "
> >
{{ toCurrency(entity.totals.totalDueDay) }} {{ toCurrency(entity.totals.totalDueDay) }}
@ -314,7 +318,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
</QCard> </QCard>
<!--Vat--> <!--Vat-->
<QCard v-if="entity.invoiceInTax.length" class="vat"> <QCard v-if="entity.invoiceInTax.length" class="vat">
<VnTitle :url="getLink('vat')" :text="t('InvoiceIn.card.vat')" /> <VnTitle :url="getLink('vat')" :text="t('invoicein.card.vat')" />
<QTable <QTable
:columns="vatColumns" :columns="vatColumns"
:rows="entity.invoiceInTax" :rows="entity.invoiceInTax"
@ -362,7 +366,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
</QCard> </QCard>
<!--Due Day--> <!--Due Day-->
<QCard v-if="entity.invoiceInDueDay.length" class="due-day"> <QCard v-if="entity.invoiceInDueDay.length" class="due-day">
<VnTitle :url="getLink('due-day')" :text="t('InvoiceIn.card.dueDay')" /> <VnTitle :url="getLink('due-day')" :text="t('invoicein.card.dueDay')" />
<QTable :columns="dueDayColumns" :rows="entity.invoiceInDueDay" flat> <QTable :columns="dueDayColumns" :rows="entity.invoiceInDueDay" flat>
<template #header="dueDayProps"> <template #header="dueDayProps">
<QTr :props="dueDayProps" class="bg"> <QTr :props="dueDayProps" class="bg">
@ -400,7 +404,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
<QCard v-if="entity.invoiceInIntrastat.length"> <QCard v-if="entity.invoiceInIntrastat.length">
<VnTitle <VnTitle
:url="getLink('intrastat')" :url="getLink('intrastat')"
:text="t('InvoiceIn.card.intrastat')" :text="t('invoicein.card.intrastat')"
/> />
<QTable <QTable
:columns="intrastatColumns" :columns="intrastatColumns"

View File

@ -83,7 +83,7 @@ const redirectToInvoiceInBasicData = (__, { id }) => {
</template> </template>
</VnSelect> </VnSelect>
<VnInput <VnInput
:label="t('InvoiceIn.list.supplierRef')" :label="t('invoicein.list.supplierRef')"
v-model="data.supplierRef" v-model="data.supplierRef"
/> />
</VnRow> </VnRow>
@ -97,10 +97,10 @@ const redirectToInvoiceInBasicData = (__, { id }) => {
map-options map-options
hide-selected hide-selected
:required="true" :required="true"
:rules="validate('InvoiceIn.companyFk')" :rules="validate('invoicein.companyFk')"
/> />
<VnInputDate <VnInputDate
:label="t('InvoiceIn.summary.issued')" :label="t('invoicein.summary.issued')"
v-model="data.issued" v-model="data.issued"
/> />
</VnRow> </VnRow>

View File

@ -68,13 +68,26 @@ function handleDaysAgo(params, daysAgo) {
<VnSelect <VnSelect
v-model="params.supplierFk" v-model="params.supplierFk"
url="Suppliers" url="Suppliers"
:fields="['id', 'nickname']" :fields="['id', 'nickname', 'name']"
:label="getLocale('supplierFk')" :label="getLocale('supplierFk')"
option-label="nickname" option-label="nickname"
dense dense
outlined outlined
rounded rounded
/> >
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.name}}
</QItemLabel>
<QItemLabel caption>
{{ `#${scope.opt?.id } , ${ scope.opt?.nickname}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem> <QItem>
@ -151,7 +164,7 @@ function handleDaysAgo(params, daysAgo) {
<QItem> <QItem>
<QItemSection> <QItemSection>
<QCheckbox <QCheckbox
:label="$t('InvoiceIn.isBooked')" :label="$t('invoicein.isBooked')"
v-model="params.isBooked" v-model="params.isBooked"
@update:model-value="searchFn()" @update:model-value="searchFn()"
toggle-indeterminate toggle-indeterminate

View File

@ -26,7 +26,7 @@ const cols = computed(() => [
{ {
align: 'left', align: 'left',
name: 'isBooked', name: 'isBooked',
label: t('InvoiceIn.isBooked'), label: t('invoicein.isBooked'),
columnFilter: false, columnFilter: false,
}, },
{ {
@ -41,7 +41,7 @@ const cols = computed(() => [
{ {
align: 'left', align: 'left',
name: 'supplierFk', name: 'supplierFk',
label: t('InvoiceIn.list.supplier'), label: t('invoicein.list.supplier'),
columnFilter: { columnFilter: {
component: 'select', component: 'select',
attrs: { attrs: {
@ -55,16 +55,16 @@ const cols = computed(() => [
{ {
align: 'left', align: 'left',
name: 'supplierRef', name: 'supplierRef',
label: t('InvoiceIn.list.supplierRef'), label: t('invoicein.list.supplierRef'),
}, },
{ {
align: 'left', align: 'left',
name: 'serial', name: 'serial',
label: t('InvoiceIn.serial'), label: t('invoicein.serial'),
}, },
{ {
align: 'left', align: 'left',
label: t('InvoiceIn.list.issued'), label: t('invoicein.list.issued'),
name: 'issued', name: 'issued',
component: null, component: null,
columnFilter: { columnFilter: {
@ -74,7 +74,7 @@ const cols = computed(() => [
}, },
{ {
align: 'left', align: 'left',
label: t('InvoiceIn.list.dueDated'), label: t('invoicein.list.dueDated'),
name: 'dueDated', name: 'dueDated',
component: null, component: null,
columnFilter: { columnFilter: {
@ -86,12 +86,12 @@ const cols = computed(() => [
{ {
align: 'left', align: 'left',
name: 'awbCode', name: 'awbCode',
label: t('InvoiceIn.list.awb'), label: t('invoicein.list.awb'),
}, },
{ {
align: 'left', align: 'left',
name: 'amount', name: 'amount',
label: t('InvoiceIn.list.amount'), label: t('invoicein.list.amount'),
format: ({ amount }) => toCurrency(amount), format: ({ amount }) => toCurrency(amount),
cardVisible: true, cardVisible: true,
}, },
@ -165,24 +165,24 @@ const cols = computed(() => [
<VnSelect <VnSelect
v-model="data.supplierFk" v-model="data.supplierFk"
url="Suppliers" url="Suppliers"
:fields="['id', 'nickname']" :fields="['id', 'nickname', 'name']"
:label="t('globals.supplier')" :label="t('globals.supplier')"
option-value="id" option-value="id"
option-label="nickname" option-label="nickname"
:filter-options="['id', 'name']" :filter-options="['id', 'name', 'nickname']"
:required="true" :required="true"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
<QItemSection> <QItemSection>
<QItemLabel>{{ scope.opt?.nickname }}</QItemLabel> <QItemLabel>{{ scope.opt?.nickname }}</QItemLabel>
<QItemLabel caption> #{{ scope.opt?.id }} </QItemLabel> <QItemLabel caption> #{{ scope.opt?.id }}, {{ scope.opt?.name }} </QItemLabel>
</QItemSection> </QItemSection>
</QItem> </QItem>
</template> </template>
</VnSelect> </VnSelect>
<VnInput <VnInput
:label="t('InvoiceIn.list.supplierRef')" :label="t('invoicein.list.supplierRef')"
v-model="data.supplierRef" v-model="data.supplierRef"
/> />
<VnSelect <VnSelect
@ -194,7 +194,7 @@ const cols = computed(() => [
option-label="code" option-label="code"
:required="true" :required="true"
/> />
<VnInputDate :label="t('InvoiceIn.summary.issued')" v-model="data.issued" /> <VnInputDate :label="t('invoicein.summary.issued')" v-model="data.issued" />
</template> </template>
</VnTable> </VnTable>
</template> </template>

View File

@ -1,4 +1,4 @@
InvoiceIn: invoicein:
serial: Serial serial: Serial
isBooked: Is booked isBooked: Is booked
list: list:
@ -12,6 +12,26 @@ InvoiceIn:
amount: Amount amount: Amount
descriptor: descriptor:
ticketList: Ticket list ticketList: Ticket list
descriptorMenu:
book: Book
unbook: Unbook
delete: Delete
clone: Clone
toBook: To book
toUnbook: To unbook
deleteInvoice: Delete invoice
invoiceDeleted: invoice deleted
cloneInvoice: Clone invoice
invoiceCloned: Invoice cloned
showAgriculturalPdf: Show agricultural receipt as PDF
sendAgriculturalPdf: Send agricultural receipt as PDF
checkSendInvoice: Are you sure you want to send it?
sendPdfInvoice: Send PDF invoice
createCorrective: Create rectificative invoice
correctiveInvoice: Rectificative invoice
originalInvoice: Original invoice
entry: Entry
emailEmpty: The email can't be empty
card: card:
client: Client client: Client
company: Company company: Company
@ -44,7 +64,8 @@ InvoiceIn:
country: Country country: Country
params: params:
search: Id or supplier name search: Id or supplier name
account: Ledger account
correctingFk: Rectificative
correctedFk: Corrected correctedFk: Corrected
isBooked: Is booked isBooked: Is booked
account: Ledger account
correctingFk: Rectificative

View File

@ -1,4 +1,4 @@
InvoiceIn: invoicein:
serial: Serie serial: Serie
isBooked: Contabilizada isBooked: Contabilizada
list: list:
@ -12,6 +12,26 @@ InvoiceIn:
amount: Importe amount: Importe
descriptor: descriptor:
ticketList: Listado de tickets ticketList: Listado de tickets
descriptorMenu:
book: Asentar
unbook: Desasentar
delete: Eliminar
clone: Clonar
toBook: Contabilizar
toUnbook: Descontabilizar
deleteInvoice: Eliminar factura
invoiceDeleted: Factura eliminada
cloneInvoice: Clonar factura
invoiceCloned: Factura clonada
showAgriculturalPdf: Ver recibo agrícola como PDF
sendAgriculturalPdf: Enviar recibo agrícola como PDF
checkSendInvoice: ¿Estás seguro que quieres enviarlo?
sendPdfInvoice: Enviar factura a PDF
createCorrective: Crear factura rectificativa
correctiveInvoice: Factura rectificativa
originalInvoice: Factura origen
entry: Entrada
emailEmpty: El email no puede estar vacío
card: card:
client: Cliente client: Cliente
company: Empresa company: Empresa
@ -42,6 +62,7 @@ InvoiceIn:
country: País country: País
params: params:
search: Id o nombre proveedor search: Id o nombre proveedor
correctedFk: Rectificada
account: Cuenta contable account: Cuenta contable
correctingFk: Rectificativa correctingFk: Rectificativa
correctedFk: Rectificada

View File

@ -10,6 +10,7 @@ import { getUrl } from 'src/composables/getUrl';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue'; import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import VnTitle from 'src/components/common/VnTitle.vue'; import VnTitle from 'src/components/common/VnTitle.vue';
import InvoiceOutDescriptorMenu from './InvoiceOutDescriptorMenu.vue';
onMounted(async () => { onMounted(async () => {
fetch(); fetch();
@ -113,6 +114,9 @@ const ticketsColumns = ref([
<template #header="{ entity: { invoiceOut } }"> <template #header="{ entity: { invoiceOut } }">
<div>{{ invoiceOut.ref }} - {{ invoiceOut.client?.socialName }}</div> <div>{{ invoiceOut.ref }} - {{ invoiceOut.client?.socialName }}</div>
</template> </template>
<template #menu="{ entity }">
<InvoiceOutDescriptorMenu :invoice-out-data="entity.invoiceOut" />
</template>
<template #body="{ entity: { invoiceOut } }"> <template #body="{ entity: { invoiceOut } }">
<QCard class="vn-one"> <QCard class="vn-one">
<VnTitle :text="t('globals.pageTitles.basicData')" /> <VnTitle :text="t('globals.pageTitles.basicData')" />

View File

@ -47,6 +47,7 @@ const states = ref();
:label="t('Amount')" :label="t('Amount')"
v-model="params.amount" v-model="params.amount"
is-outlined is-outlined
data-cy="InvoiceOutFilterAmountBtn"
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>

View File

@ -101,6 +101,7 @@ onMounted(async () => {
dense dense
outlined outlined
rounded rounded
data-cy="InvoiceOutGlobalClientSelect"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -125,6 +126,7 @@ onMounted(async () => {
dense dense
outlined outlined
rounded rounded
data-cy="InvoiceOutGlobalSerialSelect"
/> />
<VnInputDate <VnInputDate
v-model="formData.invoiceDate" v-model="formData.invoiceDate"
@ -135,6 +137,7 @@ onMounted(async () => {
v-model="formData.maxShipped" v-model="formData.maxShipped"
:label="t('maxShipped')" :label="t('maxShipped')"
is-outlined is-outlined
data-cy="InvoiceOutGlobalMaxShippedDate"
/> />
<VnSelect <VnSelect
:label="t('company')" :label="t('company')"
@ -144,6 +147,7 @@ onMounted(async () => {
dense dense
outlined outlined
rounded rounded
data-cy="InvoiceOutGlobalCompanySelect"
/> />
<VnSelect <VnSelect
:label="t('printer')" :label="t('printer')"
@ -152,6 +156,7 @@ onMounted(async () => {
dense dense
outlined outlined
rounded rounded
data-cy="InvoiceOutGlobalPrinterSelect"
/> />
</div> </div>
<QBtn <QBtn

View File

@ -126,6 +126,13 @@ const columns = computed(() => [
columnField: { component: null }, columnField: { component: null },
format: (row) => toDate(row.dued), format: (row) => toDate(row.dued),
}, },
{
align: 'left',
name: 'customsAgentFk',
label: t('invoiceOutList.tableVisibleColumns.customsAgent'),
cardVisible: true,
format: (row, dashIfEmpty) => dashIfEmpty(row.customsAgentName),
},
{ {
align: 'right', align: 'right',
name: 'tableActions', name: 'tableActions',
@ -190,6 +197,7 @@ watchEffect(selectedRows);
icon-right="cloud_download" icon-right="cloud_download"
@click="downloadPdf()" @click="downloadPdf()"
:disable="!hasSelectedCards" :disable="!hasSelectedCards"
data-cy="InvoiceOutDownloadPdfBtn"
> >
<QTooltip>{{ t('downloadPdf') }}</QTooltip> <QTooltip>{{ t('downloadPdf') }}</QTooltip>
</QBtn> </QBtn>
@ -238,6 +246,7 @@ watchEffect(selectedRows);
v-model="data.ticketFk" v-model="data.ticketFk"
:label="t('globals.ticket')" :label="t('globals.ticket')"
style="flex: 1" style="flex: 1"
data-cy="InvoiceOutCreateTicketinput"
/> />
<div <div
@ -346,12 +355,13 @@ watchEffect(selectedRows);
<VnSelect <VnSelect
url="InvoiceOutSerials" url="InvoiceOutSerials"
v-model="data.serial" v-model="data.serial"
:label="t('InvoiceIn.serial')" :label="t('invoicein.serial')"
:options="invoiceOutSerialsOptions" :options="invoiceOutSerialsOptions"
option-label="description" option-label="description"
option-value="code" option-value="code"
option-filter option-filter
:expr-builder="exprBuilder" :expr-builder="exprBuilder"
data-cy="InvoiceOutCreateSerialSelect"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">

View File

@ -13,6 +13,7 @@ invoiceOutList:
invoiceOutSerial: Serial invoiceOutSerial: Serial
ticket: Ticket ticket: Ticket
taxArea: Tax area taxArea: Tax area
customsAgent: Custom Agent
DownloadPdf: Download PDF DownloadPdf: Download PDF
InvoiceOutSummary: Summary InvoiceOutSummary: Summary
negativeBases: negativeBases:
@ -24,3 +25,15 @@ negativeBases:
hasToInvoice: Has to invoice hasToInvoice: Has to invoice
verifiedData: Verified data verifiedData: Verified data
commercial: Commercial commercial: Commercial
invoiceout:
params:
company: Company
country: Country
clientId: Client ID
clientSocialName: Client
taxableBase: Base
ticketFk: Ticket
isActive: Active
hasToInvoice: Has to invoice
hasVerifiedData: Verified data
workerName: Worker

View File

@ -15,6 +15,7 @@ invoiceOutList:
invoiceOutSerial: Serial invoiceOutSerial: Serial
ticket: Ticket ticket: Ticket
taxArea: Area taxArea: Area
customsAgent: Agente de aduanas
DownloadPdf: Descargar PDF DownloadPdf: Descargar PDF
InvoiceOutSummary: Resumen InvoiceOutSummary: Resumen
negativeBases: negativeBases:
@ -27,3 +28,15 @@ negativeBases:
hasToInvoice: Debe facturar hasToInvoice: Debe facturar
verifiedData: Datos verificados verifiedData: Datos verificados
commercial: Comercial commercial: Comercial
invoiceout:
params:
company: Empresa
country: País
clientId: ID del cliente
clientSocialName: Cliente
taxableBase: Base
ticketFk: Ticket
isActive: Activo
hasToInvoice: Debe facturar
hasVerifiedData: Datos verificados
workerName: Comercial

View File

@ -52,6 +52,7 @@ const entityId = computed(() => {
:fields="['id', 'name']" :fields="['id', 'name']"
sort-by="name ASC" sort-by="name ASC"
hide-selected hide-selected
data-cy="AddGenusSelectDialog"
> >
<template #form> <template #form>
<CreateGenusForm <CreateGenusForm
@ -68,6 +69,7 @@ const entityId = computed(() => {
:fields="['id', 'name']" :fields="['id', 'name']"
sort-by="name ASC" sort-by="name ASC"
hide-selected hide-selected
data-cy="AddSpeciesSelectDialog"
> >
<template #form> <template #form>
<CreateSpecieForm <CreateSpecieForm

View File

@ -6,17 +6,16 @@ import { useI18n } from 'vue-i18n';
import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; import CardDescriptor from 'src/components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import RegularizeStockForm from 'components/RegularizeStockForm.vue';
import ItemDescriptorImage from 'src/pages/Item/Card/ItemDescriptorImage.vue'; import ItemDescriptorImage from 'src/pages/Item/Card/ItemDescriptorImage.vue';
import useCardDescription from 'src/composables/useCardDescription'; import useCardDescription from 'src/composables/useCardDescription';
import axios from 'axios'; import axios from 'axios';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { cloneItem } from 'src/pages/Item/composables/cloneItem'; import ItemDescriptorMenu from './ItemDescriptorMenu.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
type: [Number, String], type: Number,
required: false, required: false,
default: null, default: null,
}, },
@ -29,7 +28,7 @@ const $props = defineProps({
default: null, default: null,
}, },
saleFk: { saleFk: {
type: [Number, String], type: Number,
default: null, default: null,
}, },
warehouseFk: { warehouseFk: {
@ -38,7 +37,6 @@ const $props = defineProps({
}, },
}); });
const { openCloneDialog } = cloneItem();
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const warehouseConfig = ref(null); const warehouseConfig = ref(null);
@ -46,7 +44,6 @@ const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
const regularizeStockFormDialog = ref(null);
const mounted = ref(); const mounted = ref();
const arrayDataStock = useArrayData('descriptorStock', { const arrayDataStock = useArrayData('descriptorStock', {
@ -61,14 +58,14 @@ onMounted(async () => {
const data = ref(useCardDescription()); const data = ref(useCardDescription());
const setData = async (entity) => { const setData = async (entity) => {
if (!entity) return; if (!entity) return;
data.value = useCardDescription(entity?.name, entity?.id); data.value = useCardDescription(entity.name, entity.id);
await updateStock(); await updateStock();
}; };
const getItemConfigs = async () => { const getItemConfigs = async () => {
const { data } = await axios.get('ItemConfigs/findOne'); const { data } = await axios.get('ItemConfigs/findOne');
if (!data) return; if (!data) return;
return (warehouseConfig.value = data.warehouseFk); warehouseConfig.value = data.warehouseFk;
}; };
const updateStock = async () => { const updateStock = async () => {
if (!mounted.value) return; if (!mounted.value) return;
@ -89,10 +86,6 @@ const updateStock = async () => {
if (storeData?.itemFk == entityId.value) return; if (storeData?.itemFk == entityId.value) return;
await stock.fetch({}); await stock.fetch({});
}; };
const openRegularizeStockForm = () => {
regularizeStockFormDialog.value.show();
};
</script> </script>
<template> <template>
@ -105,24 +98,12 @@ const openRegularizeStockForm = () => {
:url="`Items/${entityId}/getCard`" :url="`Items/${entityId}/getCard`"
@on-fetch="setData" @on-fetch="setData"
> >
<template #menu="{}"> <template #menu>
<QItem v-ripple clickable @click="openRegularizeStockForm()"> <ItemDescriptorMenu
<QItemSection> :entity-id="entityId"
{{ t('Regularize stock') }} :warehouse-fk="warehouseFk"
<QDialog ref="regularizeStockFormDialog"> @regularized="updateStock"
<RegularizeStockForm />
:item-fk="entityId"
:warehouse-fk="warehouseFk"
@on-data-saved="updateStock()"
/>
</QDialog>
</QItemSection>
</QItem>
<QItem v-ripple clickable @click="openCloneDialog(entityId)">
<QItemSection>
{{ t('globals.clone') }}
</QItemSection>
</QItem>
</template> </template>
<template #before> <template #before>
<ItemDescriptorImage <ItemDescriptorImage
@ -191,7 +172,6 @@ const openRegularizeStockForm = () => {
<i18n> <i18n>
es: es:
Regularize stock: Regularizar stock
Inactive article: Artículo inactivo Inactive article: Artículo inactivo
</i18n> </i18n>

View File

@ -131,7 +131,6 @@ const handlePhotoUpdated = (evt = false) => {
<i18n> <i18n>
es: es:
Regularize stock: Regularizar stock
All it's properties will be copied: Todas sus propiedades serán copiadas All it's properties will be copied: Todas sus propiedades serán copiadas
Do you want to clone this item?: ¿Desea clonar este artículo? Do you want to clone this item?: ¿Desea clonar este artículo?
warehouseText: Calculado sobre el almacén de { warehouseName } warehouseText: Calculado sobre el almacén de { warehouseName }

View File

@ -0,0 +1,44 @@
<script setup>
import RegularizeStockForm from 'components/RegularizeStockForm.vue';
import { cloneItem } from 'src/pages/Item/composables/cloneItem';
const { openCloneDialog } = cloneItem();
defineProps({
entityId: {
type: Number,
default: null,
},
warehouseFk: {
type: Number,
default: null,
},
});
defineEmits(['regularized']);
</script>
<template>
<QItem v-ripple clickable @click="$refs.regularizeStockFormDialog.show()">
<QItemSection>
{{ $t('item.regularizeStock') }}
<QDialog ref="regularizeStockFormDialog">
<RegularizeStockForm
:item-fk="entityId"
:warehouse-fk="warehouseFk"
@on-data-saved="$emit('regularized')"
/>
</QDialog>
</QItemSection>
</QItem>
<QItem v-ripple clickable @click="openCloneDialog(entityId)">
<QItemSection>
{{ $t('globals.clone') }}
</QItemSection>
</QItem>
</template>
<style lang="scss" scoped>
.weekdaysBtn {
margin: 1%;
}
</style>

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import { onMounted, computed, reactive, ref, nextTick, watch } from 'vue'; import { onMounted, computed, ref, nextTick } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute } from 'vue-router';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue'; import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue'; import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue';
@ -22,19 +22,16 @@ import VnSubToolbar from 'components/ui/VnSubToolbar.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter();
const state = useState(); const state = useState();
const user = state.getUser(); const user = state.getUser();
const today = ref(Date.vnNew()); const today = Date.vnNew();
today.setHours(0, 0, 0, 0);
const warehousesOptions = ref([]); const warehousesOptions = ref([]);
const itemBalancesRef = ref(null); const itemBalances = computed(() => arrayDataItemBalances.store.data);
const itemsBalanceFilter = reactive({ const where = computed(() => arrayDataItemBalances.store.filter.where || {});
where: { itemFk: route.params.id, warehouseFk: null, date: null }, const showWhatsBeforeInventory = ref(false);
});
const itemBalances = ref([]);
const warehouseFk = ref(null);
const _showWhatsBeforeInventory = ref(false);
const inventoriedDate = ref(null); const inventoriedDate = ref(null);
let arrayDataItemBalances = useArrayData('ItemBalances');
const originTypeMap = { const originTypeMap = {
entry: { entry: {
@ -122,36 +119,28 @@ const columns = computed(() => [
}, },
]); ]);
const showWhatsBeforeInventory = computed({
get: () => _showWhatsBeforeInventory.value,
set: (val) => {
_showWhatsBeforeInventory.value = val;
if (!val) itemsBalanceFilter.where.date = null;
else itemsBalanceFilter.where.date = inventoriedDate.value ?? new Date();
},
});
onMounted(async () => { onMounted(async () => {
today.value.setHours(0, 0, 0, 0); const ref = where.value;
if (route.query.warehouseFk) warehouseFk.value = route.query.warehouseFk; const query = route.query;
else if (user.value) warehouseFk.value = user.value.warehouseFk; inventoriedDate.value =
itemsBalanceFilter.where.warehouseFk = warehouseFk.value; (await axios.get('Configs/findOne')).data?.inventoried || today;
const { data } = await axios.get('Configs/findOne');
inventoriedDate.value = data.inventoried; if (query.warehouseFk) ref.warehouseFk = query.warehouseFk;
else if (!ref.warehouseFk && user.value) ref.warehouseFk = user.value.warehouseFk;
if (ref.date) showWhatsBeforeInventory.value = true;
ref.itemFk = route.params.id;
arrayDataItemBalances = useArrayData('ItemBalances', {
url: 'Items/getBalance',
filter: { where: ref },
});
await fetchItemBalances(); await fetchItemBalances();
await scrollToToday(); await scrollToToday();
await updateWarehouse(warehouseFk.value); await updateWarehouse(ref.warehouseFk);
}); });
watch( const fetchItemBalances = async () => await arrayDataItemBalances.fetch({});
() => router.currentRoute.value.params.id,
(newId) => {
itemsBalanceFilter.where.itemFk = newId;
itemBalancesRef.value.fetch();
}
);
const fetchItemBalances = async () => await itemBalancesRef.value.fetch();
const getBadgeAttrs = (_date) => { const getBadgeAttrs = (_date) => {
const isSameDate = date.isSameDate(today.value, _date); const isSameDate = date.isSameDate(today.value, _date);
@ -178,23 +167,13 @@ const formatDateForAttribute = (dateValue) => {
}; };
async function updateWarehouse(warehouseFk) { async function updateWarehouse(warehouseFk) {
const stock = useArrayData('descriptorStock', { const stock = useArrayData('descriptorStock', { userParams: { warehouseFk } });
userParams: {
warehouseFk,
},
});
await stock.fetch({}); await stock.fetch({});
stock.store.data.itemFk = route.params.id; stock.store.data.itemFk = route.params.id;
} }
</script> </script>
<template> <template>
<FetchData
ref="itemBalancesRef"
url="Items/getBalance"
:filter="itemsBalanceFilter"
@on-fetch="(data) => (itemBalances = data)"
/>
<FetchData <FetchData
url="Warehouses" url="Warehouses"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }" :filter="{ fields: ['id', 'name'], order: 'name ASC' }"
@ -207,27 +186,30 @@ async function updateWarehouse(warehouseFk) {
<VnSelect <VnSelect
:label="t('itemDiary.warehouse')" :label="t('itemDiary.warehouse')"
:options="warehousesOptions" :options="warehousesOptions"
hide-selected v-model="where.warehouseFk"
option-label="name"
option-value="id"
dense
v-model="itemsBalanceFilter.where.warehouseFk"
@update:model-value=" @update:model-value="
(value) => fetchItemBalances() && updateWarehouse(value) (val) => fetchItemBalances() && updateWarehouse(val)
" "
class="q-mr-lg" class="q-mr-lg"
:is-clearable="false"
/> />
<QCheckbox <QCheckbox
:label="t('itemDiary.showBefore')" :label="t('itemDiary.showBefore')"
v-model="showWhatsBeforeInventory" v-model="showWhatsBeforeInventory"
@update:model-value="fetchItemBalances" @update:model-value="
async (val) => {
if (!val) where.date = null;
else where.date = inventoriedDate;
await fetchItemBalances();
}
"
class="q-mr-lg" class="q-mr-lg"
/> />
<VnInputDate <VnInputDate
v-if="showWhatsBeforeInventory" v-if="showWhatsBeforeInventory"
:label="t('itemDiary.since')" :label="t('itemDiary.since')"
dense dense
v-model="itemsBalanceFilter.where.date" v-model="where.date"
@update:model-value="fetchItemBalances" @update:model-value="fetchItemBalances"
/> />
</div> </div>

View File

@ -36,18 +36,7 @@ const exprBuilder = (param, value) => {
} }
}; };
const where = { let arrayData = useArrayData('ItemLastEntries');
itemFk: route.params.id,
};
const arrayData = useArrayData('ItemLastEntries', {
url: 'Items/lastEntriesFilter',
order: ['landed DESC', 'buyFk DESC'],
exprBuilder: exprBuilder,
userFilter: {
where: where,
},
});
const itemLastEntries = ref([]); const itemLastEntries = ref([]);
const columns = computed(() => [ const columns = computed(() => [
@ -161,25 +150,51 @@ const getDate = (date, type) => {
}; };
const updateFilter = async () => { const updateFilter = async () => {
let filter; let landed;
if (!from.value && to.value) filter = { lte: to.value }; if (!from.value && to.value) landed = { lte: to.value };
else if (from.value && !to.value) filter = { gte: from.value }; else if (from.value && !to.value) landed = { gte: from.value };
else if (from.value && to.value) filter = { between: [from.value, to.value] }; else if (from.value && to.value) landed = { between: [from.value, to.value] };
const userFilter = arrayData.store.userFilter.where;
userFilter.landed = filter;
arrayData.store.filter.where.landed = landed;
await fetchItemLastEntries(); await fetchItemLastEntries();
}; };
onMounted(async () => { onMounted(async () => {
const _from = Date.vnNew(); const landed = arrayData.store.filter.where?.landed;
_from.setDate(_from.getDate() - 75); arrayData = useArrayData('ItemLastEntries', {
from.value = getDate(_from, 'from'); url: 'Items/lastEntriesFilter',
const _to = Date.vnNew(); order: ['landed DESC', 'buyFk DESC'],
_to.setDate(_to.getDate() + 10); exprBuilder: exprBuilder,
to.value = getDate(_to, 'to'); filter: {
where: {
itemFk: route.params.id,
landed,
},
},
});
if (landed) {
const key = Object.keys(landed)[0];
switch (key) {
case 'gte':
from.value = landed.gte;
break;
case 'lte':
to.value = landed.lte;
break;
case 'between':
from.value = landed.between[0];
to.value = landed.between[1];
break;
}
} else {
const _from = Date.vnNew();
_from.setDate(_from.getDate() - 75);
from.value = getDate(_from, 'from');
const _to = Date.vnNew();
_to.setDate(_to.getDate() + 10);
to.value = getDate(_to, 'to');
}
updateFilter(); updateFilter();

View File

@ -8,6 +8,7 @@ import VnLv from 'src/components/ui/VnLv.vue';
import ItemDescriptorImage from 'src/pages/Item/Card/ItemDescriptorImage.vue'; import ItemDescriptorImage from 'src/pages/Item/Card/ItemDescriptorImage.vue';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import VnTitle from 'src/components/common/VnTitle.vue'; import VnTitle from 'src/components/common/VnTitle.vue';
import ItemDescriptorMenu from './ItemDescriptorMenu.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -43,10 +44,13 @@ const getUrl = (id, param) => `#/Item/${id}/${param}`;
<template #header="{ entity: { item } }"> <template #header="{ entity: { item } }">
{{ item.id }} - {{ item.name }} {{ item.id }} - {{ item.name }}
</template> </template>
<template #menu>
<ItemDescriptorMenu :entity-id="entityId" :warehouse-fk="warehouseFk" />
</template>
<template #body="{ entity: { item, tags, visible, available, botanical } }"> <template #body="{ entity: { item, tags, visible, available, botanical } }">
<QCard class="vn-one photo"> <QCard class="vn-one photo">
<ItemDescriptorImage <ItemDescriptorImage
:entity-id="Number(entityId)" :entity-id="entityId"
:visible="visible" :visible="visible"
:available="available" :available="available"
:show-edit-button="false" :show-edit-button="false"
@ -89,7 +93,7 @@ const getUrl = (id, param) => `#/Item/${id}/${param}`;
<QCard class="vn-one"> <QCard class="vn-one">
<VnTitle <VnTitle
:url="getUrl(entityId, 'basic-data')" :url="getUrl(entityId, 'basic-data')"
:text="t('item.summary.basicData')" :text="t('item.summary.otherData')"
/> />
<VnLv <VnLv
:label="t('item.summary.intrastatCode')" :label="t('item.summary.intrastatCode')"

View File

@ -107,7 +107,7 @@ const submitTags = async (data) => {
@on-fetch="onItemTagsFetched" @on-fetch="onItemTagsFetched"
> >
<template #body="{ rows, validate }"> <template #body="{ rows, validate }">
<QCard class="q-px-lg q-pt-md q-pb-sm"> <QCard class="q-px-lg q-pt-md q-pb-sm" data-cy="itemTags">
<VnRow <VnRow
v-for="(row, index) in rows" v-for="(row, index) in rows"
:key="index" :key="index"

View File

@ -35,6 +35,7 @@ const editTableCellDialogRef = ref(null);
const user = state.getUser(); const user = state.getUser();
const fixedPrices = ref([]); const fixedPrices = ref([]);
const warehousesOptions = ref([]); const warehousesOptions = ref([]);
const hasSelectedRows = computed(() => rowsSelected.value.length > 0);
const rowsSelected = ref([]); const rowsSelected = ref([]);
const itemFixedPriceFilterRef = ref(); const itemFixedPriceFilterRef = ref();
@ -368,9 +369,9 @@ function handleOnDataSave({ CrudModelRef }) {
</template> </template>
</RightMenu> </RightMenu>
<VnSubToolbar> <VnSubToolbar>
<template #st-data> <template #st-actions>
<QBtn <QBtn
v-if="rowsSelected.length" :disable="!hasSelectedRows"
@click="openEditTableCellDialog()" @click="openEditTableCellDialog()"
color="primary" color="primary"
icon="edit" icon="edit"
@ -380,13 +381,13 @@ function handleOnDataSave({ CrudModelRef }) {
</QTooltip> </QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
:disable="!hasSelectedRows"
:label="tMobile('globals.remove')" :label="tMobile('globals.remove')"
color="primary" color="primary"
icon="delete" icon="delete"
flat flat
@click="(row) => confirmRemove(row, true)" @click="(row) => confirmRemove(row, true)"
:title="t('globals.remove')" :title="t('globals.remove')"
v-if="rowsSelected.length"
/> />
</template> </template>
</VnSubToolbar> </VnSubToolbar>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed, onBeforeMount } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import VnImg from 'src/components/ui/VnImg.vue'; import VnImg from 'src/components/ui/VnImg.vue';
@ -15,6 +15,9 @@ import ItemTypeDescriptorProxy from './ItemType/Card/ItemTypeDescriptorProxy.vue
import { cloneItem } from 'src/pages/Item/composables/cloneItem'; import { cloneItem } from 'src/pages/Item/composables/cloneItem';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import ItemListFilter from './ItemListFilter.vue'; import ItemListFilter from './ItemListFilter.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import axios from 'axios';
const entityId = computed(() => route.params.id); const entityId = computed(() => route.params.id);
const { openCloneDialog } = cloneItem(); const { openCloneDialog } = cloneItem();
@ -22,7 +25,9 @@ const { viewSummary } = useSummaryDialog();
const { t } = useI18n(); const { t } = useI18n();
const tableRef = ref(); const tableRef = ref();
const route = useRoute(); const route = useRoute();
const validPriorities = ref([]);
const defaultTag = ref();
const defaultPriority = ref();
const itemFilter = { const itemFilter = {
include: [ include: [
{ {
@ -90,7 +95,6 @@ const columns = computed(() => [
label: t('globals.description'), label: t('globals.description'),
name: 'description', name: 'description',
align: 'left', align: 'left',
create: true,
columnFilter: { columnFilter: {
name: 'search', name: 'search',
}, },
@ -132,13 +136,23 @@ const columns = computed(() => [
fields: ['id', 'name'], fields: ['id', 'name'],
}, },
}, },
create: true,
visible: false, visible: false,
}, },
{ {
label: t('item.list.typeName'), label: t('item.list.typeName'),
name: 'typeName', name: 'typeName',
align: 'left', align: 'left',
component: 'select',
columnFilter: {
name: 'typeFk',
attrs: {
url: 'ItemTypes',
fields: ['id', 'name'],
},
},
columnField: {
component: null,
},
}, },
{ {
label: t('item.list.category'), label: t('item.list.category'),
@ -161,6 +175,11 @@ const columns = computed(() => [
name: 'intrastat', name: 'intrastat',
align: 'left', align: 'left',
component: 'select', component: 'select',
attrs: {
url: 'Intrastats',
optionValue: 'description',
optionLabel: 'description',
},
columnFilter: { columnFilter: {
name: 'intrastat', name: 'intrastat',
attrs: { attrs: {
@ -172,7 +191,6 @@ const columns = computed(() => [
columnField: { columnField: {
component: null, component: null,
}, },
create: true,
cardVisible: true, cardVisible: true,
}, },
{ {
@ -198,7 +216,6 @@ const columns = computed(() => [
columnField: { columnField: {
component: null, component: null,
}, },
create: true,
cardVisible: true, cardVisible: true,
}, },
{ {
@ -297,12 +314,21 @@ const columns = computed(() => [
], ],
}, },
]); ]);
onBeforeMount(async () => {
const { data } = await axios.get('ItemConfigs');
defaultTag.value = data[0].defaultTag;
defaultPriority.value = data[0].defaultPriority;
data.forEach((priority) => {
validPriorities.value = priority.validPriorities;
});
});
</script> </script>
<template> <template>
<VnSearchbar <VnSearchbar
data-key="ItemList" data-key="ItemList"
:label="t('item.searchbar.label')" :label="t('item.searchbar.label')"
:info="t('You can search by id')" :info="t('item.searchbar.info')"
/> />
<RightMenu> <RightMenu>
<template #right-panel> <template #right-panel>
@ -310,15 +336,18 @@ const columns = computed(() => [
</template> </template>
</RightMenu> </RightMenu>
<VnTable <VnTable
v-if="defaultTag"
ref="tableRef" ref="tableRef"
data-key="ItemList" data-key="ItemList"
url="Items/filter" url="Items/filter"
:create="{ :create="{
urlCreate: 'Items', urlCreate: 'Items/new',
title: t('Create Item'), title: t('item.list.newItem'),
onDataSaved: () => tableRef.redirect(), onDataSaved: ({ id }) => tableRef.redirect(`${id}/basic-data`),
formInitialData: { formInitialData: {
editorFk: entityId, editorFk: entityId,
tag: defaultTag,
priority: defaultPriority,
}, },
}" }"
:order="['isActive DESC', 'name', 'id']" :order="['isActive DESC', 'name', 'id']"
@ -362,7 +391,97 @@ const columns = computed(() => [
{{ row?.subName.toUpperCase() }} {{ row?.subName.toUpperCase() }}
</div> </div>
</div> </div>
<FetchedTags :item="row" :columns="3" /> <FetchedTags :item="row" />
</template>
<template #more-create-dialog="{ data }">
<VnInput
v-model="data.provisionalName"
:label="t('globals.description')"
:is-required="true"
/>
<VnSelect
url="Tags"
v-model="data.tag"
:label="t('globals.tag')"
:fields="['id', 'name']"
option-label="name"
option-value="id"
:is-required="true"
:sort-by="['name ASC']"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption> #{{ scope.opt?.id }} </QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect
:options="validPriorities"
v-model="data.priority"
:label="t('item.create.priority')"
:is-required="true"
/>
<VnSelect
url="ItemTypes"
v-model="data.typeFk"
:label="t('item.list.typeName')"
:fields="['id', 'code', 'name']"
option-label="name"
option-value="id"
:is-required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption>
{{ scope.opt?.code }} #{{ scope.opt?.id }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect
url="Intrastats"
v-model="data.intrastatFk"
:label="t('globals.intrastat')"
:fields="['id', 'description']"
option-label="description"
option-value="id"
:is-required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.description }}</QItemLabel>
<QItemLabel caption> #{{ scope.opt?.id }} </QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect
url="Origins"
v-model="data.originFk"
:label="t('globals.origin')"
:fields="['id', 'code', 'name']"
option-label="code"
option-value="id"
:is-required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption>
{{ scope.opt?.code }} #{{ scope.opt?.id }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</template> </template>
</VnTable> </VnTable>
</template> </template>
@ -379,5 +498,4 @@ es:
Create Item: Crear artículo Create Item: Crear artículo
You can search by id: Puedes buscar por id You can search by id: Puedes buscar por id
Preview: Vista previa Preview: Vista previa
Regularize stock: Regularizar stock
</i18n> </i18n>

Some files were not shown because too many files have changed in this diff Show More