From e65e90d00f3efb6358e64c08001f15a9235097a1 Mon Sep 17 00:00:00 2001
From: joan <joan@verdnatura.es>
Date: Tue, 10 Aug 2021 11:48:29 +0200
Subject: [PATCH] Global invoicing improvements

---
 db/changes/10340-summer/00-ACL.sql            |   4 +-
 loopback/locale/en.json                       |   3 +-
 loopback/locale/es.json                       |   4 +-
 .../methods/invoiceOut/globalInvoicing.js     | 129 ++++++++++--------
 .../front/index/global-invoicing/index.html   |   8 ++
 .../front/index/global-invoicing/index.js     |  14 +-
 .../index/global-invoicing/locale/es.yml      |   8 +-
 .../front/index/global-invoicing/style.scss   |  14 +-
 .../invoiceOut/front/index/manual/index.html  |   8 ++
 .../invoiceOut/front/index/manual/index.js    |   6 +-
 .../front/index/manual/locale/es.yml          |   3 +-
 .../invoiceOut/front/index/manual/style.scss  |  14 +-
 12 files changed, 145 insertions(+), 70 deletions(-)

diff --git a/db/changes/10340-summer/00-ACL.sql b/db/changes/10340-summer/00-ACL.sql
index fd92b3c1c7..2824aae2a5 100644
--- a/db/changes/10340-summer/00-ACL.sql
+++ b/db/changes/10340-summer/00-ACL.sql
@@ -3,5 +3,7 @@ DELETE FROM `salix`.`ACL` WHERE id = 188;
 UPDATE `salix`.`ACL` tdms SET tdms.accessType = '*'
     WHERE tdms.id = 165;
 INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
-    VALUES ('InvoiceOut', 'createManualInvoice', 'WRITE', 'ALLOW', 'ROLE', 'invoicing');
+    VALUES 
+        ('InvoiceOut', 'createManualInvoice', 'WRITE', 'ALLOW', 'ROLE', 'invoicing'),
+        ('InvoiceOut', 'globalInvoicing', 'WRITE', 'ALLOW', 'ROLE', 'invoicing');
 
diff --git a/loopback/locale/en.json b/loopback/locale/en.json
index d4de6e55b8..1781e09752 100644
--- a/loopback/locale/en.json
+++ b/loopback/locale/en.json
@@ -110,5 +110,6 @@
 	"nickname": "nickname",
 	"State": "State",
 	"regular": "regular",
-	"reserved": "reserved"
+	"reserved": "reserved",
+	"Global invoicing failed": "[Global invoicing] Wasn't able to invoice some of the clients"
 }
\ No newline at end of file
diff --git a/loopback/locale/es.json b/loopback/locale/es.json
index 92cd8d3432..9c8f15a946 100644
--- a/loopback/locale/es.json
+++ b/loopback/locale/es.json
@@ -203,6 +203,6 @@
 	"This ticket is already invoiced": "Este ticket ya está facturado",
 	"A ticket with an amount of zero can't be invoiced": "No se puede facturar un ticket con importe cero",
 	"A ticket with a negative base can't be invoiced": "No se puede facturar un ticket con una base negativa",
-	"Not invoiceable": "Not invoiceable",
-	"Not invoiceable 1101": "Not invoiceable 1101"
+	"Global invoicing failed": "[Facturación global] No se han podido facturar algunos clientes",
+	"Wasn't able to invoice the following clients": "No se han podido facturar los siguientes clientes"
 }
