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
default-case: 0
no-eq-null: 0
no-console: ["error"]
no-console: ["warn"]
no-warning-comments: 0
no-empty: [error, allowEmptyCatch: true]
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 = 'A1111111' WHERE id = 5;
INSERT INTO vn.invoiceOutConfig
SET parallelism = 8;
INSERT INTO `vn`.`invoiceOutTax` (`invoiceOutFk`, `taxableBase`, `vat`, `pgcFk`)
VALUES
(1, 895.76, 89.58, 4722000010),

View File

@ -30,15 +30,10 @@ module.exports = Self => {
type: 'number',
description: 'The company id to invoice',
required: true
}, {
arg: 'printerFk',
type: 'number',
description: 'The printer to print',
required: true
}
],
returns: {
type: 'object',
type: 'number',
root: true
},
http: {
@ -50,29 +45,23 @@ module.exports = Self => {
Self.invoiceClient = async(ctx, options) => {
const args = ctx.args;
const models = Self.app.models;
const myOptions = {userId: ctx.req.accessToken.userId};
const $t = ctx.req.__; // $translate
const origin = ctx.req.headers.origin;
options = typeof options == 'object'
? Object.assign({}, options) : {};
options.userId = ctx.req.accessToken.userId;
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
if (!options.transaction)
tx = options.transaction = await Self.beginTransaction({});
const minShipped = Date.vnNew();
minShipped.setFullYear(args.maxShipped.getFullYear() - 1);
let invoiceId;
let invoiceOut;
try {
const client = await models.Client.findById(args.clientId, {
fields: ['id', 'hasToInvoiceByAddress']
}, myOptions);
}, options);
if (client.hasToInvoiceByAddress) {
await Self.rawSql('CALL ticketToInvoiceByAddress(?, ?, ?, ?)', [
@ -80,53 +69,57 @@ module.exports = Self => {
args.maxShipped,
args.addressId,
args.companyFk
], myOptions);
], options);
} else {
await Self.rawSql('CALL invoiceFromClient(?, ?, ?)', [
args.maxShipped,
client.id,
args.companyFk
], myOptions);
], options);
}
// Make invoice
const isSpanishCompany = await getIsSpanishCompany(args.companyFk, myOptions);
// Check negative bases
// Validates ticket nagative base
const hasAnyNegativeBase = await getNegativeBase(myOptions);
let 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 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)
throw new UserError('Negative basis');
// Invoicing
query = `SELECT invoiceSerial(?, ?, ?) AS serial`;
const [invoiceSerial] = await Self.rawSql(query, [
client.id,
args.companyFk,
'G'
], myOptions);
], options);
const serialLetter = invoiceSerial.serial;
query = `CALL invoiceOut_new(?, ?, NULL, @invoiceId)`;
await Self.rawSql(query, [
serialLetter,
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)
throw new UserError('No tickets to invoice', 'notInvoiced');
await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], myOptions);
invoiceOut = await models.InvoiceOut.findById(newInvoice.id, {
fields: ['id', 'ref', 'clientFk'],
include: {
relation: 'client',
scope: {
fields: ['email', 'isToBeMailed', 'salesPersonFk']
}
}
}, myOptions);
await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], options);
invoiceId = newInvoice.id;
if (tx) await tx.commit();
@ -135,66 +128,6 @@ module.exports = Self => {
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;
};
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": {
"dataSource": "vn"
},
"InvoiceOutConfig": {
"dataSource": "vn"
},
"InvoiceOutSerial": {
"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/clientsToInvoice')(Self);
require('../methods/invoiceOut/invoiceClient')(Self);
require('../methods/invoiceOut/makePdfAndNotify')(Self);
require('../methods/invoiceOut/refund')(Self);
require('../methods/invoiceOut/invoiceEmail')(Self);
require('../methods/invoiceOut/exportationPdf')(Self);

View File

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

View File

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

View File

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

View File

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