Merge pull request '#7346 manualInvoice' (!793) from 7346-manualInvoice into dev
gitea/salix-front/pipeline/head This commit looks good Details

Reviewed-on: #793
Reviewed-by: Javier Segarra <jsegarra@verdnatura.es>
Reviewed-by: Alex Moreno <alexm@verdnatura.es>
This commit is contained in:
Javi Gallego 2024-11-19 07:10:06 +00:00
commit 9c271eee08
15 changed files with 282 additions and 335 deletions

View File

@ -1,155 +0,0 @@
<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 VnSelect from 'src/components/common/VnSelect.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
/>
<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>
<VnSelect
:label="t('Ticket')"
:options="ticketsOptions"
hide-selected
option-label="id"
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">
<QItemSection>
<QItemLabel> #{{ scope.opt?.id }} </QItemLabel>
<QItemLabel caption>{{ scope.opt?.nickname }}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<span class="row items-center" style="max-width: max-content">{{
t('Or')
}}</span>
<VnSelect
:label="t('Client')"
:options="clientsOptions"
hide-selected
option-label="name"
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>
<VnRow>
<VnSelect
:label="t('Serial')"
:options="invoiceOutSerialsOptions"
hide-selected
option-label="description"
option-value="code"
v-model="data.serial"
/>
<VnSelect
:label="t('Area')"
:options="taxAreasOptions"
hide-selected
option-label="code"
option-value="code"
v-model="data.taxArea"
/>
</VnRow>
<VnRow>
<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

@ -162,9 +162,7 @@ onMounted(() => {
: $props.defaultMode;
stateStore.rightDrawer = quasar.screen.gt.xs;
columnsVisibilitySkipped.value = [
...splittedColumns.value.columns
.filter((c) => c.visible == false)
.map((c) => c.name),
...splittedColumns.value.columns.filter((c) => !c.visible).map((c) => c.name),
...['tableActions'],
];
createForm.value = $props.create;
@ -237,7 +235,7 @@ function splitColumns(columns) {
if (col.create) splittedColumns.value.create.push(col);
if (col.cardVisible) splittedColumns.value.cardVisible.push(col);
if ($props.isEditable && col.disable == null) col.disable = false;
if ($props.useModel && col.columnFilter != false)
if ($props.useModel && col.columnFilter !== false)
col.columnFilter = { inWhere: true, ...col.columnFilter };
splittedColumns.value.columns.push(col);
}

View File

@ -1,8 +1,7 @@
<script setup>
import { onMounted, watch, computed, ref } from 'vue';
import { onMounted, watch, computed, ref, useAttrs } from 'vue';
import { date } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useAttrs } from 'vue';
import VnDate from './VnDate.vue';
import { useRequired } from 'src/composables/useRequired';

View File

@ -2,5 +2,12 @@
const model = defineModel({ type: Boolean, required: true });
</script>
<template>
<QRadio v-model="model" v-bind="$attrs" dense :dark="true" class="q-mr-sm" />
<QRadio
v-model="model"
v-bind="$attrs"
dense
:dark="true"
class="q-mr-sm"
size="xs"
/>
</template>

View File

@ -1,5 +1,5 @@
<template>
<div class="vn-row q-gutter-md q-mb-md">
<div class="vn-row q-gutter-md">
<slot />
</div>
</template>
@ -18,6 +18,9 @@
&:not(.wrap) {
flex-direction: column;
}
&[fixed] {
flex-direction: row;
}
}
}
</style>

View File

@ -96,7 +96,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
Object.assign(params, userParams);
params.filter.skip = store.skip;
if (store.order && store.order.length) params.filter.order = store.order;
if (store.order?.length) params.filter.order = store.order;
else delete params.filter.order;
params.filter = JSON.stringify(params.filter);

View File

@ -506,6 +506,7 @@ invoiceOut:
invoiceWithFutureDate: Exists an invoice with a future date
noTicketsToInvoice: There are not tickets to invoice
criticalInvoiceError: 'Critical invoicing error, process stopped'
invalidSerialTypeForAll: The serial type must be global when invoicing all clients
table:
addressId: Address id
streetAddress: Street
@ -857,6 +858,7 @@ components:
downloadFile: Download file
openCard: View
openSummary: Summary
viewSummary: Summary
cardDescriptor:
mainList: Main list
summary: Summary

View File

@ -509,6 +509,7 @@ invoiceOut:
invoiceWithFutureDate: Existe una factura con una fecha futura
noTicketsToInvoice: No existen tickets para facturar
criticalInvoiceError: Error crítico en la facturación proceso detenido
invalidSerialTypeForAll: El tipo de serie debe ser global cuando se facturan todos los clientes
table:
addressId: Id dirección
streetAddress: Dirección fiscal

