refs #5772 Parallelism added to PDF generation
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Juan Ferrer 2023-06-23 11:42:30 +02:00
parent d4dceac74c
commit 47a4a9950f
12 changed files with 231 additions and 150 deletions

View File

@ -17,7 +17,7 @@ rules:
camelcase: 0 camelcase: 0
default-case: 0 default-case: 0
no-eq-null: 0 no-eq-null: 0
no-console: ["error"] no-console: ["warn"]
no-warning-comments: 0 no-warning-comments: 0
no-empty: [error, allowEmptyCatch: true] no-empty: [error, allowEmptyCatch: true]
complexity: 0 complexity: 0

View File

@ -0,0 +1,13 @@
INSERT INTO salix.ACL (model,property,accessType,permission,principalType,principalId)
VALUES
('InvoiceOut','makePdfAndNotify','WRITE','ALLOW','ROLE','invoicing'),
('InvoiceOutConfig','*','READ','ALLOW','ROLE','invoicing');
CREATE OR REPLACE TABLE vn.invoiceOutConfig (
id INT UNSIGNED auto_increment NOT NULL,
parallelism int UNSIGNED DEFAULT 1 NOT NULL,
PRIMARY KEY (id)
)
ENGINE=InnoDB
DEFAULT CHARSET=utf8mb3
COLLATE=utf8mb3_unicode_ci;

View File

@ -603,6 +603,9 @@ UPDATE `vn`.`invoiceOut` SET ref = 'T3333333' WHERE id = 3;
UPDATE `vn`.`invoiceOut` SET ref = 'T4444444' WHERE id = 4; UPDATE `vn`.`invoiceOut` SET ref = 'T4444444' WHERE id = 4;
UPDATE `vn`.`invoiceOut` SET ref = 'A1111111' WHERE id = 5; UPDATE `vn`.`invoiceOut` SET ref = 'A1111111' WHERE id = 5;
INSERT INTO vn.invoiceOutConfig
SET parallelism = 8;
INSERT INTO `vn`.`invoiceOutTax` (`invoiceOutFk`, `taxableBase`, `vat`, `pgcFk`) INSERT INTO `vn`.`invoiceOutTax` (`invoiceOutFk`, `taxableBase`, `vat`, `pgcFk`)
VALUES VALUES
(1, 895.76, 89.58, 4722000010), (1, 895.76, 89.58, 4722000010),

View File

