0
0
Fork 0

Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into 7323_WorkerMigration_End

This commit is contained in:
Jon Elias 2024-07-08 13:16:06 +02:00
commit 960c011024
48 changed files with 1939 additions and 538 deletions

View File

@ -55,10 +55,10 @@ const onResponseError = (error) => {
} }
if (session.isLoggedIn() && response?.status === 401) { if (session.isLoggedIn() && response?.status === 401) {
session.destroy(); session.destroy(false);
const hash = window.location.hash; const hash = window.location.hash;
const url = hash.slice(1); const url = hash.slice(1);
Router.push({ path: url }); Router.push(`/login?redirect=${url}`);
} else if (!session.isLoggedIn()) { } else if (!session.isLoggedIn()) {
return Promise.reject(error); return Promise.reject(error);
} }

View File

@ -46,22 +46,6 @@ const onDataSaved = async (formData, requestResponse) => {
@on-fetch="(data) => (taxAreasOptions = data)" @on-fetch="(data) => (taxAreasOptions = data)"
auto-load auto-load
/> />
<FetchData
url="Tickets"
:filter="{
fields: ['id', 'nickname'],
where: { refFk: null },
order: 'shipped DESC',
}"
@on-fetch="(data) => (ticketsOptions = data)"
auto-load
/>
<FetchData
url="Clients"
:filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }"
@on-fetch="(data) => (clientsOptions = data)"
auto-load
/>
<FormModelPopup <FormModelPopup
ref="formModelPopupRef" ref="formModelPopupRef"
:title="t('Create manual invoice')" :title="t('Create manual invoice')"
@ -84,6 +68,10 @@ const onDataSaved = async (formData, requestResponse) => {
option-value="id" option-value="id"
v-model="data.ticketFk" v-model="data.ticketFk"
@update:model-value="data.clientFk = null" @update:model-value="data.clientFk = null"
url="Tickets"
:where="{ refFk: null }"
:fields="['id', 'nickname']"
:filter-options="{ order: 'shipped DESC' }"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -105,6 +93,9 @@ const onDataSaved = async (formData, requestResponse) => {
option-value="id" option-value="id"
v-model="data.clientFk" v-model="data.clientFk"
@update:model-value="data.ticketFk = null" @update:model-value="data.ticketFk = null"
url="Clients"
:fields="['id', 'name']"
:filter-options="{ order: 'name ASC' }"
/> />
<VnInputDate :label="t('Max date')" v-model="data.maxShipped" /> <VnInputDate :label="t('Max date')" v-model="data.maxShipped" />
</VnRow> </VnRow>
@ -116,7 +107,6 @@ const onDataSaved = async (formData, requestResponse) => {
option-label="description" option-label="description"
option-value="code" option-value="code"
v-model="data.serial" v-model="data.serial"
:required="true"
/> />
<VnSelect <VnSelect
:label="t('Area')" :label="t('Area')"
@ -125,7 +115,6 @@ const onDataSaved = async (formData, requestResponse) => {
option-label="code" option-label="code"
option-value="code" option-value="code"
v-model="data.taxArea" v-model="data.taxArea"
:required="true"
/> />
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">

View File

@ -10,7 +10,6 @@ import VnPaginate from 'components/ui/VnPaginate.vue';
import VnConfirm from 'components/ui/VnConfirm.vue'; import VnConfirm from 'components/ui/VnConfirm.vue';
import SkeletonTable from 'components/ui/SkeletonTable.vue'; import SkeletonTable from 'components/ui/SkeletonTable.vue';
import { tMobile } from 'src/composables/tMobile'; import { tMobile } from 'src/composables/tMobile';
import VnSubToolbar from './ui/VnSubToolbar.vue';
const { push } = useRouter(); const { push } = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();

View File

@ -8,7 +8,7 @@ import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
import FormPopup from './FormPopup.vue'; import FormPopup from './FormPopup.vue';
import { useDialogPluginComponent } from 'quasar';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
@ -18,7 +18,7 @@ const $props = defineProps({
default: () => {}, default: () => {},
}, },
}); });
const { dialogRef } = useDialogPluginComponent();
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
@ -116,90 +116,92 @@ const makeInvoice = async () => {
@on-fetch="(data) => (invoiceCorrectionTypesOptions = data)" @on-fetch="(data) => (invoiceCorrectionTypesOptions = data)"
auto-load auto-load
/> />
<FormPopup <QDialog ref="dialogRef">
<FormPopup
@on-submit="makeInvoice()" @on-submit="makeInvoice()"
:title="t('Transfer invoice')" :title="t('Transfer invoice')"
:custom-submit-button-label="t('Transfer client')" :custom-submit-button-label="t('Transfer client')"
:default-cancel-button="false" :default-cancel-button="false"
> >
<template #form-inputs> <template #form-inputs>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<VnSelect <VnSelect
:label="t('Client')" :label="t('Client')"
:options="clientsOptions" :options="clientsOptions"
hide-selected hide-selected
option-label="name" option-label="name"
option-value="id" option-value="id"
v-model="transferInvoiceParams.newClientFk" v-model="transferInvoiceParams.newClientFk"
:required="true" :required="true"
url="Clients" url="Clients"
:fields="['id', 'name', 'hasToInvoiceByAddress']" :fields="['id', 'name', 'hasToInvoiceByAddress']"
auto-load auto-load
> >
<template #option="scope"> <template #option="scope">
<QItem <QItem
v-bind="scope.itemProps" v-bind="scope.itemProps"
@click="selectedClient(scope.opt)" @click="selectedClient(scope.opt)"
> >
<QItemSection> <QItemSection>
<QItemLabel> <QItemLabel>
#{{ scope.opt?.id }} - {{ scope.opt?.name }} #{{ scope.opt?.id }} - {{ scope.opt?.name }}
</QItemLabel> </QItemLabel>
</QItemSection> </QItemSection>
</QItem> </QItem>
</template> </template>
</VnSelect> </VnSelect>
<VnSelect <VnSelect
:label="t('Rectificative type')" :label="t('Rectificative type')"
:options="rectificativeTypeOptions" :options="rectificativeTypeOptions"
hide-selected hide-selected
option-label="description" option-label="description"
option-value="id" option-value="id"
v-model="transferInvoiceParams.cplusRectificationTypeFk" v-model="transferInvoiceParams.cplusRectificationTypeFk"
:required="true" :required="true"
/> />
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<VnSelect <VnSelect
:label="t('Class')" :label="t('Class')"
:options="siiTypeInvoiceOutsOptions" :options="siiTypeInvoiceOutsOptions"
hide-selected hide-selected
option-label="description" option-label="description"
option-value="id" option-value="id"
v-model="transferInvoiceParams.siiTypeInvoiceOutFk" v-model="transferInvoiceParams.siiTypeInvoiceOutFk"
:required="true" :required="true"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
<QItemSection> <QItemSection>
<QItemLabel> <QItemLabel>
{{ scope.opt?.code }} - {{ scope.opt?.code }} -
{{ scope.opt?.description }} {{ scope.opt?.description }}
</QItemLabel> </QItemLabel>
</QItemSection> </QItemSection>
</QItem> </QItem>
</template> </template>
</VnSelect> </VnSelect>
<VnSelect <VnSelect
:label="t('Type')" :label="t('Type')"
:options="invoiceCorrectionTypesOptions" :options="invoiceCorrectionTypesOptions"
hide-selected hide-selected
option-label="description" option-label="description"
option-value="id" option-value="id"
v-model="transferInvoiceParams.invoiceCorrectionTypeFk" v-model="transferInvoiceParams.invoiceCorrectionTypeFk"
:required="true" :required="true"
/> />
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div> <div>
<QCheckbox :label="t('Bill destination client')" v-model="checked" /> <QCheckbox :label="t('Bill destination client')" v-model="checked" />
<QIcon name="info" class="cursor-info q-ml-sm" size="sm"> <QIcon name="info" class="cursor-info q-ml-sm" size="sm">
<QTooltip>{{ t('transferInvoiceInfo') }}</QTooltip> <QTooltip>{{ t('transferInvoiceInfo') }}</QTooltip>
</QIcon> </QIcon>
</div> </div>
</VnRow> </VnRow>
</template> </template>
</FormPopup> </FormPopup>
</QDialog>
</template> </template>
<i18n> <i18n>

View File

@ -67,6 +67,10 @@ const $props = defineProps({
type: Object, type: Object,
default: () => ({ card: false, table: false }), default: () => ({ card: false, table: false }),
}, },
table: {
type: Object,
default: () => ({}),
},
}); });
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore(); const stateStore = useStateStore();
@ -249,7 +253,7 @@ defineExpose({
</template> </template>
<template #body="{ rows }"> <template #body="{ rows }">
<QTable <QTable
v-bind="$attrs['q-table']" v-bind="table"
class="vnTable" class="vnTable"
:columns="splittedColumns.columns" :columns="splittedColumns.columns"
:rows="rows" :rows="rows"

View File

@ -52,7 +52,7 @@ const toggleMarkAll = (val) => {
const getConfig = async (url, filter) => { const getConfig = async (url, filter) => {
const response = await axios.get(url, { const response = await axios.get(url, {
params: { filter: filter }, params: { filter: JSON.stringify(filter) },
}); });
return response.data && response.data.length > 0 ? response.data[0] : null; return response.data && response.data.length > 0 ? response.data[0] : null;
}; };
@ -60,7 +60,7 @@ const getConfig = async (url, filter) => {
const fetchViewConfigData = async () => { const fetchViewConfigData = async () => {
try { try {
const userConfigFilter = { const userConfigFilter = {
where: { tableCode: $props.tableCode, userFk: user.id }, where: { tableCode: $props.tableCode, userFk: user.value.id },
}; };
const userConfig = await getConfig('UserConfigViews', userConfigFilter); const userConfig = await getConfig('UserConfigViews', userConfigFilter);
@ -74,8 +74,14 @@ const fetchViewConfigData = async () => {
const defaultConfig = await getConfig('DefaultViewConfigs', defaultConfigFilter); const defaultConfig = await getConfig('DefaultViewConfigs', defaultConfigFilter);
if (defaultConfig) { if (defaultConfig) {
// Si el backend devuelve una configuración por defecto la usamos
setUserConfigViewData(defaultConfig.columns); setUserConfigViewData(defaultConfig.columns);
return; return;
} else {
// Si no hay configuración por defecto mostramos todas las columnas
const defaultColumns = {};
$props.allColumns.forEach((col) => (defaultColumns[col] = true));
setUserConfigViewData(defaultColumns);
} }
} catch (err) { } catch (err) {
console.err('Error fetching config view data', err); console.err('Error fetching config view data', err);

View File

@ -165,9 +165,9 @@ const toModule = computed(() =>
<QTooltip> <QTooltip>
{{ t('components.cardDescriptor.moreOptions') }} {{ t('components.cardDescriptor.moreOptions') }}
</QTooltip> </QTooltip>
<QMenu> <QMenu ref="menuRef">
<QList> <QList>
<slot name="menu" :entity="entity" /> <slot name="menu" :entity="entity" :menu-ref="menuRef" />
</QList> </QList>
</QMenu> </QMenu>
</QBtn> </QBtn>

View File

@ -7,7 +7,7 @@ import VnImg from 'src/components/ui/VnImg.vue';
import OrderCatalogItemDialog from 'pages/Order/Card/OrderCatalogItemDialog.vue'; import OrderCatalogItemDialog from 'pages/Order/Card/OrderCatalogItemDialog.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import toCurrency from '../../../filters/toCurrency'; import { toCurrency } from 'filters/index';
const DEFAULT_PRICE_KG = 0; const DEFAULT_PRICE_KG = 0;
@ -18,6 +18,10 @@ defineProps({
type: Object, type: Object,
required: true, required: true,
}, },
isCatalog: {
type: Boolean,
default: false,
},
}); });
const dialog = ref(null); const dialog = ref(null);
@ -28,7 +32,7 @@ const dialog = ref(null);
<QCard class="card shadow-6"> <QCard class="card shadow-6">
<div class="img-wrapper"> <div class="img-wrapper">
<VnImg :id="item.id" zoom-size="lg" class="image" /> <VnImg :id="item.id" zoom-size="lg" class="image" />
<div v-if="item.hex" class="item-color-container"> <div v-if="item.hex && isCatalog" class="item-color-container">
<div <div
class="item-color" class="item-color"
:style="{ backgroundColor: `#${item.hex}` }" :style="{ backgroundColor: `#${item.hex}` }"
@ -50,11 +54,12 @@ const dialog = ref(null);
</template> </template>
<div class="footer"> <div class="footer">
<div class="price"> <div class="price">
<p> <p v-if="isCatalog">
{{ item.available }} {{ t('to') }} {{ item.available }} {{ t('to') }}
{{ toCurrency(item.price) }} {{ toCurrency(item.price) }}
</p> </p>
<QIcon name="add_circle" class="icon"> <slot name="price" />
<QIcon v-if="isCatalog" name="add_circle" class="icon">
<QTooltip>{{ t('globals.add') }}</QTooltip> <QTooltip>{{ t('globals.add') }}</QTooltip>
<QPopupProxy ref="dialog"> <QPopupProxy ref="dialog">
<OrderCatalogItemDialog <OrderCatalogItemDialog

View File

@ -58,6 +58,7 @@ const $props = defineProps({
}, },
}); });
defineExpose({ search });
const emit = defineEmits([ const emit = defineEmits([
'update:modelValue', 'update:modelValue',
'refresh', 'refresh',
@ -192,6 +193,14 @@ function formatValue(value) {
</script> </script>
<template> <template>
<QBtn
class="q-mt-lg q-mr-xs q-mb-lg"
round
color="primary"
style="position: fixed; z-index: 1; right: 0; bottom: 0"
icon="search"
@click="search()"
></QBtn>
<QForm @submit="search" id="filterPanelForm"> <QForm @submit="search" id="filterPanelForm">
<QList dense> <QList dense>
<QItem class="q-mt-xs"> <QItem class="q-mt-xs">
@ -265,23 +274,6 @@ function formatValue(value) {
<QList dense class="list q-gutter-y-sm q-mt-sm"> <QList dense class="list q-gutter-y-sm q-mt-sm">
<slot name="body" :params="userParams" :search-fn="search"></slot> <slot name="body" :params="userParams" :search-fn="search"></slot>
</QList> </QList>
<template v-if="$props.searchButton">
<QItem>
<QItemSection class="q-py-sm">
<QBtn
:label="t('Search')"
class="full-width"
color="primary"
dense
icon="search"
rounded
:type="disableSubmitEvent ? 'button' : 'submit'"
unelevated
/>
</QItemSection>
</QItem>
<QSeparator />
</template>
</QForm> </QForm>
<QInnerLoading <QInnerLoading
:label="t('globals.pleaseWait')" :label="t('globals.pleaseWait')"

View File

@ -28,7 +28,7 @@ const $props = defineProps({
const show = ref(false); const show = ref(false);
const token = useSession().getTokenMultimedia(); const token = useSession().getTokenMultimedia();
const timeStamp = ref(`timestamp=${Date.now()}`); const timeStamp = ref(`timestamp=${Date.now()}`);
import noImage from '/public/no-user.png'; import noImage from '/no-user.png';
import { useRole } from 'src/composables/useRole'; import { useRole } from 'src/composables/useRole';
const url = computed(() => { const url = computed(() => {
const isEmployee = useRole().isEmployee(); const isEmployee = useRole().isEmployee();

View File

@ -58,31 +58,37 @@ export function useSession() {
} }
} }
} }
async function destroy() { async function destroy(destroyTokens = true) {
const tokens = { const tokens = {
tokenMultimedia: 'Accounts/logout', tokenMultimedia: 'Accounts/logout',
token: 'VnUsers/logout', token: 'VnUsers/logout',
}; };
const storage = keepLogin() ? localStorage : sessionStorage; const storage = keepLogin() ? localStorage : sessionStorage;
let destroyTokenPromises = [];
try {
if (destroyTokens) {
const { data: isValidToken } = await axios.get('VnUsers/validateToken');
if (isValidToken)
destroyTokenPromises = Object.entries(tokens).map(([key, url]) =>
destroyToken(url, storage, key)
);
}
} finally {
localStorage.clear();
sessionStorage.clear();
await Promise.allSettled(destroyTokenPromises);
const { setUser } = useState();
for (const [key, url] of Object.entries(tokens)) { setUser({
await destroyToken(url, storage, key); id: 0,
name: '',
nickname: '',
lang: '',
darkMode: null,
});
stopRenewer();
} }
localStorage.clear();
sessionStorage.clear();
const { setUser } = useState();
setUser({
id: 0,
name: '',
nickname: '',
lang: '',
darkMode: null,
});
stopRenewer();
} }
async function login(data) { async function login(data) {

View File

@ -550,10 +550,14 @@ ticket:
observation: Notes observation: Notes
ticketAdvance: Advance tickets ticketAdvance: Advance tickets
futureTickets: Future tickets futureTickets: Future tickets
expedition: Expedition
purchaseRequest: Purchase request purchaseRequest: Purchase request
weeklyTickets: Weekly tickets weeklyTickets: Weekly tickets
services: Service services: Service
tracking: Tracking tracking: Tracking
components: Components
pictures: Pictures
packages: Packages
list: list:
nickname: Nickname nickname: Nickname
state: State state: State

View File

@ -239,6 +239,13 @@ globals:
mailForwarding: Reenvío de correo mailForwarding: Reenvío de correo
mailAlias: Alias de correo mailAlias: Alias de correo
privileges: Privilegios privileges: Privilegios
observation: Notas
expedition: Expedición
services: Servicios
tracking: Estados
components: Componentes
pictures: Fotos
packages: Bultos
created: Fecha creación created: Fecha creación
worker: Trabajador worker: Trabajador
now: Ahora now: Ahora
@ -548,10 +555,14 @@ ticket:
observation: Notas observation: Notas
ticketAdvance: Adelantar tickets ticketAdvance: Adelantar tickets
futureTickets: Tickets a futuro futureTickets: Tickets a futuro
expedition: Expedición
purchaseRequest: Petición de compra purchaseRequest: Petición de compra
weeklyTickets: Tickets programados weeklyTickets: Tickets programados
services: Servicios services: Servicios
tracking: Estados tracking: Estados
components: Componentes
pictures: Fotos
packages: Bultos
list: list:
nickname: Alias nickname: Alias
state: Estado state: Estado

View File

@ -408,7 +408,6 @@ function handleLocation(data, location) {
default-mode="table" default-mode="table"
redirect="customer" redirect="customer"
auto-load auto-load
:disable-option="{ card: true }"
> >
<template #more-create-dialog="{ data }"> <template #more-create-dialog="{ data }">
<VnLocation <VnLocation

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { onMounted, ref, computed, onUpdated } from 'vue'; import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { onMounted, ref, computed, reactive, onUnmounted } from 'vue'; import { onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import VnTable from 'components/VnTable/VnTable.vue'; import VnTable from 'components/VnTable/VnTable.vue';

View File

@ -1,8 +1,6 @@
<script setup> <script setup>
import { onMounted, ref, computed } from 'vue'; import { onMounted, ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useRoute } from 'vue-router';
import EntryFilter from './EntryFilter.vue'; import EntryFilter from './EntryFilter.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
@ -12,8 +10,6 @@ import { toDate } from 'src/filters';
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute();
const entityId = computed(() => route.params.id);
const tableRef = ref(); const tableRef = ref();
const entryFilter = { const entryFilter = {

View File

@ -63,8 +63,8 @@ const setData = (entity) => (data.value = useCardDescription(entity.ref, entity.
@on-fetch="setData" @on-fetch="setData"
data-key="invoiceOutData" data-key="invoiceOutData"
> >
<template #menu="{ entity }"> <template #menu="{ entity, menuRef }">
<InvoiceOutDescriptorMenu :invoice-out-data="entity" /> <InvoiceOutDescriptorMenu :invoice-out-data="entity" :menu-ref="menuRef" />
</template> </template>
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv :label="t('invoiceOut.card.issued')" :value="toDate(entity.issued)" /> <VnLv :label="t('invoiceOut.card.issued')" :value="toDate(entity.issued)" />

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref, onBeforeMount } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
@ -11,13 +11,20 @@ import useNotify from 'src/composables/useNotify';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { usePrintService } from 'composables/usePrintService'; import { usePrintService } from 'composables/usePrintService';
import { useVnConfirm } from 'composables/useVnConfirm'; import { useVnConfirm } from 'composables/useVnConfirm';
import { getUrl } from 'src/composables/getUrl';
import axios from 'axios'; import axios from 'axios';
onBeforeMount(async () => (salixUrl.value = await getUrl('')));
const $props = defineProps({ const $props = defineProps({
invoiceOutData: { invoiceOutData: {
type: Object, type: Object,
default: () => {}, default: () => {},
}, },
menuRef: {
type: Object,
default: () => {},
},
}); });
const { notify } = useNotify(); const { notify } = useNotify();
@ -28,8 +35,7 @@ const { t } = useI18n();
const { openReport, sendEmail } = usePrintService(); const { openReport, sendEmail } = usePrintService();
const { openConfirmationModal } = useVnConfirm(); const { openConfirmationModal } = useVnConfirm();
const quasar = useQuasar(); const quasar = useQuasar();
const salixUrl = ref();
const transferInvoiceDialogRef = ref();
const invoiceFormType = ref('pdf'); const invoiceFormType = ref('pdf');
const defaultEmailAddress = ref($props.invoiceOutData.client?.email); const defaultEmailAddress = ref($props.invoiceOutData.client?.email);
@ -115,6 +121,7 @@ const refundInvoice = async (withWarehouse) => {
try { try {
const params = { ref: $props.invoiceOutData.ref, withWarehouse: withWarehouse }; const params = { ref: $props.invoiceOutData.ref, withWarehouse: withWarehouse };
const { data } = await axios.post('InvoiceOuts/refund', params); const { data } = await axios.post('InvoiceOuts/refund', params);
location.href = window.origin + `/#/ticket/${data[0].id}/sale`;
notify( notify(
t('refundInvoiceSuccessMessage', { t('refundInvoiceSuccessMessage', {
refundTicket: data[0].id, refundTicket: data[0].id,
@ -125,11 +132,22 @@ const refundInvoice = async (withWarehouse) => {
console.error('Error generating invoice out pdf', err); console.error('Error generating invoice out pdf', err);
} }
}; };
const showTransferInvoiceForm = async () => {
quasar.dialog({
component: TransferInvoiceForm,
componentProps: {
invoiceOutData: $props.invoiceOutData,
},
});
};
</script> </script>
<template> <template>
<QItem v-ripple clickable @click="transferInvoiceDialogRef.show()"> <QItem v-ripple clickable>
<QItemSection>{{ t('Transfer invoice to...') }}</QItemSection> <QItemSection @click="showTransferInvoiceForm()">{{
t('Transfer invoice to...')
}}</QItemSection>
</QItem> </QItem>
<QItem v-ripple clickable> <QItem v-ripple clickable>
<QItemSection>{{ t('Show invoice...') }}</QItemSection> <QItemSection>{{ t('Show invoice...') }}</QItemSection>
@ -222,10 +240,6 @@ const refundInvoice = async (withWarehouse) => {
{{ t('Create a single ticket with all the content of the current invoice') }} {{ t('Create a single ticket with all the content of the current invoice') }}
</QTooltip> </QTooltip>
</QItem> </QItem>
<QDialog ref="transferInvoiceDialogRef">
<TransferInvoiceForm :invoice-out-data="invoiceOutData" />
</QDialog>
</template> </template>
<i18n> <i18n>

View File

@ -3,7 +3,7 @@ import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import axios from 'axios'; import axios from 'axios';
import { toCurrency, toDate } from 'src/filters'; import { toCurrency, toDate, toPercentage } from 'src/filters';
import CardSummary from 'components/ui/CardSummary.vue'; import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
@ -57,12 +57,14 @@ const taxColumns = ref([
name: 'quantity', name: 'quantity',
label: 'invoiceOut.summary.rate', label: 'invoiceOut.summary.rate',
field: (row) => row.rate, field: (row) => row.rate,
format: (value) => toPercentage(value / 100),
sortable: true, sortable: true,
}, },
{ {
name: 'invoiceOuted', name: 'invoiceOuted',
label: 'invoiceOut.summary.fee', label: 'invoiceOut.summary.fee',
field: (row) => row.vat, field: (row) => row.vat,
format: (value) => toCurrency(value),
sortable: true, sortable: true,
}, },
]); ]);
@ -113,7 +115,7 @@ const ticketsColumns = ref([
</template> </template>
<template #body="{ entity: { invoiceOut } }"> <template #body="{ entity: { invoiceOut } }">
<QCard class="vn-one"> <QCard class="vn-one">
<VnTitle :text="t('invoiceOut.pageTitles.basicData')" /> <VnTitle :text="t('globals.pageTitles.basicData')" />
<VnLv <VnLv
:label="t('invoiceOut.summary.issued')" :label="t('invoiceOut.summary.issued')"
:value="toDate(invoiceOut.issued)" :value="toDate(invoiceOut.issued)"
@ -164,7 +166,7 @@ const ticketsColumns = ref([
</template> </template>
<template #body-cell-item="{ value }"> <template #body-cell-item="{ value }">
<QTd> <QTd>
<QBtn flat color="primary"> <QBtn flat class="link">
{{ value }} {{ value }}
<TicketDescriptorProxy :id="value" /> <TicketDescriptorProxy :id="value" />
</QBtn> </QBtn>
@ -172,7 +174,7 @@ const ticketsColumns = ref([
</template> </template>
<template #body-cell-quantity="{ value, row }"> <template #body-cell-quantity="{ value, row }">
<QTd> <QTd>
<QBtn class="no-uppercase" flat color="primary" dense> <QBtn class="no-uppercase link" flat dense>
{{ value }} {{ value }}
<CustomerDescriptorProxy :id="row.id" /> <CustomerDescriptorProxy :id="row.id" />
</QBtn> </QBtn>

View File

@ -41,7 +41,7 @@ function setWorkers(data) {
<span>{{ formatFn(tag.value) }}</span> <span>{{ formatFn(tag.value) }}</span>
</div> </div>
</template> </template>
<template #body="{ params, searchFn }"> <template #body="{ params }">
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInput <VnInput
@ -93,7 +93,6 @@ function setWorkers(data) {
<QItemSection> <QItemSection>
<QCheckbox <QCheckbox
:label="t('Has PDF')" :label="t('Has PDF')"
@update:model-value="searchFn()"
toggle-indeterminate toggle-indeterminate
v-model="params.hasPdf" v-model="params.hasPdf"
/> />

View File

@ -30,7 +30,7 @@ const selectedCustomerId = ref(null);
const tableColumnComponents = { const tableColumnComponents = {
clientId: { clientId: {
component: QBtn, component: QBtn,
props: { flat: true, color: 'blue' }, props: { flat: true, class: 'link' },
event: (prop) => selectCustomerId(prop.value), event: (prop) => selectCustomerId(prop.value),
}, },
clientName: { clientName: {
@ -52,7 +52,12 @@ const tableColumnComponents = {
}; };
const columns = computed(() => [ const columns = computed(() => [
{ label: 'Id', field: 'clientId', name: 'clientId', align: 'left' }, {
align: 'left',
name: 'clientId',
label: 'Id',
field: 'clientId',
},
{ {
label: t('invoiceOut.globalInvoices.table.client'), label: t('invoiceOut.globalInvoices.table.client'),
field: 'clientName', field: 'clientName',

View File

@ -48,7 +48,7 @@ const getStatus = computed({
onMounted(async () => { onMounted(async () => {
await invoiceOutGlobalStore.init(); await invoiceOutGlobalStore.init();
formData.value = { ...formInitialData.value }; formData.value = formInitialData.value.invoiceDate;
}); });
</script> </script>

View File

@ -1,96 +1,177 @@
<script setup> <script setup>
import { onMounted, onUnmounted, ref } from 'vue'; import { onMounted, onUnmounted, ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import InvoiceOutSummary from './Card/InvoiceOutSummary.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue';
import CreateManualInvoiceForm from 'src/components/CreateManualInvoiceForm.vue';
import InvoiceOutFilter from './InvoiceOutFilter.vue';
import { toDate, toCurrency } from 'src/filters/index';
import { useStateStore } from 'stores/useStateStore';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useSession } from 'src/composables/useSession'; import { usePrintService } from 'composables/usePrintService';
import RightMenu from 'src/components/common/RightMenu.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import InvoiceOutSummary from './Card/InvoiceOutSummary.vue';
import { toCurrency, toDate } from 'src/filters/index';
import { useStateStore } from 'stores/useStateStore';
import { QBtn } from 'quasar';
import { watchEffect } from 'vue';
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter();
const stateStore = useStateStore(); const stateStore = useStateStore();
const session = useSession();
const tokenMultimedia = session.getTokenMultimedia();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const tableRef = ref();
const invoiceOutSerialsOptions = ref([]);
const ticketsOptions = ref([]);
const customerOptions = ref([]);
const selectedRows = ref([]);
const hasSelectedCards = computed(() => selectedRows.value.length > 0);
const MODEL = 'InvoiceOuts';
const { openReport } = usePrintService();
const manualInvoiceDialogRef = ref(null); const columns = computed(() => [
const selectedCards = ref(new Map()); {
align: 'left',
name: 'id',
label: t('invoiceOutList.tableVisibleColumns.id'),
chip: {
condition: () => true,
},
isId: true,
},
{
align: 'left',
name: 'ref',
label: t('invoiceOutList.tableVisibleColumns.ref'),
isTitle: true,
component: 'select',
attrs: {
url: MODEL,
optionLabel: 'ref',
optionValue: 'id',
},
columnField: {
component: null,
},
},
{
align: 'left',
name: 'Issued',
label: t('invoiceOutList.tableVisibleColumns.issued'),
component: 'date',
format: (row) => toDate(row.issued),
columnField: {
component: null,
},
},
{
align: 'left',
name: 'clientSocialName',
label: t('invoiceOutModule.customer'),
cardVisible: true,
component: 'select',
attrs: {
url: 'Clients',
fields: ['id', 'name'],
},
columnField: {
component: null,
},
},
{
align: 'left',
name: 'companyCode',
label: t('invoiceOutModule.company'),
cardVisible: true,
component: 'select',
attrs: {
url: 'Companies',
optionLabel: 'code',
optionValue: 'id',
},
columnField: {
component: null,
},
},
{
align: 'left',
name: 'amount',
label: t('invoiceOutModule.amount'),
cardVisible: true,
format: (row) => toCurrency(row.amount),
},
{
align: 'left',
name: 'created',
label: t('invoiceOutList.tableVisibleColumns.created'),
component: 'date',
columnField: {
component: null,
},
format: (row) => toDate(row.created),
},
{
align: 'left',
name: 'dued',
label: t('invoiceOutList.tableVisibleColumns.dueDate'),
component: 'date',
columnField: {
component: null,
},
format: (row) => toDate(row.dued),
},
{
align: 'right',
name: 'tableActions',
actions: [
{
title: t('InvoiceOutSummary'),
icon: 'preview',
action: (row) => viewSummary(row.id, InvoiceOutSummary),
},
{
title: t('DownloadPdf'),
icon: 'vn:ticket',
isPrimary: true,
action: (row) => openPdf(row.id),
},
],
},
]);
onMounted(() => (stateStore.rightDrawer = true)); onMounted(() => (stateStore.rightDrawer = true));
onUnmounted(() => (stateStore.rightDrawer = false)); onUnmounted(() => (stateStore.rightDrawer = false));
function navigate(id) { function openPdf(id) {
router.push({ path: `/invoice-out/${id}` }); try {
openReport(`${MODEL}/${id}/download`);
} catch (err) {
console.error('Error opening PDF', err);
}
} }
const toggleIndividualCard = (cardData) => { function downloadPdf() {
if (selectedCards.value.has(cardData.id)) {
selectedCards.value.delete(cardData.id);
return;
}
selectedCards.value.set(cardData.id, cardData);
};
const toggleAllCards = (cardsData) => {
const allSelected = selectedCards.value.size === cardsData.length;
if (!allSelected) {
// Si no todas las tarjetas están seleccionadas, selecciónalas todas
cardsData.forEach((data) => {
if (!selectedCards.value.has(data.id)) {
selectedCards.value.set(data.id, data);
}
});
} else {
// Si todas las tarjetas están seleccionadas, deselecciónalas todas
selectedCards.value.clear();
}
};
const openPdf = () => {
try { try {
if (selectedCards.value.size === 0) return; if (selectedRows.value.size === 0) return;
const selectedCardsArray = Array.from(selectedCards.value.values()); const selectedCardsArray = Array.from(selectedRows.value.values());
if (selectedCards.value.size === 1) { if (selectedRows.value.size === 1) {
const [invoiceOut] = selectedCardsArray; const [invoiceOut] = selectedCardsArray;
const url = `api/InvoiceOuts/${invoiceOut.id}/download?access_token=${tokenMultimedia}`; openPdf(invoiceOut.id);
window.open(url, '_blank');
} else { } else {
const invoiceOutIdsArray = selectedCardsArray.map( const invoiceOutIdsArray = selectedCardsArray.map(
(invoiceOut) => invoiceOut.id (invoiceOut) => invoiceOut.id
); );
const invoiceOutIds = invoiceOutIdsArray.join(','); const invoiceOutIds = invoiceOutIdsArray.join(',');
const params = new URLSearchParams({ const params = {
access_token: tokenMultimedia,
ids: invoiceOutIds, ids: invoiceOutIds,
}); };
const url = `api/InvoiceOuts/downloadZip?${params}`; openReport(`${MODEL}/downloadZip`, params);
window.open(url, '_blank');
} }
} catch (err) { } catch (err) {
console.error('Error opening PDF'); console.error('Error opening PDF');
} }
}; }
const openCreateInvoiceModal = () => { watchEffect(selectedRows);
manualInvoiceDialogRef.value.show();
};
</script> </script>
<template> <template>
@ -99,115 +180,84 @@ const openCreateInvoiceModal = () => {
:label="t('searchInvoice')" :label="t('searchInvoice')"
data-key="InvoiceOutList" data-key="InvoiceOutList"
/> />
<RightMenu> <VnSubToolbar>
<template #right-panel> <template #st-actions>
<InvoiceOutFilter data-key="InvoiceOutList" /> <QBtn
color="primary"
icon-right="cloud_download"
@click="downloadPdf()"
:disable="!hasSelectedCards"
>
<QTooltip>{{ t('globals.downloadPdf') }}</QTooltip>
</QBtn>
</template> </template>
</RightMenu> </VnSubToolbar>
<QPage> <VnTable
<VnPaginate ref="tableRef"
auto-load data-key="invoiceOut"
data-key="InvoiceOutList" :url="`${MODEL}/filter`"
:order="['issued DESC', 'id DESC']" :create="{
url="InvoiceOuts/filter" urlCreate: 'InvoiceOuts/createManualInvoice',
> title: t('Create Manual Invoice'),
<template #body="{ rows }"> onDataSaved: ({ id }) => tableRef.redirect(id),
<VnSubToolbar class="justify-end"> formInitialData: {
<template #st-actions> active: true,
<QBtn },
@click="openPdf()" }"
class="q-mr-md" v-model:selected="selectedRows"
color="primary" order="id DESC"
icon="cloud_download" :columns="columns"
:disable="selectedCards.size === 0" default-mode="table"
> redirect="invoice-out"
<QTooltip>{{ t('globals.downloadPdf') }}</QTooltip> auto-load
</QBtn> :table="{
<QCheckbox 'row-key': 'id',
left-label selection: 'multiple',
:label="t('globals.markAll')" }"
@click="toggleAllCards(rows)" >
:model-value="selectedCards.size === rows.length" <template #more-create-dialog="{ data }">
class="q-mr-md" <VnSelect
/> url="Tickets"
</template> v-model="data.ticketFk"
</VnSubToolbar> :label="t('invoiceOutList.tableVisibleColumns.ticket')"
<div class="flex flex-center q-pa-md"> :options="ticketsOptions"
<div class="vn-card-list"> option-label="nickname"
<CardList option-value="id"
:element="row" />
:id="row.id" <VnSelect
:show-checkbox="true" url="Clients"
:is-selected="selectedCards.has(row.id)" v-model="data.clientFk"
:key="row.id" :label="t('invoiceOutModule.customer')"
:title="row.ref" :options="customerOptions"
@click="navigate(row.id)" option-label="name"
@toggle-card-check="toggleIndividualCard(row)" option-value="id"
v-for="row of rows" />
> <VnInputDate
<template #list-items> :label="t('invoiceOutList.tableVisibleColumns.dueDate')"
<VnLv v-model="data.maxShipped"
:label="t('invoiceOut.list.issued')" />
:value="toDate(row.issued)" <VnSelect
/> url="InvoiceOutSerials"
<VnLv v-model="data.invoiceOutSerial"
:label="t('invoiceOut.list.amount')" :label="t('invoiceOutList.tableVisibleColumns.invoiceOutSerial')"
:value="toCurrency(row.amount)" :options="invoiceOutSerialsOptions"
/> option-label="description"
<VnLv :label="t('invoiceOut.list.client')"> option-value="code"
<template #value> />
<span class="link" @click.stop> <VnSelect
{{ row?.clientSocialName }} url="TaxAreas"
<CustomerDescriptorProxy v-model="data.area"
:id="row?.clientFk" :label="t('invoiceOutList.tableVisibleColumns.taxArea')"
/> :options="taxAreasOptions"
</span> option-label="code"
</template> option-value="code"
</VnLv> />
<VnLv <QInput
:label="t('invoiceOut.list.shortCreated')" v-model="data.reference"
:title-label="t('invoiceOut.list.created')" :label="t('invoiceOutList.tableVisibleColumns.ref')"
:value="toDate(row.created)" />
/> </template>
<VnLv </VnTable>
:label="t('invoiceOut.list.company')"
:value="row.companyCode"
/>
<VnLv
:label="t('invoiceOut.list.shortDued')"
:title-label="t('invoiceOut.list.dued')"
:value="toDate(row.dued)"
/>
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, InvoiceOutSummary)"
color="primary"
style="margin-top: 15px"
type="submit"
/>
</template>
</CardList>
</div>
</div>
</template>
</VnPaginate>
<QPageSticky :offset="[20, 20]">
<QBtn fab icon="add" color="primary" @click="openCreateInvoiceModal()" />
<QTooltip>
{{ t('createInvoice') }}
</QTooltip>
</QPageSticky>
<QDialog
ref="manualInvoiceDialogRef"
transition-show="scale"
transition-hide="scale"
>
<CreateManualInvoiceForm />
</QDialog>
</QPage>
</template> </template>
<i18n> <i18n>

View File

@ -1,200 +1,177 @@
<script setup> <script setup>
import { computed, onBeforeMount } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import InvoiceOutNegativeFilter from './InvoiceOutNegativeBasesFilter.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import VnTable from 'src/components/VnTable/VnTable.vue';
import { useInvoiceOutGlobalStore } from 'src/stores/invoiceOutGlobal.js'; import { useInvoiceOutGlobalStore } from 'src/stores/invoiceOutGlobal.js';
import { useStateStore } from 'stores/useStateStore'; import { useArrayData } from 'src/composables/useArrayData';
import { useArrayData } from 'composables/useArrayData';
import RightMenu from 'src/components/common/RightMenu.vue';
const invoiceOutGlobalStore = useInvoiceOutGlobalStore();
const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const tableRef = ref();
const arrayData = useArrayData('InvoiceOutNegative', { const invoiceOutGlobalStore = useInvoiceOutGlobalStore();
url: 'InvoiceOuts/negativeBases', const userParams = {
limit: 0, from: Date.vnFirstDayOfMonth().toISOString(),
userParams: { to: Date.vnLastDayOfMonth().toISOString(),
from: Date.vnFirstDayOfMonth().toISOString(), };
to: Date.vnLastDayOfMonth().toISOString(), useArrayData('InvoiceOutNegative', { userParams });
},
exprBuilder: (param, value) => {
switch (param) {
case 'from':
case 'to':
return;
default:
return { [param]: value };
}
},
});
onBeforeMount(async () => {
await arrayData.fetch({ append: false });
stateStore.rightDrawer = true;
});
const columns = computed(() => [ const columns = computed(() => [
{ {
label: t('invoiceOut.negativeBases.company'), align: 'left',
field: 'company',
name: 'company', name: 'company',
align: 'left', label: t('invoiceOutModule.company'),
component: 'select',
cardVisible: true,
attrs: {
url: 'Companies',
optionLabel: 'code',
optionValue: 'id',
},
columnField: {
component: null,
},
}, },
{ {
label: t('invoiceOut.negativeBases.country'), align: 'left',
field: 'country',
name: 'country', name: 'country',
align: 'left', label: t('negativeBases.country'),
component: 'select',
attrs: {
url: 'Countries',
optionLabel: 'name',
optionValue: 'id',
},
columnField: {
component: null,
},
}, },
{ {
label: t('invoiceOut.negativeBases.clientId'), align: 'left',
field: 'clientId',
name: 'clientId', name: 'clientId',
align: 'left', label: t('negativeBases.clientId'),
cardVisible: true,
}, },
{ {
label: t('invoiceOut.negativeBases.client'),
field: 'clientSocialName',
name: 'client',
align: 'left', align: 'left',
name: 'clientSocialName',
label: t('invoiceOutModule.customer'),
component: 'select',
isTitle: true,
cardVisible: true,
attrs: {
url: 'Clients',
fields: ['id', 'name'],
},
columnField: {
component: null,
},
}, },
{ {
label: t('invoiceOut.negativeBases.amount'), align: 'left',
field: 'amount',
name: 'amount', name: 'amount',
align: 'left', label: t('invoiceOutModule.amount'),
format: (value) => toCurrency(value), format: (row) => toCurrency(row.amount),
cardVisible: true,
}, },
{ {
label: t('invoiceOut.negativeBases.base'),
field: 'taxableBase',
name: 'base',
align: 'left', align: 'left',
name: 'taxableBase',
label: t('negativeBases.base'),
}, },
{ {
label: t('invoiceOut.negativeBases.ticketId'),
field: 'ticketFk',
name: 'ticketId',
align: 'left', align: 'left',
name: 'ticketFk',
label: t('negativeBases.ticketId'),
cardVisible: true,
}, },
{ {
label: t('invoiceOut.negativeBases.active'),
field: 'isActive',
name: 'active',
align: 'left', align: 'left',
name: 'isActive',
label: t('negativeBases.active'),
}, },
{ {
label: t('invoiceOut.negativeBases.hasToInvoice'), align: 'left',
field: 'hasToInvoice',
name: 'hasToInvoice', name: 'hasToInvoice',
align: 'left', label: t('negativeBases.hasToInvoice'),
}, },
{ {
label: t('invoiceOut.negativeBases.verifiedData'),
field: 'isTaxDataChecked',
name: 'verifiedData',
align: 'left', align: 'left',
name: 'hasVerifiedData',
label: t('negativeBases.verifiedData'),
}, },
{ {
label: t('invoiceOut.negativeBases.comercial'),
field: 'workerName',
name: 'worker',
align: 'left', align: 'left',
label: t('customer.extendedList.tableVisibleColumns.salesPersonFk'),
name: 'workerName',
component: 'select',
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'],
where: { role: 'salesPerson' },
},
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.workerName),
}, },
]); ]);
const downloadCSV = async () => { const downloadCSV = async () => {
const params = {}; // filter.value;
const filterParams = { const filterParams = {
limit: 20, limit: 20,
where: { where: {
and: [], and: [],
}, },
}; };
for (const param in params) {
if (params[param]) filterParams.where.and.push({ [param]: params[param] }); for (const [key, value] of Object.entries(userParams)) {
if (value) {
filterParams.where.and.push({ [key]: value });
}
} }
await invoiceOutGlobalStore.getNegativeBasesCsv( await invoiceOutGlobalStore.getNegativeBasesCsv(
arrayData.value.store.userParams.from, userParams.from,
arrayData.value.store.userParams.to, userParams.to,
params filterParams
); );
}; };
</script> </script>
<template> <template>
<VnSubToolbar> <VnSubToolbar>
<template #st-actions> <template #st-actions>
<QBtn color="primary" icon-right="download" no-caps @click="downloadCSV()"> <QBtn color="primary" icon-right="download" @click="downloadCSV()">
<QTooltip>{{ t('Download as CSV') }}</QTooltip> <QTooltip>{{ t('Download as CSV') }}</QTooltip>
</QBtn> </QBtn>
</template> </template>
</VnSubToolbar> </VnSubToolbar>
<RightMenu> <VnTable
<template #right-panel> ref="tableRef"
<InvoiceOutNegativeFilter data-key="InvoiceOutNegative" /> data-key="negativeFilter"
</template> url="InvoiceOuts/negativeBases"
</RightMenu> :user-params="userParams"
<QPage class="column items-center q-pa-md"> :expr-builder="
<QTable (param, value) => {
:columns="columns" switch (param) {
:rows="arrayData.store.data" case 'from':
row-key="clientId" case 'to':
class="full-width q-mt-md" return;
> default:
<template #body-cell-clientId="{ row }"> return { [param]: value };
<QTd> }
<QBtn flat dense class="link"> {{ row.clientId }}</QBtn> }
<CustomerDescriptorProxy :id="row.clientId" /> "
</QTd> :limit="0"
</template> :columns="columns"
<template #body-cell-ticketId="{ row }"> default-mode="table"
<QTd> auto-load
<QBtn flat dense class="link"> {{ row.ticketFk }}</QBtn> :is-editable="false"
<TicketDescriptorProxy :id="row.ticketFk" /> :use-model="true"
</QTd> >
</template> </VnTable>
<template #body-cell-worker="{ row }">
<QTd>
<QBtn class="no-uppercase link" flat dense>{{ row.workerName }}</QBtn>
<WorkerDescriptorProxy :id="row.comercialId" />
</QTd>
</template>
<template #body-cell-active="{ row }">
<QTd>
<QCheckbox :model-value="!!row.isActive" disable />
</QTd>
</template>
<template #body-cell-hasToInvoice="{ row }">
<QTd>
<QCheckbox :model-value="!!row.hasToInvoice" disable />
</QTd>
</template>
<template #body-cell-verifiedData="{ row }">
<QTd>
<QCheckbox :model-value="!!row.isTaxDataChecked" disable />
</QTd>
</template>
</QTable>
</QPage>
</template> </template>
<style lang="scss" scoped>
.col-content {
border-radius: 4px;
padding: 6px;
}
.no-uppercase {
text-transform: none;
}
</style>
<i18n> <i18n>
es: es:
Download as CSV: Descargar como CSV Download as CSV: Descargar como CSV

View File

@ -0,0 +1,25 @@
invoiceOutModule:
customer: Client
amount: Amount
company: Company
invoiceOutList:
tableVisibleColumns:
id: ID
ref: Reference
issued: Issued
created: Created
dueDate: Max date
invoiceOutSerial: Serial
ticket: Ticket
taxArea: Tax area
DownloadPdf: Download PDF
InvoiceOutSummary: Summary
negativeBases:
country: Country
clientId: Client ID
base: Base
ticketId: Ticket
active: Active
hasToInvoice: Has to invoice
verifiedData: Verified data
commercial: Commercial

View File

@ -1,2 +1,31 @@
Search invoice: Buscar factura emitida Search invoice: Buscar factura emitida
You can search by invoice reference: Puedes buscar por referencia de la factura You can search by invoice reference: Puedes buscar por referencia de la factura
invoiceOutModule:
customer: Cliente
amount: Importe
company: Empresa
invoiceOutList:
tableVisibleColumns:
id: ID
ref: Referencia
issued: Fecha emisión
customer: Cliente
company: Empresa
amount: Importe
created: F. creación
dueDate: F. máxima
invoiceOutSerial: Serial
ticket: Ticket
taxArea: Area
DownloadPdf: Descargar PDF
InvoiceOutSummary: Resumen
negativeBases:
country: País
clientId: ID del cliente
client: Cliente
base: Base
ticketId: Ticket
active: Activo
hasToInvoice: Debe facturar
verifiedData: Datos verificados
commercial: Comercial

View File

@ -4,7 +4,7 @@ import { useRoute } from 'vue-router';
import { onMounted, onUnmounted, ref } from 'vue'; import { onMounted, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnPaginate from 'components/ui/VnPaginate.vue'; import VnPaginate from 'components/ui/VnPaginate.vue';
import OrderCatalogItem from 'pages/Order/Card/OrderCatalogItem.vue'; import CatalogItem from 'components/ui/CatalogItem.vue';
import OrderCatalogFilter from 'pages/Order/Card/OrderCatalogFilter.vue'; import OrderCatalogFilter from 'pages/Order/Card/OrderCatalogFilter.vue';
const route = useRoute(); const route = useRoute();
@ -77,7 +77,12 @@ function extractValueTags(items) {
<div v-if="rows && !rows?.length" class="no-result"> <div v-if="rows && !rows?.length" class="no-result">
{{ t('globals.noResults') }} {{ t('globals.noResults') }}
</div> </div>
<OrderCatalogItem v-for="row in rows" :key="row.id" :item="row" /> <CatalogItem
v-for="row in rows"
:key="row.id"
:item="row"
is-catalog
/>
</div> </div>
</template> </template>
</VnPaginate> </VnPaginate>

View File

@ -0,0 +1,78 @@
<script setup>
import { reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnInput from 'src/components/common/VnInput.vue';
import VnRow from 'components/ui/VnRow.vue';
import FormModelPopup from 'components/FormModelPopup.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const $props = defineProps({
ticket: {
type: Boolean,
default: false,
},
withRoute: {
type: Boolean,
default: false,
},
selectedExpeditions: {
type: Array,
default: () => [],
},
});
const { t } = useI18n();
const router = useRouter();
const { notify } = useNotify();
const newTicketFormData = reactive({});
const createTicket = async () => {
try {
const expeditionIds = $props.selectedExpeditions.map(
(expedition) => expedition.id
);
const params = {
clientId: $props.ticket.clientFk,
landed: newTicketFormData.landed,
warehouseId: $props.ticket.warehouseFk,
addressId: $props.ticket.addressFk,
agencyModeId: $props.ticket.agencyModeFk,
routeId: newTicketFormData.routeFk,
expeditionIds: expeditionIds,
};
const { data } = await axios.post('Expeditions/moveExpeditions', params);
notify(t('globals.dataSaved'), 'positive');
router.push({ name: 'TicketSummary', params: { id: data.id } });
} catch (error) {
console.error(error);
}
};
</script>
<template>
<FormModelPopup
model="expeditionNewTicket"
:form-initial-data="newTicketFormData"
:save-fn="createTicket"
>
<template #form-inputs="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<VnInputDate :label="t('expedition.landed')" v-model="data.landed" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnInput
v-if="withRoute"
:label="t('expedition.routeId')"
v-model="data.routeFk"
/>
</VnRow>
</template>
</FormModelPopup>
</template>

View File

@ -0,0 +1,354 @@
<script setup>
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import FetchedTags from 'components/ui/FetchedTags.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import FetchData from 'components/FetchData.vue';
import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue';
import { useStateStore } from 'stores/useStateStore';
import { dashIfEmpty } from 'src/filters';
import { useArrayData } from 'composables/useArrayData';
import { toCurrency } from 'filters/index';
import axios from 'axios';
const route = useRoute();
const stateStore = useStateStore();
const { t } = useI18n();
const salesRef = ref(null);
const arrayData = useArrayData('ticketData');
const { store } = arrayData;
const ticketData = computed(() => store.data);
const components = ref([]);
const componentsList = ref([]);
const theoricalCost = ref(0);
const ticketVolume = ref(null);
watch(
() => ticketData.value,
async () => {
await nextTick();
salesRef.value.fetch();
getComponentsSum();
getTheoricalCost();
if (ticketData.value?.zone && ticketData.value?.zone?.isVolumetric)
getTicketVolume();
},
{ immediate: true }
);
const salesFilter = computed(() => ({
order: 'concept ASC',
include: [
{
relation: 'item',
},
{
relation: 'components',
scope: {
fields: ['componentFk', 'value'],
include: {
relation: 'component',
scope: {
fields: ['typeFk', 'name'],
include: {
relation: 'componentType',
scope: {
fields: ['type', 'isBase', 'name'],
},
},
},
},
},
},
],
where: { ticketFk: route.params.id },
}));
const columns = computed(() => [
{
label: t('components.item'),
name: 'item',
align: 'left',
},
{
label: t('components.description'),
name: 'description',
align: 'left',
},
{
label: t('components.quantity'),
name: 'quantity',
field: 'quantity',
align: 'left',
format: (val) => dashIfEmpty(val),
},
{
label: t('components.serie'),
name: 'serie',
align: 'left',
},
{
label: t('components.components'),
name: 'components',
align: 'left',
},
{
label: t('components.import'),
name: 'import',
align: 'left',
},
{
label: t('components.total'),
name: 'total',
align: 'left',
},
]);
const getBase = computed(() => {
let sum = 0;
for (let sale of components.value) {
for (let saleComponent of sale.components) {
if (saleComponent.component.componentType.isBase) {
sum += sale.quantity * saleComponent.value;
}
}
}
return sum;
});
const getTotal = computed(() => {
let total = 0;
for (let sale of components.value) {
for (let saleComponent of sale.components) {
total += sale.quantity * saleComponent.value;
}
}
return total;
});
const getComponentsSum = async () => {
try {
const { data } = await axios.get(`Tickets/${route.params.id}/getComponentsSum`);
componentsList.value = data;
} catch (error) {
console.error(error);
}
};
const getTheoricalCost = async () => {
try {
const { data } = await axios.get(`Tickets/${route.params.id}/freightCost`);
theoricalCost.value = data;
} catch (error) {
console.error(error);
}
};
const getTicketVolume = async () => {
try {
if (!ticketData.value) return;
const { data } = await axios.get(`Tickets/${ticketData.value.id}/getVolume`);
ticketVolume.value = data[0].volume;
} catch (error) {
console.error(error);
}
};
onMounted(() => {
stateStore.rightDrawer = true;
});
onUnmounted(() => (stateStore.rightDrawer = false));
</script>
<template>
<FetchData
ref="salesRef"
url="Sales"
:filter="salesFilter"
@on-fetch="(data) => (components = data)"
auto-load
/>
<RightMenu>
<template #right-panel>
<QCard
class="q-pa-sm color-vn-text"
bordered
flat
style="border-color: black"
>
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('components.total') }}
</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label"
>{{ t('components.baseToCommission') }}:
</span>
<span>{{ toCurrency(getBase) }}</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label"
>{{ t('components.totalWithoutVat') }}:
</span>
<span>{{ toCurrency(getTotal) }}</span>
</QCardSection>
</QCard>
<QCard
class="q-pa-sm color-vn-text"
bordered
flat
style="border-color: black"
>
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('components.components') }}
</span>
</QCardSection>
<QCardSection
v-for="(component, index) in componentsList"
:key="index"
horizontal
>
<span v-if="component.name" class="q-mr-xs color-vn-label">
{{ component.name }}:
</span>
<span v-if="component.value">{{
toCurrency(component.value, 'EUR', 3)
}}</span>
</QCardSection>
</QCard>
<QCard
class="q-pa-sm color-vn-text"
bordered
flat
style="border-color: black"
>
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('components.zoneBreakdown') }}
</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('components.price') }}:
</span>
<span>{{ toCurrency(ticketData?.zonePrice, 'EUR', 2) }}</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('components.bonus') }}:
</span>
<span>{{ toCurrency(ticketData?.zoneBonus, 'EUR', 2) }}</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('components.zone') }}:
</span>
<span class="link">
{{ dashIfEmpty(ticketData?.zone?.name) }}
<ZoneDescriptorProxy :id="ticketData?.zone?.id" />
</span>
</QCardSection>
<QCardSection v-if="ticketData?.zone?.isVolumetric" horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('components.volume') }}:
</span>
<span>{{ ticketVolume }}</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('components.packages') }}:
</span>
<span>{{ dashIfEmpty(ticketData?.packages) }}</span>
</QCardSection>
</QCard>
<QCard
class="q-pa-sm color-vn-text"
bordered
flat
style="border-color: black"
>
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('components.theoricalCost') }}
</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('components.totalPrice') }}:
</span>
<span>{{ toCurrency(theoricalCost, 'EUR', 2) }}</span>
</QCardSection>
</QCard>
</template>
</RightMenu>
<QTable
:rows="components"
:columns="columns"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:no-data-label="t('globals.noResults')"
>
<template #body-cell-item="{ row }">
<QTd>
<QBtn flat color="primary">
<span class="link">{{ row.itemFk }}</span>
<ItemDescriptorProxy :id="row.itemFk" />
</QBtn>
</QTd>
</template>
<template #body-cell-description="{ row }">
<QTd>
<div class="column">
<span>{{ row.item.name }}</span>
<span class="color-vn-label">{{ row.item.subName }}</span>
<FetchedTags :item="row.item" :max-length="6" />
</div>
</QTd>
</template>
<template #body-cell-serie="{ row }">
<QTd>
<div class="column">
<span v-for="(saleComponent, index) in row.components" :key="index">
{{ saleComponent.component?.componentType?.name }}
</span>
</div>
</QTd>
</template>
<template #body-cell-components="{ row }">
<QTd>
<div class="column">
<span v-for="(saleComponent, index) in row.components" :key="index">
{{ saleComponent.component?.name }}
</span>
</div>
</QTd>
</template>
<template #body-cell-import="{ row }">
<QTd>
<div class="column text-right">
<span v-for="(saleComponent, index) in row.components" :key="index">
{{ toCurrency(saleComponent.value, 'EUR', 3) }}
</span>
</div>
</QTd>
</template>
<template #body-cell-total="{ row }">
<QTd>
<div class="column text-right">
<span v-for="(saleComponent, index) in row.components" :key="index">
{{ toCurrency(saleComponent.value * row.quantity, 'EUR', 3) }}
</span>
</div>
</QTd>
</template>
</QTable>
</template>

View File

@ -66,6 +66,23 @@ const filter = {
fields: ['id', 'name'], fields: ['id', 'name'],
}, },
}, },
{
relation: 'zone',
scope: {
fields: [
'agencyModeFk',
'bonus',
'hour',
'id',
'isVolumetric',
'itemMaxSize',
'm3Max',
'name',
'price',
'travelingDays',
],
},
},
], ],
}; };

View File

@ -0,0 +1,474 @@
<script setup>
import { onMounted, ref, computed, onUnmounted, reactive, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import TicketEditManaProxy from './TicketEditMana.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import ExpeditionNewTicket from './ExpeditionNewTicket.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency, toPercentage } from 'src/filters';
import { useArrayData } from 'composables/useArrayData';
import { useVnConfirm } from 'composables/useVnConfirm';
import useNotify from 'src/composables/useNotify.js';
import { toDateTimeFormat } from 'src/filters/date';
import axios from 'axios';
const route = useRoute();
const stateStore = useStateStore();
const { t } = useI18n();
const { notify } = useNotify();
const { openConfirmationModal } = useVnConfirm();
const editPriceProxyRef = ref(null);
const newTicketDialogRef = ref(null);
const logsTableDialogRef = ref(null);
const expeditionsLogsData = ref([]);
const selectedExpeditions = ref([]);
const allColumnNames = ref([]);
const visibleColumns = ref([]);
const newTicketWithRoute = ref(false);
const itemsOptions = ref([]);
const exprBuilder = (param, value) => {
switch (param) {
case 'expeditionFk':
return { id: value };
case 'packageItemName':
return { packagingItemFk: value };
}
};
const expeditionsFilter = computed(() => ({
where: { ticketFk: route.params.id },
order: ['created DESC'],
}));
const expeditionsArrayData = useArrayData('ticketExpeditions', {
url: 'Expeditions/filter',
filter: expeditionsFilter.value,
exprBuilder: exprBuilder,
});
const expeditionsStore = expeditionsArrayData.store;
const ticketExpeditions = computed(() => expeditionsStore.data);
const ticketArrayData = useArrayData('ticketData');
const ticketStore = ticketArrayData.store;
const ticketData = computed(() => ticketStore.data);
const refetchExpeditions = async () => {
await expeditionsArrayData.applyFilter({
params: { filter: JSON.stringify(expeditionsFilter.value) },
});
};
watch(
() => route.params.id,
async () => await refetchExpeditions(),
{ immediate: true }
);
const params = reactive({});
const applyColumnFilter = async (col) => {
try {
const paramKey = col.columnFilter?.filterParamKey || col.field;
params[paramKey] = col.columnFilter.filterValue;
await expeditionsArrayData.addFilter({ params });
} catch (err) {
console.error('Error applying column filter', err);
}
};
const getInputEvents = (col) => {
return col.columnFilter.type === 'select'
? { 'update:modelValue': () => applyColumnFilter(col) }
: {
'keyup.enter': () => applyColumnFilter(col),
};
};
const columns = computed(() => [
{
label: t('expedition.id'),
name: 'id',
field: 'id',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterParamKey: 'expeditionFk',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('expedition.item'),
name: 'item',
align: 'left',
columnFilter: {
component: VnInput,
type: 'text',
filterParamKey: 'packageItemName',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('expedition.name'),
name: 'name',
field: 'packageItemName',
align: 'left',
columnFilter: {
component: VnSelect,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemsOptions.value,
'option-value': 'id',
'option-label': 'name',
dense: true,
},
},
},
{
label: t('expedition.packageType'),
name: 'packageType',
field: 'freightItemName',
align: 'left',
columnFilter: {
component: VnInput,
type: 'text',
// filterParamKey: 'expeditionFk',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('expedition.counter'),
name: 'counter',
field: 'counter',
align: 'left',
columnFilter: null,
},
{
label: t('expedition.externalId'),
name: 'externalId',
field: 'externalId',
align: 'left',
columnFilter: null,
},
{
label: t('expedition.created'),
name: 'created',
field: 'created',
align: 'left',
columnFilter: null,
format: (value) => toDateTimeFormat(value),
},
{
label: t('expedition.state'),
name: 'state',
field: 'state',
align: 'left',
columnFilter: null,
},
{
label: '',
name: 'history',
align: 'left',
columnFilter: null,
},
]);
const logTableColumns = computed(() => [
{
label: t('expedition.state'),
name: 'state',
field: 'state',
align: 'left',
sortable: true,
},
{
label: t('expedition.name'),
name: 'name',
align: 'name',
columnFilter: null,
},
{
label: t('expedition.created'),
name: 'created',
field: 'created',
align: 'left',
columnFilter: null,
format: (value) => toDateTimeFormat(value),
},
]);
const showNewTicketDialog = (withRoute = false) => {
newTicketWithRoute.value = withRoute;
newTicketDialogRef.value.show();
};
const deleteExpedition = async () => {
try {
const expeditionIds = selectedExpeditions.value.map(
(expedition) => expedition.id
);
const params = { expeditionIds };
await axios.post('Expeditions/deleteExpeditions', params);
await refetchExpeditions();
selectedExpeditions.value = [];
notify(t('expedition.expeditionRemoved'), 'positive');
} catch (error) {
console.error(error);
}
};
const showLog = async (expedition) => {
await getExpeditionState(expedition);
logsTableDialogRef.value.show();
};
const getExpeditionState = async (expedition) => {
try {
const filter = {
where: { expeditionFk: expedition.id },
order: ['created DESC'],
};
const { data } = await axios.get(`ExpeditionStates/filter`, {
params: { filter: JSON.stringify(filter) },
});
expeditionsLogsData.value = data;
} catch (error) {
console.error(error);
}
};
onMounted(async () => {
stateStore.rightDrawer = true;
const filteredColumns = columns.value.filter((col) => col.name !== 'history');
allColumnNames.value = filteredColumns.map((col) => col.name);
// await expeditionsArrayData.fetch({ append: false });
});
onUnmounted(() => (stateStore.rightDrawer = false));
</script>
<template>
<FetchData
url="Items"
auto-load
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
@on-fetch="(data) => (itemsOptions = data)"
/>
<VnSubToolbar>
<template #st-data>
<TableVisibleColumns
:all-columns="allColumnNames"
table-code="expeditionIndex"
labels-traductions-path="expedition"
@on-config-saved="visibleColumns = [...$event, 'history']"
/>
</template>
<template #st-actions>
<QBtnGroup push class="q-gutter-x-sm" flat>
<QBtnDropdown
ref="btnDropdownRef"
color="primary"
:label="t('expedition.move')"
:disable="!selectedExpeditions.length"
>
<template #label>
<QTooltip>{{ t('Select lines to see the options') }}</QTooltip>
</template>
<QList>
<QItem
clickable
v-close-popup
v-ripple
@click="showNewTicketDialog(false)"
>
<QItemSection>
<QItemLabel>{{
t('expedition.newTicketWithoutRoute')
}}</QItemLabel>
</QItemSection>
</QItem>
<QItem
clickable
v-close-popup
v-ripple
@click="showNewTicketDialog(true)"
>
<QItemSection>
<QItemLabel>{{
t('expedition.newTicketWithRoute')
}}</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QBtnDropdown>
<QBtn
:disable="!selectedExpeditions.length"
icon="delete"
color="primary"
@click="
openConfirmationModal(
'',
t('expedition.removeExpeditionSubtitle'),
deleteExpedition
)
"
/>
</QBtnGroup>
</template>
</VnSubToolbar>
<QTable
:rows="ticketExpeditions"
:columns="columns"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
selection="multiple"
v-model:selected="selectedExpeditions"
:visible-columns="visibleColumns"
:no-data-label="t('globals.noResults')"
>
<template #top-row="{ cols }">
<QTr>
<QTd />
<QTd v-for="(col, index) in cols" :key="index" style="max-width: 100px">
<component
:is="col.columnFilter.component"
v-if="col.columnFilter"
v-model="col.columnFilter.filterValue"
v-bind="col.columnFilter.attrs"
v-on="col.columnFilter.event(col)"
dense
/>
</QTd>
</QTr>
</template>
<template #body-cell-item="{ row }">
<QTd auto-width @click.stop>
<QBtn flat color="primary">{{ row.packagingItemFk }}</QBtn>
<ItemDescriptorProxy :id="row.packagingItemFk" />
</QTd>
</template>
<template #body-cell-available="{ row }">
<QTd @click.stop>
<QBadge :color="row.available < 0 ? 'alert' : 'transparent'" dense>
{{ row.available }}
</QBadge>
</QTd>
</template>
<template #body-cell-price="{ row }">
<QTd>
<template v-if="isTicketEditable && row.id">
<QBtn flat color="primary" dense @click="onOpenEditPricePopover(row)">
{{ toCurrency(row.price) }}
</QBtn>
<TicketEditManaProxy
ref="editPriceProxyRef"
:mana="mana"
:new-price="getNewPrice"
@save="updatePrice(row)"
>
<VnInput
v-model.number="edit.price"
:label="t('ticketSale.price')"
type="number"
/>
</TicketEditManaProxy>
</template>
<span v-else>{{ toCurrency(row.price) }}</span>
</QTd>
</template>
<template #body-cell-discount="{ row }">
<QTd>
<template v-if="!isLocked && row.id">
<QBtn
flat
color="primary"
dense
@click="onOpenEditDiscountPopover(row)"
>
{{ toPercentage(row.discount / 100) }}
</QBtn>
<TicketEditManaProxy
:mana="mana"
:new-price="getNewPrice"
@save="changeDiscount(row)"
>
<VnInput
v-model.number="edit.discount"
:label="t('ticketSale.discount')"
type="number"
/>
</TicketEditManaProxy>
</template>
<span v-else>{{ toPercentage(row.discount / 100) }}</span>
</QTd>
</template>
<template #body-cell-history="{ row }">
<QTd>
<QBtn
@click.stop="showLog(row)"
color="primary"
icon="history"
size="md"
flat
>
<QTooltip class="text-no-wrap">
{{ t('expedition.historyAction') }}
</QTooltip>
</QBtn>
</QTd>
</template>
</QTable>
<QDialog ref="newTicketDialogRef" transition-show="scale" transition-hide="scale">
<ExpeditionNewTicket
:ticket="ticketData"
:with-route="newTicketWithRoute"
:selected-expeditions="selectedExpeditions"
/>
</QDialog>
<QDialog ref="logsTableDialogRef" transition-show="scale" transition-hide="scale">
<QTable
ref="tableRef"
data-key="TicketExpeditionLog"
:rows="expeditionsLogsData"
:columns="logTableColumns"
class="q-pa-sm"
>
<template #body-cell-name="{ row }">
<QTd auto-width>
<QBtn flat dense color="primary">{{ row.name }}</QBtn>
<WorkerDescriptorProxy :id="row.workerFk" />
</QTd>
</template>
</QTable>
</QDialog>
</template>

View File

@ -0,0 +1,132 @@
<script setup>
import { ref, watch, computed, reactive } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CrudModel from 'components/CrudModel.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import { useArrayData } from 'src/composables/useArrayData';
const route = useRoute();
const { t } = useI18n();
const ticketPackagingsCrudRef = ref(null);
const listPackagingsOptions = ref([]);
const arrayData = useArrayData('TicketPackagings');
const { store } = arrayData;
const crudModelFilter = reactive({
fields: ['id', 'ticketFk', 'packagingFk', 'quantity', 'created'],
where: { ticketFk: route.params.id },
});
const crudModelRequiredData = computed(() => ({
packagingFk: null,
quantity: 0,
created: Date.vnNew(),
ticketFk: route.params.id,
}));
const handleInputQuantityClear = (row) => {
if (row.quantity === null || row.quantity === undefined) row.quantity = 0;
};
watch(
() => route.params.id,
async () => {
crudModelFilter.where.ticketFk = route.params.id;
store.filter = crudModelFilter;
await ticketPackagingsCrudRef.value.reload();
}
);
</script>
<template>
<FetchData
@on-fetch="(data) => (listPackagingsOptions = data)"
auto-load
:filter="{ fields: ['packagingFk', 'name'], order: 'name ASC', limit: 30 }"
url="Packagings/listPackaging"
/>
<div class="flex justify-center">
<CrudModel
ref="ticketPackagingsCrudRef"
data-key="TicketPackagings"
url="TicketPackagings"
model="TicketPackagings"
:filter="crudModelFilter"
:data-required="crudModelRequiredData"
:default-remove="false"
auto-load
style="max-width: 800px"
>
<template #body="{ rows, validate }">
<QCard class="q-px-lg q-py-md">
<div
v-for="(row, index) in rows"
:key="index"
class="q-mb-md row items-center q-gutter-x-md"
>
<VnSelect
:label="t('package.package')"
:options="listPackagingsOptions"
hide-selected
option-label="name"
option-value="packagingFk"
v-model="row.packagingFk"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.name }}
</QItemLabel>
<QItemLabel caption>
#{{ scope.opt?.itemFk }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnInput
:label="t('package.quantity')"
v-model.number="row.quantity"
class="col"
type="number"
min="1"
:required="true"
@update:model-value="handleInputQuantityClear(row)"
:rules="validate('TicketPackaging.quantity')"
/>
<VnInputDate :label="t('package.added')" v-model="row.created" />
<QIcon
name="delete"
size="sm"
class="cursor-pointer"
color="primary"
@click="ticketPackagingsCrudRef.remove([row])"
>
<QTooltip>
{{ t('package.removePackage') }}
</QTooltip>
</QIcon>
</div>
<QIcon
name="add_circle"
class="fill-icon-on-hover q-ml-md"
size="sm"
color="primary"
@click="ticketPackagingsCrudRef.insert()"
>
<QTooltip>
{{ t('package.addPackage') }}
</QTooltip>
</QIcon>
</QCard>
</template>
</CrudModel>
</div>
</template>

View File

@ -0,0 +1,60 @@
<script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import CatalogItem from 'components/ui/CatalogItem.vue';
import { toCurrency } from 'filters/index';
const { t } = useI18n();
const route = useRoute();
const salesFilter = {
include: {
relation: 'item',
scope: {
field: ['name', 'image'],
},
},
where: { ticketFk: route.params.id },
};
const sales = ref([]);
</script>
<template>
<FetchData
ref="salesRef"
url="sales"
:filter="salesFilter"
@on-fetch="(data) => (sales = data)"
auto-load
/>
<div class="pictures-list">
<CatalogItem v-for="(sale, index) in sales" :key="index" :item="sale.item">
<template #price>
<div class="q-mt-md full-width row justify-between items-center">
<span class="text-h6">{{ sale.quantity }}</span>
<span>{{ t('by') }}</span>
<span class="text-h6">{{ toCurrency(sale?.price) }}</span>
</div>
</template>
</CatalogItem>
</div>
</template>
<style lang="scss" scoped>
.pictures-list {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
justify-content: center;
gap: 16px;
}
</style>
<i18n>
es:
by: por
</i18n>

View File

@ -774,6 +774,5 @@ es:
Continue anyway?: ¿Continuar de todas formas? Continue anyway?: ¿Continuar de todas formas?
You are going to delete lines of the ticket: Vas a eliminar lineas del ticket You are going to delete lines of the ticket: Vas a eliminar lineas del ticket
Add item: Añadir artículo Add item: Añadir artículo
Select lines to see the options: Selecciona líneas para ver las opciones
Transfer lines: Transferir líneas Transfer lines: Transferir líneas
</i18n> </i18n>

View File

@ -77,7 +77,6 @@ const getDefaultTaxClass = async () => {
params: { filter: JSON.stringify(filter) }, params: { filter: JSON.stringify(filter) },
}); });
defaultTaxClass.value = data; defaultTaxClass.value = data;
console.log('defaultTaxClass', defaultTaxClass.value);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }

View File

@ -6,7 +6,6 @@ import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import toDateString from 'filters/toDateString';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
const { t } = useI18n(); const { t } = useI18n();

View File

@ -4,7 +4,6 @@ import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import VnPaginate from 'src/components/ui/VnPaginate.vue'; import VnPaginate from 'src/components/ui/VnPaginate.vue';
import { toDate, toDateString, toCurrency } from 'src/filters/index';
import TicketSummary from './Card/TicketSummary.vue'; import TicketSummary from './Card/TicketSummary.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import TicketFilter from './TicketFilter.vue'; import TicketFilter from './TicketFilter.vue';

View File

@ -93,6 +93,25 @@ futureTickets:
moveTicketSuccess: Tickets moved successfully! moveTicketSuccess: Tickets moved successfully!
searchInfo: Search future tickets by date searchInfo: Search future tickets by date
futureTicket: Future tickets futureTicket: Future tickets
expedition:
id: Expedition
item: Item
name: Name
packageType: Package type
counter: Counter
externalId: externalId
created: Created
state: State
historyAction: Status log
newTicketWithRoute: New ticket with route
newTicketWithoutRoute: New ticket without route
landed: Landed
routeId: Route id
deleteExpedition: Delete expedition
expeditionRemoved: Expedition removed
removeExpeditionSubtitle: Are you sure you want to delete this expedition?
worker: Worker
move: Move
basicData: basicData:
next: Next next: Next
back: Back back: Back
@ -159,8 +178,32 @@ service:
addService: Add service addService: Add service
quantityInfo: To create services with negative amounts mark the service on the source ticket and press the pay button. quantityInfo: To create services with negative amounts mark the service on the source ticket and press the pay button.
createRefundSuccess: 'The following refund ticket have been created: { ticketId }' createRefundSuccess: 'The following refund ticket have been created: { ticketId }'
components:
item: Item
description: Description
quantity: Quantity
serie: Serie
components: Components
import: Import
total: Total
baseToCommission: Base to commission
totalWithoutVat: Total without VAT
zoneBreakdown: Zone breakdown
price: Price
bonus: Bonus
zone: Zone
volume: Volume
theoricalCost: Theorical cost
totalPrice: Total price
packages: Packages
tracking: tracking:
state: State state: State
worker: Worker worker: Worker
created: Created created: Created
addState: Add state addState: Add state
package:
package: Package
quantity: Quantity
added: Added
addPackage: Add package
removePackage: Remove package

View File

@ -164,5 +164,49 @@ ticketSale:
shipped: F. Envío shipped: F. Envío
agency: Agencia agency: Agencia
address: Consignatario address: Consignatario
components:
item: Artículo
description: Descripción
quantity: Cantidad
serie: Serie
components: Componentes
import: Importe
total: Total
baseToCommission: Base comisionable
totalWithoutVat: Total sin IVA
zoneBreakdown: Desglose zona
price: Precio
bonus: Bonificación
zone: Zona
volume: Volúmen
theoricalCost: Porte teórico
totalPrice: Precio total
packages: Bultos
expedition:
id: Expedición
item: Artículo
name: Nombre
packageType: Package type
counter: Contador
externalId: externalId
created: Fecha creación
state: Estado
historyAction: Historial de estados
newTicketWithRoute: Nuevo ticket con ruta
newTicketWithoutRoute: Nuevo ticket sin ruta
landed: F. entrega
routeId: Id ruta
deleteExpedition: Eliminar expedición
expeditionRemoved: Expedición eliminada
removeExpeditionSubtitle: ¿Está seguro de eliminar esta expedición?
worker: Trabajador
move: Mover
package:
package: Embalaje
quantity: Cantidad
added: Añadido
addPackage: Añadir embalaje
removePackage: Quitar embalaje
Search ticket: Buscar tickets Search ticket: Buscar tickets
You can search by ticket id or alias: Puedes buscar por id o alias del ticket You can search by ticket id or alias: Puedes buscar por id o alias del ticket
Select lines to see the options: Selecciona líneas para ver las opciones

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, computed, onUpdated } from 'vue'; import { ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';

View File

@ -71,6 +71,7 @@ const columns = computed(() => [
url: 'agencyModes', url: 'agencyModes',
fields: ['id', 'name'], fields: ['id', 'name'],
}, },
format: (row) => row.agencyModeName,
columnField: { columnField: {
component: null, component: null,
}, },
@ -112,7 +113,10 @@ const columns = computed(() => [
attrs: { attrs: {
url: 'warehouses', url: 'warehouses',
fields: ['id', 'name'], fields: ['id', 'name'],
optionLabel: 'name',
optionValue: 'id',
}, },
format: (row) => row.warehouseInName,
columnField: { columnField: {
component: null, component: null,
}, },
@ -129,6 +133,7 @@ const columns = computed(() => [
url: 'warehouses', url: 'warehouses',
fields: ['id', 'name'], fields: ['id', 'name'],
}, },
format: (row) => row.warehouseOutName,
columnField: { columnField: {
component: null, component: null,
}, },
@ -196,7 +201,6 @@ const columns = computed(() => [
default-mode="table" default-mode="table"
auto-load auto-load
redirect="travel" redirect="travel"
:right-search="false"
:is-editable="false" :is-editable="false"
:use-model="true" :use-model="true"
/> />

View File

@ -35,7 +35,7 @@ export default {
name: 'InvoiceOutGlobal', name: 'InvoiceOutGlobal',
meta: { meta: {
title: 'globalInvoicing', title: 'globalInvoicing',
icon: 'contact_support', icon: 'Date_Range',
}, },
component: () => import('src/pages/InvoiceOut/InvoiceOutGlobal.vue'), component: () => import('src/pages/InvoiceOut/InvoiceOutGlobal.vue'),
}, },

View File

@ -14,15 +14,19 @@ export default {
main: ['TicketList', 'TicketAdvance', 'TicketWeekly', 'TicketFuture'], main: ['TicketList', 'TicketAdvance', 'TicketWeekly', 'TicketFuture'],
card: [ card: [
'TicketBasicData', 'TicketBasicData',
'TicketBoxing', 'TicketPurchaseRequest',
'TicketSms',
'TicketSale', 'TicketSale',
'TicketLog', 'TicketLog',
'TicketPurchaseRequest', 'TicketExpedition',
'TicketService', 'TicketService',
'TicketTracking',
'TicketVolume', 'TicketVolume',
'TicketNotes', 'TicketNotes',
'TicketTracking',
'TicketBoxing',
'TicketSms',
'TicketPicture',
'TicketComponents',
'TicketPackage',
], ],
}, },
children: [ children: [
@ -70,8 +74,8 @@ export default {
component: () => import('src/pages/Ticket/TicketFuture.vue'), component: () => import('src/pages/Ticket/TicketFuture.vue'),
}, },
{ {
path: 'advance',
name: 'TicketAdvance', name: 'TicketAdvance',
path: 'advance',
meta: { meta: {
title: 'ticketAdvance', title: 'ticketAdvance',
icon: 'keyboard_double_arrow_left', icon: 'keyboard_double_arrow_left',
@ -142,12 +146,76 @@ export default {
}, },
component: () => import('src/pages/Ticket/Card/TicketLog.vue'), component: () => import('src/pages/Ticket/Card/TicketLog.vue'),
}, },
{
path: 'observation',
name: 'TicketNotes',
meta: {
title: 'notes',
icon: 'vn:notes',
},
component: () => import('src/pages/Ticket/Card/TicketNotes.vue'),
},
{
path: 'picture',
name: 'TicketPicture',
meta: {
title: 'pictures',
icon: 'vn:photo',
},
component: () => import('src/pages/Ticket/Card/TicketPicture.vue'),
},
{
path: 'volume',
name: 'TicketVolume',
meta: {
title: 'volume',
icon: 'vn:volume',
},
component: () => import('src/pages/Ticket/Card/TicketVolume.vue'),
},
{
path: 'expedition',
name: 'TicketExpedition',
meta: {
title: 'expedition',
icon: 'vn:package',
},
component: () => import('src/pages/Ticket/Card/TicketExpedition.vue'),
},
{
path: 'service',
name: 'TicketService',
meta: {
title: 'services',
icon: 'vn:services',
},
component: () => import('src/pages/Ticket/Card/TicketService.vue'),
},
{
path: 'package',
name: 'TicketPackage',
meta: {
title: 'packages',
icon: 'vn:bucket',
},
component: () => import('src/pages/Ticket/Card/TicketPackage.vue'),
},
{
path: 'components',
name: 'TicketComponents',
meta: {
title: 'components',
icon: 'vn:components',
},
component: () => import('src/pages/Ticket/Card/TicketComponents.vue'),
},
{ {
path: 'boxing', path: 'boxing',
name: 'TicketBoxing', name: 'TicketBoxing',
meta: { meta: {
title: 'boxing', title: 'boxing',
icon: 'vn:package', icon: 'science',
}, },
component: () => import('src/pages/Ticket/Card/TicketBoxing.vue'), component: () => import('src/pages/Ticket/Card/TicketBoxing.vue'),
}, },
@ -160,34 +228,6 @@ export default {
}, },
component: () => import('src/pages/Ticket/Card/TicketSms.vue'), component: () => import('src/pages/Ticket/Card/TicketSms.vue'),
}, },
{
path: 'service',
name: 'TicketService',
meta: {
title: 'services',
icon: 'vn:services',
},
component: () => import('src/pages/Ticket/Card/TicketService.vue'),
},
{
path: 'volume',
name: 'TicketVolume',
meta: {
title: 'volume',
icon: 'vn:volume',
},
component: () => import('src/pages/Ticket/Card/TicketVolume.vue'),
},
{
path: 'observation',
name: 'TicketNotes',
meta: {
title: 'notes',
icon: 'vn:notes',
},
component: () => import('src/pages/Ticket/Card/TicketNotes.vue'),
},
], ],
}, },
], ],

