0
0
Fork 0

merge with dev

This commit is contained in:
William Buezas 2023-11-22 15:26:59 -03:00
commit d0cb96ce23
18 changed files with 1295 additions and 144 deletions

10
.vscode/settings.json vendored
View File

@ -14,5 +14,13 @@
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"cSpell.words": ["axios"]
"cSpell.words": ["axios"],
"editor.tabSize": 2,
"files.autoSave": "onFocusChange",
"files.trimTrailingWhitespace": true,
"editor.hover.enabled": true,
"editor.formatOnPaste": true,
"editor.wordWrapColumn": 80,
"prettier.singleQuote": true
}

12
package-lock.json generated
View File

@ -1,14 +1,14 @@
{
"name": "salix-front",
"version": "23.40.01",
"version": "23.48.01",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "salix-front",
"version": "0.0.1",
"version": "23.48.01",
"dependencies": {
"@quasar/cli": "^2.2.1",
"@quasar/cli": "^2.3.0",
"@quasar/extras": "^1.16.4",
"axios": "^1.4.0",
"chromium": "^3.0.3",
@ -946,9 +946,9 @@
}
},
"node_modules/@quasar/cli": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@quasar/cli/-/cli-2.2.1.tgz",
"integrity": "sha512-PMwJ76IeeNRRBw+08hUMjhqGC6JKJ/t1zIb+IOiyR5D4rkBR26Ha/Z46OD3KfwUprq4Q8s4ieB1+d3VY8FhPKg==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@quasar/cli/-/cli-2.3.0.tgz",
"integrity": "sha512-DNFDemicj3jXe5+Ib+5w9Bwj1U3yoHQkqn0bU/qysIl/p0MmGA1yqOfUF0V4fw/5or1dfCvStIA/oZxUcC+2pQ==",
"dependencies": {
"@quasar/ssl-certificate": "^1.0.0",
"ci-info": "^3.8.0",

View File

@ -52,6 +52,16 @@ watch($props, async () => {
entity.value = null;
await fetch();
});
const options = [
'Transferir factura a ...',
'Ver factura ...',
'Enviar factura ...',
'Eliminar factura',
'Asentar factura',
'Regenerar PDF factura',
'Abono ...',
];
</script>
<template>
@ -60,13 +70,13 @@ watch($props, async () => {
<div class="header bg-primary q-pa-sm">
<RouterLink :to="{ name: `${module}List` }">
<QBtn
round
flat
dense
size="md"
icon="view_list"
color="white"
class="link"
color="white"
dense
flat
icon="view_list"
round
size="md"
>
<QTooltip>
{{ t('components.cardDescriptor.mainList') }}
@ -75,28 +85,32 @@ watch($props, async () => {
</RouterLink>
<RouterLink :to="{ name: `${module}Summary`, params: { id: entity.id } }">
<QBtn
round
flat
dense
size="md"
icon="launch"
color="white"
class="link"
color="white"
dense
flat
icon="launch"
round
size="md"
>
<QTooltip>
{{ t('components.cardDescriptor.summary') }}
</QTooltip>
<QMenu auto-close>
<QList dense v-for="option in options" :key="option">
<QItem v-ripple clickable>
{{ option }}
</QItem>
</QList>
</QMenu>
</QBtn>
</RouterLink>
<QBtn
v-if="slots.menu"
size="md"
icon="more_vert"
color="white"
round
flat
dense
flat
icon="more_vert"
round
size="md"
v-if="slots.menu"
>
<QTooltip>
{{ t('components.cardDescriptor.moreOptions') }}

View File

@ -0,0 +1,103 @@
<script setup>
const $props = defineProps({
id: { type: Number, default: null },
title: { type: String, default: null },
});
</script>
<template>
<QCard class="card q-mb-md cursor-pointer q-hoverable bg-white-7 q-pa-lg">
<div>
<slot name="title">
<div class="flex">
<div class="title text-primary text-weight-bold text-h5">
{{ $props.title ?? `#${$props.id}` }}
</div>
<div class="q_chip">
<QChip outline color="grey" size="sm">ID: 12345</QChip>
</div>
</div>
</slot>
<div class="card-list-body">
<div class="list-items row flex-wrap-wrap">
<slot name="list-items" />
</div>
<div class="actions">
<slot name="actions" />
</div>
</div>
</div>
</QCard>
</template>
<style lang="scss">
.title {
margin-right: 25px;
}
.q_chip {
display: flex;
flex-direction: column;
justify-content: center;
}
.card-list-body {
display: flex;
justify-content: space-between;
margin-top: 10px;
.vn-label-value {
display: flex;
justify-content: flex-start;
gap: 2%;
width: 50%;
.label {
width: 35%;
color: var(--vn-label);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.value {
width: 65%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.actions {
display: flex;
flex-direction: column;
justify-content: center;
width: 25%;
}
}
@media (max-width: $breakpoint-xs) {
.card-list-body {
flex-wrap: wrap;
justify-content: center;
.vn-label-value {
width: 100%;
}
.actions {
width: 100%;
margin-top: 15px;
padding: 0 15%;
justify-content: center;
}
}
}
</style>
<style lang="scss" scoped>
.card {
transition: background-color 0.2s;
}
.card:hover {
background-color: var(--vn-gray);
}
.list-items {
width: 75%;
}
</style>

View File

@ -106,6 +106,7 @@ function formatValue(value) {
return `"${value}"`;
}
</script>
<template>
<QForm @submit="search">
<QList dense>
@ -119,32 +120,32 @@ function formatValue(value) {
<div class="q-gutter-xs">
<QBtn
@click="clearFilters"
icon="filter_list_off"
color="primary"
size="sm"
dense
flat
icon="filter_list_off"
padding="none"
round
flat
dense
size="sm"
>
<QTooltip>{{ t('Remove filters') }}</QTooltip>
</QBtn>
<QBtn
@click="reload"
icon="refresh"
color="primary"
size="sm"
dense
flat
icon="refresh"
padding="none"
round
flat
dense
size="sm"
>
<QTooltip>{{ t('Refresh') }}</QTooltip>
</QBtn>
</div>
</QItemSection>
</QItem>
<QItem>
<QItem class="q-mb-sm">
<div
v-if="tags.length === 0"
class="text-grey font-xs text-center full-width"
@ -153,14 +154,14 @@ function formatValue(value) {
</div>
<div>
<QChip
v-for="chip of tags"
:key="chip.label"
@remove="remove(chip.label)"
icon="label"
color="primary"
class="text-dark"
size="sm"
color="primary"
icon="label"
removable
size="sm"
v-for="chip of tags"
>
<slot name="tags" :tag="chip" :format-fn="formatValue">
<div class="q-gutter-x-xs">
@ -172,35 +173,35 @@ function formatValue(value) {
</div>
</QItem>
<QSeparator />
<template v-if="props.searchButton">
<QItem>
<QItemSection class="q-py-sm">
<QBtn
:label="t('Search')"
type="submit"
color="primary"
class="full-width"
icon="search"
unelevated
rounded
dense
/>
</QItemSection>
</QItem>
<QSeparator />
</template>
</QList>
<slot name="body" :params="userParams" :search-fn="search"></slot>
<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="submit"
unelevated
/>
</QItemSection>
</QItem>
<QSeparator />
</template>
</QForm>
<QInnerLoading
:showing="isLoading"
:label="t('globals.pleaseWait')"
:showing="isLoading"
color="primary"
/>
</template>
<i18n>
es:
es:
No filters applied: No se han aplicado filtros
Applied filters: Filtros aplicados
Remove filters: Eliminar filtros

View File

@ -0,0 +1,15 @@
import { Notify } from 'quasar';
import { i18n } from 'src/boot/i18n';
export default function useNotify() {
const notify = (message, type) => {
Notify.create({
message: i18n.global.t(message),
type: type,
});
};
return {
notify,
};
}

View File

@ -8,6 +8,7 @@ const user = ref({
nickname: '',
lang: '',
darkMode: null,
companyFk: null,
});
const roles = ref([]);
@ -23,6 +24,7 @@ export function useState() {
nickname: user.value.nickname,
lang: user.value.lang,
darkMode: user.value.darkMode,
companyFk: user.value.companyFk,
};
});
}
@ -34,6 +36,7 @@ export function useState() {
nickname: data.nickname,
lang: data.lang,
darkMode: data.darkMode,
companyFk: data.companyFk,
};
}
@ -59,7 +62,6 @@ export function useState() {
delete state.value[name];
}
return {
getUser,
setUser,
@ -69,6 +71,6 @@ export function useState() {
get,
unset,
drawer,
headerMounted
headerMounted,
};
}

View File

@ -5,10 +5,17 @@ export function useUserConfig() {
const state = useState();
async function fetch() {
const { data } = await axios.get('UserConfigs/getUserConfig');
const user = state.getUser().value;
user.darkMode = data.darkMode;
state.setUser(user);
try {
const { data } = await axios.get('UserConfigs/getUserConfig');
const user = state.getUser().value;
user.darkMode = data.darkMode;
user.companyFk = data.companyFk;
state.setUser(user);
return data;
} catch (error) {
console.error('Error fetching user config:', error);
}
}
return {

View File

@ -9,13 +9,10 @@ export default function (value, symbol = 'EUR', fractionSize = 2) {
style: 'currency',
currency: symbol,
minimumFractionDigits: fractionSize,
maximumFractionDigits: fractionSize
maximumFractionDigits: fractionSize,
};
const lang = locale.value == 'es' ? 'de' : locale.value;
return new Intl.NumberFormat(lang, options)
.format(value);
}
return new Intl.NumberFormat(lang, options).format(value);
}

View File

@ -348,6 +348,8 @@ export default {
pageTitles: {
invoiceOuts: 'Invoices Out',
list: 'List',
negativeBases: 'Negative Bases',
globalInvoicing: 'Global invoicing',
createInvoiceOut: 'Create invoice out',
summary: 'Summary',
basicData: 'Basic Data',
@ -406,6 +408,21 @@ export default {
streetAddress: 'Street',
},
},
negativeBases: {
from: 'From',
to: 'To',
company: 'Company',
country: 'Country',
clientId: 'Client Id',
client: 'Client',
amount: 'Amount',
base: 'Base',
ticketId: 'Ticket Id',
active: 'Active',
hasToInvoice: 'Has to Invoice',
verifiedData: 'Verified Data',
comercial: 'Comercial',
},
},
worker: {
pageTitles: {

View File

@ -348,6 +348,8 @@ export default {
pageTitles: {
invoiceOuts: 'Fact. emitidas',
list: 'Listado',
negativeBases: 'Bases Negativas',
globalInvoicing: 'Facturación global',
createInvoiceOut: 'Crear fact. emitida',
summary: 'Resumen',
basicData: 'Datos básicos',
@ -408,6 +410,21 @@ export default {
streetAddress: 'Dirección fiscal',
},
},
negativeBases: {
from: 'Desde',
to: 'Hasta',
company: 'Empresa',
country: 'País',
clientId: 'ID Cliente',
client: 'Cliente',
amount: 'Importe',
base: 'Base',
ticketId: 'ID Ticket',
active: 'Activo',
hasToInvoice: 'Tiene que facturar',
verifiedData: 'Datos verificados',
comercial: 'Comercial',
},
},
worker: {
pageTitles: {
@ -573,8 +590,8 @@ export default {
logOut: 'Cerrar sesión',
},
smartCard: {
openCard: 'Ver ficha',
openSummary: 'Abrir detalles',
openCard: 'Ficha',
openSummary: 'Detalles',
viewDescription: 'Ver descripción',
},
cardDescriptor: {

View File

@ -43,66 +43,72 @@ function setWorkers(data) {
<QItemSection>
<QInput
:label="t('Customer ID')"
v-model="params.clientFk"
class="q-mt-sm"
dense
lazy-rules
>
<template #prepend>
<QIcon name="vn:client" size="sm"></QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput :label="t('FI')" v-model="params.fi" lazy-rules>
<template #prepend>
<QIcon name="badge" size="sm"></QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput :label="t('Amount')" v-model="params.amount" lazy-rules>
<template #prepend>
<QIcon name="euro" size="sm"></QIcon>
</template>
</QInput>
outlined
rounded
v-model="params.clientFk"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput
:label="t('FI')"
class="q-mt-sm"
dense
lazy-rules
outlined
rounded
v-model="params.fi"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput
:label="t('Amount')"
class="q-mt-sm"
dense
lazy-rules
outlined
rounded
v-model="params.amount"
/>
</QItemSection>
</QItem>
<QItem class="q-mt-sm">
<QItemSection>
<QInput
:label="t('Min')"
dense
lazy-rules
outlined
rounded
type="number"
v-model.number="params.min"
lazy-rules
>
<template #prepend>
<QIcon name="euro" size="sm"></QIcon>
</template>
</QInput>
/>
</QItemSection>
<QItemSection>
<QInput
:label="t('Max')"
dense
lazy-rules
outlined
rounded
type="number"
v-model.number="params.max"
lazy-rules
>
<template #prepend>
<QIcon name="euro" size="sm"></QIcon>
</template>
</QInput>
/>
</QItemSection>
</QItem>
<QItem class="q-mb-md">
<QItemSection>
<QCheckbox
v-model="params.hasPdf"
@update:model-value="searchFn()"
:label="t('Has PDF')"
@update:model-value="searchFn()"
toggle-indeterminate
v-model="params.hasPdf"
/>
</QItemSection>
</QItem>
@ -112,15 +118,18 @@ function setWorkers(data) {
<QItemSection>
<QInput
:label="t('Issued')"
v-model="params.issued"
dense
mask="date"
outlined
rounded
v-model="params.issued"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
transition-show="scale"
>
<QDate v-model="params.issued" landscape>
<div
@ -134,9 +143,9 @@ function setWorkers(data) {
/>
<QBtn
:label="t('globals.confirm')"
@click="save"
color="primary"
flat
@click="save"
v-close-popup
/>
</div>
@ -151,15 +160,18 @@ function setWorkers(data) {
<QItemSection>
<QInput
:label="t('Created')"
v-model="params.created"
dense
mask="date"
outlined
rounded
v-model="params.created"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
transition-show="scale"
>
<QDate v-model="params.created" landscape>
<div
@ -173,9 +185,9 @@ function setWorkers(data) {
/>
<QBtn
:label="t('globals.confirm')"
@click="save"
color="primary"
flat
@click="save"
v-close-popup
/>
</div>
@ -188,13 +200,20 @@ function setWorkers(data) {
</QItem>
<QItem>
<QItemSection>
<QInput :label="t('Dued')" v-model="params.dued" mask="date">
<QInput
:label="t('Dued')"
dense
mask="date"
outlined
rounded
v-model="params.dued"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
transition-show="scale"
>
<QDate v-model="params.dued" landscape>
<div
@ -208,9 +227,9 @@ function setWorkers(data) {
/>
<QBtn
:label="t('globals.confirm')"
@click="save"
color="primary"
flat
@click="save"
v-close-popup
/>
</div>

View File

@ -0,0 +1,188 @@
<script setup>
import { onMounted, ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useInvoiceOutGlobalStore } from 'src/stores/invoiceOutGlobal.js';
import { storeToRefs } from 'pinia';
const { t } = useI18n();
const invoiceOutGlobalStore = useInvoiceOutGlobalStore();
// invoiceOutGlobalStore state and getters
const {
initialDataLoading,
formInitialData,
clientsOptions,
companiesOptions,
printersOptions,
invoicing,
printer,
status,
} = storeToRefs(invoiceOutGlobalStore);
// invoiceOutGlobalStore actions
const { makeInvoice, setPrinterValue, setStatusValue } = invoiceOutGlobalStore;
const formData = ref({
companyFk: null,
invoiceDate: null,
maxShipped: null,
clientId: null,
});
const getPrinter = computed({
get() {
return printer.value;
},
set(value) {
setPrinterValue(value.value);
},
});
const getStatus = computed({
get() {
return status.value;
},
set(value) {
setStatusValue(value.value);
},
});
const clientsToInvoice = ref('all');
const filteredClientsOptions = ref([]);
onMounted(async () => {
await invoiceOutGlobalStore.init();
formData.value = { ...formInitialData.value };
});
const inputSelectfilter = (val, update) => {
if (val === '') {
update(() => {
filteredClientsOptions.value = JSON.parse(
JSON.stringify(clientsOptions.value)
);
});
return;
}
update(() => {
const searchQuery = val.toLowerCase();
filteredClientsOptions.value = clientsOptions.value.filter(
(option) => option.label.toLowerCase().indexOf(searchQuery) > -1
);
});
};
</script>
<template>
<QForm
v-if="!initialDataLoading"
@submit="makeInvoice(formData, clientsToInvoice)"
class="q-pa-md text-white"
>
<div class="q-gutter-md">
<QRadio
v-model="clientsToInvoice"
dense
val="all"
:label="t('allClients')"
:dark="true"
/>
<QRadio
v-model="clientsToInvoice"
dense
val="one"
:label="t('oneClient')"
:dark="true"
/>
<QSelect
v-if="clientsToInvoice === 'one'"
:options="filteredClientsOptions"
v-model="formData.clientId"
filled
use-input
:label="t('client')"
@filter="inputSelectfilter"
transition-show="jump-up"
transition-hide="jump-up"
/>
<QInput
v-model="formData.invoiceDate"
type="date"
filled
mask="date"
:label="t('invoiceDate')"
/>
<QInput
v-model="formData.maxShipped"
type="date"
filled
mask="date"
:label="t('maxShipped')"
/>
<QSelect
filled
:options="companiesOptions"
v-model="formData.companyFk"
:label="t('company')"
transition-show="jump-up"
transition-hide="jump-up"
/>
<QSelect
filled
:options="printersOptions"
v-model="getPrinter"
:label="t('printer')"
transition-show="jump-up"
transition-hide="jump-up"
/>
</div>
<QBtn
v-if="!invoicing"
:label="t('invoiceOut')"
type="submit"
color="primary"
class="full-width q-mt-md"
unelevated
rounded
dense
/>
<QBtn
v-if="invoicing"
:label="t('stop')"
color="primary"
class="full-width q-mt-md"
unelevated
rounded
dense
@click="getStatus = 'stopping'"
/>
</QForm>
</template>
<i18n>
{
"en": {
"invoiceDate": "Invoice date",
"maxShipped": "Max date",
"allClients": "All clients",
"oneClient": "One client",
"company": "Company",
"printer": "Printer",
"invoiceOut": "Invoice out",
"client": "Client",
"stop": "Stop"
},
"es": {
"invoiceDate": "Fecha de factura",
"maxShipped": "Fecha límite",
"allClients": "Todos los clientes",
"oneClient": "Un solo cliente",
"company": "Empresa",
"printer": "Impresora",
"invoiceOut": "Facturar",
"client": "Cliente",
"stop": "Parar"
}
}
</i18n>

View File

@ -10,7 +10,7 @@ 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 CardList2 from 'src/components/ui/CardList2.vue';
const stateStore = useStateStore();
const router = useRouter();
@ -73,7 +73,7 @@ function viewSummary(id) {
auto-load
>
<template #body="{ rows }">
<CardList
<CardList2
v-for="row of rows"
:key="row.id"
:title="row.ref"
@ -111,21 +111,21 @@ function viewSummary(id) {
</template>
<template #actions>
<QBtn
flat
icon="arrow_circle_right"
:label="t('components.smartCard.openCard')"
@click.stop="navigate(row.id)"
>
<QTooltip>
{{ t('components.smartCard.openCard') }}
</QTooltip>
</QBtn>
<QBtn flat icon="preview" @click.stop="viewSummary(row.id)">
<QTooltip>
{{ t('components.smartCard.openSummary') }}
</QTooltip>
</QBtn>
color="white"
outline
type="reset"
/>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id)"
color="primary"
style="margin-top: 15px"
type="submit"
/>
</template>
</CardList>
</CardList2>
</template>
</VnPaginate>
</div>

View File

@ -0,0 +1,400 @@
<script setup>
import { onMounted, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
/* import { QBadge, QBtn } from 'quasar';
import CustomerDescriptor from 'src/pages/Customer/Card/CustomerDescriptor.vue'; */
import invoiceOutService from 'src/services/InvoiceOut.service';
import { toCurrency } from 'src/filters';
import { QBadge, QCheckbox, exportFile } from 'quasar';
const rows = ref([]);
const { t } = useI18n();
// invoiceOutGlobalStore state and getters
const payload = ref({
from: new Date('2001-01-01'),
to: new Date('2001-01-31'),
});
const filter = ref({
company: null,
country: null,
clientId: null,
client: null,
amount: null,
base: null,
ticketId: null,
active: null,
hasToInvoice: null,
verifiedData: null,
comercial: null,
});
const tableColumnComponents = {
company: {
component: 'span',
props: {},
},
country: {
component: 'span',
props: {},
},
clientId: {
component: 'a',
props: { href: '#' },
},
client: {
component: 'span',
props: {},
},
amount: {
component: 'span',
props: {},
},
base: {
component: 'span',
props: {},
},
ticketId: {
component: 'span',
props: {},
},
active: {
component: 'span',
props: { type: 'boolean' },
},
hasToInvoice: {
component: 'span',
props: { type: 'boolean' },
},
verifiedData: {
component: 'span',
props: { type: 'boolean' },
},
comercial: {
component: 'a',
props: { href: '#' },
},
};
const columns = ref([
{
label: 'company',
field: 'company',
name: 'company',
align: 'left',
},
{
label: 'country',
field: 'country',
name: 'country',
align: 'left',
},
{
label: 'clientId',
field: 'clientId',
name: 'clientId',
align: 'left',
},
{
label: 'client',
field: 'clientSocialName',
name: 'client',
align: 'left',
},
{
label: 'amount',
field: 'amount',
name: 'amount',
align: 'left',
format: (value) => toCurrency(value),
},
{
label: 'base',
field: 'taxableBase',
name: 'base',
align: 'left',
},
{
label: 'ticketId',
field: 'ticketFk',
name: 'ticketId',
align: 'left',
},
{
label: 'active',
field: 'isActive',
name: 'active',
align: 'left',
},
{
label: 'hasToInvoice',
field: 'hasToInvoice',
name: 'hasToInvoice',
align: 'left',
},
{
label: 'verifiedData',
field: 'isTaxDataChecked',
name: 'verifiedData',
align: 'left',
},
{
label: 'comercial',
field: 'comercialName',
name: 'comercial',
align: 'left',
},
]);
const wrapCsvValue = (val, formatFn, row) => {
let formatted = formatFn !== void 0 ? formatFn(val, row) : val;
formatted = formatted === void 0 || formatted === null ? '' : String(formatted);
formatted = formatted.split('"').join('""');
return `"${formatted}"`;
};
const exportTable = () => {
const content = [columns.value.map((col) => wrapCsvValue(col.label))]
.concat(
rows.value.map((row) =>
columns.value
.map((col) =>
wrapCsvValue(
typeof col.field === 'function'
? col.field(row)
: row[col.field === void 0 ? col.name : col.field],
col.format,
row
)
)
.join(',')
)
)
.join('\r\n');
const status = exportFile('table-export.csv', content, 'text/csv');
if (status !== true) {
console.log('Browser denied file download...');
}
};
const search = async () => {
const and = [];
Object.keys(filter.value).forEach((key) => {
if (filter.value[key]) {
and.push({
[key]: filter.value[key],
});
}
});
const params = {
...payload.value,
filter: {
limit: 20,
where: { and },
},
};
rows.value = await invoiceOutService.getNegativeBases(params);
};
const refresh = () => {
payload.value = {
from: new Date('2001-01-01'),
to: new Date('2001-01-31'),
};
filter.value = {
company: null,
country: null,
clientId: null,
client: null,
amount: null,
base: null,
ticketId: null,
active: null,
hasToInvoice: null,
verifiedData: null,
comercial: null,
};
search();
};
onMounted(async () => {
refresh();
});
</script>
<template>
<QPage class="column items-center q-pa-md">
<QTable
flat
:rows="rows"
:columns="columns"
hide-bottom
row-key="clientId"
class="full-width q-mt-md"
>
<template #top-left>
<div class="row justify-start items-end">
<QInput
dense
v-model="payload.from"
type="date"
mask="date"
class="q-mr-md q"
:label="t('invoiceOut.negativeBases.from')"
/>
<QInput
dense
v-model="payload.to"
type="date"
mask="date"
class="q-mr-md q"
:label="t('invoiceOut.negativeBases.to')"
/>
<QBtn
color="primary"
icon-right="archive"
no-caps
@click="exportTable"
/>
</div>
</template>
<template #top-right>
<div class="row justify-start items-center">
<span class="q-mr-md text-grey-7">
{{ rows.length }} {{ t('results') }}
</span>
<QBtn
color="primary"
icon-right="search"
no-caps
class="q-mr-sm"
@click="search"
/>
<QBtn color="primary" icon-right="refresh" no-caps @click="refresh" />
</div>
</template>
<template #header="props">
<QTr :props="props" class="full-height">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
<div class="column justify-start items-start full-height">
{{ t(`invoiceOut.negativeBases.${col.label}`) }}
<QInput
:disable="
[
'isActive',
'hasToInvoice',
'isTaxDataChecked',
].includes(col.field)
"
:borderless="
[
'isActive',
'hasToInvoice',
'isTaxDataChecked',
].includes(col.field)
"
dense
standout
v-model="filter[col.field]"
type="text"
/>
</div>
</QTh>
</QTr>
</template>
<template #body-cell="props">
<QTd :props="props">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="tableColumnComponents[props.col.name].props"
@click="tableColumnComponents[props.col.name].event(props)"
>
<span
v-if="
tableColumnComponents[props.col.name].props.type !=
'boolean'
"
>
{{ props.value }}
</span>
<span v-else>
<QBadge v-if="props.value" color="grey">
<QIcon name="check" size="xs" />
</QBadge>
<QBadge v-else color="grey" outline>
<QIcon name="" size="xs" />
</QBadge>
</span>
<QPopupProxy>
<CustomerDescriptor
v-if="selectedCustomerId === props.value"
:id="selectedCustomerId"
/>
</QPopupProxy>
</component>
</QTd>
</template>
</QTable>
</QPage>
</template>
<style lang="scss" scoped>
.card {
display: flex;
justify-content: center;
width: 100%;
background-color: #292929;
padding: 16px;
.card-section {
display: flex;
flex-direction: column;
padding: 0px;
}
.status-text {
font-size: 14px;
color: #eeeeee;
}
.text {
font-size: 14px;
color: #aaaaaa;
}
}
.col-content {
border-radius: 4px;
padding: 6px 6px 6px 6px;
}
</style>
<i18n>
{
"en": {
"status": {
"packageInvoicing": "Build packaging tickets",
"invoicing": "Invoicing client",
"stopping": "Stopping process",
"done": "Ended process"
},
"of": "of"
},
"es": {
"status":{
"packageInvoicing": "Generación de tickets de empaque",
"invoicing": "Facturando a cliente",
"stopping": "Deteniendo proceso",
"done": "Proceso detenido",
},
"of": "de"
}
}
</i18n>

View File

@ -5,12 +5,12 @@ export default {
name: 'InvoiceOut',
meta: {
title: 'invoiceOuts',
icon: 'vn:invoice-out'
icon: 'vn:invoice-out',
},
component: RouterView,
redirect: { name: 'InvoiceOutMain' },
menus: {
main: ['InvoiceOutList'],
main: ['InvoiceOutList', 'InvoiceOutGlobal', 'InvoiceOutNegativeBases'],
card: [],
},
children: [
@ -28,8 +28,27 @@ export default {
icon: 'view_list',
},
component: () => import('src/pages/InvoiceOut/InvoiceOutList.vue'),
}
]
},
{
path: 'global-invoicing',
name: 'InvoiceOutGlobal',
meta: {
title: 'globalInvoicing',
icon: 'view_list',
},
component: () => import('src/pages/InvoiceOut/InvoiceOutGlobal.vue'),
},
{
path: 'negative-bases',
name: 'InvoiceOutNegativeBases',
meta: {
title: 'negativeBases',
icon: 'view_list',
},
component: () =>
import('src/pages/InvoiceOut/InvoiceOutNegativeBases.vue'),
},
],
},
{
name: 'InvoiceOutCard',
@ -41,11 +60,12 @@ export default {
name: 'InvoiceOutSummary',
path: 'summary',
meta: {
title: 'summary'
title: 'summary',
},
component: () => import('src/pages/InvoiceOut/Card/InvoiceOutSummary.vue'),
}
]
component: () =>
import('src/pages/InvoiceOut/Card/InvoiceOutSummary.vue'),
},
],
},
]
],
};

View File

@ -0,0 +1,57 @@
import axios from 'axios';
const request = async (method, url, params = {}) => {
try {
let response;
if (method === 'GET') {
response = await axios.get(url, { params });
} else if (method === 'POST') {
response = await axios.post(url, params);
}
return response.data;
} catch (err) {
console.error(`Error with ${method} request to ${url}`, err);
return err.response;
}
};
const invoiceOutService = {
getNegativeBases: async (params) => {
return await request('GET', 'InvoiceOuts/negativeBases', params);
},
getInvoiceDate: async (params) => {
return await request('GET', 'InvoiceOuts/getInvoiceDate', params);
},
getFindOne: async (params) => {
return await request('GET', 'InvoiceOutConfigs/findOne', params);
},
getCompanies: async (filter) => {
return await request('GET', 'Companies', { filter });
},
getPrinters: async (filter) => {
return await request('GET', 'Printers', { filter });
},
getClients: async (filter) => {
return await request('GET', 'Clients', { filter });
},
getClientsToInvoice: async (params) => {
return await request('POST', 'InvoiceOuts/clientsToInvoice', params);
},
invoiceClient: async (params) => {
return await request('POST', 'InvoiceOuts/invoiceClient', params);
},
makePdfAndNotify: async (invoiceId, params) => {
return await request('POST', `InvoiceOuts/${invoiceId}/makePdfAndNotify`, params);
},
};
export default invoiceOutService;

View File

@ -0,0 +1,286 @@
import { defineStore } from 'pinia';
import { useUserConfig } from 'src/composables/useUserConfig';
import invoiceOutService from 'src/services/InvoiceOut.service.js';
import useNotify from 'src/composables/useNotify.js';
const { notify } = useNotify();
export const useInvoiceOutGlobalStore = defineStore({
id: 'invoiceOutGlobal',
state: () => ({
initialDataLoading: true,
formInitialData: {
companyFk: null,
invoiceDate: null,
maxShipped: null,
clientId: null,
},
clientsOptions: [],
companiesOptions: [],
printersOptions: [],
addresses: [],
minInvoicingDate: null,
parallelism: null,
invoicing: false,
isInvoicing: false,
status: null,
addressIndex: 0,
printer: null,
errors: [],
nRequests: 0,
nPdfs: 0,
totalPdfs: 0,
}),
actions: {
async init() {
await this.fetchAllData();
},
async fetchAllData() {
try {
const userInfo = await useUserConfig().fetch();
const date = Date.vnNew();
this.formInitialData.maxShipped = new Date(
date.getFullYear(),
date.getMonth(),
0
)
.toISOString()
.substring(0, 10);
await Promise.all([
this.fetchClients(),
this.fetchParallelism(),
this.fetchCompanies(userInfo.companyFk),
this.fetchPrinters(),
this.fetchInvoiceOutConfig(userInfo.companyFk),
]);
this.initialDataLoading = false;
} catch (err) {
console.error('Error fetching invoice out global initial data');
}
},
async fetchClients() {
const clientsFilter = { fields: ['id', 'name'], order: 'id', limit: 30 };
const clientsResponse = await invoiceOutService.getClients(clientsFilter);
this.clientsOptions = clientsResponse.map((client) => {
return { value: client.id, label: client.name };
});
},
async fetchCompanies(companyFk) {
const companiesFilters = { order: ['code'] };
const companiesResponse = await invoiceOutService.getCompanies(
companiesFilters
);
this.companiesOptions = companiesResponse.map((company) => {
return { value: company.id, label: company.code };
});
this.formInitialData.companyFk = this.companiesOptions.find(
(company) => companyFk === company.value
);
},
async fetchPrinters() {
const printersFilters = {
fields: ['id', 'name'],
where: { isLabeler: false },
order: 'name ASC',
limit: 30,
};
const printersResponse = await invoiceOutService.getPrinters(printersFilters);
this.printersOptions = printersResponse.map((printer) => {
return { value: printer.id, label: printer.name };
});
},
async fetchInvoiceOutConfig(companyFk) {
const params = { companyFk: companyFk };
const { issued } = await invoiceOutService.getInvoiceDate(params);
const stringDate = issued.substring(0, 10);
this.minInvoicingDate = stringDate;
this.formInitialData.invoiceDate = stringDate;
},
async fetchParallelism() {
const filter = { fields: ['parallelism'] };
const { parallelism } = await invoiceOutService.getFindOne(filter);
this.parallelism = parallelism;
},
async makeInvoice(formData, clientsToInvoice) {
this.invoicing = true;
this.status = 'packageInvoicing';
try {
const params = {
invoiceDate: new Date(formData.invoiceDate),
maxShipped: new Date(formData.maxShipped),
clientId: formData.clientId ? formData.clientId.value : null,
companyFk: formData.companyFk.value,
};
this.validateMakeInvoceParams(params, clientsToInvoice);
if (clientsToInvoice == 'all') params.clientId = undefined;
const addressesResponse = await invoiceOutService.getClientsToInvoice(
params
);
this.addresses = addressesResponse;
if (!this.addresses || !this.addresses.length > 0) {
notify(
'invoiceOut.globalInvoices.errors.noTicketsToInvoice',
'negative'
);
throw new Error("There aren't addresses to invoice");
}
this.addresses.forEach(async (address) => {
await this.invoiceClient(address, formData);
});
} catch (err) {
this.handleError(err);
}
},
validateMakeInvoceParams(params, clientsToInvoice) {
if (clientsToInvoice === 'one' && !params.clientId) {
notify('invoiceOut.globalInvoices.errors.chooseValidClient', 'negative');
throw new Error('Invalid client');
}
if (!params.invoiceDate || !params.maxShipped) {
notify('invoiceOut.globalInvoices.errors.fillDates', 'negative');
throw new Error('Missing dates');
}
if (params.invoiceDate < params.maxShipped) {
notify(
'invoiceOut.globalInvoices.errors.invoiceDateLessThanMaxDate',
'negative'
);
throw new Error('Invalid date range');
}
const invoiceDateTime = new Date(params.invoiceDate).getTime();
const minInvoiceDateTime = new Date(this.minInvoicingDate).getTime();
if (this.minInvoicingDate && invoiceDateTime < minInvoiceDateTime) {
notify(
'invoiceOut.globalInvoices.errors.invoiceWithFutureDate',
'negative'
);
throw new Error('Invoice date in the future');
}
if (!params.companyFk) {
notify('invoiceOut.globalInvoices.errors.chooseValidCompany', 'negative');
throw new Error('Invalid company');
}
if (!this.printer) {
notify('invoiceOut.globalInvoices.errors.chooseValidPrinter', 'negative');
throw new Error('Invalid printer');
}
},
async invoiceClient(address, formData) {
if (this.nRequests === this.parallelism || this.isInvoicing) return;
if (this.status === 'stopping') {
if (this.nRequests) return;
this.invoicing = false;
this.status = 'done';
return;
}
const params = {
clientId: address.clientId,
addressId: address.id,
invoiceDate: new Date(formData.invoiceDate),
maxShipped: new Date(formData.maxShipped),
companyFk: formData.companyFk.value,
};
this.status = 'invoicing';
this.invoicing = true;
const invoiceResponse = await invoiceOutService.invoiceClient(params);
if (invoiceResponse.data.error) {
if (invoiceResponse.status >= 400 && invoiceResponse.status < 500) {
this.invoiceClientError(address, invoiceResponse);
this.addressIndex++;
return;
} else {
this.invoicing = false;
this.status = 'done';
notify(
'invoiceOut.globalInvoices.errors.criticalInvoiceError',
'negative'
);
throw new Error('Critical invoicing error, process stopped');
}
} else {
this.isInvoicing = false;
if (invoiceResponse.data) {
this.makePdfAndNotify(invoiceResponse.data, address);
}
}
},
async makePdfAndNotify(invoiceId, client) {
try {
this.nRequests++;
this.totalPdfs++;
const params = { printerFk: this.printer.value };
await invoiceOutService.makePdfAndNotify(invoiceId, params);
this.nPdfs++;
this.nRequests--;
} catch (err) {
this.invoiceClientError(client, err, true);
}
},
invoiceClientError(client, response, isWarning) {
const message = response.data?.error?.message || response.message;
this.errors.unshift({ client, message, isWarning });
},
handleError(err) {
this.invoicing = false;
this.status = null;
throw err;
},
// State mutations actions
setPrinterValue(printer) {
this.printer = printer;
},
setStatusValue(status) {
this.status = status;
},
},
getters: {
getNAddresses(state) {
return state.addresses.length;
},
getPercentage(state) {
if (this.getNAdresses <= 0 || !state.addressIndex) {
return 0;
}
let porcentaje = (state.addressIndex / this.getNAddresses) * 100;
return porcentaje;
},
getAddressNumber(state) {
return state.addressIndex;
},
},
});