@ -30,15 +30,10 @@ module.exports = Self => {
type: 'number', type: 'number',
description: 'The company id to invoice', description: 'The company id to invoice',
required: true required: true
}, {
arg: 'printerFk',
type: 'number',
description: 'The printer to print',
required: true
} }
], ],
returns: { returns: {
type: 'object', type: 'number',
root: true root: true
}, },
http: { http: {
@ -50,29 +45,23 @@ module.exports = Self => {
Self.invoiceClient = async(ctx, options) => { Self.invoiceClient = async(ctx, options) => {
const args = ctx.args; const args = ctx.args;
const models = Self.app.models; const models = Self.app.models;
const myOptions = {userId: ctx.req.accessToken.userId};
const $t = ctx.req.__; // $translate options = typeof options == 'object'
const origin = ctx.req.headers.origin; ? Object.assign({}, options) : {};
options.userId = ctx.req.accessToken.userId;
let tx; let tx;
if (!options.transaction)
if (typeof options == 'object') tx = options.transaction = await Self.beginTransaction({});
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
const minShipped = Date.vnNew(); const minShipped = Date.vnNew();
minShipped.setFullYear(args.maxShipped.getFullYear() - 1); minShipped.setFullYear(args.maxShipped.getFullYear() - 1);
let invoiceId; let invoiceId;
let invoiceOut;
try { try {
const client = await models.Client.findById(args.clientId, { const client = await models.Client.findById(args.clientId, {
fields: ['id', 'hasToInvoiceByAddress'] fields: ['id', 'hasToInvoiceByAddress']
}, myOptions); }, options);
if (client.hasToInvoiceByAddress) { if (client.hasToInvoiceByAddress) {
await Self.rawSql('CALL ticketToInvoiceByAddress(?, ?, ?, ?)', [ await Self.rawSql('CALL ticketToInvoiceByAddress(?, ?, ?, ?)', [
@ -80,53 +69,57 @@ module.exports = Self => {
args.maxShipped, args.maxShipped,
args.addressId, args.addressId,
args.companyFk args.companyFk
], myOptions); ], options);
} else { } else {
await Self.rawSql('CALL invoiceFromClient(?, ?, ?)', [ await Self.rawSql('CALL invoiceFromClient(?, ?, ?)', [
args.maxShipped, args.maxShipped,
client.id, client.id,
args.companyFk args.companyFk
], myOptions); ], options);
} }
// Make invoice // Check negative bases
const isSpanishCompany = await getIsSpanishCompany(args.companyFk, myOptions);
// Validates ticket nagative base let query =
const hasAnyNegativeBase = await getNegativeBase(myOptions); `SELECT COUNT(*) isSpanishCompany
FROM supplier s
JOIN country c ON c.id = s.countryFk
AND c.code = 'ES'
WHERE s.id = ?`;
const [supplierCompany] = await Self.rawSql(query, [
args.companyFk
], options);
const isSpanishCompany = supplierCompany?.isSpanishCompany;
query = 'SELECT hasAnyNegativeBase() AS base';
const [result] = await Self.rawSql(query, null, options);
const hasAnyNegativeBase = result?.base;
if (hasAnyNegativeBase && isSpanishCompany) if (hasAnyNegativeBase && isSpanishCompany)
throw new UserError('Negative basis'); throw new UserError('Negative basis');
// Invoicing
query = `SELECT invoiceSerial(?, ?, ?) AS serial`; query = `SELECT invoiceSerial(?, ?, ?) AS serial`;
const [invoiceSerial] = await Self.rawSql(query, [ const [invoiceSerial] = await Self.rawSql(query, [
client.id, client.id,
args.companyFk, args.companyFk,
'G' 'G'
], myOptions); ], options);
const serialLetter = invoiceSerial.serial; const serialLetter = invoiceSerial.serial;
query = `CALL invoiceOut_new(?, ?, NULL, @invoiceId)`; query = `CALL invoiceOut_new(?, ?, NULL, @invoiceId)`;
await Self.rawSql(query, [ await Self.rawSql(query, [
serialLetter, serialLetter,
args.invoiceDate args.invoiceDate
], myOptions); ], options);
const [newInvoice] = await Self.rawSql(`SELECT @invoiceId id`, null, myOptions); const [newInvoice] = await Self.rawSql(`SELECT @invoiceId id`, null, options);
if (!newInvoice) if (!newInvoice)
throw new UserError('No tickets to invoice', 'notInvoiced'); throw new UserError('No tickets to invoice', 'notInvoiced');
await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], myOptions); await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], options);
invoiceOut = await models.InvoiceOut.findById(newInvoice.id, {
fields: ['id', 'ref', 'clientFk'],
include: {
relation: 'client',
scope: {
fields: ['email', 'isToBeMailed', 'salesPersonFk']
}
}
}, myOptions);
invoiceId = newInvoice.id; invoiceId = newInvoice.id;
if (tx) await tx.commit(); if (tx) await tx.commit();
@ -135,66 +128,6 @@ module.exports = Self => {
throw e; throw e;
} }
try {
await Self.makePdf(invoiceId);
} catch (err) {
console.error(err);
throw new UserError('Error while generating PDF', 'pdfError');
}
if (invoiceOut.client().isToBeMailed) {
try {
ctx.args = {
reference: invoiceOut.ref,
recipientId: args.clientId,
recipient: invoiceOut.client().email
};
await models.InvoiceOut.invoiceEmail(ctx, invoiceOut.ref);
} catch (err) {
const message = $t('Mail not sent', {
clientId: args.clientId,
clientUrl: `${origin}/#!/claim/${args.id}/summary`
});
const salesPersonId = invoiceOut.client().salesPersonFk;
if (salesPersonId)
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message);
throw new UserError('Error when sending mail to client', 'mailNotSent');
}
} else {
const query = `
CALL vn.report_print(
'invoice',
?,
account.myUser_getId(),
JSON_OBJECT('refFk', ?),
'normal'
);`;
await models.InvoiceOut.rawSql(query, [args.printerFk, invoiceOut.ref]);
}
return invoiceId; 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(*) isSpanishCompany
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.isSpanishCompany;
}
}; };

View File

@ -0,0 +1,87 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('makePdfAndNotify', {
description: 'Create invoice PDF and send it to client',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'number',
description: 'The invoice id',
required: true,
http: {source: 'path'}
}, {
arg: 'printerFk',
type: 'number',
description: 'The printer to print',
required: true
}
],
http: {
path: '/:id/makePdfAndNotify',
verb: 'POST'
}
});
Self.makePdfAndNotify = async function(ctx, id, printerFk) {
const models = Self.app.models;
options = typeof options == 'object'
? Object.assign({}, options) : {};
options.userId = ctx.req.accessToken.userId;
try {
await Self.makePdf(id, options);
} catch (err) {
console.error(err);
throw new UserError('Error while generating PDF', 'pdfError');
}
const invoiceOut = await Self.findById(id, {
fields: ['ref', 'clientFk'],
include: {
relation: 'client',
scope: {
fields: ['id', 'email', 'isToBeMailed', 'salesPersonFk']
}
}
}, options);
const ref = invoiceOut.ref;
const client = invoiceOut.client();
if (client.isToBeMailed) {
try {
ctx.args = {
reference: ref,
recipientId: client.id,
recipient: client.email
};
await Self.invoiceEmail(ctx, ref);
} catch (err) {
const origin = ctx.req.headers.origin;
const message = ctx.req.__('Mail not sent', {
clientId: client.id,
clientUrl: `${origin}/#!/claim/${id}/summary`
});
const salesPersonId = client.salesPersonFk;
if (salesPersonId)
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message);
throw new UserError('Error when sending mail to client', 'mailNotSent');
}
} else {
const query = `
CALL vn.report_print(
'invoice',
?,
account.myUser_getId(),
JSON_OBJECT('refFk', ?),
'normal'
);`;
await Self.rawSql(query, [printerFk, ref], options);
}
};
};

