feature/InvoicesOutCorrections #232

Merged
jsegarra merged 25 commits from :feature/InvoicesOutCorrections into dev 2024-03-20 13:34:35 +00:00
18 changed files with 910 additions and 271 deletions

View File

@ -0,0 +1,174 @@
<script setup>
import { reactive, ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FormModelPopup from './FormModelPopup.vue';
import VnInputDate from './common/VnInputDate.vue';
const emit = defineEmits(['onDataSaved']);
const { t } = useI18n();
const router = useRouter();
const manualInvoiceFormData = reactive({
maxShipped: Date.vnNew(),
});
const formModelPopupRef = ref();
const invoiceOutSerialsOptions = ref([]);
const taxAreasOptions = ref([]);
const ticketsOptions = ref([]);
const clientsOptions = ref([]);
const isLoading = computed(() => formModelPopupRef.value?.isLoading);
const onDataSaved = async (formData, requestResponse) => {
emit('onDataSaved', formData, requestResponse);
if (requestResponse && requestResponse.id)
router.push({ name: 'InvoiceOutSummary', params: { id: requestResponse.id } });
};
</script>
<template>
<FetchData
url="InvoiceOutSerials"
:filter="{ where: { code: { neq: 'R' } }, order: ['code'] }"
@on-fetch="(data) => (invoiceOutSerialsOptions = data)"
auto-load
/>
<FetchData
url="TaxAreas"
:filter="{ order: ['code'] }"
@on-fetch="(data) => (taxAreasOptions = data)"
auto-load
/>
<FetchData
url="Tickets"
:filter="{ fields: ['id', 'nickname'], order: 'shipped DESC', limit: 30 }"
@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')"
url-create="InvoiceOuts/createManualInvoice"
model="invoiceOut"
:form-initial-data="manualInvoiceFormData"
@on-data-saved="onDataSaved"
>
<template #form-inputs="{ data }">
<span v-if="isLoading" class="text-primary invoicing-text">
<QIcon name="warning" class="fill-icon q-mr-sm" size="md" />
{{ t('Invoicing in progress...') }}
</span>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Ticket')"
:options="ticketsOptions"
hide-selected
option-label="id"
option-value="id"
v-model="data.ticketFk"
@update:model-value="data.clientFk = null"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel> #{{ scope.opt?.id }} </QItemLabel>
<QItemLabel caption>{{
scope.opt?.nickname
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<span class="row items-center" style="max-width: max-content">{{
t('Or')
}}</span>
<div class="col">
<VnSelectFilter
:label="t('Client')"
:options="clientsOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.clientFk"
@update:model-value="data.ticketFk = null"
/>
</div>
<div class="col">
<VnInputDate :label="t('Max date')" v-model="data.maxShipped" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Serial')"
:options="invoiceOutSerialsOptions"
hide-selected
option-label="description"
option-value="code"
v-model="data.serial"
:required="true"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('Area')"
:options="taxAreasOptions"
hide-selected
option-label="code"
option-value="code"
v-model="data.taxArea"
:required="true"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnInput
:label="t('Reference')"
type="textarea"
v-model="data.reference"
fill-input
autogrow
/>
</VnRow>
</template>
</FormModelPopup>
</template>
<style lang="scss" scoped>
.invoicing-text {
display: flex;
justify-content: center;
align-items: center;
color: $primary;
font-size: 24px;
margin-bottom: 8px;
}
</style>
<i18n>
es:
Create manual invoice: Crear factura manual
Ticket: Ticket
Client: Cliente
Max date: Fecha límite
Serial: Serie
Area: Area
Reference: Referencia
Or: O
Invoicing in progress...: Facturación en progreso...
</i18n>

View File

@ -78,10 +78,6 @@ const $props = defineProps({
const emit = defineEmits(['onFetch', 'onDataSaved']);
defineExpose({
save,
});
const componentIsRendered = ref(false);
onMounted(async () => {
@ -227,6 +223,11 @@ watch(formUrl, async () => {
reset();
fetch();
});
defineExpose({
save,
isLoading,
});
</script>
<template>
<div class="column items-center full-width">

View File

@ -1,5 +1,5 @@
<script setup>
import { ref } from 'vue';
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue';
@ -39,21 +39,28 @@ const $props = defineProps({
const { t } = useI18n();
const formModelRef = ref(null);
const closeButton = ref(null);
const isLoading = ref(false);
const onDataSaved = (formData, requestResponse) => {
emit('onDataSaved', formData, requestResponse);
closeForm();
};
const closeForm = () => {
const isLoading = computed(() => formModelRef.value?.isLoading);
const closeForm = async () => {
if (closeButton.value) closeButton.value.click();
};
defineExpose({
isLoading,
});
</script>
<template>
<FormModel
ref="formModelRef"
:form-initial-data="formInitialData"
:observe-form-changes="false"
:default-actions="false"

View File

@ -0,0 +1,96 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
const emit = defineEmits(['onSubmit']);
const $props = defineProps({
title: {
type: String,
default: '',
},
subtitle: {
type: String,
default: '',
},
defaultSubmitButton: {
type: Boolean,
default: true,
},
defaultCancelButton: {
type: Boolean,
default: true,
},
customSubmitButtonLabel: {
type: String,
default: '',
},
});
const { t } = useI18n();
const closeButton = ref(null);
const isLoading = ref(false);
const onSubmit = () => {
emit('onSubmit');
closeForm();
};
const closeForm = () => {
if (closeButton.value) closeButton.value.click();
};
</script>
<template>
<QForm
@submit="onSubmit($event)"
class="all-pointer-events full-width"
style="max-width: 800px"
>
<QCard class="q-pa-lg">
<span ref="closeButton" class="close-icon" v-close-popup>
<QIcon name="close" size="sm" />
</span>
<h1 class="title">{{ title }}</h1>
<p>{{ subtitle }}</p>
<slot name="form-inputs" />
<div class="q-mt-lg row justify-end">
<QBtn
v-if="defaultSubmitButton"
:label="customSubmitButtonLabel || t('globals.save')"
type="submit"
color="primary"
:disabled="isLoading"
:loading="isLoading"
/>
<QBtn
v-if="defaultCancelButton"
:label="t('globals.cancel')"
color="primary"
flat
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
v-close-popup
/>
<slot name="customButtons" />
</div>
</QCard>
</QForm>
</template>
<style lang="scss" scoped>
.title {
font-size: 17px;
font-weight: bold;
line-height: 20px;
}
.close-icon {
position: absolute;
top: 20px;
right: 20px;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,168 @@
<script setup>
import { ref, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import FormPopup from './FormPopup.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const $props = defineProps({
invoiceOutData: {
type: Object,
default: () => {},
},
});
const { t } = useI18n();
const router = useRouter();
const { notify } = useNotify();
const transferInvoiceParams = reactive({
id: $props.invoiceOutData?.id,
refFk: $props.invoiceOutData?.ref,
});
const closeButton = ref(null);
const clientsOptions = ref([]);
const rectificativeTypeOptions = ref([]);
const siiTypeInvoiceOutsOptions = ref([]);
const invoiceCorrectionTypesOptions = ref([]);
const closeForm = () => {
if (closeButton.value) closeButton.value.click();
};
const transferInvoice = async () => {
try {
const { data } = await axios.post(
'InvoiceOuts/transferInvoice',
transferInvoiceParams
);
notify(t('Transferred invoice'), 'positive');
closeForm();
router.push('InvoiceOutSummary', { id: data.id });
} catch (err) {
console.error('Error transfering invoice', err);
}
};
</script>
<template>
<FetchData
url="Clients"
@on-fetch="(data) => (clientsOptions = data)"
:filter="{ fields: ['id', 'name'], order: 'id', limit: 30 }"
auto-load
/>
<FetchData
url="CplusRectificationTypes"
:filter="{ order: 'description' }"
@on-fetch="(data) => (rectificativeTypeOptions = data)"
auto-load
/>
<FetchData
url="SiiTypeInvoiceOuts"
:filter="{ where: { code: { like: 'R%' } } }"
@on-fetch="(data) => (siiTypeInvoiceOutsOptions = data)"
auto-load
/>
<FetchData
url="InvoiceCorrectionTypes"
@on-fetch="(data) => (invoiceCorrectionTypesOptions = data)"
auto-load
/>
<FormPopup
@on-submit="transferInvoice()"
:title="t('Transfer invoice')"
:custom-submit-button-label="t('Transfer client')"
:default-cancel-button="false"
>
<template #form-inputs>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Client')"
:options="clientsOptions"
hide-selected
option-label="name"
option-value="id"
v-model="transferInvoiceParams.newClientFk"
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
#{{ scope.opt?.id }} -
{{ scope.opt?.name }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col">
<VnSelectFilter
:label="t('Rectificative type')"
:options="rectificativeTypeOptions"
hide-selected
option-label="description"
option-value="id"
v-model="transferInvoiceParams.cplusRectificationTypeFk"
:required="true"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Class')"
:options="siiTypeInvoiceOutsOptions"
hide-selected
option-label="description"
option-value="id"
v-model="transferInvoiceParams.siiTypeInvoiceOutFk"
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.code }} -
{{ scope.opt?.description }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col">
<VnSelectFilter
:label="t('Type')"
:options="invoiceCorrectionTypesOptions"
hide-selected
option-label="description"
option-value="id"
v-model="transferInvoiceParams.invoiceCorrectionTypeFk"
:required="true"
/>
</div>
</VnRow>
</template>
</FormPopup>
</template>
<i18n>
es:
Transfer invoice: Transferir factura
Transfer client: Transferir cliente
Client: Cliente
Rectificative type: Tipo rectificativa
Class: Clase
Type: Tipo
Transferred invoice: Factura transferida
</i18n>

View File

@ -0,0 +1,23 @@
import VnConfirm from 'components/ui/VnConfirm.vue';
import { useQuasar } from 'quasar';
export function useVnConfirm() {
const quasar = useQuasar();
const openConfirmationModal = (title, message, promise, successFn) => {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: title,
message: message,
promise: promise,
},
})
.onOk(async () => {
if (successFn) successFn();
});
};
return { openConfirmationModal };
}

View File

@ -97,6 +97,10 @@ select:-webkit-autofill {
background-color: var(--vn-light-gray);
}
.fill-icon {
font-variation-settings: 'FILL' 1;
}
.vn-table-separation-row {
height: 16px !important;
background-color: var(--vn-gray) !important;

View File

@ -592,6 +592,7 @@ export default {
company: 'Company',
dued: 'Due date',
shortDued: 'Due date',
amount: 'Amount',
},
card: {
issued: 'Issued',
@ -625,7 +626,7 @@ export default {
fillDates: 'Invoice date and the max date should be filled',
invoiceDateLessThanMaxDate: 'Invoice date can not be less than max date',
invoiceWithFutureDate: 'Exists an invoice with a future date',
noTicketsToInvoice: 'There are not clients to invoice',
noTicketsToInvoice: 'There are not tickets to invoice',
criticalInvoiceError: 'Critical invoicing error, process stopped',
},
table: {

View File

@ -592,6 +592,7 @@ export default {
company: 'Empresa',
dued: 'Fecha vencimineto',
shortDued: 'F. vencimiento',
amount: 'Importe',
},
card: {
issued: 'Fecha emisión',
@ -627,7 +628,7 @@ export default {
invoiceDateLessThanMaxDate:
'La fecha de la factura no puede ser menor que la fecha máxima',
invoiceWithFutureDate: 'Existe una factura con una fecha futura',
noTicketsToInvoice: 'No hay clientes para facturar',
noTicketsToInvoice: 'No existen tickets para facturar',
criticalInvoiceError: 'Error crítico en la facturación, proceso detenido',
},
table: {

View File

@ -2,13 +2,15 @@
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toCurrency, toDate } from 'src/filters';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription';
import InvoiceOutDescriptorMenu from './InvoiceOutDescriptorMenu.vue';
import useCardDescription from 'src/composables/useCardDescription';
import { toCurrency, toDate } from 'src/filters';
const $props = defineProps({
id: {
type: Number,
@ -23,7 +25,6 @@ const { t } = useI18n();
const entityId = computed(() => {
return $props.id || route.params.id;
});
const descriptor = ref();
const filter = {
include: [
@ -42,6 +43,8 @@ const filter = {
],
};
const descriptor = ref();
function ticketFilter(invoice) {
return JSON.stringify({ refFk: invoice.ref });
}
@ -61,7 +64,7 @@ const setData = (entity) => (data.value = useCardDescription(entity.ref, entity.
data-key="invoiceOutData"
>
<template #menu="{ entity }">
<InvoiceOutDescriptorMenu :invoice-out="entity" />
<InvoiceOutDescriptorMenu :invoice-out-data="entity" />
</template>
<template #body="{ entity }">
<VnLv :label="t('invoiceOut.card.issued')" :value="toDate(entity.issued)" />

View File

@ -1,40 +1,260 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import TransferInvoiceForm from 'src/components/TransferInvoiceForm.vue';
import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import useNotify from 'src/composables/useNotify';
import { useSession } from 'src/composables/useSession';
import { usePrintService } from 'composables/usePrintService';
import { useVnConfirm } from 'composables/useVnConfirm';
import axios from 'axios';
const $props = defineProps({
invoiceOutData: {
type: Object,
default: () => {},
},
});
const { notify } = useNotify();
const router = useRouter();
const session = useSession();
const token = session.getToken();
const { t } = useI18n();
const { openReport, sendEmail } = usePrintService();
const { openConfirmationModal } = useVnConfirm();
const quasar = useQuasar();
const transferInvoiceDialogRef = ref();
const invoiceFormType = ref('pdf');
const defaultEmailAddress = ref($props.invoiceOutData.client?.email);
const showInvoicePdf = () => {
const url = `api/InvoiceOuts/${$props.invoiceOutData.id}/download?access_token=${token}`;
window.open(url, '_blank');
};
const showInvoiceCsv = () => {
openReport(`InvoiceOuts/${$props.invoiceOutData.ref}/invoice-csv`, {
recipientId: $props.invoiceOutData.client.id,
});
};
const showSendInvoiceDialog = (type) => {
invoiceFormType.value = type;
quasar.dialog({
component: SendEmailDialog,
componentProps: {
data: {
address: defaultEmailAddress.value,
},
promise: sendEmailInvoice,
},
});
};
const sendEmailInvoice = async ({ address }) => {
try {
if (!address) notify(`The email can't be empty`, 'negative');
if (invoiceFormType.value === 'pdf') {
return sendEmail(`InvoiceOuts/${$props.invoiceOutData.ref}/invoice-email`, {
recipientId: $props.invoiceOutData.client.id,
recipient: address,
});
} else {
return sendEmail(
`InvoiceOuts/${$props.invoiceOutData.ref}/invoice-csv-email`,
{
recipientId: $props.invoiceOutData.client.id,
recipient: address,
}
);
}
} catch (err) {
console.error('Error sending email', err);
}
};
const redirectToInvoiceOutList = () => {
router.push({ name: 'InvoiceOutList' });
};
const deleteInvoice = async () => {
try {
await axios.post(`InvoiceOuts/${$props.invoiceOutData.id}/delete`);
notify(t('InvoiceOut deleted'), 'positive');
} catch (err) {
console.error('Error deleting invoice out', err);
}
};
const bookInvoice = async () => {
try {
await axios.post(`InvoiceOuts/${$props.invoiceOutData.ref}/book`);
notify(t('InvoiceOut booked'), 'positive');
} catch (err) {
console.error('Error booking invoice out', err);
}
};
const generateInvoicePdf = async () => {
try {
await axios.post(`InvoiceOuts/${$props.invoiceOutData.id}/createPdf`);
notify(t('The invoice PDF document has been regenerated'), 'positive');
} catch (err) {
console.error('Error generating invoice out pdf', err);
}
};
const refundInvoice = async (withWarehouse) => {
try {
const params = { ref: $props.invoiceOutData.ref, withWarehouse: withWarehouse };
const { data } = await axios.post('InvoiceOuts/refund', params);
notify(
t('refundInvoiceSuccessMessage', {
refundTicket: data[0].id,
}),
'positive'
);
} catch (err) {
console.error('Error generating invoice out pdf', err);
}
};
</script>
<template>
<QItem v-ripple clickable>
<QItemSection>{{ t('Transfer invoice to') }}</QItemSection>
<QItem v-ripple clickable @click="transferInvoiceDialogRef.show()">
<QItemSection>{{ t('Transfer invoice to...') }}</QItemSection>
</QItem>
<QItem v-ripple clickable>
<QItemSection>{{ t('See invoice') }}</QItemSection>
<QItemSection>{{ t('Show invoice...') }}</QItemSection>
<QItemSection side>
<QIcon name="keyboard_arrow_right" />
</QItemSection>
<QMenu anchor="top end" self="top start">
<QList>
<QItem v-ripple clickable @click="showInvoicePdf()">
<QItemSection>{{ t('As PDF') }}</QItemSection>
</QItem>
<QItem v-ripple clickable @click="showInvoiceCsv()">
<QItemSection>{{ t('As CSV') }}</QItemSection>
</QItem>
</QList>
</QMenu>
</QItem>
<QItem v-ripple clickable>
<QItemSection>{{ t('Send invoice') }}</QItemSection>
<QItemSection>{{ t('Send invoice...') }}</QItemSection>
<QItemSection side>
<QIcon name="keyboard_arrow_right" />
</QItemSection>
<QMenu anchor="top end" self="top start">
<QList>
<QItem v-ripple clickable @click="showSendInvoiceDialog('pdf')">
<QItemSection>{{ t('Send PDF') }}</QItemSection>
</QItem>
<QItem v-ripple clickable @click="showSendInvoiceDialog('csv')">
<QItemSection>{{ t('Send CSV') }}</QItemSection>
</QItem>
</QList>
</QMenu>
</QItem>
<QItem v-ripple clickable>
<QItem
v-ripple
clickable
@click="
openConfirmationModal(
t('Confirm deletion'),
t('Are you sure you want to delete this invoice?'),
deleteInvoice,
redirectToInvoiceOutList
)
"
>
<QItemSection>{{ t('Delete invoice') }}</QItemSection>
</QItem>
<QItem v-ripple clickable>
<QItemSection>{{ t('Post invoice') }}</QItemSection>
<QItem
v-ripple
clickable
@click="
openConfirmationModal(
'',
t('Are you sure you want to book this invoice?'),
bookInvoice
)
"
>
<QItemSection>{{ t('Book invoice') }}</QItemSection>
</QItem>
<QItem
v-ripple
clickable
@click="
openConfirmationModal(
t('Generate PDF invoice document'),
t('Are you sure you want to generate/regenerate the PDF invoice?'),
generateInvoicePdf
)
"
>
<QItemSection>{{ t('Generate PDF invoice') }}</QItemSection>
</QItem>
<QItem v-ripple clickable>
<QItemSection>{{ t('Regenerate invoice PDF') }}</QItemSection>
</QItem>
<QItem v-ripple clickable>
<QItemSection>{{ t('Pass') }}</QItemSection>
<QItemSection>{{ t('Refund...') }}</QItemSection>
<QItemSection side>
<QIcon name="keyboard_arrow_right" />
</QItemSection>
<QMenu anchor="top end" self="top start">
<QList>
<QItem v-ripple clickable @click="refundInvoice(true)">
<QItemSection>{{ t('With warehouse') }}</QItemSection>
</QItem>
<QItem v-ripple clickable @click="refundInvoice(false)">
<QItemSection>{{ t('Without warehouse') }}</QItemSection>
</QItem>
</QList>
</QMenu>
<QTooltip>
{{ 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>
es:
Transfer invoice to: Transferir factura a
See invoice: Ver factura
Send invoice: Enviar factura
Transfer invoice to...: Transferir factura a...
Show invoice...: Ver factura...
Send invoice...: Enviar factura...
Delete invoice: Eliminar factura
Post invoice: Asentar factura
Regenerate invoice PDF: Regenerar PDF factura
Pass: Abono
Book invoice: Asentar factura
Generate PDF invoice: Generar PDF factura
Refund...: Abono
As PDF: como PDF
As CSV: como CSV
Send PDF: Enviar PDF
Send CSV: Enviar CSV
With warehouse: Con almacén
Without warehouse: Sin almacén
InvoiceOut deleted: Factura eliminada
Confirm deletion: Confirmar eliminación
Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura?
Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura?
InvoiceOut booked: Factura asentada
Generate PDF invoice document: Generar PDF de la factura
Are you sure you want to generate/regenerate the PDF invoice?: ¿Seguro que quieres generar/regenerar el PDF de la factura?
The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado
Create a single ticket with all the content of the current invoice: Crear un ticket único con todo el contenido de la factura actual
refundInvoiceSuccessMessage: Se ha creado el siguiente ticket de abono {refundTicket}
The email can't be empty: El email no puede estar vacío
en:
refundInvoiceSuccessMessage: The following refund ticket have been created {refundTicket}
</i18n>

View File

@ -7,6 +7,8 @@ import { toCurrency, toDate } from 'src/filters';
import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import { getUrl } from 'src/composables/getUrl';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
onMounted(async () => {
fetch();
@ -67,29 +69,33 @@ const taxColumns = ref([
const ticketsColumns = ref([
{
name: 'item',
label: 'invoiceOut.summary.ticketId',
label: t('invoiceOut.summary.ticketId'),
field: (row) => row.id,
sortable: true,
align: 'left',
},
{
name: 'quantity',
label: 'invoiceOut.summary.nickname',
label: t('invoiceOut.summary.nickname'),
field: (row) => row.nickname,
sortable: true,
align: 'left',
},
{
name: 'landed',
label: 'invoiceOut.summary.shipped',
label: t('invoiceOut.summary.shipped'),
field: (row) => row.shipped,
format: (value) => toDate(value),
sortable: true,
align: 'left',
},
{
name: 'landed',
label: 'invoiceOut.summary.totalWithVat',
label: t('invoiceOut.summary.totalWithVat'),
field: (row) => row.totalWithVat,
format: (value) => toCurrency(value),
sortable: true,
align: 'left',
},
]);
</script>
@ -151,12 +157,21 @@ const ticketsColumns = ref([
<QIcon name="open_in_new" />
</a>
<QTable v-if="tickets" :columns="ticketsColumns" :rows="tickets" flat>
<template #header="props">
<QTr :props="props">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }}
</QTh>
</QTr>
<template #body-cell-item="{ value }">
<QTd>
<QBtn flat color="primary">
{{ value }}
<TicketDescriptorProxy :id="value" />
</QBtn>
</QTd>
</template>
<template #body-cell-quantity="{ value, row }">
<QTd>
<QBtn flat color="primary" dense>
{{ value }}
<CustomerDescriptorProxy :id="row.id" />
</QBtn>
</QTd>
</template>
</QTable>
</QCard>

View File

@ -21,6 +21,7 @@ const {
nPdfs,
totalPdfs,
errors,
addresses,
} = storeToRefs(invoiceOutGlobalStore);
const selectedCustomerId = ref(null);
@ -86,6 +87,14 @@ const selectCustomerId = (id) => {
selectedCustomerId.value = id;
};
const statusText = computed(() => {
return status.value === 'invoicing'
? `${t(`status.${status.value}`)} ${
addresses.value[getAddressNumber.value]?.clientId
}`
: t(`status.${status.value}`);
});
onMounted(() => (stateStore.rightDrawer = true));
onUnmounted(() => {
stateStore.rightDrawer = false;
@ -103,7 +112,7 @@ onUnmounted(() => {
<QPage class="column items-center q-pa-md">
<QCard v-if="status" class="card">
<QCardSection class="card-section">
<span class="text">{{ t(`status.${status}`) }}</span>
<span class="text">{{ statusText }}</span>
<span class="text">{{
t('invoiceOut.globalInvoices.statusCard.percentageText', {
getPercentage: getPercentage,

View File

@ -13,13 +13,8 @@ const { t } = useI18n();
const invoiceOutGlobalStore = useInvoiceOutGlobalStore();
// invoiceOutGlobalStore state and getters
const {
initialDataLoading,
formInitialData,
invoicing,
status,
} = storeToRefs(invoiceOutGlobalStore);
const { initialDataLoading, formInitialData, invoicing, status } =
storeToRefs(invoiceOutGlobalStore);
// invoiceOutGlobalStore actions
const { makeInvoice, setStatusValue } = invoiceOutGlobalStore;
@ -32,13 +27,7 @@ const printersOptions = ref([]);
const clientsOptions = ref([]);
const formData = ref({
companyFk: null,
invoiceDate: null,
maxShipped: null,
clientId: null,
printer: null,
});
const formData = ref({});
const optionsInitialData = computed(() => {
return (

View File

@ -2,25 +2,32 @@
import { onMounted, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { exportFile, useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import InvoiceOutSummary from './Card/InvoiceOutSummary.vue';
import { toDate, toCurrency } from 'src/filters/index';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import InvoiceOutFilter from './InvoiceOutFilter.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';
const { t } = useI18n();
const selectedCards = ref(new Map());
const quasar = useQuasar();
const router = useRouter();
const stateStore = useStateStore();
const session = useSession();
const token = session.getToken();
const { viewSummary } = useSummaryDialog();
const manualInvoiceDialogRef = ref(null);
const selectedCards = ref(new Map());
onMounted(() => (stateStore.rightDrawer = true));
onUnmounted(() => (stateStore.rightDrawer = false));
@ -52,37 +59,37 @@ const toggleAllCards = (cardsData) => {
}
};
const downloadCsv = () => {
if (selectedCards.value.size === 0) return;
const selectedCardsArray = Array.from(selectedCards.value.values());
let file;
for (var i = 0; i < selectedCardsArray.length; i++) {
if (i == 0) file += Object.keys(selectedCardsArray[i]).join(';') + '\n';
file +=
Object.keys(selectedCardsArray[i])
.map(function (key) {
return selectedCardsArray[i][key];
})
.join(';') + '\n';
}
const status = exportFile('file.csv', file, {
encoding: 'windows-1252',
mimeType: 'text/csv;charset=windows-1252;',
});
if (status === true) {
quasar.notify({
message: t('fileAllowed'),
color: 'positive',
icon: 'check',
});
} else {
quasar.notify({
message: t('fileDenied'),
color: 'negative',
icon: 'warning',
});
const openPdf = () => {
try {
if (selectedCards.value.size === 0) return;
const selectedCardsArray = Array.from(selectedCards.value.values());
if (selectedCards.value.size === 1) {
const [invoiceOut] = selectedCardsArray;
const url = `api/InvoiceOuts/${invoiceOut.id}/download?access_token=${token}`;
window.open(url, '_blank');
} else {
const invoiceOutIdsArray = selectedCardsArray.map(
(invoiceOut) => invoiceOut.id
);
const invoiceOutIds = invoiceOutIdsArray.join(',');
const params = new URLSearchParams({
access_token: token,
ids: invoiceOutIds,
});
const url = `api/InvoiceOuts/downloadZip?${params}`;
window.open(url, '_blank');
}
} catch (err) {
console.error('Error opening PDF');
}
};
const openCreateInvoiceModal = () => {
manualInvoiceDialogRef.value.show();
};
</script>
<template>
@ -119,52 +126,21 @@ const downloadCsv = () => {
<VnPaginate
auto-load
data-key="InvoiceOutList"
order="issued DESC, id DESC"
:order="['issued DESC', 'id DESC']"
url="InvoiceOuts/filter"
>
<template #body="{ rows }">
<VnSubToolbar class="bg-vn-dark justify-end">
<template #st-actions>
<QBtn
@click="downloadCsv()"
class="q-mr-xl"
@click="openPdf()"
class="q-mr-md"
color="primary"
icon="cloud_download"
:disable="selectedCards.size === 0"
jsegarra marked this conversation as resolved Outdated

jejeje pero en Salix está el icono, creo que quedaría mejor

jejeje pero en Salix está el icono, creo que quedaría mejor

Perfecto, removi el label y agregue el icono tal como esta en salix

Commit: 06fa4aebc5

Perfecto, removi el label y agregue el icono tal como esta en salix Commit: https://gitea.verdnatura.es/verdnatura/salix-front/commit/06fa4aebc5b095e3b7f78736bf9b73350a5dc567
:label="t('globals.download')"
/>
<!-- <QBtnDropdown
class="q-mr-xl"
color="primary"
:disable="!manageCheckboxes && arrayElements.length < 1"
:label="t('globals.download')"
v-else
>
<QList>
<QItem clickable v-close-popup @click="downloadCsv(rows)">
<QItemSection>
<QItemLabel>
{{
t('globals.allRows', {
numberRows: rows.length,
})
}}
</QItemLabel>
</QItemSection>
</QItem>
<QItem clickable v-close-popup @click="downloadCsv(rows)">
<QItemSection>
<QItemLabel>
{{
t('globals.selectRows', {
numberRows: rows.length,
})
}}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QBtnDropdown> -->
<QTooltip>{{ t('downloadPdf') }}</QTooltip>
</QBtn>
<QCheckbox
left-label
:label="t('globals.markAll')"
@ -189,18 +165,23 @@ const downloadCsv = () => {
>
<template #list-items>
<VnLv
:label="t('invoiceOut.list.shortIssued')"
:title-label="t('invoiceOut.list.issued')"
:label="t('invoiceOut.list.issued')"
:value="toDate(row.issued)"
/>
<VnLv
:label="t('invoiceOut.list.amount')"
:value="toCurrency(row.amount)"
/>
<VnLv
:label="t('invoiceOut.list.client')"
:value="row.clientSocialName"
/>
<VnLv :label="t('invoiceOut.list.client')">
<template #value>
<span class="link" @click.stop>
{{ row?.clientSocialName }}
<CustomerDescriptorProxy
:id="row?.clientFk"
/>
</span>
</template>
</VnLv>
<VnLv
:label="t('invoiceOut.list.shortCreated')"
:title-label="t('invoiceOut.list.created')"
@ -217,13 +198,6 @@ const downloadCsv = () => {
/>
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openCard')"
@click.stop="navigate(row.id)"
class="bg-vn-dark"
outline
type="reset"
/>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, InvoiceOutSummary)"
@ -237,6 +211,20 @@ const downloadCsv = () => {
</div>
</template>
</VnPaginate>
<QPageSticky :offset="[20, 20]">
<QBtn fab icon="add" color="primary" @click="openCreateInvoiceModal()" />
<QTooltip>
{{ t('createInvoice') }}
</QTooltip>
</QPageSticky>
<QDialog
ref="manualInvoiceDialogRef"
jsegarra marked this conversation as resolved Outdated

👀...dos veces el mismo texto?

👀...dos veces el mismo texto?

Buena observación, se me pasó, agregué la traducción correcta

Commit: 049e3c722a

Buena observación, se me pasó, agregué la traducción correcta Commit: https://gitea.verdnatura.es/verdnatura/salix-front/commit/049e3c722a897e48bb8dbc118b14eaaf3e0962f7
transition-show="scale"
transition-hide="scale"
>
<CreateManualInvoiceForm />
</QDialog>
</QPage>
</template>
@ -246,9 +234,13 @@ en:
fileDenied: Browser denied file download...
fileAllowed: Successful download of CSV file
youCanSearchByInvoiceReference: You can search by invoice reference
downloadPdf: Download PDF
createInvoice: Make invoice
es:
searchInvoice: Buscar factura emitida
fileDenied: El navegador denegó la descarga de archivos...
fileAllowed: Descarga exitosa de archivo CSV
youCanSearchByInvoiceReference: Puedes buscar por referencia de la factura
downloadPdf: Descargar PDF
createInvoice: Crear factura
</i18n>

View File

@ -1,17 +1,17 @@
<script setup>
import { ref, computed, onBeforeMount } from 'vue';
import { ref, computed, onBeforeMount, onMounted, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { QCheckbox, QBtn } from 'quasar';
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 { useInvoiceOutGlobalStore } from 'src/stores/invoiceOutGlobal.js';
import { useStateStore } from 'stores/useStateStore';
import { useArrayData } from 'composables/useArrayData';
import VnUserLink from 'src/components/ui/VnUserLink.vue';
const invoiceOutGlobalStore = useInvoiceOutGlobalStore();
const stateStore = useStateStore();
@ -45,78 +45,16 @@ onBeforeMount(async () => {
stateStore.rightDrawer = true;
});
const componentIsRendered = ref(false);
onMounted(() =>
nextTick(() => {
componentIsRendered.value = true;
})
);
const rows = computed(() => arrayData.value.store.data);
const selectedCustomerId = ref(0);
const selectedWorkerId = ref(0);
const tableColumnComponents = {
company: {
component: 'span',
props: () => {},
event: () => {},
},
country: {
component: 'span',
props: () => {},
event: () => {},
},
clientId: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: (prop) => selectCustomerId(prop.value),
},
client: {
component: 'span',
props: () => {},
event: () => {},
},
amount: {
component: 'span',
props: () => {},
event: () => {},
},
base: {
component: 'span',
props: () => {},
event: () => {},
},
ticketId: {
component: 'span',
props: () => {},
event: () => {},
},
active: {
component: QCheckbox,
props: (prop) => ({
disable: true,
'model-value': Boolean(prop.value),
}),
event: () => {},
},
hasToInvoice: {
component: QCheckbox,
props: (prop) => ({
disable: true,
'model-value': Boolean(prop.value),
}),
event: () => {},
},
verifiedData: {
component: QCheckbox,
props: (prop) => ({
disable: true,
'model-value': Boolean(prop.value),
}),
event: () => {},
},
comercial: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: (prop) => selectWorkerId(prop.row.comercialId),
},
};
const columns = computed(() => [
{
label: t('invoiceOut.negativeBases.company'),
@ -205,20 +143,17 @@ const downloadCSV = async () => {
params
);
};
const selectCustomerId = (id) => {
selectedCustomerId.value = id;
};
const selectWorkerId = (id) => {
selectedWorkerId.value = id;
};
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<QBtn color="primary" icon-right="archive" no-caps @click="downloadCSV()" />
<Teleport
to="#st-actions"
v-if="stateStore?.isSubToolbarShown() && componentIsRendered"
>
<QBtn color="primary" icon-right="download" no-caps @click="downloadCSV()">
<QTooltip>{{ t('Download as CSV') }}</QTooltip>
</QBtn>
</Teleport>
</template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
@ -236,31 +171,37 @@ const selectWorkerId = (id) => {
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
>
<template #body-cell="props">
<QTd :props="props">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="tableColumnComponents[props.col.name].props(props)"
@click="tableColumnComponents[props.col.name].event(props)"
>
<template
v-if="
props.col.name !== 'active' &&
props.col.name !== 'hasToInvoice' &&
props.col.name !== 'verifiedData'
"
>{{ props.value }}
</template>
<CustomerDescriptorProxy
v-if="props.col.name === 'clientId'"
:id="selectedCustomerId"
/>
<VnUserLink
v-if="props.col.name === 'comercial'"
:worker-id="selectedWorkerId"
/>
</component>
<template #body-cell-clientId="{ row }">
<QTd>
<QBtn flat dense color="blue"> {{ row.clientId }}</QBtn>
<CustomerDescriptorProxy :id="row.clientId" />
</QTd>
</template>
<template #body-cell-ticketId="{ row }">
<QTd>
<QBtn flat dense color="blue"> {{ row.ticketFk }}</QBtn>
<TicketDescriptorProxy :id="row.ticketFk" />
</QTd>
</template>
<template #body-cell-comercial="{ row }">
<QTd>
<QBtn flat dense color="blue">{{ row.comercialName }}</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>
@ -274,4 +215,7 @@ const selectWorkerId = (id) => {
}
</style>
<i18n></i18n>
<i18n>
es:
Download as CSV: Descargar como CSV
</i18n>

View File

@ -44,7 +44,7 @@ export default {
name: 'InvoiceOutNegativeBases',
meta: {
title: 'negativeBases',
icon: 'view_list',
icon: 'vn:ticket',
},
component: () =>
import('src/pages/InvoiceOut/InvoiceOutNegativeBases.vue'),

View File

@ -73,6 +73,9 @@ export const useInvoiceOutGlobalStore = defineStore({
const stringDate = data.issued.substring(0, 10);
this.minInvoicingDate = stringDate;
this.formInitialData.invoiceDate = stringDate;
this.minInvoicingDate = new Date(data.issued);
this.formInitialData.invoiceDate = this.minInvoicingDate;
} catch (err) {
console.error('Error fetching invoice out global initial data');
throw new Error();
@ -103,12 +106,8 @@ export const useInvoiceOutGlobalStore = defineStore({
if (clientsToInvoice == 'all') params.clientId = undefined;
const addressesResponse = await await axios.post(
'InvoiceOuts/clientsToInvoice',
params
);
this.addresses = addressesResponse.data;
const { data } = await axios.post('InvoiceOuts/clientsToInvoice', params);
this.addresses = data;
if (!this.addresses || !this.addresses.length > 0) {
notify(
@ -118,9 +117,9 @@ export const useInvoiceOutGlobalStore = defineStore({
throw new Error("There aren't addresses to invoice");
}
this.addresses.forEach(async (address) => {
for (const address of this.addresses) {
await this.invoiceClient(address, formData);
});
}
} catch (err) {
this.handleError(err);
}
@ -186,15 +185,10 @@ export const useInvoiceOutGlobalStore = defineStore({
this.status = 'invoicing';
this.invoicing = true;
const invoiceResponse = await axios.post(
'InvoiceOuts/invoiceClient',
params
);
if (invoiceResponse.data) {
this.makePdfAndNotify(invoiceResponse.data, address);
}
const { data } = await axios.post('InvoiceOuts/invoiceClient', params);
if (data) await this.makePdfAndNotify(data, address);
this.addressIndex++;
this.isInvoicing = false;
} catch (err) {
if (
@ -203,8 +197,7 @@ export const useInvoiceOutGlobalStore = defineStore({
err.response.status >= 400 &&
err.response.status < 500
) {
this.invoiceClientError(address, err.response);
this.addressIndex++;
this.invoiceClientError(address, err.response?.data?.error?.message);
return;
} else {
this.invoicing = false;
@ -227,12 +220,11 @@ export const useInvoiceOutGlobalStore = defineStore({
this.nPdfs++;
this.nRequests--;
} catch (err) {
this.invoiceClientError(client, err, true);
this.invoiceClientError(client, err.response?.data?.error?.message, true);
}
},
invoiceClientError(client, response, isWarning) {
const message = response.data?.error?.message || response.message;
invoiceClientError(client, message, isWarning) {
this.errors.unshift({ client, message, isWarning });
},