\ No newline at end of file
diff --git a/modules/invoiceOut/back/methods/invoiceOut/globalInvoicing.js b/modules/invoiceOut/back/methods/invoiceOut/globalInvoicing.js
index df79a6c099..4507369020 100644
--- a/modules/invoiceOut/back/methods/invoiceOut/globalInvoicing.js
+++ b/modules/invoiceOut/back/methods/invoiceOut/globalInvoicing.js
@@ -42,9 +42,9 @@ module.exports = Self => {
     });
 
     Self.globalInvoicing = async(ctx, options) => {
-        const models = Self.app.models;
         const args = ctx.args;
         const invoicesIds = [];
+        const failedClients = [];
 
         let tx;
         const myOptions = {};
@@ -81,7 +81,7 @@ module.exports = Self => {
             const minShipped = new Date();
             minShipped.setFullYear(minShipped.getFullYear() - 1);
 
-            // Liquidacion de cubos y carros
+            // Packaging liquidation
             const vIsAllInvoiceable = false;
             const clientsWithPackaging = await getClientsWithPackaging(ctx, myOptions);
             for (let client of clientsWithPackaging) {
@@ -93,77 +93,70 @@ module.exports = Self => {
                 ], myOptions);
             }
 
-            const company = await models.Company.findById(args.companyFk, null, myOptions);
             const invoiceableClients = await getInvoiceableClients(ctx, myOptions);
 
             if (!invoiceableClients.length) return;
 
             for (let client of invoiceableClients) {
-                // esto es para los que no tienen rol de invoicing??
-                /* const [clientTax] = await Self.rawSql('SELECT vn.clientTaxArea(?, ?) AS taxArea', [
-                    client.id,
-                    args.companyFk
-                ], myOptions);
-                const clientTaxArea = clientTax.taxArea;
-                if (clientTaxArea != 'WORLD' && company.code === 'VNL' && hasRole('invoicing')) {
-                    // Exit process??
-                    console.log(clientTaxArea);
-                    throw new UserError('Not invoiceable ' + client.id);
-                }
- */
-                if (client.hasToInvoiceByAddress) {
-                    await Self.rawSql('CALL ticketToInvoiceByAddress(?, ?, ?, ?)', [
-                        minShipped,
-                        args.maxShipped,
-                        client.addressFk,
-                        args.companyFk
-                    ], myOptions);
-                } else {
-                    await Self.rawSql('CALL invoiceFromClient(?, ?, ?)', [
-                        args.maxShipped,
+                try {
+                    if (client.hasToInvoiceByAddress) {
+                        await Self.rawSql('CALL ticketToInvoiceByAddress(?, ?, ?, ?)', [
+                            minShipped,
+                            args.maxShipped,
+                            client.addressFk,
+                            args.companyFk
+                        ], myOptions);
+                    } else {
+                        await Self.rawSql('CALL invoiceFromClient(?, ?, ?)', [
+                            args.maxShipped,
+                            client.id,
+                            args.companyFk
+                        ], myOptions);
+                    }
+
+                    // Make invoice
+                    const isSpanishCompany = await getIsSpanishCompany(args.companyFk, myOptions);
+
+                    // Validates ticket nagative base
+                    const hasAnyNegativeBase = await getNegativeBase(myOptions);
+                    if (hasAnyNegativeBase && isSpanishCompany)
+                        continue;
+
+                    query = `SELECT invoiceSerial(?, ?, ?) AS serial`;
+                    const [invoiceSerial] = await Self.rawSql(query, [
                         client.id,
-                        args.companyFk
+                        args.companyFk,
+                        'G'
                     ], myOptions);
-                }
+                    const serialLetter = invoiceSerial.serial;
 
-                // Make invoice
+                    query = `CALL invoiceOut_new(?, ?, NULL, @invoiceId)`;
+                    await Self.rawSql(query, [
+                        serialLetter,
+                        args.invoiceDate
+                    ], myOptions);
 
-                const isSpanishCompany = await getIsSpanishCompany(args.companyFk, myOptions);
+                    const [newInvoice] = await Self.rawSql(`SELECT @invoiceId id`, null, myOptions);
+                    if (newInvoice.id) {
+                        await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], myOptions);
 
-                // Validates ticket nagative base
-                const hasAnyNegativeBase = await getNegativeBase(myOptions);
-                if (hasAnyNegativeBase && isSpanishCompany)
+                        invoicesIds.push(newInvoice.id);
+                    }
+                } catch (e) {
+                    failedClients.push({
+                        id: client.id,
+                        stacktrace: e
+                    });
                     continue;
-
-                query = `SELECT invoiceSerial(?, ?, ?) AS serial`;
-                const [invoiceSerial] = await Self.rawSql(query, [
-                    client.id,
-                    args.companyFk,
-                    'G'
-                ], myOptions);
-                const serialLetter = invoiceSerial.serial;
-
-                query = `CALL invoiceOut_new(?, ?, NULL, @invoiceId)`;
-                await Self.rawSql(query, [
-                    serialLetter,
-                    args.invoiceDate
-                ], myOptions);
-
-                const [newInvoice] = await Self.rawSql(`SELECT @invoiceId id`, null, myOptions);
-                if (newInvoice.id) {
-                    await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], myOptions);
-
-                    invoicesIds.push(newInvoice.id);
                 }
-
-                // IMPRIMIR PDF ID 3?
             }
 
-            // Print invoice to network printer
+            if (failedClients.length > 0)
+                await notifyFailures(ctx, failedClients, myOptions);
 
             if (tx) await tx.commit();
 