View File

@ -183,7 +183,7 @@ onMounted(async () => {
<i18n>
en:
invoiceDate: Invoice date
maxShipped: Max date
maxShipped: Max date ticket
allClients: All clients
oneClient: One client
company: Company
@ -195,7 +195,7 @@ en:
es:
invoiceDate: Fecha de factura
maxShipped: Fecha límite
maxShipped: Fecha límite ticket
allClients: Todos los clientes
oneClient: Un solo cliente
company: Empresa

View File

@ -6,15 +6,19 @@ import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { usePrintService } from 'composables/usePrintService';
import VnTable from 'components/VnTable/VnTable.vue';
import { usePrintService } from 'src/composables/usePrintService';
import VnTable from 'src/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 CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue';
import axios from 'axios';
import RightMenu from 'src/components/common/RightMenu.vue';
import InvoiceOutFilter from './InvoiceOutFilter.vue';
import VnRow from 'src/components/ui/VnRow.vue';
import VnRadio from 'src/components/common/VnRadio.vue';
import VnInput from 'src/components/common/VnInput.vue';
import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue';
const { t } = useI18n();
const stateStore = useStateStore();
@ -26,99 +30,86 @@ const selectedRows = ref([]);
const hasSelectedCards = computed(() => selectedRows.value.length > 0);
const MODEL = 'InvoiceOuts';
const { openReport } = usePrintService();
const addressOptions = ref([]);
const selectedOption = ref('ticket');
async function fetchClientAddress(id) {
const { data } = await axios.get(
`Clients/${id}/addresses?filter[order]=isActive DESC`
);
addressOptions.value = data;
}
const exprBuilder = (_, value) => {
return {
or: [{ code: value }, { description: { like: `%${value}%` } }],
};
};
const columns = computed(() => [
{
align: 'center',
name: 'id',
label: t('invoiceOutList.tableVisibleColumns.id'),
chip: {
condition: () => true,
},
chip: { condition: () => true },
isId: true,
columnFilter: {
name: 'search',
},
columnFilter: { name: 'search' },
},
{
align: 'left',
name: 'ref',
label: t('invoiceOutList.tableVisibleColumns.ref'),
label: t('globals.reference'),
isTitle: true,
component: 'select',
attrs: {
url: MODEL,
optionLabel: 'ref',
optionValue: 'id',
},
columnField: {
component: null,
},
attrs: { url: MODEL, optionLabel: 'ref', optionValue: 'id' },
columnField: { component: null },
},
{
align: 'left',
name: 'Issued',
label: t('invoiceOutList.tableVisibleColumns.issued'),
name: 'issued',
label: t('invoiceOut.summary.issued'),
component: 'date',
format: (row) => toDate(row.issued),
columnField: {
component: null,
},
columnField: { component: null },
},
{
align: 'left',
name: 'clientFk',
label: t('invoiceOutModule.customer'),
label: t('globals.client'),
cardVisible: true,
component: 'select',
attrs: {
url: 'Clients',
fields: ['id', 'name'],
},
columnField: {
component: null,
},
attrs: { url: 'Clients', fields: ['id', 'name'] },
columnField: { component: null },
},
{
align: 'left',
name: 'companyCode',
label: t('invoiceOutModule.company'),
label: t('globals.company'),
cardVisible: true,
component: 'select',
attrs: {
url: 'Companies',
optionLabel: 'code',
optionValue: 'id',
},
columnField: {
component: null,
},
attrs: { url: 'Companies', optionLabel: 'code', optionValue: 'id' },
columnField: { component: null },
},
{
align: 'left',
name: 'amount',
label: t('invoiceOutModule.amount'),
label: t('globals.amount'),
cardVisible: true,
format: (row) => toCurrency(row.amount),
},
{
align: 'left',
name: 'created',
label: t('invoiceOutList.tableVisibleColumns.created'),
label: t('globals.created'),
component: 'date',
columnField: {
component: null,
},
columnField: { component: null },
format: (row) => toDate(row.created),
},
{
align: 'left',
name: 'dued',
label: t('invoiceOutList.tableVisibleColumns.dueDate'),
label: t('invoiceOut.summary.dued'),
component: 'date',
columnField: {
component: null,
},
columnField: { component: null },
format: (row) => toDate(row.dued),
},
{
@ -128,11 +119,12 @@ const columns = computed(() => [
{
title: t('components.smartCard.viewSummary'),
icon: 'preview',
isPrimary: true,
action: (row) => viewSummary(row.id, InvoiceOutSummary),
},
{
title: t('DownloadPdf'),
icon: 'vn:ticket',
title: t('globals.downloadPdf'),
icon: 'cloud_download',
isPrimary: true,
action: (row) => openPdf(row.id),
},
@ -181,7 +173,7 @@ watchEffect(selectedRows);
<template>
<VnSearchbar
:info="t('youCanSearchByInvoiceReference')"
:label="t('searchInvoice')"
:label="t('Search invoice')"
data-key="invoiceOutList"
/>
<RightMenu>
@ -197,7 +189,7 @@ watchEffect(selectedRows);
@click="downloadPdf()"
:disable="!hasSelectedCards"
>
<QTooltip>{{ t('globals.downloadPdf') }}</QTooltip>
<QTooltip>{{ t('downloadPdf') }}</QTooltip>
</QBtn>
</template>
</VnSubToolbar>
@ -207,11 +199,9 @@ watchEffect(selectedRows);
:url="`${MODEL}/filter`"
:create="{
urlCreate: 'InvoiceOuts/createManualInvoice',
title: t('Create manual invoice'),
title: t('createManualInvoice'),
onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {
active: true,
},
formInitialData: { active: true },
}"
:right-search="false"
v-model:selected="selectedRows"
@ -231,45 +221,158 @@ watchEffect(selectedRows);
</span>
</template>
<template #more-create-dialog="{ data }">
<div class="flex no-wrap flex-center">
<VnSelect
url="Tickets"
<div class="row q-col-gutter-xs">
<div class="col-12">
<div class="q-col-gutter-xs">
<VnRow fixed>
<VnRadio
v-model="selectedOption"
val="ticket"
:label="t('globals.ticket')"
class="q-my-none q-mr-md"
/>
<VnInput
v-show="selectedOption === 'ticket'"
v-model="data.ticketFk"
:label="t('invoiceOutList.tableVisibleColumns.ticket')"
option-label="id"
:label="t('globals.ticket')"
style="flex: 1"
/>
<div
class="row q-col-gutter-xs q-ml-none"
v-show="selectedOption !== 'ticket'"
>
<div class="col">
<VnSelect
v-model="data.clientFk"
:label="t('globals.client')"
url="Clients"
:options="customerOptions"
option-label="name"
option-value="id"
@update:model-value="fetchClientAddress"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItem
v-bind="scope.itemProps"
@click="selectedClient(scope.opt)"
>
<QItemSection>
<QItemLabel> #{{ scope.opt?.id }} </QItemLabel>
<QItemLabel caption>{{ scope.opt?.nickname }}</QItemLabel>
<QItemLabel>
#{{ scope.opt?.id }} -
{{ scope.opt?.name }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<span class="q-ml-md">O</span>
</div>
<div class="col">
<VnSelect
url="Clients"
v-model="data.clientFk"
:label="t('invoiceOutModule.customer')"
:options="customerOptions"
option-label="name"
v-model="data.addressFk"
:label="t('ticket.summary.consignee')"
:options="addressOptions"
option-label="nickname"
option-value="id"
v-if="
data.clientFk &&
selectedOption === 'consignatario'
"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel
:class="{
'color-vn-label':
!scope.opt?.isActive,
}"
>
{{
`${
!scope.opt?.isActive
? t('inactive')
: ''
} `
}}
<span>
{{
scope.opt?.nickname
}}</span
>
<span
v-if="
scope.opt?.province ||
scope.opt?.city ||
scope.opt?.street
"
>, {{ scope.opt?.street }},
{{ scope.opt?.city }},
{{
scope.opt?.province?.name
}}
-
{{
scope.opt?.agencyMode
?.name
}}</span
>
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</div>
</div>
</VnRow>
<VnRow fixed>
<VnRadio
v-model="selectedOption"
val="cliente"
:label="t('globals.client')"
class="q-my-none q-mr-md"
/>
</VnRow>
<VnRow fixed>
<VnRadio
v-model="selectedOption"
val="consignatario"
:label="t('ticket.summary.consignee')"
class="q-my-none q-mr-md"
/>
</VnRow>
</div>
</div>
<div class="full-width">
<VnRow class="row q-col-gutter-xs">
<VnSelect
url="InvoiceOutSerials"
v-model="data.serial"
:label="t('invoiceOutList.tableVisibleColumns.invoiceOutSerial')"
:label="t('invoiceIn.serial')"
:options="invoiceOutSerialsOptions"
option-label="description"
option-value="code"
/>
option-filter
:expr-builder="exprBuilder"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.code }} -
{{ scope.opt?.description }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnInputDate
:label="t('invoiceOutList.tableVisibleColumns.dueDate')"
:label="t('invoiceOut.summary.dued')"
v-model="data.maxShipped"
/>
</VnRow>
<VnRow class="row q-col-gutter-xs">
<VnSelect
url="TaxAreas"
v-model="data.taxArea"
@ -278,27 +381,43 @@ watchEffect(selectedRows);
option-label="code"
option-value="code"
/>
<QInput
<VnInput
v-model="data.reference"
:label="t('invoiceOutList.tableVisibleColumns.ref')"
:label="t('globals.reference')"
/>
</VnRow>
</div>
</div>
</template>
</VnTable>
</template>
<style lang="scss" scoped>
#formModel .vn-row {
min-height: 45px;
.q-radio {
align-self: flex-end;
flex: 0.3;
}
> .q-input,
> .q-select {
flex: 0.75;
}
}
</style>
<i18n>
en:
searchInvoice: Search issued invoice
fileDenied: Browser denied file download...
fileAllowed: Successful download of CSV file
invoiceId: Invoice ID
youCanSearchByInvoiceReference: You can search by invoice reference
createInvoice: Make invoice
Create manual invoice: Create manual invoice
createManualInvoice: Create Manual Invoice
inactive: (Inactive)
es:
searchInvoice: Buscar factura emitida
fileDenied: El navegador denegó la descarga de archivos...
fileAllowed: Descarga exitosa de archivo CSV
invoiceId: ID de factura
youCanSearchByInvoiceReference: Puedes buscar por referencia de la factura
createInvoice: Crear factura
Create manual invoice: Crear factura manual
createManualInvoice: Crear factura manual
inactive: (Inactivo)
</i18n>

View File

@ -2,6 +2,7 @@ invoiceOutModule:
customer: Client
amount: Amount
company: Company
address: Address
invoiceOutList:
tableVisibleColumns:
id: ID

View File

@ -4,6 +4,7 @@ invoiceOutModule:
customer: Cliente
amount: Importe
company: Empresa
address: Consignatario
invoiceOutList:
tableVisibleColumns:
id: ID

View File

@ -1,6 +1,6 @@
<script setup>
import { useRouter } from 'vue-router';
import { onMounted, ref } from 'vue';
import { reactive, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { useState } from 'composables/useState';
@ -9,7 +9,6 @@ import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'components/common/VnSelect.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import { useDialogPluginComponent } from 'quasar';
import { reactive } from 'vue';
const { t } = useI18n();
const state = useState();
@ -58,10 +57,6 @@ const fetchAgencyList = async (landed, addressFk) => {
}
};
// const fetchOrderDetails = (order) => {
// fetchAddressList(order?.addressFk);
// fetchAgencyList(order?.landed, order?.addressFk);
// };
const $props = defineProps({
clientFk: {
type: Number,
@ -73,39 +68,6 @@ const initialFormState = reactive({
addressId: null,
clientFk: $props.clientFk,
});
// const orderMapper = (order) => {
// return {
// addressId: order.addressFk,
// agencyModeId: order.agencyModeFk,
// landed: new Date(order.landed).toISOString(),
// };
// };
// const orderFilter = {
// include: [
// { relation: 'agencyMode', scope: { fields: ['name'] } },
// {
// relation: 'address',
// scope: { fields: ['nickname'] },
// },
// { relation: 'rows', scope: { fields: ['id'] } },
// {
// relation: 'client',
// scope: {
// fields: [
// 'salesPersonFk',
// 'name',
// 'isActive',
// 'isFreezed',
// 'isTaxDataChecked',
// ],
// include: {
// relation: 'salesPersonUser',
// scope: { fields: ['id', 'name'] },
// },
// },
// },
// ],
// };
const onClientChange = async (clientId = $props.clientFk) => {
try {

View File

@ -113,7 +113,7 @@ export default {
name: 'SupplierAccounts',
meta: {
title: 'accounts',
icon: 'vn:account',
icon: 'vn:credit',
},
component: () =>
import('src/pages/Supplier/Card/SupplierAccounts.vue'),

View File

@ -162,6 +162,15 @@ export const useInvoiceOutGlobalStore = defineStore({
);
throw new Error('Invalid Serial Type');
}
if (clientsToInvoice === 'all' && params.serialType !== 'global') {
notify(
'invoiceOut.globalInvoices.errors.invalidSerialTypeForAll',
'negative'
);
throw new Error('For "all" clients, the serialType must be "global"');
}
if (!params.companyFk) {
notify('invoiceOut.globalInvoices.errors.chooseValidCompany', 'negative');
throw new Error('Invalid company');