diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql
index c44191381..cbc467f6a 100644
--- a/db/dump/fixtures.sql
+++ b/db/dump/fixtures.sql
@@ -127,11 +127,12 @@ INSERT INTO `vn`.`warehouseAlias`(`id`, `name`)
INSERT INTO `vn`.`warehouse`(`id`, `name`, `code`, `isComparative`, `isInventory`, `hasAvailable`, `isManaged`, `hasStowaway`, `hasDms`, `hasComission`, `aliasFk`, `countryFk`)
VALUES
- (1, 'Warehouse One', 'ALG', 1, 1, 1, 1, 1, 1, 1, 2, 1),
- (2, 'Warehouse Two', NULL, 1, 1, 1, 1, 0, 0, 1, 2, 13),
- (3, 'Warehouse Three', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1),
- (4, 'Warehouse Four', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1),
- (5, 'Warehouse Five', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1);
+ (1, 'Warehouse One', 'ALG', 1, 1, 1, 1, 1, 1, 1, 2, 1),
+ (2, 'Warehouse Two', NULL, 1, 1, 1, 1, 0, 0, 1, 2, 13),
+ (3, 'Warehouse Three', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1),
+ (4, 'Warehouse Four', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1),
+ (5, 'Warehouse Five', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1),
+ (13, 'Inventory', NULL, 1, 1, 1, 0, 0, 0, 0, 2, 1);
INSERT INTO `vn`.`sector`(`id`, `description`, `warehouseFk`, `isPreviousPreparedByPacking`, `code`, `pickingPlacement`, `path`)
VALUES
diff --git a/loopback/locale/es.json b/loopback/locale/es.json
index 1d73cbbf3..12003cf08 100644
--- a/loopback/locale/es.json
+++ b/loopback/locale/es.json
@@ -192,5 +192,7 @@
"Can't invoice to past": "No se puede facturar a pasado",
"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"
+ "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"
}
\ No newline at end of file
diff --git a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js
index 5f43e4a32..d8962e430 100644
--- a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js
+++ b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js
@@ -71,18 +71,22 @@ module.exports = Self => {
await fs.mkdir(src, {recursive: true});
+ if (tx) await tx.commit();
+
const writeStream = fs.createWriteStream(fileSrc);
writeStream.on('open', () => {
response.pipe(writeStream);
});
- writeStream.on('finish', async function() {
- writeStream.end();
+ return await new Promise(resolve => {
+ writeStream.on('finish', () => {
+ writeStream.end();
+
+ resolve(invoiceOut);
+ });
});
- if (tx) await tx.commit();
-
- return invoiceOut;
+ // return invoiceOut;
} catch (e) {
if (tx) await tx.rollback();
if (fs.existsSync(fileSrc))
diff --git a/modules/invoiceOut/back/methods/invoiceOut/globalInvoicing.js b/modules/invoiceOut/back/methods/invoiceOut/globalInvoicing.js
new file mode 100644
index 000000000..df79a6c09
--- /dev/null
+++ b/modules/invoiceOut/back/methods/invoiceOut/globalInvoicing.js
@@ -0,0 +1,246 @@
+const UserError = require('vn-loopback/util/user-error');
+
+module.exports = Self => {
+ Self.remoteMethodCtx('globalInvoicing', {
+ description: 'Make a global invoice',
+ accessType: 'WRITE',
+ accepts: [
+ {
+ arg: 'invoiceDate',
+ type: 'date',
+ description: 'The invoice date'
+ },
+ {
+ arg: 'maxShipped',
+ type: 'date',
+ description: 'The maximum shipped date'
+ },
+ {
+ arg: 'fromClientId',
+ type: 'number',
+ description: 'The minimum client id'
+ },
+ {
+ arg: 'toClientId',
+ type: 'number',
+ description: 'The maximum client id'
+ },
+ {
+ arg: 'companyFk',
+ type: 'number',
+ description: 'The company id to invoice'
+ }
+ ],
+ returns: {
+ type: 'object',
+ root: true
+ },
+ http: {
+ path: '/globalInvoicing',
+ verb: 'POST'
+ }
+ });
+
+ Self.globalInvoicing = async(ctx, options) => {
+ const models = Self.app.models;
+ const args = ctx.args;
+ const invoicesIds = [];
+
+ let tx;
+ const myOptions = {};
+
+ if (typeof options == 'object')
+ Object.assign(myOptions, options);
+
+ if (!myOptions.transaction) {
+ tx = await Self.beginTransaction({});
+ myOptions.transaction = tx;
+ }
+
+ let query;
+ try {
+ query = `
+ SELECT MAX(issued) issued
+ FROM vn.invoiceOut io
+ JOIN vn.time t ON t.dated = io.issued
+ WHERE io.serial = 'A'
+ AND t.year = YEAR(?)
+ AND io.companyFk = ?`;
+ const [maxIssued] = await Self.rawSql(query, [
+ args.invoiceDate,
+ args.companyFk
+ ], myOptions);
+
+ const maxSerialDate = maxIssued.issued || args.invoiceDate;
+ if (args.invoiceDate < maxSerialDate)
+ args.invoiceDate = maxSerialDate;
+
+ if (args.invoiceDate < args.maxShipped)
+ args.maxShipped = args.invoiceDate;
+
+ const minShipped = new Date();
+ minShipped.setFullYear(minShipped.getFullYear() - 1);
+
+ // Liquidacion de cubos y carros
+ const vIsAllInvoiceable = false;
+ const clientsWithPackaging = await getClientsWithPackaging(ctx, myOptions);
+ for (let client of clientsWithPackaging) {
+ await Self.rawSql('CALL packageInvoicing(?, ?, ?, ?, @newTicket)', [
+ client.id,
+ args.invoiceDate,
+ args.companyFk,
+ vIsAllInvoiceable
+ ], 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,
+ 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,
+ '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 (tx) await tx.commit();
+
+ // Print invoices
+ for (let invoiceId of invoicesIds)
+ await Self.createPdf(ctx, invoiceId);
+
+ return {};
+ } catch (e) {
+ if (tx) await tx.rollback();
+ throw e;
+ }
+ };
+
+ async function getNegativeBase(options) {
+ const models = Self.app.models;
+ const query = 'SELECT hasAnyNegativeBase() AS base';
+ const [result] = await models.InvoiceOut.rawSql(query, null, options);
+
+ return result && result.base;
+ }
+
+ async function getIsSpanishCompany(companyId, options) {
+ const models = Self.app.models;
+ const query = `SELECT COUNT(*) AS total
+ FROM supplier s
+ JOIN country c ON c.id = s.countryFk
+ AND c.code = 'ES'
+ WHERE s.id = ?`;
+ const [supplierCompany] = await models.InvoiceOut.rawSql(query, [
+ companyId
+ ], options);
+
+ return supplierCompany && supplierCompany.total;
+ }
+
+ async function getClientsWithPackaging(ctx, options) {
+ const models = Self.app.models;
+ const args = ctx.args;
+ const query = `SELECT DISTINCT clientFk AS id
+ FROM ticket t
+ JOIN ticketPackaging tp ON t.id = tp.ticketFk
+ WHERE t.shipped BETWEEN '2017-11-21' AND ?
+ AND t.clientFk BETWEEN ? AND ?`;
+ return models.InvoiceOut.rawSql(query, [
+ args.maxShipped,
+ args.fromClientId,
+ args.toClientId
+ ], options);
+ }
+
+ async function getInvoiceableClients(ctx, options) {
+ const models = Self.app.models;
+ const args = ctx.args;
+ const minShipped = new Date();
+ minShipped.setFullYear(minShipped.getFullYear() - 1);
+
+ const query = `SELECT
+ c.id,
+ SUM(IFNULL(s.quantity * s.price * (100-s.discount)/100, 0) + IFNULL(ts.quantity * ts.price,0)) AS sumAmount,
+ c.hasToInvoiceByAddress,
+ c.email,
+ c.isToBeMailed,
+ a.id addressFk
+ FROM ticket t
+ LEFT JOIN sale s ON s.ticketFk = t.id
+ LEFT JOIN ticketService ts ON ts.ticketFk = t.id
+ JOIN address a ON a.id = t.addressFk
+ JOIN client c ON c.id = t.clientFk
+ WHERE ISNULL(t.refFk) AND c.id BETWEEN ? AND ?
+ AND t.shipped BETWEEN ? AND util.dayEnd(?)
+ AND t.companyFk = ? AND c.hasToInvoice
+ AND c.isTaxDataChecked
+ GROUP BY c.id, IF(c.hasToInvoiceByAddress,a.id,TRUE) HAVING sumAmount > 0`;
+
+ return models.InvoiceOut.rawSql(query, [
+ args.fromClientId,
+ args.toClientId,
+ minShipped,
+ args.maxShipped,
+ args.companyFk
+ ], options);
+ }
+};
diff --git a/modules/invoiceOut/back/models/invoice-out.js b/modules/invoiceOut/back/models/invoice-out.js
index 8a1dda41f..3b2822ada 100644
--- a/modules/invoiceOut/back/models/invoice-out.js
+++ b/modules/invoiceOut/back/models/invoice-out.js
@@ -7,4 +7,5 @@ module.exports = Self => {
require('../methods/invoiceOut/book')(Self);
require('../methods/invoiceOut/createPdf')(Self);
require('../methods/invoiceOut/createManualInvoice')(Self);
+ require('../methods/invoiceOut/globalInvoicing')(Self);
};
diff --git a/modules/invoiceOut/front/index.js b/modules/invoiceOut/front/index.js
index bdb87f9a9..3bb6d54d2 100644
--- a/modules/invoiceOut/front/index.js
+++ b/modules/invoiceOut/front/index.js
@@ -8,3 +8,4 @@ import './card';
import './descriptor';
import './descriptor-popover';
import './index/manual';
+import './index/global-invoicing';
diff --git a/modules/invoiceOut/front/index/global-invoicing/index.html b/modules/invoiceOut/front/index/global-invoicing/index.html
new file mode 100644
index 000000000..fd26afab9
--- /dev/null
+++ b/modules/invoiceOut/front/index/global-invoicing/index.html
@@ -0,0 +1,62 @@
+
+ Create global invoice
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{::id}} - {{::name}}
+
+
+ {{::id}} - {{::name}}
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/invoiceOut/front/index/global-invoicing/index.js b/modules/invoiceOut/front/index/global-invoicing/index.js
new file mode 100644
index 000000000..1dd943758
--- /dev/null
+++ b/modules/invoiceOut/front/index/global-invoicing/index.js
@@ -0,0 +1,77 @@
+import ngModule from '../../module';
+import Dialog from 'core/components/dialog';
+import './style.scss';
+
+class Controller extends Dialog {
+ constructor($element, $, $transclude) {
+ super($element, $, $transclude);
+
+ this.invoice = {
+ maxShipped: new Date()
+ };
+
+ this.getMinClientId();
+ this.getMaxClientId();
+ }
+
+ getMinClientId() {
+ this.getClientId('min').then(res => {
+ this.invoice.fromClientId = res.data.id;
+ });
+ }
+
+ getMaxClientId() {
+ this.getClientId('max').then(res => {
+ this.invoice.toClientId = res.data.id;
+ });
+ }
+
+ getClientId(func) {
+ const order = func == 'min' ? 'ASC' : 'DESC';
+ const params = {
+ filter: {
+ order: 'id ' + order,
+ limit: 1
+ }
+ };
+ return this.$http.get('Clients/findOne', {params});
+ }
+
+ get companyFk() {
+ return this.invoice.companyFk;
+ }
+
+ set companyFk(value) {
+ this.invoice.companyFk = value;
+ }
+
+ responseHandler(response) {
+ try {
+ 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.serial || !this.invoice.taxArea)
+ throw new Error('Some fields are required'); */
+
+ return this.$http.post(`InvoiceOuts/globalInvoicing`, this.invoice)
+ .then(() => super.responseHandler(response))
+ .then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
+ } catch (e) {
+ this.vnApp.showError(this.$t(e.message));
+ return false;
+ }
+ }
+}
+
+Controller.$inject = ['$element', '$scope', '$transclude'];
+
+ngModule.vnComponent('vnInvoiceOutGlobalInvoicing', {
+ slotTemplate: require('./index.html'),
+ controller: Controller,
+ bindings: {
+ companyFk: ''
+ }
+});
diff --git a/modules/invoiceOut/front/index/global-invoicing/index.spec.js b/modules/invoiceOut/front/index/global-invoicing/index.spec.js
new file mode 100644
index 000000000..f19030129
--- /dev/null
+++ b/modules/invoiceOut/front/index/global-invoicing/index.spec.js
@@ -0,0 +1,66 @@
+import './index';
+
+describe('InvoiceOut', () => {
+ describe('Component vnInvoiceOutManual', () => {
+ let controller;
+ let $httpBackend;
+
+ beforeEach(ngModule('invoiceOut'));
+
+ beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
+ $httpBackend = _$httpBackend_;
+ let $scope = $rootScope.$new();
+ const $element = angular.element('');
+ const $transclude = {
+ $$boundTransclude: {
+ $$slots: []
+ }
+ };
+ controller = $componentController('vnInvoiceOutManual', {$element, $scope, $transclude});
+ }));
+
+ describe('responseHandler()', () => {
+ it('should throw an error when clientFk property is set and the maxShipped is not filled', () => {
+ jest.spyOn(controller.vnApp, 'showError');
+
+ controller.invoice = {
+ clientFk: 1101,
+ serial: 'T',
+ taxArea: 'B'
+ };
+
+ controller.responseHandler('accept');
+
+ expect(controller.vnApp.showError).toHaveBeenCalledWith(`Client and the max shipped should be filled`);
+ });
+
+ it('should throw an error when some required fields are not filled in', () => {
+ jest.spyOn(controller.vnApp, 'showError');
+
+ controller.invoice = {
+ ticketFk: 1101
+ };
+
+ controller.responseHandler('accept');
+
+ expect(controller.vnApp.showError).toHaveBeenCalledWith(`Some fields are required`);
+ });
+
+ it('should make an http POST query and then call to the parent showSuccess() method', () => {
+ jest.spyOn(controller.vnApp, 'showSuccess');
+
+ controller.invoice = {
+ ticketFk: 1101,
+ serial: 'T',
+ taxArea: 'B'
+ };
+
+ $httpBackend.expect('POST', `InvoiceOuts/createManualInvoice`).respond({id: 1});
+ controller.responseHandler('accept');
+ $httpBackend.flush();
+
+ expect(controller.vnApp.showSuccess).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/modules/invoiceOut/front/index/global-invoicing/locale/es.yml b/modules/invoiceOut/front/index/global-invoicing/locale/es.yml
new file mode 100644
index 000000000..b4b13945c
--- /dev/null
+++ b/modules/invoiceOut/front/index/global-invoicing/locale/es.yml
@@ -0,0 +1,3 @@
+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
diff --git a/modules/invoiceOut/front/index/global-invoicing/style.scss b/modules/invoiceOut/front/index/global-invoicing/style.scss
new file mode 100644
index 000000000..4e2435c21
--- /dev/null
+++ b/modules/invoiceOut/front/index/global-invoicing/style.scss
@@ -0,0 +1,5 @@
+.vn-invoice-out-global-invoicing {
+ tpl-body {
+ width: 500px
+ }
+}
\ No newline at end of file
diff --git a/modules/invoiceOut/front/index/index.html b/modules/invoiceOut/front/index/index.html
index 9d0cc4337..999fafe95 100644
--- a/modules/invoiceOut/front/index/index.html
+++ b/modules/invoiceOut/front/index/index.html
@@ -74,6 +74,11 @@
ng-click="manualInvoicing.show()">
Manual invoicing
+
+ Global invoicing
+
@@ -87,4 +92,8 @@
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/modules/invoiceOut/front/index/locale/es.yml b/modules/invoiceOut/front/index/locale/es.yml
index eb22e1779..560666e9c 100644
--- a/modules/invoiceOut/front/index/locale/es.yml
+++ b/modules/invoiceOut/front/index/locale/es.yml
@@ -3,4 +3,5 @@ Issued: Fecha factura
Due date: Fecha vencimiento
Has PDF: PDF disponible
Minimum: Minimo
-Maximum: Máximo
\ No newline at end of file
+Maximum: Máximo
+Global invoicing: Facturación global
\ No newline at end of file
diff --git a/modules/ticket/back/methods/ticket/makeInvoice.js b/modules/ticket/back/methods/ticket/makeInvoice.js
index d8c2dc5c9..ce7380568 100644
--- a/modules/ticket/back/methods/ticket/makeInvoice.js
+++ b/modules/ticket/back/methods/ticket/makeInvoice.js
@@ -100,13 +100,14 @@ module.exports = function(Self) {
}, myOptions);
}
- if (serial != 'R' && invoiceId) {
+ if (serial != 'R' && invoiceId)
await Self.rawSql('CALL invoiceOutBooking(?)', [invoiceId], myOptions);
- await models.InvoiceOut.createPdf(ctx, invoiceId, myOptions);
- }
if (tx) await tx.commit();
+ if (serial != 'R' && invoiceId)
+ await models.InvoiceOut.createPdf(ctx, invoiceId);
+
return {invoiceFk: invoiceId, serial: serial};
} catch (e) {
if (tx) await tx.rollback();