-            // Print invoices
+            // Print invoices PDF
             for (let invoiceId of invoicesIds)
                 await Self.createPdf(ctx, invoiceId);
 
@@ -243,4 +236,28 @@ module.exports = Self => {
             args.companyFk
         ], options);
     }
+
+    async function notifyFailures(ctx, failedClients, options) {
+        const models = Self.app.models;
+        const userId = ctx.req.accessToken.userId;
+        const $t = ctx.req.__; // $translate
+
+        const worker = await models.EmailUser.findById(userId, null, options);
+        const subject = $t('Global invoicing failed');
+        let body = $t(`Wasn't able to invoice the following clients`) + ':<br/><br/>';
+
+        for (client of failedClients) {
+            body += `ID: <strong>${client.id}</strong>
+                <br/> <strong>${client.stacktrace}</strong><br/><br/>`;
+        }
+
+        await Self.rawSql(`
+            INSERT INTO vn.mail (sender, replyTo, sent, subject, body)
+                VALUES (?, ?, FALSE, ?, ?)`, [
+            worker.email,
+            worker.email,
+            subject,
+            body
+        ], options);
+    }
 };
diff --git a/modules/invoiceOut/front/index/global-invoicing/index.html b/modules/invoiceOut/front/index/global-invoicing/index.html
index fd26afab97..9fd412d0eb 100644
--- a/modules/invoiceOut/front/index/global-invoicing/index.html
+++ b/modules/invoiceOut/front/index/global-invoicing/index.html
@@ -14,6 +14,14 @@
         data="companies"
         order="code">
     </vn-crud-model>
+    <div 
+        class="progress vn-my-md" 
+        ng-if="$ctrl.isInvoicing">
+        <vn-horizontal>
+            <vn-icon vn-none icon="warning"></vn-icon> 
+            <span vn-none translate>Invoicing in progress...</span>
+        </vn-horizontal>
+    </div>
     <vn-horizontal>
         <vn-date-picker
             vn-one