View File

@ -237,7 +237,7 @@ export const useInvoiceOutGlobalStore = defineStore({
async getNegativeBasesCsv() { async getNegativeBasesCsv() {
try { try {
const arrayData = useArrayData('InvoiceOutNegative'); const arrayData = useArrayData('InvoiceOutNegative');
const params = arrayData.store.currentFilter; const params = arrayData.store.userParams;
const { data } = await axios.get('InvoiceOuts/negativeBasesCsv', { const { data } = await axios.get('InvoiceOuts/negativeBasesCsv', {
params, params,

View File

@ -1,20 +1,20 @@
// describe('WagonTypeCreate', () => { describe('EntryMy when is supplier', () => {
// beforeEach(() => { beforeEach(() => {
// cy.viewport(1920, 1080); cy.viewport(1920, 1080);
// cy.login('customer'); cy.login('supplier');
// cy.visit(`/#/entry/my`, { cy.visit(`/#/entry/my`, {
// onBeforeLoad(win) { onBeforeLoad(win) {
// cy.stub(win, 'open'); cy.stub(win, 'open');
// }, },
// }); });
// cy.waitForElement('.q-page', 6000); cy.waitForElement('.q-page', 6000);
// }); });
// it('should create edit and remove new dms', () => { it('should open buyLabel when is supplier', () => {
// cy.get( cy.get(
// '[to="/null/2"] > .q-card > .column > .q-btn > .q-btn__content > .q-icon' '[to="/null/2"] > .q-card > .column > .q-btn > .q-btn__content > .q-icon'
// ).click(); ).click();
// cy.get('.q-card__actions > .q-btn').click(); cy.get('.q-card__actions > .q-btn').click();
// cy.window().its('open').should('be.called'); cy.window().its('open').should('be.called');
// }); });
// }); });

View File

@ -0,0 +1,39 @@
/// <reference types="cypress" />
describe('Logout', () => {
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/dashboard`);
cy.waitForElement('.q-page', 6000);
});
describe('by user', () => {
it('should logout', () => {
cy.get(
'#user > .q-btn__content > .q-avatar > .q-avatar__content > .q-img > .q-img__container > .q-img__image'
).click();
cy.get('.block').click();
});
});
describe('not user', () => {
beforeEach(() => {
cy.intercept('GET', '**/VnUsers/acl', {
statusCode: 401,
body: {
error: {
statusCode: 401,
name: 'Error',
message: 'Authorization Required',
code: 'AUTHORIZATION_REQUIRED',
},
},
statusMessage: 'AUTHORIZATION_REQUIRED',
}).as('someRoute');
});
it('when token not exists', () => {
cy.reload();
cy.get('.q-notification__message').should(
'have.text',
'Authorization Required'
);
});
});
});

View File

@ -55,6 +55,7 @@ describe('session', () => {
expect(user.value).toEqual(previousUser); expect(user.value).toEqual(previousUser);
vi.spyOn(axios, 'post').mockResolvedValue({ data: true }); vi.spyOn(axios, 'post').mockResolvedValue({ data: true });
vi.spyOn(axios, 'get').mockResolvedValue({ data: true });
await session.destroy(); await session.destroy();
user = state.getUser(); user = state.getUser();