Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into 6825-visibleColumns
gitea/salix-front/pipeline/pr-dev This commit looks good Details

This commit is contained in:
Alex Moreno 2024-07-08 13:25:14 +02:00
commit 4a8493f035
36 changed files with 801 additions and 519 deletions

View File

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

View File

@ -46,22 +46,6 @@ const onDataSaved = async (formData, requestResponse) => {
@on-fetch="(data) => (taxAreasOptions = data)"
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
ref="formModelPopupRef"
:title="t('Create manual invoice')"
@ -84,6 +68,10 @@ const onDataSaved = async (formData, requestResponse) => {
option-value="id"
v-model="data.ticketFk"
@update:model-value="data.clientFk = null"
url="Tickets"
:where="{ refFk: null }"
:fields="['id', 'nickname']"
:filter-options="{ order: 'shipped DESC' }"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
@ -105,6 +93,9 @@ const onDataSaved = async (formData, requestResponse) => {
option-value="id"
v-model="data.clientFk"
@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" />
</VnRow>
@ -116,7 +107,6 @@ const onDataSaved = async (formData, requestResponse) => {
option-label="description"
option-value="code"
v-model="data.serial"
:required="true"
/>
<VnSelect
:label="t('Area')"
@ -125,7 +115,6 @@ const onDataSaved = async (formData, requestResponse) => {
option-label="code"
option-value="code"
v-model="data.taxArea"
:required="true"
/>
</VnRow>
<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 SkeletonTable from 'components/ui/SkeletonTable.vue';
import { tMobile } from 'src/composables/tMobile';
import VnSubToolbar from './ui/VnSubToolbar.vue';
const { push } = useRouter();
const quasar = useQuasar();

View File

@ -8,7 +8,7 @@ import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue';
import VnSelect from 'components/common/VnSelect.vue';
import FormPopup from './FormPopup.vue';
import { useDialogPluginComponent } from 'quasar';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
@ -18,7 +18,7 @@ const $props = defineProps({
default: () => {},
},
});
const { dialogRef } = useDialogPluginComponent();
const quasar = useQuasar();
const { t } = useI18n();
const router = useRouter();
@ -116,6 +116,7 @@ const makeInvoice = async () => {
@on-fetch="(data) => (invoiceCorrectionTypesOptions = data)"
auto-load
/>
<QDialog ref="dialogRef">
<FormPopup
@on-submit="makeInvoice()"
:title="t('Transfer invoice')"
@ -200,6 +201,7 @@ const makeInvoice = async () => {
</VnRow>
</template>
</FormPopup>
</QDialog>
</template>
<i18n>

View File

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

View File

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

View File

@ -58,6 +58,7 @@ const $props = defineProps({
},
});
defineExpose({ search });
const emit = defineEmits([
'update:modelValue',
'refresh',
@ -192,6 +193,14 @@ function formatValue(value) {
</script>
<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">
<QList dense>
<QItem class="q-mt-xs">
@ -265,23 +274,6 @@ function formatValue(value) {
<QList dense class="list q-gutter-y-sm q-mt-sm">
<slot name="body" :params="userParams" :search-fn="search"></slot>
</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>
<QInnerLoading
:label="t('globals.pleaseWait')"

View File

@ -28,7 +28,7 @@ const $props = defineProps({
const show = ref(false);
const token = useSession().getTokenMultimedia();
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';
const url = computed(() => {
const isEmployee = useRole().isEmployee();

View File

@ -58,20 +58,25 @@ export function useSession() {
}
}
}
async function destroy() {
async function destroy(destroyTokens = true) {
const tokens = {
tokenMultimedia: 'Accounts/logout',
token: 'VnUsers/logout',
};
const storage = keepLogin() ? localStorage : sessionStorage;
for (const [key, url] of Object.entries(tokens)) {
await destroyToken(url, storage, key);
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();
setUser({
@ -84,6 +89,7 @@ export function useSession() {
stopRenewer();
}
}
async function login(data) {
setSession(data);

View File

@ -557,6 +557,7 @@ ticket:
tracking: Tracking
components: Components
pictures: Pictures
packages: Packages
list:
nickname: Nickname
state: State

View File

@ -239,6 +239,13 @@ globals:
mailForwarding: Reenvío de correo
mailAlias: Alias de correo
privileges: Privilegios
observation: Notas
expedition: Expedición
services: Servicios
tracking: Estados
components: Componentes
pictures: Fotos
packages: Bultos
created: Fecha creación
worker: Trabajador
now: Ahora
@ -555,6 +562,7 @@ ticket:
tracking: Estados
components: Componentes
pictures: Fotos
packages: Bultos
list:
nickname: Alias
state: Estado

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ const selectedCustomerId = ref(null);
const tableColumnComponents = {
clientId: {
component: QBtn,
props: { flat: true, color: 'blue' },
props: { flat: true, class: 'link' },
event: (prop) => selectCustomerId(prop.value),
},
clientName: {
@ -52,7 +52,12 @@ const tableColumnComponents = {
};
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'),
field: 'clientName',

View File

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

View File

@ -1,96 +1,177 @@
<script setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { onMounted, onUnmounted, ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import InvoiceOutSummary from './Card/InvoiceOutSummary.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInputDate from 'src/components/common/VnInputDate.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 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 { useSession } from 'src/composables/useSession';
import RightMenu from 'src/components/common/RightMenu.vue';
import { usePrintService } from 'composables/usePrintService';
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 router = useRouter();
const stateStore = useStateStore();
const session = useSession();
const tokenMultimedia = session.getTokenMultimedia();
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 selectedCards = ref(new Map());
const columns = computed(() => [
{
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));
onUnmounted(() => (stateStore.rightDrawer = false));
function navigate(id) {
router.push({ path: `/invoice-out/${id}` });
function openPdf(id) {
try {
openReport(`${MODEL}/${id}/download`);
} catch (err) {
console.error('Error opening PDF', err);
}
}
const toggleIndividualCard = (cardData) => {
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 = () => {
function downloadPdf() {
try {
if (selectedCards.value.size === 0) return;
const selectedCardsArray = Array.from(selectedCards.value.values());
if (selectedRows.value.size === 0) return;
const selectedCardsArray = Array.from(selectedRows.value.values());
if (selectedCards.value.size === 1) {
if (selectedRows.value.size === 1) {
const [invoiceOut] = selectedCardsArray;
const url = `api/InvoiceOuts/${invoiceOut.id}/download?access_token=${tokenMultimedia}`;
window.open(url, '_blank');
openPdf(invoiceOut.id);
} else {
const invoiceOutIdsArray = selectedCardsArray.map(
(invoiceOut) => invoiceOut.id
);
const invoiceOutIds = invoiceOutIdsArray.join(',');
const params = new URLSearchParams({
access_token: tokenMultimedia,
const params = {
ids: invoiceOutIds,
});
};
const url = `api/InvoiceOuts/downloadZip?${params}`;
window.open(url, '_blank');
openReport(`${MODEL}/downloadZip`, params);
}
} catch (err) {
console.error('Error opening PDF');
}
};
}
const openCreateInvoiceModal = () => {
manualInvoiceDialogRef.value.show();
};
watchEffect(selectedRows);
</script>
<template>
@ -99,115 +180,84 @@ const openCreateInvoiceModal = () => {
:label="t('searchInvoice')"
data-key="InvoiceOutList"
/>
<RightMenu>
<template #right-panel>
<InvoiceOutFilter data-key="InvoiceOutList" />
</template>
</RightMenu>
<QPage>
<VnPaginate
auto-load
data-key="InvoiceOutList"
:order="['issued DESC', 'id DESC']"
url="InvoiceOuts/filter"
>
<template #body="{ rows }">
<VnSubToolbar class="justify-end">
<VnSubToolbar>
<template #st-actions>
<QBtn
@click="openPdf()"
class="q-mr-md"
color="primary"
icon="cloud_download"
:disable="selectedCards.size === 0"
icon-right="cloud_download"
@click="downloadPdf()"
:disable="!hasSelectedCards"
>
<QTooltip>{{ t('globals.downloadPdf') }}</QTooltip>
</QBtn>
<QCheckbox
left-label
:label="t('globals.markAll')"
@click="toggleAllCards(rows)"
:model-value="selectedCards.size === rows.length"
class="q-mr-md"
/>
</template>
</VnSubToolbar>
<div class="flex flex-center q-pa-md">
<div class="vn-card-list">
<CardList
:element="row"
:id="row.id"
:show-checkbox="true"
:is-selected="selectedCards.has(row.id)"
:key="row.id"
:title="row.ref"
@click="navigate(row.id)"
@toggle-card-check="toggleIndividualCard(row)"
v-for="row of rows"
<VnTable
ref="tableRef"
data-key="invoiceOut"
:url="`${MODEL}/filter`"
:create="{
urlCreate: 'InvoiceOuts/createManualInvoice',
title: t('Create Manual Invoice'),
onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {
active: true,
},
}"
v-model:selected="selectedRows"
order="id DESC"
:columns="columns"
default-mode="table"
redirect="invoice-out"
auto-load
:table="{
'row-key': 'id',
selection: 'multiple',
}"
>
<template #list-items>
<VnLv
:label="t('invoiceOut.list.issued')"
:value="toDate(row.issued)"
<template #more-create-dialog="{ data }">
<VnSelect
url="Tickets"
v-model="data.ticketFk"
:label="t('invoiceOutList.tableVisibleColumns.ticket')"
:options="ticketsOptions"
option-label="nickname"
option-value="id"
/>
<VnLv
:label="t('invoiceOut.list.amount')"
:value="toCurrency(row.amount)"
<VnSelect
url="Clients"
v-model="data.clientFk"
:label="t('invoiceOutModule.customer')"
:options="customerOptions"
option-label="name"
option-value="id"
/>
<VnLv :label="t('invoiceOut.list.client')">
<template #value>
<span class="link" @click.stop>
{{ row?.clientSocialName }}
<CustomerDescriptorProxy
:id="row?.clientFk"
<VnInputDate
:label="t('invoiceOutList.tableVisibleColumns.dueDate')"
v-model="data.maxShipped"
/>
</span>
</template>
</VnLv>
<VnLv
:label="t('invoiceOut.list.shortCreated')"
:title-label="t('invoiceOut.list.created')"
:value="toDate(row.created)"
<VnSelect
url="InvoiceOutSerials"
v-model="data.invoiceOutSerial"
:label="t('invoiceOutList.tableVisibleColumns.invoiceOutSerial')"
:options="invoiceOutSerialsOptions"
option-label="description"
option-value="code"
/>
<VnLv
:label="t('invoiceOut.list.company')"
:value="row.companyCode"
<VnSelect
url="TaxAreas"
v-model="data.area"
:label="t('invoiceOutList.tableVisibleColumns.taxArea')"
:options="taxAreasOptions"
option-label="code"
option-value="code"
/>
<VnLv
:label="t('invoiceOut.list.shortDued')"
:title-label="t('invoiceOut.list.dued')"
:value="toDate(row.dued)"
<QInput
v-model="data.reference"
:label="t('invoiceOutList.tableVisibleColumns.ref')"
/>
</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>
</VnTable>
</template>
<i18n>

View File

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

@ -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

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

View File

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

View File

@ -201,3 +201,9 @@ tracking:
worker: Worker
created: Created
addState: Add state
package:
package: Package
quantity: Quantity
added: Added
addPackage: Add package
removePackage: Remove package

View File

@ -201,6 +201,12 @@ expedition:
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
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>
import { ref, computed, onUpdated } from 'vue';
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';

View File

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

View File

@ -26,6 +26,7 @@ export default {
'TicketSms',
'TicketPicture',
'TicketComponents',
'TicketPackage',
],
},
children: [
@ -145,24 +146,6 @@ export default {
},
component: () => import('src/pages/Ticket/Card/TicketLog.vue'),
},
{
path: 'picture',
name: 'TicketPicture',
meta: {
title: 'pictures',
icon: 'vn:photo',
},
component: () => import('src/pages/Ticket/Card/TicketPicture.vue'),
},
{
path: 'picture',
name: 'TicketPicture',
meta: {
title: 'pictures',
icon: 'vn:photo',
},
component: () => import('src/pages/Ticket/Card/TicketPicture.vue'),
},
{
path: 'observation',
name: 'TicketNotes',
@ -172,6 +155,15 @@ export default {
},
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',
@ -181,15 +173,7 @@ export default {
},
component: () => import('src/pages/Ticket/Card/TicketVolume.vue'),
},
{
path: 'components',
name: 'TicketComponents',
meta: {
title: 'components',
icon: 'vn:components',
},
component: () => import('src/pages/Ticket/Card/TicketComponents.vue'),
},
{
path: 'expedition',
name: 'TicketExpedition',
@ -208,6 +192,24 @@ export default {
},
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',
name: 'TicketBoxing',

View File

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

View File

@ -1,20 +1,20 @@
// describe('WagonTypeCreate', () => {
// beforeEach(() => {
// cy.viewport(1920, 1080);
// cy.login('customer');
// cy.visit(`/#/entry/my`, {
// onBeforeLoad(win) {
// cy.stub(win, 'open');
// },
// });
// cy.waitForElement('.q-page', 6000);
// });
describe('EntryMy when is supplier', () => {
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('supplier');
cy.visit(`/#/entry/my`, {
onBeforeLoad(win) {
cy.stub(win, 'open');
},
});
cy.waitForElement('.q-page', 6000);
});
// it('should create edit and remove new dms', () => {
// cy.get(
// '[to="/null/2"] > .q-card > .column > .q-btn > .q-btn__content > .q-icon'
// ).click();
// cy.get('.q-card__actions > .q-btn').click();
// cy.window().its('open').should('be.called');
// });
// });
it('should open buyLabel when is supplier', () => {
cy.get(
'[to="/null/2"] > .q-card > .column > .q-btn > .q-btn__content > .q-icon'
).click();
cy.get('.q-card__actions > .q-btn').click();
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);
vi.spyOn(axios, 'post').mockResolvedValue({ data: true });
vi.spyOn(axios, 'get').mockResolvedValue({ data: true });
await session.destroy();
user = state.getUser();