forked from verdnatura/salix-front
The negative bases view was created for issued invoices.
This commit is contained in:
commit
5246427e8d
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,10 +5,17 @@ export function useUserConfig() {
|
|||
const state = useState();
|
||||
|
||||
async function fetch() {
|
||||
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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -349,6 +349,7 @@ export default {
|
|||
invoiceOuts: 'Invoices Out',
|
||||
list: 'List',
|
||||
negativeBases: 'Negative Bases',
|
||||
globalInvoicing: 'Global invoicing',
|
||||
createInvoiceOut: 'Create invoice out',
|
||||
summary: 'Summary',
|
||||
basicData: 'Basic Data',
|
||||
|
@ -390,6 +391,18 @@ export default {
|
|||
shipped: 'Shipped',
|
||||
totalWithVat: 'Amount',
|
||||
},
|
||||
globalInvoices: {
|
||||
errors: {
|
||||
chooseValidClient: 'Choose a valid client',
|
||||
chooseValidCompany: 'Choose a valid company',
|
||||
chooseValidPrinter: 'Choose a valid printer',
|
||||
fillDates: 'Invoice date and the max date should be filled',
|
||||
invoiceDateLessThanMaxDate: "Invoice date can't be less than max date",
|
||||
invoiceWithFutureDate: 'Exists an invoice with a future date',
|
||||
noTicketsToInvoice: "There aren't clients to invoice",
|
||||
criticalInvoiceError: 'Critical invoicing error, process stopped',
|
||||
},
|
||||
},
|
||||
},
|
||||
worker: {
|
||||
pageTitles: {
|
||||
|
|
|
@ -349,6 +349,7 @@ export default {
|
|||
invoiceOuts: 'Fact. emitidas',
|
||||
list: 'Listado',
|
||||
negativeBases: 'Bases Negativas',
|
||||
globalInvoicing: 'Facturación global',
|
||||
createInvoiceOut: 'Crear fact. emitida',
|
||||
summary: 'Resumen',
|
||||
basicData: 'Datos básicos',
|
||||
|
@ -390,6 +391,20 @@ export default {
|
|||
shipped: 'F. envío',
|
||||
totalWithVat: 'Importe',
|
||||
},
|
||||
globalInvoices: {
|
||||
errors: {
|
||||
chooseValidClient: 'Selecciona un cliente válido',
|
||||
chooseValidCompany: 'Selecciona una empresa válida',
|
||||
chooseValidPrinter: 'Selecciona una impresora válida',
|
||||
fillDates:
|
||||
'La fecha de la factura y la fecha máxima deben estar completas',
|
||||
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',
|
||||
criticalInvoiceError: 'Error crítico en la facturación, proceso detenido',
|
||||
},
|
||||
},
|
||||
},
|
||||
worker: {
|
||||
pageTitles: {
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
<script setup>
|
||||
import { onMounted, onUnmounted, computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStateStore } from 'stores/useStateStore';
|
||||
import InvoiceOutGlobalForm from './InvoiceOutGlobalForm.vue';
|
||||
import { useInvoiceOutGlobalStore } from 'src/stores/invoiceOutGlobal.js';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { QBadge, QBtn } from 'quasar';
|
||||
import CustomerDescriptor from 'src/pages/Customer/Card/CustomerDescriptor.vue';
|
||||
|
||||
const stateStore = useStateStore();
|
||||
const { t } = useI18n();
|
||||
const invoiceOutGlobalStore = useInvoiceOutGlobalStore();
|
||||
|
||||
// invoiceOutGlobalStore state and getters
|
||||
const {
|
||||
status,
|
||||
getPercentage,
|
||||
getAddressNumber,
|
||||
getNAddresses,
|
||||
nPdfs,
|
||||
totalPdfs,
|
||||
errors,
|
||||
} = storeToRefs(invoiceOutGlobalStore);
|
||||
|
||||
const selectedCustomerId = ref(null);
|
||||
|
||||
const tableColumnComponents = {
|
||||
clientId: {
|
||||
component: QBtn,
|
||||
props: { flat: true, color: 'blue' },
|
||||
event: (prop) => selectCustomerId(prop.value),
|
||||
},
|
||||
clientName: {
|
||||
component: 'span',
|
||||
props: {},
|
||||
},
|
||||
id: {
|
||||
component: 'span',
|
||||
props: {},
|
||||
},
|
||||
nickname: {
|
||||
component: 'span',
|
||||
props: {},
|
||||
},
|
||||
message: {
|
||||
component: QBadge,
|
||||
props: { color: 'red' },
|
||||
},
|
||||
};
|
||||
|
||||
const columns = ref([
|
||||
{ label: 'Id', field: 'clientId', name: 'clientId', align: 'left' },
|
||||
{ label: 'Cliente', field: 'clientName', name: 'clientName', align: 'left' },
|
||||
{ label: 'Id dirección', field: 'id', name: 'id', align: 'left' },
|
||||
{ label: 'Dirección fiscal', field: 'nickname', name: 'nickname', align: 'left' },
|
||||
{ label: 'Error', field: 'message', name: 'message', align: 'left' },
|
||||
]);
|
||||
|
||||
const cardStatusText = computed(() => {
|
||||
return t(`status.${status.value}`);
|
||||
});
|
||||
|
||||
const percentageStatusText = computed(() => {
|
||||
return `${getPercentage.value}% (${getAddressNumber.value} ${t('of')} ${
|
||||
getNAddresses.value
|
||||
})`;
|
||||
});
|
||||
|
||||
const pdfStatusText = computed(() => {
|
||||
return `${nPdfs.value} ${t('of')} ${totalPdfs.value} PDFs`;
|
||||
});
|
||||
|
||||
const rows = computed(() => {
|
||||
if (!errors && !errors.length > 0) return [];
|
||||
return errors.value.map((row) => {
|
||||
return {
|
||||
...row.client,
|
||||
message: row.message,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const selectCustomerId = (id) => {
|
||||
selectedCustomerId.value = id;
|
||||
};
|
||||
|
||||
onMounted(() => (stateStore.rightDrawer = true));
|
||||
onUnmounted(() => {
|
||||
stateStore.rightDrawer = false;
|
||||
invoiceOutGlobalStore.$reset();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
|
||||
<QScrollArea class="fit text-grey-8">
|
||||
<InvoiceOutGlobalForm />
|
||||
</QScrollArea>
|
||||
</QDrawer>
|
||||
|
||||
<QPage class="column items-center q-pa-md">
|
||||
<QCard v-if="status" class="card">
|
||||
<QCardSection class="card-section">
|
||||
<span class="status-text">{{ cardStatusText }}</span>
|
||||
<span class="text">{{ percentageStatusText }}</span>
|
||||
<span class="text">{{ pdfStatusText }}</span>
|
||||
</QCardSection>
|
||||
</QCard>
|
||||
|
||||
<QTable
|
||||
v-if="rows.length > 0"
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
hide-bottom
|
||||
row-key="id"
|
||||
class="full-width q-mt-md"
|
||||
>
|
||||
<template #body-cell="props">
|
||||
<QTd :props="props">
|
||||
<component
|
||||
:is="tableColumnComponents[props.col.name].component"
|
||||
v-bind="tableColumnComponents[props.col.name].props"
|
||||
@click="tableColumnComponents[props.col.name].event(props)"
|
||||
class="col-content"
|
||||
>
|
||||
{{ props.value }}
|
||||
<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>
|
|
@ -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>
|
|
@ -1,104 +1,260 @@
|
|||
<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 { 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 columns = ref([
|
||||
{ label: 'Company', field: 'company', name: 'company', align: 'left' },
|
||||
{ label: 'Country', field: 'country', name: 'country', align: 'left' },
|
||||
{ label: 'Client Id', 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: 'Ticket Id', field: 'ticketFk', name: 'ticketId', align: 'left' },
|
||||
{ label: 'Active', field: 'isActive', name: 'active', align: 'left' },
|
||||
{
|
||||
label: 'Has to invoice',
|
||||
field: 'hasToInvoice',
|
||||
name: 'hasToInvoice',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
label: 'Verified data',
|
||||
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...');
|
||||
/* $q.notify({
|
||||
message: 'Browser denied file download...',
|
||||
color: 'negative',
|
||||
icon: 'warning',
|
||||
}); */
|
||||
}
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
console.log(payload.value);
|
||||
rows.value = await invoiceOutService.getNegativeBases(payload.value);
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
payload.value = {
|
||||
from: new Date('2001-01-01'),
|
||||
to: new Date('2001-01-31'),
|
||||
};
|
||||
search();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
refresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="stateStore.isHeaderMounted()">
|
||||
<Teleport to="#searchbar">
|
||||
<VnSearchbar
|
||||
data-key="InvoiceOutList"
|
||||
:label="t('Search invoice')"
|
||||
:info="t('You can search by invoice reference')"
|
||||
/>
|
||||
</Teleport>
|
||||
<Teleport to="#actions-append">
|
||||
<div class="row q-gutter-x-sm">
|
||||
<QBtn flat @click="stateStore.toggleRightDrawer()" round dense icon="menu">
|
||||
<QTooltip bottom anchor="bottom right">
|
||||
{{ t("globals.collapseMenu") }}
|
||||
</QTooltip>
|
||||
</QBtn>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
<QPage class="column items-center q-pa-md">
|
||||
<QTable :columns="columns" :rows="invoiceOut.taxesBreakdown" flat>
|
||||
<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-sm q"
|
||||
:label="t('from')"
|
||||
/>
|
||||
<QInput
|
||||
dense
|
||||
v-model="payload.to"
|
||||
type="date"
|
||||
mask="date"
|
||||
class="q-mr-md q"
|
||||
:label="t('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">
|
||||
<QTr :props="props" class="full-height">
|
||||
<QTh v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ t(col.label) }}
|
||||
<div class="column justify-start items-start full-height">
|
||||
{{ col.label }}
|
||||
<QInput
|
||||
:disable="
|
||||
[
|
||||
'isActive',
|
||||
'hasToInvoice',
|
||||
'isTaxDataChecked',
|
||||
].includes(col.field)
|
||||
"
|
||||
:borderless="
|
||||
[
|
||||
'isActive',
|
||||
'hasToInvoice',
|
||||
'isTaxDataChecked',
|
||||
].includes(col.field)
|
||||
"
|
||||
dense
|
||||
standout
|
||||
v-model="payload[col.field]"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</QTh>
|
||||
</QTr>
|
||||
</template>
|
||||
<!-- <template #body-cell="props">
|
||||
<QTd :props="props">
|
||||
<component
|
||||
:is="tableColumnComponents[props.col.name].component"
|
||||
v-bind="tableColumnComponents[props.col.name].props"
|
||||
@click="tableColumnComponents[props.col.name].event(props)"
|
||||
class="col-content"
|
||||
>
|
||||
{{ props.value }}
|
||||
<QPopupProxy>
|
||||
<CustomerDescriptor
|
||||
v-if="selectedCustomerId === props.value"
|
||||
:id="selectedCustomerId"
|
||||
/>
|
||||
</QPopupProxy>
|
||||
</component>
|
||||
</QTd>
|
||||
</template> -->
|
||||
</QTable>
|
||||
</QPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useQuasar } from "quasar";
|
||||
import { useStateStore } from "stores/useStateStore";
|
||||
import InvoiceOutSummaryDialog from "./Card/InvoiceOutSummaryDialog.vue";
|
||||
import { toCurrency } from "src/filters/index";
|
||||
import VnSearchbar from "src/components/ui/VnSearchbar.vue";
|
||||
|
||||
const stateStore = useStateStore();
|
||||
const router = useRouter();
|
||||
const quasar = useQuasar();
|
||||
const { t } = useI18n();
|
||||
|
||||
onMounted(() => (stateStore.rightDrawer = true));
|
||||
onUnmounted(() => (stateStore.rightDrawer = false));
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
name: 'item',
|
||||
label: 'invoiceOut.summary.type',
|
||||
field: (row) => row.name,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'landed',
|
||||
label: 'invoiceOut.summary.taxableBase',
|
||||
field: (row) => row.taxableBase,
|
||||
format: (value) => toCurrency(value),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'quantity',
|
||||
label: 'invoiceOut.summary.rate',
|
||||
field: (row) => row.rate,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'invoiceOuted',
|
||||
label: 'invoiceOut.summary.fee',
|
||||
field: (row) => row.vat,
|
||||
sortable: true,
|
||||
},
|
||||
]);
|
||||
|
||||
function navigate(id) {
|
||||
router.push({ path: `/invoice-out/${id}` });
|
||||
}
|
||||
|
||||
function viewSummary(id) {
|
||||
quasar.dialog({
|
||||
component: InvoiceOutSummaryDialog,
|
||||
componentProps: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-list {
|
||||
.card {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-width: 60em;
|
||||
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>
|
||||
es:
|
||||
Search invoice: Buscar factura emitida
|
||||
You can search by invoice reference: Puedes buscar por referencia de la factura
|
||||
{
|
||||
"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>
|
||||
|
|
|
@ -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', 'InvoiceOutNegativeBases'],
|
||||
main: ['InvoiceOutList', 'InvoiceOutGlobal', 'InvoiceOutNegativeBases'],
|
||||
card: [],
|
||||
},
|
||||
children: [
|
||||
|
@ -29,6 +29,15 @@ export default {
|
|||
},
|
||||
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',
|
||||
|
@ -36,9 +45,10 @@ export default {
|
|||
title: 'negativeBases',
|
||||
icon: 'view_list',
|
||||
},
|
||||
component: () => import('src/pages/InvoiceOut/InvoiceOutNegativeBases.vue'),
|
||||
component: () =>
|
||||
import('src/pages/InvoiceOut/InvoiceOutNegativeBases.vue'),
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'InvoiceOutCard',
|
||||
|
@ -50,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'),
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
});
|
Loading…
Reference in New Issue