diff --git a/modules/invoiceOut/front/index/global-invoicing/index.js b/modules/invoiceOut/front/index/global-invoicing/index.js
index 1dd9437585..7a251f87ff 100644
--- a/modules/invoiceOut/front/index/global-invoicing/index.js
+++ b/modules/invoiceOut/front/index/global-invoicing/index.js
@@ -6,6 +6,7 @@ class Controller extends Dialog {
     constructor($element, $, $transclude) {
         super($element, $, $transclude);
 
+        this.isInvoicing = false;
         this.invoice = {
             maxShipped: new Date()
         };
@@ -50,17 +51,20 @@ class Controller extends Dialog {
             if (response !== 'accept')
                 return super.responseHandler(response);
 
-            /*   if (this.invoice.clientFk && !this.invoice.maxShipped)
-                throw new Error('Client and the max shipped should be filled');
+            if (!this.invoice.invoiceDate || !this.invoice.maxShipped)
+                throw new Error('Invoice date and the max date should be filled');
 
-            if (!this.invoice.serial || !this.invoice.taxArea)
-                throw new Error('Some fields are required'); */
+            if (!this.invoice.fromClientId || !this.invoice.toClientId)
+                throw new Error('Choose a valid clients range');
 
+            this.isInvoicing = true;
             return this.$http.post(`InvoiceOuts/globalInvoicing`, this.invoice)
                 .then(() => super.responseHandler(response))
-                .then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
+                .then(() => this.vnApp.showSuccess(this.$t('Data saved!')))
+                .finally(() => this.isInvoicing = false);
         } catch (e) {
             this.vnApp.showError(this.$t(e.message));
+            this.isInvoicing = false;
             return false;
         }
     }
diff --git a/modules/invoiceOut/front/index/global-invoicing/locale/es.yml b/modules/invoiceOut/front/index/global-invoicing/locale/es.yml
index b4b13945c6..51e165e2a7 100644
--- a/modules/invoiceOut/front/index/global-invoicing/locale/es.yml
+++ b/modules/invoiceOut/front/index/global-invoicing/locale/es.yml
@@ -1,3 +1,9 @@
 Create global invoice: Crear factura global
 Some fields are required: Algunos campos son obligatorios
-Max date: Fecha límite
\ No newline at end of file
+Max date: Fecha límite
+Invoicing in progress...: Facturación en progreso...
+Invoice date: Fecha de factura
+From client: Desde el cliente
+To client: Hasta el cliente
+Invoice date and the max date should be filled: La fecha de factura y la fecha límite deben rellenarse
+Choose a valid clients range: Selecciona un rango válido de clientes
\ No newline at end of file
diff --git a/modules/invoiceOut/front/index/global-invoicing/style.scss b/modules/invoiceOut/front/index/global-invoicing/style.scss
index 4e2435c215..d0bd9e2149 100644
--- a/modules/invoiceOut/front/index/global-invoicing/style.scss
+++ b/modules/invoiceOut/front/index/global-invoicing/style.scss
@@ -1,5 +1,17 @@
+@import "variables";
+
 .vn-invoice-out-global-invoicing {
     tpl-body {
-        width: 500px
+        width: 500px;
+
+        .progress {
+            font-weight: bold;
+            text-align: center;
+            font-size: 1.5rem;
+            color: $color-primary;
+            vn-horizontal {
+                justify-content: center
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/modules/invoiceOut/front/index/manual/index.html b/modules/invoiceOut/front/index/manual/index.html
index f2fd10d6f2..3148d3f948 100644
--- a/modules/invoiceOut/front/index/manual/index.html
+++ b/modules/invoiceOut/front/index/manual/index.html
@@ -14,6 +14,14 @@
         data="taxAreas"
         order="code">
     </vn-crud-model>
+    <div 
+        class="progress vn-my-md" 
+        ng-if="$ctrl.isInvoicing">
+        <vn-horizontal>
+            <vn-icon vn-none icon="warning"></vn-icon> 
+            <span vn-none translate>Invoicing in progress...</span>
+        </vn-horizontal>
+    </div>
     <vn-horizontal class="manifold-panel">
         <vn-autocomplete
             url="Tickets"
diff --git a/modules/invoiceOut/front/index/manual/index.js b/modules/invoiceOut/front/index/manual/index.js
index 77dd93de7b..ed50e37dac 100644
--- a/modules/invoiceOut/front/index/manual/index.js
+++ b/modules/invoiceOut/front/index/manual/index.js
@@ -6,6 +6,7 @@ class Controller extends Dialog {
     constructor($element, $, $transclude) {
         super($element, $, $transclude);
 
+        this.isInvoicing = false;
         this.invoice = {
             maxShipped: new Date()
         };
@@ -22,14 +23,17 @@ class Controller extends Dialog {
             if (!this.invoice.serial || !this.invoice.taxArea)
                 throw new Error('Some fields are required');
 
+            this.isInvoicing = true;
             return this.$http.post(`InvoiceOuts/createManualInvoice`, this.invoice)
                 .then(res => {
                     this.$state.go('invoiceOut.card.summary', {id: res.data.id});
                     super.responseHandler(response);
                 })
-                .then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
+                .then(() => this.vnApp.showSuccess(this.$t('Data saved!')))
+                .finally(() => this.isInvoicing = false);
         } catch (e) {
             this.vnApp.showError(this.$t(e.message));
+            this.isInvoicing = false;
             return false;
         }
     }
diff --git a/modules/invoiceOut/front/index/manual/locale/es.yml b/modules/invoiceOut/front/index/manual/locale/es.yml
index 826057c8d0..370e823d06 100644
--- a/modules/invoiceOut/front/index/manual/locale/es.yml
+++ b/modules/invoiceOut/front/index/manual/locale/es.yml
@@ -2,4 +2,5 @@ Create manual invoice: Crear factura manual
 Some fields are required: Algunos campos son obligatorios
 Client and max shipped fields should be filled: Los campos de cliente y fecha límite deben rellenarse
 Max date: Fecha límite
-Serial: Serie
\ No newline at end of file
+Serial: Serie
+Invoicing in progress...: Facturación en progreso...
\ No newline at end of file
diff --git a/modules/invoiceOut/front/index/manual/style.scss b/modules/invoiceOut/front/index/manual/style.scss
index 9984721571..18e6f35138 100644
--- a/modules/invoiceOut/front/index/manual/style.scss
+++ b/modules/invoiceOut/front/index/manual/style.scss
@@ -1,5 +1,17 @@
+@import "variables";
+
 .vn-invoice-out-manual {
     tpl-body {
-        width: 500px
+        width: 500px;
+
+        .progress {
+            font-weight: bold;
+            text-align: center;
+            font-size: 1.5rem;
+            color: $color-primary;
+            vn-horizontal {
+                justify-content: center
+            }
+        }
     }
 }
\ No newline at end of file