diff --git a/src/composables/useNotify.js b/src/composables/useNotify.js
new file mode 100644
index 0000000000..30bb70b28e
--- /dev/null
+++ b/src/composables/useNotify.js
@@ -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,
+ };
+}
diff --git a/src/composables/useState.js b/src/composables/useState.js
index f0ff830abd..1c797e9922 100644
--- a/src/composables/useState.js
+++ b/src/composables/useState.js
@@ -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,
};
}
diff --git a/src/composables/useUserConfig.js b/src/composables/useUserConfig.js
index 767ffb54e5..2a22ae3397 100644
--- a/src/composables/useUserConfig.js
+++ b/src/composables/useUserConfig.js
@@ -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 {
diff --git a/src/i18n/en/index.js b/src/i18n/en/index.js
index 948332d469..106bc1a6a5 100644
--- a/src/i18n/en/index.js
+++ b/src/i18n/en/index.js
@@ -348,6 +348,7 @@ export default {
pageTitles: {
invoiceOuts: 'Invoices Out',
list: 'List',
+ globalInvoicing: 'Global invoicing',
createInvoiceOut: 'Create invoice out',
summary: 'Summary',
basicData: 'Basic Data',
@@ -389,6 +390,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: {
diff --git a/src/i18n/es/index.js b/src/i18n/es/index.js
index 9b452ab227..c15d6e714e 100644
--- a/src/i18n/es/index.js
+++ b/src/i18n/es/index.js
@@ -348,6 +348,7 @@ export default {
pageTitles: {
invoiceOuts: 'Fact. emitidas',
list: 'Listado',
+ globalInvoicing: 'Facturación global',
createInvoiceOut: 'Crear fact. emitida',
summary: 'Resumen',
basicData: 'Datos básicos',
@@ -389,6 +390,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: {
diff --git a/src/pages/InvoiceOut/InvoiceOutGlobal.vue b/src/pages/InvoiceOut/InvoiceOutGlobal.vue
new file mode 100644
index 0000000000..2b260ae9eb
--- /dev/null
+++ b/src/pages/InvoiceOut/InvoiceOutGlobal.vue
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ cardStatusText }}
+ {{ percentageStatusText }}
+ {{ pdfStatusText }}
+
+
+
+
+
+
+
+ {{ props.value }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ "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"
+ }
+ }
+
diff --git a/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue b/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue
new file mode 100644
index 0000000000..baa1b86edf
--- /dev/null
+++ b/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue
@@ -0,0 +1,188 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ "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"
+ }
+ }
+
diff --git a/src/router/modules/invoiceOut.js b/src/router/modules/invoiceOut.js
index daf5041e9c..e8e18a8109 100644
--- a/src/router/modules/invoiceOut.js
+++ b/src/router/modules/invoiceOut.js
@@ -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'],
card: [],
},
children: [
@@ -28,8 +28,17 @@ 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'),
+ },
+ ],
},
{
name: 'InvoiceOutCard',
@@ -41,11 +50,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'),
+ },
+ ],
},
- ]
+ ],
};
diff --git a/src/services/InvoiceOut.service.js b/src/services/InvoiceOut.service.js
new file mode 100644
index 0000000000..0399434839
--- /dev/null
+++ b/src/services/InvoiceOut.service.js
@@ -0,0 +1,53 @@
+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 = {
+ 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;
diff --git a/src/stores/invoiceOutGlobal.js b/src/stores/invoiceOutGlobal.js
new file mode 100644
index 0000000000..7223f76554
--- /dev/null
+++ b/src/stores/invoiceOutGlobal.js
@@ -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;
+ },
+ },
+});