diff --git a/.vscode/settings.json b/.vscode/settings.json
index c27d01a76..b5da1e8e6 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,14 +1,8 @@
-// Coloque su configuración en este archivo para sobrescribir la configuración predeterminada y de usuario.
-{
- // Carácter predeterminado de final de línea.
- "files.eol": "\n",
- "editor.bracketPairColorization.enabled": true,
- "editor.guides.bracketPairs": true,
- "editor.formatOnSave": true,
- "editor.defaultFormatter": "dbaeumer.vscode-eslint",
- "editor.codeActionsOnSave": ["source.fixAll.eslint"],
- "eslint.validate": [
- "javascript",
- "json"
- ]
-}
\ No newline at end of file
+// Coloque su configuración en este archivo para sobrescribir la configuración predeterminada y de usuario.
+{
+ // Carácter predeterminado de final de línea.
+ "files.eol": "\n",
+ "editor.codeActionsOnSave": {
+ "source.fixAll.eslint": true
+ }
+}
diff --git a/db/changes/10503-november/00-aclInvoiceOut.sql b/db/changes/10503-november/00-aclInvoiceOut.sql
new file mode 100644
index 000000000..7715045b5
--- /dev/null
+++ b/db/changes/10503-november/00-aclInvoiceOut.sql
@@ -0,0 +1,7 @@
+INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
+ VALUES
+ ('InvoiceOut', 'clientsToInvoice', 'WRITE', 'ALLOW', 'ROLE', 'invoicing');
+
+INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
+ VALUES
+ ('InvoiceOut', 'invoiceClient', 'WRITE', 'ALLOW', 'ROLE', 'invoicing');
\ No newline at end of file
diff --git a/db/changes/10503-november/00-deleteInvoiceOutQueue.sql b/db/changes/10503-november/00-deleteInvoiceOutQueue.sql
new file mode 100644
index 000000000..87a3059a0
--- /dev/null
+++ b/db/changes/10503-november/00-deleteInvoiceOutQueue.sql
@@ -0,0 +1 @@
+DROP TABLE `vn`.`invoiceOutQueue`;
\ No newline at end of file
diff --git a/db/changes/10503-november/00-editTrackedACL.sql b/db/changes/10503-november/00-editTrackedACL.sql
new file mode 100644
index 000000000..e768fb7c7
--- /dev/null
+++ b/db/changes/10503-november/00-editTrackedACL.sql
@@ -0,0 +1,4 @@
+INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
+ VALUES
+ ('Sale', 'editTracked', 'WRITE', 'ALLOW', 'ROLE', 'production'),
+ ('Sale', 'editFloramondo', 'WRITE', 'ALLOW', 'ROLE', 'salesAssistant');
diff --git a/db/changes/10503-november/delete.keep b/db/changes/10503-november/delete.keep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql
index 6624e99f4..49cf639ef 100644
--- a/db/dump/fixtures.sql
+++ b/db/dump/fixtures.sql
@@ -984,7 +984,7 @@ INSERT INTO `vn`.`sale`(`id`, `itemFk`, `ticketFk`, `concept`, `quantity`, `pric
(30, 4, 18, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, util.VN_CURDATE()),
(31, 2, 23, 'Melee weapon combat fist 15cm', -5, 7.08, 0, 0, 0, util.VN_CURDATE()),
(32, 1, 24, 'Ranged weapon longbow 2m', -1, 8.07, 0, 0, 0, util.VN_CURDATE()),
- (33, 5, 14, 'Ranged weapon pistol 9mm', 50, 1.79, 0, 0, 0, util.VN_CURDATE());
+ (33, 5, 14, 'Ranged weapon pistol 9mm', 50, 1.79, 0, 0, 0, util.VN_CURDATE());
INSERT INTO `vn`.`saleChecked`(`saleFk`, `isChecked`)
VALUES
@@ -1146,10 +1146,11 @@ INSERT INTO `vncontrol`.`accion`(`accion_id`, `accion`)
INSERT INTO `vn`.`saleTracking`(`saleFk`, `isChecked`, `created`, `originalQuantity`, `workerFk`, `actionFk`, `id`, `stateFk`)
VALUES
- (1, 0, util.VN_CURDATE(), 5, 55, 3, 1, 14),
- (1, 1, util.VN_CURDATE(), 5, 54, 3, 2, 8),
- (2, 1, util.VN_CURDATE(), 10, 40, 4, 3, 8),
- (3, 1, util.VN_CURDATE(), 2, 40, 4, 4, 8);
+ (1, 0, util.VN_CURDATE(), 5, 55, 3, 1, 14),
+ (1, 1, util.VN_CURDATE(), 5, 54, 3, 2, 8),
+ (2, 1, util.VN_CURDATE(), 10, 40, 4, 3, 8),
+ (3, 1, util.VN_CURDATE(), 2, 40, 4, 4, 8),
+ (31, 1, util.VN_CURDATE(), -5, 40, 4, 5, 8);
INSERT INTO `vn`.`itemBarcode`(`id`, `itemFk`, `code`)
VALUES
@@ -1356,7 +1357,8 @@ INSERT INTO `vn`.`ticketWeekly`(`ticketFk`, `weekDay`)
(2, 1),
(3, 2),
(4, 4),
- (5, 6);
+ (5, 6),
+ (15, 6);
INSERT INTO `vn`.`travel`(`id`,`shipped`, `landed`, `warehouseInFk`, `warehouseOutFk`, `agencyModeFk`, `m3`, `kg`,`ref`, `totalEntries`, `cargoSupplierFk`)
VALUES
@@ -2699,7 +2701,7 @@ INSERT INTO `util`.`notificationSubscription` (`notificationFk`, `userFk`)
INSERT INTO `vn`.`routeConfig` (`id`, `defaultWorkCenterFk`)
VALUES
(1, 9);
-
+
INSERT INTO `vn`.`productionConfig` (`isPreviousPreparationRequired`, `ticketPrintedMax`, `ticketTrolleyMax`, `rookieDays`, `notBuyingMonths`, `id`, `isZoneClosedByExpeditionActivated`, `maxNotReadyCollections`, `minTicketsToCloseZone`, `movingTicketDelRoute`, `defaultZone`, `defautlAgencyMode`, `hasUniqueCollectionTime`, `maxCollectionWithoutUser`, `pendingCollectionsOrder`, `pendingCollectionsAge`)
VALUES
(0, 8, 80, 0, 0, 1, 0, 15, 25, -1, 697, 1328, 0, 1, 8, 6);
@@ -2712,6 +2714,10 @@ INSERT INTO `vn`.`ticketCollection` (`ticketFk`, `collectionFk`, `created`, `lev
VALUES
(9, 3, util.VN_NOW(), NULL, 0, NULL, NULL, NULL, NULL);
+INSERT INTO `vn`.`saleCloned` (`saleClonedFk`, `saleOriginalFk`)
+ VALUES
+ (29, 25);
+
UPDATE `account`.`user`
SET `hasGrant` = 1
WHERE `id` = 66;
diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js
index f0d726ed6..1391731de 100644
--- a/e2e/helpers/selectors.js
+++ b/e2e/helpers/selectors.js
@@ -969,6 +969,7 @@ export default {
manualInvoiceTaxArea: 'vn-autocomplete[ng-model="$ctrl.invoice.taxArea"]',
saveInvoice: 'button[response="accept"]',
globalInvoiceForm: '.vn-invoice-out-global-invoicing',
+ globalInvoiceClientsRange: 'vn-radio[val="clientsRange"]',
globalInvoiceDate: '[ng-model="$ctrl.invoice.invoiceDate"]',
globalInvoiceFromClient: '[ng-model="$ctrl.invoice.fromClientId"]',
globalInvoiceToClient: '[ng-model="$ctrl.invoice.toClientId"]',
diff --git a/e2e/paths/05-ticket/09_weekly.spec.js b/e2e/paths/05-ticket/09_weekly.spec.js
index d04138ee5..0ba57aa9d 100644
--- a/e2e/paths/05-ticket/09_weekly.spec.js
+++ b/e2e/paths/05-ticket/09_weekly.spec.js
@@ -19,7 +19,7 @@ describe('Ticket descriptor path', () => {
it('should count the amount of tickets in the turns section', async() => {
const result = await page.countElement(selectors.ticketsIndex.weeklyTicket);
- expect(result).toEqual(5);
+ expect(result).toEqual(6);
});
it('should go back to the ticket index then search and access a ticket summary', async() => {
@@ -104,7 +104,7 @@ describe('Ticket descriptor path', () => {
await page.doSearch();
const nResults = await page.countElement(selectors.ticketsIndex.searchWeeklyResult);
- expect(nResults).toEqual(5);
+ expect(nResults).toEqual(6);
});
it('should update the agency then remove it afterwards', async() => {
diff --git a/e2e/paths/09-invoice-out/04_globalInvoice.spec.js b/e2e/paths/09-invoice-out/04_globalInvoice.spec.js
index b62c889db..74efafd2d 100644
--- a/e2e/paths/09-invoice-out/04_globalInvoice.spec.js
+++ b/e2e/paths/09-invoice-out/04_globalInvoice.spec.js
@@ -33,6 +33,7 @@ describe('InvoiceOut global invoice path', () => {
it('should create a global invoice for charles xavier today', async() => {
await page.pickDate(selectors.invoiceOutIndex.globalInvoiceDate);
+ await page.waitToClick(selectors.invoiceOutIndex.globalInvoiceClientsRange);
await page.autocompleteSearch(selectors.invoiceOutIndex.globalInvoiceFromClient, 'Petter Parker');
await page.autocompleteSearch(selectors.invoiceOutIndex.globalInvoiceToClient, 'Petter Parker');
await page.waitToClick(selectors.invoiceOutIndex.saveInvoice);
@@ -48,4 +49,15 @@ describe('InvoiceOut global invoice path', () => {
expect(currentInvoices).toBeGreaterThan(invoicesBefore);
});
+
+ it('should create a global invoice for all clients today', async() => {
+ await page.waitToClick(selectors.invoiceOutIndex.createInvoice);
+ await page.waitToClick(selectors.invoiceOutIndex.createGlobalInvoice);
+ await page.waitForSelector(selectors.invoiceOutIndex.globalInvoiceForm);
+ await page.pickDate(selectors.invoiceOutIndex.globalInvoiceDate);
+ await page.waitToClick(selectors.invoiceOutIndex.saveInvoice);
+ const message = await page.waitForSnackbar();
+
+ expect(message.text).toContain('Data saved!');
+ });
});
diff --git a/loopback/locale/en.json b/loopback/locale/en.json
index f03441c0f..887a657a3 100644
--- a/loopback/locale/en.json
+++ b/loopback/locale/en.json
@@ -136,5 +136,6 @@
"Not enough privileges to edit a client": "Not enough privileges to edit a client",
"Claim pickup order sent": "Claim pickup order sent [({{claimId}})]({{{claimUrl}}}) to client *{{clientName}}*",
"You don't have grant privilege": "You don't have grant privilege",
- "You don't own the role and you can't assign it to another user": "You don't own the role and you can't assign it to another user"
+ "You don't own the role and you can't assign it to another user": "You don't own the role and you can't assign it to another user",
+ "Sale(s) blocked, please contact production": "Sale(s) blocked, please contact production"
}
diff --git a/loopback/server/boot/acl.js b/loopback/server/boot/acl.js
new file mode 100644
index 000000000..f9375e0c1
--- /dev/null
+++ b/loopback/server/boot/acl.js
@@ -0,0 +1,22 @@
+
+module.exports = function(app) {
+ app.models.ACL.checkAccessAcl = async(ctx, modelId, property, accessType = '*') => {
+ const models = app.models;
+ const context = {
+ accessToken: ctx.req.accessToken,
+ model: models[modelId],
+ property: property,
+ modelId: modelId,
+ accessType: accessType,
+ sharedMethod: {
+ name: property,
+ aliases: [],
+ sharedClass: true
+ }
+ };
+
+ const acl = await models.ACL.checkAccessForContext(context);
+
+ return acl.permission == 'ALLOW';
+ };
+};
diff --git a/modules/invoiceOut/back/methods/invoiceOut/globalInvoicing.js b/modules/invoiceOut/back/methods/invoiceOut/clientsToInvoice.js
similarity index 50%
rename from modules/invoiceOut/back/methods/invoiceOut/globalInvoicing.js
rename to modules/invoiceOut/back/methods/invoiceOut/clientsToInvoice.js
index 2aa277b6f..d42184ae5 100644
--- a/modules/invoiceOut/back/methods/invoiceOut/globalInvoicing.js
+++ b/modules/invoiceOut/back/methods/invoiceOut/clientsToInvoice.js
@@ -1,6 +1,6 @@
module.exports = Self => {
- Self.remoteMethodCtx('globalInvoicing', {
- description: 'Make a global invoice',
+ Self.remoteMethodCtx('clientsToInvoice', {
+ description: 'Get the clients to make global invoicing',
accessType: 'WRITE',
accepts: [
{
@@ -29,19 +29,22 @@ module.exports = Self => {
description: 'The company id to invoice'
}
],
- returns: {
- type: 'object',
- root: true
+ returns: [{
+ arg: 'clientsAndAddresses',
+ type: ['object']
},
+ {
+ arg: 'invoice',
+ type: 'object'
+ }],
http: {
- path: '/globalInvoicing',
+ path: '/clientsToInvoice',
verb: 'POST'
}
});
- Self.globalInvoicing = async(ctx, options) => {
+ Self.clientsToInvoice = async(ctx, options) => {
const args = ctx.args;
-
let tx;
const myOptions = {};
@@ -53,8 +56,6 @@ module.exports = Self => {
myOptions.transaction = tx;
}
- const invoicesIds = [];
- const failedClients = [];
let query;
try {
query = `
@@ -78,6 +79,9 @@ module.exports = Self => {
const minShipped = new Date();
minShipped.setFullYear(minShipped.getFullYear() - 1);
+ minShipped.setMonth(1);
+ minShipped.setDate(1);
+ minShipped.setHours(0, 0, 0, 0);
// Packaging liquidation
const vIsAllInvoiceable = false;
@@ -93,110 +97,51 @@ module.exports = Self => {
const invoiceableClients = await getInvoiceableClients(ctx, myOptions);
- if (!invoiceableClients.length) return;
+ if (!invoiceableClients) return;
- for (let client of invoiceableClients) {
- 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);
- }
+ const clientsAndAddresses = invoiceableClients.map(invoiceableClient => {
+ return {
+ clientId: invoiceableClient.id,
+ addressId: invoiceableClient.addressFk
- // 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);
-
- query = `INSERT IGNORE INTO invoiceOutQueue(invoiceFk) VALUES(?)`;
- await Self.rawSql(query, [newInvoice.id], myOptions);
-
- invoicesIds.push(newInvoice.id);
- }
- } catch (e) {
- failedClients.push({
- id: client.id,
- stacktrace: e
- });
- continue;
- }
+ };
}
-
- if (failedClients.length > 0)
- await notifyFailures(ctx, failedClients, myOptions);
+ );
if (tx) await tx.commit();
+
+ return [
+ clientsAndAddresses,
+ {
+ invoiceDate: args.invoiceDate,
+ maxShipped: args.maxShipped,
+ fromClientId: args.fromClientId,
+ toClientId: args.toClientId,
+ companyFk: args.companyFk,
+ minShipped: minShipped
+ }
+ ];
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
-
- return invoicesIds;
};
- 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
+ JOIN client c ON c.id = t.clientFk
WHERE t.shipped BETWEEN '2017-11-21' AND ?
- AND t.clientFk BETWEEN ? AND ?`;
+ AND t.clientFk >= ?
+ AND (t.clientFk <= ? OR ? IS NULL)
+ AND c.isActive`;
return models.InvoiceOut.rawSql(query, [
args.maxShipped,
args.fromClientId,
+ args.toClientId,
args.toClientId
], options);
}
@@ -225,42 +170,20 @@ module.exports = Self => {
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 ?
+ WHERE ISNULL(t.refFk) AND c.id >= ?
+ AND (t.clientFk <= ? OR ? IS NULL)
AND t.shipped BETWEEN ? AND util.dayEnd(?)
AND t.companyFk = ? AND c.hasToInvoice
- AND c.isTaxDataChecked
+ AND c.isTaxDataChecked AND c.isActive
GROUP BY c.id, IF(c.hasToInvoiceByAddress,a.id,TRUE) HAVING sumAmount > 0`;
return models.InvoiceOut.rawSql(query, [
args.fromClientId,
args.toClientId,
+ args.toClientId,
minShipped,
args.maxShipped,
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`) + ':
';
-
- for (client of failedClients) {
- body += `ID: ${client.id}
-
${client.stacktrace}
`;
- }
-
- 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/back/methods/invoiceOut/invoiceClient.js b/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js
new file mode 100644
index 000000000..b04747d16
--- /dev/null
+++ b/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js
@@ -0,0 +1,190 @@
+module.exports = Self => {
+ Self.remoteMethodCtx('invoiceClient', {
+ description: 'Make a invoice of a client',
+ accessType: 'WRITE',
+ accepts: [{
+ arg: 'clientId',
+ type: 'number',
+ description: 'The client id to invoice',
+ required: true
+ },
+ {
+ arg: 'addressId',
+ type: 'number',
+ description: 'The address id to invoice',
+ required: true
+ },
+ {
+ arg: 'invoiceDate',
+ type: 'date',
+ description: 'The invoice date',
+ required: true
+ },
+ {
+ arg: 'maxShipped',
+ type: 'date',
+ description: 'The maximum shipped date',
+ required: true
+ },
+ {
+ arg: 'companyFk',
+ type: 'number',
+ description: 'The company id to invoice',
+ required: true
+ },
+ {
+ arg: 'minShipped',
+ type: 'date',
+ description: 'The minium shupped date',
+ required: true
+ }],
+ returns: {
+ type: 'object',
+ root: true
+ },
+ http: {
+ path: '/invoiceClient',
+ verb: 'POST'
+ }
+ });
+
+ Self.invoiceClient = async(ctx, options) => {
+ const args = ctx.args;
+ const models = Self.app.models;
+ const myOptions = {};
+ let tx;
+
+ if (typeof options == 'object')
+ Object.assign(myOptions, options);
+
+ if (!myOptions.transaction) {
+ tx = await Self.beginTransaction({});
+ myOptions.transaction = tx;
+ }
+
+ let invoiceId;
+ let invoiceOut;
+ try {
+ const client = await models.Client.findById(args.clientId, {
+ fields: ['id', 'hasToInvoiceByAddress']
+ }, myOptions);
+ try {
+ if (client.hasToInvoiceByAddress) {
+ await Self.rawSql('CALL ticketToInvoiceByAddress(?, ?, ?, ?)', [
+ args.minShipped,
+ args.maxShipped,
+ args.addressId,
+ 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)
+ return tx.rollback();
+
+ 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);
+
+ invoiceId = newInvoice.id;
+ }
+ } catch (e) {
+ const failedClient = {
+ id: client.id,
+ stacktrace: e
+ };
+ await notifyFailures(ctx, failedClient, myOptions);
+ }
+
+ invoiceOut = await models.InvoiceOut.findById(invoiceId, {
+ include: {
+ relation: 'client'
+ }
+ }, myOptions);
+
+ if (tx) await tx.commit();
+ } catch (e) {
+ if (tx) await tx.rollback();
+ throw e;
+ }
+
+ ctx.args = {
+ reference: invoiceOut.ref,
+ recipientId: invoiceOut.clientFk,
+ recipient: invoiceOut.client().email
+ };
+ try {
+ await models.InvoiceOut.invoiceEmail(ctx);
+ } catch (err) {}
+
+ return invoiceId;
+ };
+
+ 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 notifyFailures(ctx, failedClient, 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`) + ':
';
+
+ body += `ID: ${failedClient.id}
+
${failedClient.stacktrace}
`;
+
+ 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/back/methods/invoiceOut/invoiceEmail.js b/modules/invoiceOut/back/methods/invoiceOut/invoiceEmail.js
index 83564e3ab..83cb84881 100644
--- a/modules/invoiceOut/back/methods/invoiceOut/invoiceEmail.js
+++ b/modules/invoiceOut/back/methods/invoiceOut/invoiceEmail.js
@@ -40,19 +40,40 @@ module.exports = Self => {
}
});
- Self.invoiceEmail = async ctx => {
+ Self.invoiceEmail = async(ctx, reference) => {
const args = Object.assign({}, ctx.args);
+ const {InvoiceOut} = Self.app.models;
const params = {
recipient: args.recipient,
lang: ctx.req.getLocale()
};
+ const invoiceOut = await InvoiceOut.findOne({
+ where: {
+ ref: reference
+ }
+ });
+
delete args.ctx;
for (const param in args)
params[param] = args[param];
const email = new Email('invoice', params);
- return email.send();
+ const [stream, ...headers] = await Self.download(ctx, invoiceOut.id);
+ const name = headers[1];
+ const fileName = name.replace(/filename="(.*)"/gm, '$1');
+
+ const mailOptions = {
+ overrideAttachments: true,
+ attachments: [
+ {
+ filename: fileName,
+ content: stream
+ }
+ ]
+ };
+
+ return email.send(mailOptions);
};
};
diff --git a/modules/invoiceOut/back/methods/invoiceOut/sendQueued.js b/modules/invoiceOut/back/methods/invoiceOut/sendQueued.js
deleted file mode 100644
index a1730ac81..000000000
--- a/modules/invoiceOut/back/methods/invoiceOut/sendQueued.js
+++ /dev/null
@@ -1,133 +0,0 @@
-const {Email, Report, storage} = require('vn-print');
-
-module.exports = Self => {
- Self.remoteMethod('sendQueued', {
- description: 'Send all queued invoices',
- accessType: 'WRITE',
- accepts: [],
- returns: {
- type: 'object',
- root: true
- },
- http: {
- path: '/send-queued',
- verb: 'POST'
- }
- });
-
- Self.sendQueued = async() => {
- const invoices = await Self.rawSql(`
- SELECT
- io.id,
- io.clientFk,
- io.issued,
- io.ref,
- c.email recipient,
- c.salesPersonFk,
- c.isToBeMailed,
- c.hasToInvoice,
- co.hasDailyInvoice,
- eu.email salesPersonEmail
- FROM invoiceOutQueue ioq
- JOIN invoiceOut io ON io.id = ioq.invoiceFk
- JOIN client c ON c.id = io.clientFk
- JOIN province p ON p.id = c.provinceFk
- JOIN country co ON co.id = p.countryFk
- LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
- WHERE status = ''`);
-
- let invoiceId;
- for (const invoiceOut of invoices) {
- try {
- const tx = await Self.beginTransaction({});
- const myOptions = {transaction: tx};
-
- invoiceId = invoiceOut.id;
-
- const args = {
- reference: invoiceOut.ref,
- recipientId: invoiceOut.clientFk,
- recipient: invoiceOut.recipient,
- replyTo: invoiceOut.salesPersonEmail
- };
-
- const invoiceReport = new Report('invoice', args);
- const stream = await invoiceReport.toPdfStream();
-
- const issued = invoiceOut.issued;
- const year = issued.getFullYear().toString();
- const month = (issued.getMonth() + 1).toString();
- const day = issued.getDate().toString();
-
- const fileName = `${year}${invoiceOut.ref}.pdf`;
-
- // Store invoice
- storage.write(stream, {
- type: 'invoice',
- path: `${year}/${month}/${day}`,
- fileName: fileName
- });
-
- await Self.rawSql(`
- UPDATE invoiceOut
- SET hasPdf = true
- WHERE id = ?`,
- [invoiceOut.id], myOptions);
-
- const isToBeMailed = invoiceOut.recipient && invoiceOut.salesPersonFk && invoiceOut.isToBeMailed;
-
- if (isToBeMailed) {
- const mailOptions = {
- overrideAttachments: true,
- attachments: []
- };
-
- const invoiceAttachment = {
- filename: fileName,
- content: stream
- };
-
- if (invoiceOut.serial == 'E' && invoiceOut.companyCode == 'VNL') {
- const exportation = new Report('exportation', args);
- const stream = await exportation.toPdfStream();
- const fileName = `CITES-${invoiceOut.ref}.pdf`;
-
- mailOptions.attachments.push({
- filename: fileName,
- content: stream
- });
- }
-
- mailOptions.attachments.push(invoiceAttachment);
-
- const email = new Email('invoice', args);
- await email.send(mailOptions);
- }
- // Update queue status
- const date = new Date();
- await Self.rawSql(`
- UPDATE invoiceOutQueue
- SET status = "printed",
- printed = ?
- WHERE invoiceFk = ?`,
- [date, invoiceOut.id], myOptions);
-
- await tx.commit();
- } catch (error) {
- await tx.rollback();
-
- await Self.rawSql(`
- UPDATE invoiceOutQueue
- SET status = ?
- WHERE invoiceFk = ?`,
- [error.message, invoiceId]);
-
- throw e;
- }
- }
-
- return {
- message: 'Success'
- };
- };
-};
diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/downloadZip.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/downloadZip.spec.js
index 7a9e184ea..08f049783 100644
--- a/modules/invoiceOut/back/methods/invoiceOut/specs/downloadZip.spec.js
+++ b/modules/invoiceOut/back/methods/invoiceOut/specs/downloadZip.spec.js
@@ -13,7 +13,7 @@ describe('InvoiceOut downloadZip()', () => {
};
it('should return part of link to dowloand the zip', async() => {
- const tx = await models.Order.beginTransaction({});
+ const tx = await models.InvoiceOut.beginTransaction({});
try {
const options = {transaction: tx};
@@ -30,7 +30,7 @@ describe('InvoiceOut downloadZip()', () => {
});
it('should return an error if the size of the files is too large', async() => {
- const tx = await models.Order.beginTransaction({});
+ const tx = await models.InvoiceOut.beginTransaction({});
let error;
try {
diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/globalInvoicing.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/globalInvoicing.spec.js
deleted file mode 100644
index f7546b72e..000000000
--- a/modules/invoiceOut/back/methods/invoiceOut/specs/globalInvoicing.spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-const models = require('vn-loopback/server/server').models;
-
-describe('InvoiceOut globalInvoicing()', () => {
- const userId = 1;
- const companyFk = 442;
- const clientId = 1101;
- const invoicedTicketId = 8;
- const invoiceSerial = 'A';
- const activeCtx = {
- accessToken: {userId: userId},
- __: value => {
- return value;
- }
- };
- const ctx = {req: activeCtx};
-
- it('should make a global invoicing', async() => {
- spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true)));
-
- const tx = await models.InvoiceOut.beginTransaction({});
- const options = {transaction: tx};
-
- try {
- ctx.args = {
- invoiceDate: new Date(),
- maxShipped: new Date(),
- fromClientId: clientId,
- toClientId: 1106,
- companyFk: companyFk
- };
- const result = await models.InvoiceOut.globalInvoicing(ctx, options);
- const ticket = await models.Ticket.findById(invoicedTicketId, null, options);
-
- expect(result.length).toBeGreaterThan(0);
- expect(ticket.refFk).toContain(invoiceSerial);
-
- await tx.rollback();
- } catch (e) {
- await tx.rollback();
- throw e;
- }
- });
-});
diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/invoiceClient.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/invoiceClient.spec.js
new file mode 100644
index 000000000..5f890de26
--- /dev/null
+++ b/modules/invoiceOut/back/methods/invoiceOut/specs/invoiceClient.spec.js
@@ -0,0 +1,56 @@
+const models = require('vn-loopback/server/server').models;
+
+describe('InvoiceOut invoiceClient()', () => {
+ const userId = 1;
+ const clientId = 1101;
+ const addressId = 121;
+ const companyFk = 442;
+ const minShipped = new Date();
+ minShipped.setFullYear(minShipped.getFullYear() - 1);
+ minShipped.setMonth(1);
+ minShipped.setDate(1);
+ minShipped.setHours(0, 0, 0, 0);
+ const invoiceSerial = 'A';
+ const activeCtx = {
+ getLocale: () => {
+ return 'en';
+ },
+ accessToken: {userId: userId},
+ __: value => {
+ return value;
+ }
+ };
+ const ctx = {req: activeCtx};
+
+ it('should make a global invoicing', async() => {
+ spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true)));
+ spyOn(models.InvoiceOut, 'invoiceEmail');
+
+ const tx = await models.InvoiceOut.beginTransaction({});
+ const options = {transaction: tx};
+
+ try {
+ ctx.args = {
+ clientId: clientId,
+ addressId: addressId,
+ invoiceDate: new Date(),
+ maxShipped: new Date(),
+ companyFk: companyFk,
+ minShipped: minShipped
+ };
+ const invoiceOutId = await models.InvoiceOut.invoiceClient(ctx, options);
+ const invoiceOut = await models.InvoiceOut.findById(invoiceOutId, null, options);
+ const [firstTicket] = await models.Ticket.find({
+ where: {refFk: invoiceOut.ref}
+ }, options);
+
+ expect(invoiceOutId).toBeGreaterThan(0);
+ expect(firstTicket.refFk).toContain(invoiceSerial);
+
+ await tx.rollback();
+ } catch (e) {
+ await tx.rollback();
+ throw e;
+ }
+ });
+});
diff --git a/modules/invoiceOut/back/models/invoice-out.js b/modules/invoiceOut/back/models/invoice-out.js
index 9533ad484..88adae2ef 100644
--- a/modules/invoiceOut/back/models/invoice-out.js
+++ b/modules/invoiceOut/back/models/invoice-out.js
@@ -8,11 +8,11 @@ module.exports = Self => {
require('../methods/invoiceOut/book')(Self);
require('../methods/invoiceOut/createPdf')(Self);
require('../methods/invoiceOut/createManualInvoice')(Self);
- require('../methods/invoiceOut/globalInvoicing')(Self);
+ require('../methods/invoiceOut/clientsToInvoice')(Self);
+ require('../methods/invoiceOut/invoiceClient')(Self);
require('../methods/invoiceOut/refund')(Self);
require('../methods/invoiceOut/invoiceEmail')(Self);
require('../methods/invoiceOut/exportationPdf')(Self);
- require('../methods/invoiceOut/sendQueued')(Self);
require('../methods/invoiceOut/invoiceCsv')(Self);
require('../methods/invoiceOut/invoiceCsvEmail')(Self);
};
diff --git a/modules/invoiceOut/front/index/global-invoicing/index.html b/modules/invoiceOut/front/index/global-invoicing/index.html
index 3d245b8d8..c2d1c4304 100644
--- a/modules/invoiceOut/front/index/global-invoicing/index.html
+++ b/modules/invoiceOut/front/index/global-invoicing/index.html
@@ -13,13 +13,24 @@
url="Companies"
data="companies"
order="code">
-
+