View File

@ -2,6 +2,9 @@
"InvoiceOut": { "InvoiceOut": {
"dataSource": "vn" "dataSource": "vn"
}, },
"InvoiceOutConfig": {
"dataSource": "vn"
},
"InvoiceOutSerial": { "InvoiceOutSerial": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -0,0 +1,22 @@
{
"name": "InvoiceOutConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "invoiceOutConfig"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"description": "Identifier"
},
"parallelism": {
"type": "number",
"required": true
}
}
}

View File

@ -12,6 +12,7 @@ module.exports = Self => {
require('../methods/invoiceOut/createManualInvoice')(Self); require('../methods/invoiceOut/createManualInvoice')(Self);
require('../methods/invoiceOut/clientsToInvoice')(Self); require('../methods/invoiceOut/clientsToInvoice')(Self);
require('../methods/invoiceOut/invoiceClient')(Self); require('../methods/invoiceOut/invoiceClient')(Self);
require('../methods/invoiceOut/makePdfAndNotify')(Self);
require('../methods/invoiceOut/refund')(Self); require('../methods/invoiceOut/refund')(Self);
require('../methods/invoiceOut/invoiceEmail')(Self); require('../methods/invoiceOut/invoiceEmail')(Self);
require('../methods/invoiceOut/exportationPdf')(Self); require('../methods/invoiceOut/exportationPdf')(Self);

View File

@ -1,7 +1,6 @@
<vn-card <vn-card
ng-if="$ctrl.status" ng-if="$ctrl.status"
class="vn-w-lg vn-pa-md" class="status vn-w-lg vn-pa-md">
style="height: 80px; display: flex; align-items: center; justify-content: center; gap: 20px;">
<vn-spinner <vn-spinner
enable="$ctrl.status != 'done'"> enable="$ctrl.status != 'done'">
</vn-spinner> </vn-spinner>
@ -20,8 +19,15 @@
Ended process Ended process
</span> </span>
</div> </div>
<div ng-if="$ctrl.nAddresses" class="text-caption text-secondary"> <div ng-if="$ctrl.nAddresses">
{{$ctrl.percentage | percentage: 0}} ({{$ctrl.addressNumber}} {{'of' | translate}} {{$ctrl.nAddresses}}) <div class="text-caption text-secondary">
{{$ctrl.percentage | percentage: 0}}
({{$ctrl.addressNumber}} <span translate>of</span> {{$ctrl.nAddresses}})
</div>
<div class="text-caption text-secondary">
{{$ctrl.nPdfs}} <span translate>of</span> {{$ctrl.totalPdfs}}
<span translate>PDFs</span>
</div>
</div> </div>
</div> </div>
</vn-card> </vn-card>
@ -141,7 +147,7 @@
<vn-submit <vn-submit
ng-if="$ctrl.invoicing" ng-if="$ctrl.invoicing"
label="Stop" label="Stop"
ng-click="$ctrl.stopInvoicing()"> ng-click="$ctrl.status = 'stopping'">
</vn-submit> </vn-submit>
</vn-vertical> </vn-vertical>
</form> </form>

View File

@ -9,30 +9,21 @@ class Controller extends Section {
Object.assign(this, { Object.assign(this, {
maxShipped: new Date(date.getFullYear(), date.getMonth(), 0), maxShipped: new Date(date.getFullYear(), date.getMonth(), 0),
clientsToInvoice: 'all', clientsToInvoice: 'all',
companyFk: this.vnConfig.companyFk
}); });
this.$http.get('UserConfigs/getUserConfig') const params = {companyFk: this.companyFk};
.then(res => {
this.companyFk = res.data.companyFk;
this.getInvoiceDate(this.companyFk);
});
}
getInvoiceDate(companyFk) {
const params = {companyFk: companyFk};
this.fetchInvoiceDate(params);
}
fetchInvoiceDate(params) {
this.$http.get('InvoiceOuts/getInvoiceDate', {params}) this.$http.get('InvoiceOuts/getInvoiceDate', {params})
.then(res => { .then(res => {
this.minInvoicingDate = res.data.issued ? new Date(res.data.issued) : null; this.minInvoicingDate = res.data.issued ? new Date(res.data.issued) : null;
this.invoiceDate = this.minInvoicingDate; this.invoiceDate = this.minInvoicingDate;
}); });
}
stopInvoicing() { const filter = {fields: ['parallelism']};
this.status = 'stopping'; this.$http.get('InvoiceOutConfigs/findOne', {filter})
.then(res => {
this.parallelism = res.data.parallelism || 1;
});
} }
makeInvoice() { makeInvoice() {
@ -70,8 +61,11 @@ class Controller extends Section {
if (!this.addresses.length) if (!this.addresses.length)
throw new UserError(`There aren't tickets to invoice`); throw new UserError(`There aren't tickets to invoice`);
this.nRequests = 0;
this.nPdfs = 0;
this.totalPdfs = 0;
this.addressIndex = 0; this.addressIndex = 0;
return this.invoiceOut(); this.invoiceClient();
}) })
.catch(err => this.handleError(err)); .catch(err => this.handleError(err));
} catch (err) { } catch (err) {
@ -85,8 +79,11 @@ class Controller extends Section {
throw err; throw err;
} }
invoiceOut() { invoiceClient() {
if (this.addressIndex == this.addresses.length || this.status == 'stopping') { if (this.nRequests == this.parallelism || this.isInvoicing) return;
if (this.addressIndex >= this.addresses.length || this.status == 'stopping') {
if (this.nRequests) return;
this.invoicing = false; this.invoicing = false;
this.status = 'done'; this.status = 'done';
return; return;
@ -95,36 +92,27 @@ class Controller extends Section {
this.status = 'invoicing'; this.status = 'invoicing';
const address = this.addresses[this.addressIndex]; const address = this.addresses[this.addressIndex];
this.currentAddress = address; this.currentAddress = address;
this.isInvoicing = true;
const params = { const params = {
clientId: address.clientId, clientId: address.clientId,
addressId: address.id, addressId: address.id,
invoiceDate: this.invoiceDate, invoiceDate: this.invoiceDate,
maxShipped: this.maxShipped, maxShipped: this.maxShipped,
companyFk: this.companyFk, companyFk: this.companyFk
printerFk: this.printerFk,
}; };
this.$http.post(`InvoiceOuts/invoiceClient`, params) this.$http.post(`InvoiceOuts/invoiceClient`, params)
.then(() => this.invoiceNext()) .then(res => {
this.isInvoicing = false;
if (res.data)
this.makePdfAndNotify(res.data, address);
this.invoiceNext();
})
.catch(res => { .catch(res => {
this.isInvoicing = false;
if (res.status >= 400 && res.status < 500) { if (res.status >= 400 && res.status < 500) {
const error = res.data?.error; this.invoiceError(address, res);
let isWarning;
const filter = {
where: {
id: address.clientId
}
};
switch (error?.code) {
case 'pdfError':
case 'mailNotSent':
isWarning = true;
break;
}
const message = error?.message || res.message;
this.errors.unshift({address, message, isWarning});
this.invoiceNext(); this.invoiceNext();
} else { } else {
this.invoicing = false; this.invoicing = false;
@ -136,7 +124,27 @@ class Controller extends Section {
invoiceNext() { invoiceNext() {
this.addressIndex++; this.addressIndex++;
this.invoiceOut(); this.invoiceClient();
}
makePdfAndNotify(invoiceId, address) {
this.nRequests++;
this.totalPdfs++;
const params = {printerFk: this.printerFk};
this.$http.post(`InvoiceOuts/${invoiceId}/makePdfAndNotify`, params)
.catch(res => {
this.invoiceError(address, res, true);
})
.finally(() => {
this.nPdfs++;
this.nRequests--;
this.invoiceClient();
});
}
invoiceError(address, res, isWarning) {
const message = res.data?.error?.message || res.message;
this.errors.unshift({address, message, isWarning});
} }
get nAddresses() { get nAddresses() {

View File

@ -10,6 +10,7 @@ Build packaging tickets: Generando tickets de embalajes
Address id: Id dirección Address id: Id dirección
Printer: Impresora Printer: Impresora
of: de of: de
PDFs: PDFs
Client: Cliente Client: Cliente
Current client id: Id cliente actual Current client id: Id cliente actual
Invoicing client: Facturando cliente Invoicing client: Facturando cliente

View File

@ -1,17 +1,21 @@
@import "variables"; @import "variables";
vn-invoice-out-global-invoicing{ vn-invoice-out-global-invoicing {
h5 {
h5{
color: $color-primary; color: $color-primary;
} }
.status {
height: 80px;
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
}
#error { #error {
line-break: normal; line-break: normal;
overflow-wrap: break-word; overflow-wrap: break-word;
white-space: normal; white-space: normal;
} }
} }