Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2817-translate_ticket_request_message
This commit is contained in:
commit
e12be7bf0d
|
@ -1,5 +1,5 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@babel/preset-env',
|
||||
'@babel/env',
|
||||
],
|
||||
};
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
const request = require('request-promise-native');
|
||||
const got = require('got');
|
||||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('send', {
|
||||
description: 'Send a RocketChat message',
|
||||
accessType: 'WRITE',
|
||||
accepts: [{
|
||||
arg: 'to',
|
||||
type: 'String',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'User (@) or channel (#) to send the message'
|
||||
}, {
|
||||
arg: 'message',
|
||||
type: 'String',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The message'
|
||||
}],
|
||||
returns: {
|
||||
type: 'Object',
|
||||
type: 'object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
|
@ -30,8 +30,15 @@ module.exports = Self => {
|
|||
const sender = await models.Account.findById(accessToken.userId);
|
||||
const recipient = to.replace('@', '');
|
||||
|
||||
if (sender.name != recipient)
|
||||
return sendMessage(sender, to, message);
|
||||
if (sender.name != recipient) {
|
||||
let {body} = await sendMessage(sender, to, message);
|
||||
if (body)
|
||||
body = JSON.parse(body);
|
||||
else
|
||||
body = false;
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
@ -65,12 +72,15 @@ module.exports = Self => {
|
|||
if (!this.auth || this.auth && !this.auth.authToken) {
|
||||
const config = await getConfig();
|
||||
const uri = `${config.api}/login`;
|
||||
const res = await send(uri, {
|
||||
let {body} = await send(uri, {
|
||||
user: config.user,
|
||||
password: config.password
|
||||
});
|
||||
|
||||
this.auth = res.data;
|
||||
if (body) {
|
||||
body = JSON.parse(body);
|
||||
this.auth = body.data;
|
||||
}
|
||||
}
|
||||
|
||||
return this.auth;
|
||||
|
@ -93,29 +103,29 @@ module.exports = Self => {
|
|||
/**
|
||||
* Send unauthenticated request
|
||||
* @param {*} uri - Request uri
|
||||
* @param {*} body - Request params
|
||||
* @param {*} params - Request params
|
||||
* @param {*} options - Request options
|
||||
*
|
||||
* @return {Object} Request response
|
||||
*/
|
||||
async function send(uri, body, options) {
|
||||
async function send(uri, params, options = {}) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
return new Promise(resolve => {
|
||||
return resolve({statusCode: 200, message: 'Fake notification sent'});
|
||||
return resolve({
|
||||
body: JSON.stringify(
|
||||
{statusCode: 200, message: 'Fake notification sent'}
|
||||
)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
method: 'POST',
|
||||
uri: uri,
|
||||
body: body,
|
||||
headers: {'content-type': 'application/json'},
|
||||
json: true
|
||||
body: params
|
||||
};
|
||||
|
||||
if (options) Object.assign(defaultOptions, options);
|
||||
|
||||
return request(defaultOptions);
|
||||
return got.post(uri, defaultOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -128,7 +138,7 @@ module.exports = Self => {
|
|||
async function sendAuth(uri, body) {
|
||||
const login = await getAuthToken();
|
||||
const options = {
|
||||
headers: {'content-type': 'application/json'}
|
||||
headers: {}
|
||||
};
|
||||
|
||||
if (login) {
|
||||
|
|
|
@ -7,7 +7,8 @@ module.exports = Self => {
|
|||
type: 'Number',
|
||||
required: true,
|
||||
description: 'The worker id of the destinatary'
|
||||
}, {
|
||||
},
|
||||
{
|
||||
arg: 'message',
|
||||
type: 'String',
|
||||
required: true,
|
||||
|
|
|
@ -48,7 +48,7 @@ module.exports = Self => {
|
|||
throw new UserError(`You don't have enough privileges`);
|
||||
|
||||
if (process.env.NODE_ENV == 'test')
|
||||
throw new UserError(`You can't upload images on the test environment`);
|
||||
throw new UserError(`Action not allowed on the test environment`);
|
||||
|
||||
// Upload file to temporary path
|
||||
const tempContainer = await TempContainer.container(args.collection);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
|
||||
VALUES
|
||||
('Genus', '*', 'WRITE', 'ALLOW', 'ROLE', 'logisticBoss'),
|
||||
('Specie', '*', 'WRITE', 'ALLOW', 'ROLE', 'logisticBoss');
|
||||
('Specie', '*', 'WRITE', 'ALLOW', 'ROLE', 'logisticBoss'),
|
||||
('InvoiceOut', 'createPdf', 'WRITE', 'ALLOW', 'ROLE', 'invoicing');
|
||||
|
|
|
@ -67138,7 +67138,7 @@ BEGIN
|
|||
isTaxDataChecked = FALSE;
|
||||
|
||||
|
||||
SELECT * FROM tmp.ticketProblems;
|
||||
-- SELECT * FROM tmp.ticketProblems;
|
||||
|
||||
DROP TEMPORARY TABLE
|
||||
tmp.clientGetDebt,
|
||||
|
|
|
@ -162,7 +162,7 @@ describe('Ticket descriptor path', () => {
|
|||
});
|
||||
|
||||
it(`should regenerate the invoice using the descriptor menu`, async() => {
|
||||
const expectedMessage = 'Invoice sent for a regeneration, will be available in a few minutes';
|
||||
const expectedMessage = 'The invoice PDF document has been regenerated';
|
||||
|
||||
await page.waitToClick(selectors.ticketDescriptor.moreMenu);
|
||||
await page.waitForContentLoaded();
|
||||
|
|
|
@ -81,6 +81,9 @@ vn-table {
|
|||
width: 1px;
|
||||
text-align: center;
|
||||
}
|
||||
&[shrink-date] {
|
||||
width: 100px;
|
||||
}
|
||||
&[expand] {
|
||||
max-width: 400px;
|
||||
min-width: 0;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import '@babel/polyfill';
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
import * as ng from 'angular';
|
||||
export {ng};
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -9,7 +9,6 @@
|
|||
"url": "https://gitea.verdnatura.es/verdnatura/salix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "^7.2.5",
|
||||
"@uirouter/angularjs": "^1.0.20",
|
||||
"angular": "^1.7.5",
|
||||
"angular-animate": "^1.7.8",
|
||||
|
@ -17,7 +16,6 @@
|
|||
"angular-translate-loader-partial": "^2.18.1",
|
||||
"js-yaml": "^3.13.1",
|
||||
"mg-crud": "^1.1.2",
|
||||
"npm": "^6.11.3",
|
||||
"oclazyload": "^0.6.3",
|
||||
"require-yaml": "0.0.1",
|
||||
"validator": "^6.3.0"
|
||||
|
|
24
gulpfile.js
24
gulpfile.js
|
@ -3,7 +3,7 @@ const gulp = require('gulp');
|
|||
const PluginError = require('plugin-error');
|
||||
const argv = require('minimist')(process.argv.slice(2));
|
||||
const log = require('fancy-log');
|
||||
const request = require('request');
|
||||
const got = require('got');
|
||||
const e2eConfig = require('./e2e/helpers/config.js');
|
||||
const Docker = require('./db/docker.js');
|
||||
|
||||
|
@ -143,8 +143,9 @@ backTest.description = `Watches for changes in modules to execute backTest task`
|
|||
|
||||
// End to end tests
|
||||
function e2eSingleRun() {
|
||||
require('@babel/register')({presets: ['@babel/preset-env']});
|
||||
require('@babel/polyfill');
|
||||
require('@babel/register')({presets: ['@babel/env']});
|
||||
require('core-js/stable');
|
||||
require('regenerator-runtime/runtime');
|
||||
|
||||
const jasmine = require('gulp-jasmine');
|
||||
const SpecReporter = require('jasmine-spec-reporter').SpecReporter;
|
||||
|
@ -224,17 +225,20 @@ async function backendStatus() {
|
|||
return new Promise(resolve => {
|
||||
let timer;
|
||||
let attempts = 1;
|
||||
timer = setInterval(() => {
|
||||
const url = `${e2eConfig.url}/api/Applications/status`;
|
||||
request.get(url, (err, res) => {
|
||||
if (err || attempts > 100) // 250ms * 100 => 25s timeout
|
||||
throw new Error('Could not connect to backend');
|
||||
else if (res && res.body == 'true') {
|
||||
timer = setInterval(async() => {
|
||||
try {
|
||||
const url = `${e2eConfig.url}/api/Applications/status`;
|
||||
const {body} = await got.get(url);
|
||||
|
||||
if (body == 'true') {
|
||||
clearInterval(timer);
|
||||
resolve(attempts);
|
||||
} else
|
||||
attempts++;
|
||||
});
|
||||
} catch (error) {
|
||||
if (error || attempts > 100) // 250ms * 100 => 25s timeout
|
||||
throw new Error('Could not connect to backend');
|
||||
}
|
||||
}, milliseconds);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -164,7 +164,7 @@
|
|||
"Amount cannot be zero": "El importe no puede ser cero",
|
||||
"Company has to be official": "Empresa inválida",
|
||||
"You can not select this payment method without a registered bankery account": "No se puede utilizar este método de pago si no has registrado una cuenta bancaria",
|
||||
"You can't upload images on the test environment": "No puedes subir imágenes en el entorno de pruebas",
|
||||
"Action not allowed on the test environment": "Esta acción no está permitida en el entorno de pruebas",
|
||||
"The selected ticket is not suitable for this route": "El ticket seleccionado no es apto para esta ruta",
|
||||
"Sorts whole route": "Reordena ruta entera",
|
||||
"New ticket request has been created with price": "Se ha creado una nueva petición de compra '{{description}}' para el día <strong>{{shipped}}</strong>, con una cantidad de <strong>{{quantity}}</strong> y un precio de <strong>{{price}} €</strong>",
|
||||
|
|
|
@ -68,5 +68,16 @@
|
|||
"image/jpeg",
|
||||
"image/jpg"
|
||||
]
|
||||
},
|
||||
"invoiceStorage": {
|
||||
"name": "invoiceStorage",
|
||||
"connector": "loopback-component-storage",
|
||||
"provider": "filesystem",
|
||||
"root": "./storage/pdfs/invoice",
|
||||
"maxFileSize": "52428800",
|
||||
"allowedContentTypes": [
|
||||
"application/octet-stream",
|
||||
"application/pdf"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<vn-tr>
|
||||
<vn-th field="id" number>Id</vn-th>
|
||||
<vn-th field="clientFk">Client</vn-th>
|
||||
<vn-th field="created" center expand>Created</vn-th>
|
||||
<vn-th field="created" center shrink-date>Created</vn-th>
|
||||
<vn-th field="workerFk">Worker</vn-th>
|
||||
<vn-th field="claimStateFk">State</vn-th>
|
||||
<vn-th></vn-th>
|
||||
|
@ -29,7 +29,7 @@
|
|||
{{::claim.name}}
|
||||
</span>
|
||||
</vn-td>
|
||||
<vn-td center expand>{{::claim.created | date:'dd/MM/yyyy'}}</vn-td>
|
||||
<vn-td center shrink-date>{{::claim.created | date:'dd/MM/yyyy'}}</vn-td>
|
||||
<vn-td expand>
|
||||
<span
|
||||
vn-click-stop="workerDescriptor.show($event, claim.workerFk)"
|
||||
|
|
|
@ -199,7 +199,7 @@
|
|||
<span
|
||||
ng-click="ticketDescriptor.show($event, action.sale.ticket.id)"
|
||||
class="link">
|
||||
{{::action.sale.ticket.id | zeroFill:6}}
|
||||
{{::action.sale.ticket.id}}
|
||||
</span>
|
||||
</vn-td>
|
||||
<vn-td expand>{{::action.claimBeggining.description}}</vn-td>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const request = require('request-promise-native');
|
||||
const got = require('got');
|
||||
const UserError = require('vn-loopback/util/user-error');
|
||||
const getFinalState = require('vn-loopback/util/hook').getFinalState;
|
||||
const isMultiple = require('vn-loopback/util/hook').isMultiple;
|
||||
|
@ -299,8 +299,8 @@ module.exports = Self => {
|
|||
recipientId: instance.id,
|
||||
recipient: instance.email
|
||||
};
|
||||
await request.get(`${origin}/api/email/payment-update`, {
|
||||
qs: params
|
||||
await got.get(`${origin}/api/email/payment-update`, {
|
||||
query: params
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -21,10 +21,20 @@ module.exports = Self => {
|
|||
});
|
||||
|
||||
Self.book = async ref => {
|
||||
let ticketAddress = await Self.app.models.Ticket.findOne({where: {invoiceOut: ref}});
|
||||
let invoiceCompany = await Self.app.models.InvoiceOut.findOne({where: {ref: ref}});
|
||||
let [taxArea] = await Self.rawSql(`Select vn.addressTaxArea(?, ?) AS code`, [ticketAddress.address, invoiceCompany.company]);
|
||||
const models = Self.app.models;
|
||||
const ticketAddress = await models.Ticket.findOne({
|
||||
where: {invoiceOut: ref}
|
||||
});
|
||||
const invoiceCompany = await models.InvoiceOut.findOne({
|
||||
where: {ref: ref}
|
||||
});
|
||||
let query = 'SELECT vn.addressTaxArea(?, ?) AS code';
|
||||
const [taxArea] = await Self.rawSql(query, [
|
||||
ticketAddress.address,
|
||||
invoiceCompany.company
|
||||
]);
|
||||
|
||||
return Self.rawSql(`CALL vn.invoiceOutAgain(?, ?)`, [ref, taxArea.code]);
|
||||
query = 'CALL vn.invoiceOutAgain(?, ?)';
|
||||
return Self.rawSql(query, [ref, taxArea.code]);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
const fs = require('fs-extra');
|
||||
const got = require('got');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('createPdf', {
|
||||
description: 'Creates an invoice PDF',
|
||||
accessType: 'WRITE',
|
||||
accepts: [
|
||||
{
|
||||
arg: 'id',
|
||||
type: 'number',
|
||||
description: 'The invoice id',
|
||||
http: {source: 'path'}
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/:id/createPdf`,
|
||||
verb: 'POST'
|
||||
}
|
||||
});
|
||||
|
||||
Self.createPdf = async function(ctx, id, options) {
|
||||
const models = Self.app.models;
|
||||
const headers = ctx.req.headers;
|
||||
const origin = headers.origin;
|
||||
const authorization = headers.authorization;
|
||||
|
||||
if (process.env.NODE_ENV == 'test')
|
||||
throw new UserError(`Action not allowed on the test environment`);
|
||||
|
||||
let tx;
|
||||
let newOptions = {};
|
||||
|
||||
if (typeof options == 'object')
|
||||
Object.assign(newOptions, options);
|
||||
|
||||
if (!newOptions.transaction) {
|
||||
tx = await Self.beginTransaction({});
|
||||
newOptions.transaction = tx;
|
||||
}
|
||||
|
||||
let fileSrc;
|
||||
try {
|
||||
const invoiceOut = await Self.findById(id, null, newOptions);
|
||||
await invoiceOut.updateAttributes({
|
||||
hasPdf: true
|
||||
}, newOptions);
|
||||
|
||||
const response = got.stream(`${origin}/api/report/invoice`, {
|
||||
query: {
|
||||
authorization: authorization,
|
||||
invoiceId: id
|
||||
}
|
||||
});
|
||||
|
||||
const invoiceYear = invoiceOut.created.getFullYear().toString();
|
||||
const container = await models.InvoiceContainer.container(invoiceYear);
|
||||
const rootPath = container.client.root;
|
||||
const fileName = `${invoiceOut.ref}.pdf`;
|
||||
fileSrc = path.join(rootPath, invoiceYear, fileName);
|
||||
|
||||
const writeStream = fs.createWriteStream(fileSrc);
|
||||
writeStream.on('open', () => {
|
||||
response.pipe(writeStream);
|
||||
});
|
||||
|
||||
writeStream.on('finish', async function() {
|
||||
writeStream.end();
|
||||
});
|
||||
|
||||
if (tx) await tx.commit();
|
||||
|
||||
return invoiceOut;
|
||||
} catch (e) {
|
||||
if (tx) await tx.rollback();
|
||||
if (fs.existsSync(fileSrc))
|
||||
await fs.unlink(fileSrc);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,51 +0,0 @@
|
|||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('regenerate', {
|
||||
description: 'Sends an invoice to a regeneration queue',
|
||||
accessType: 'WRITE',
|
||||
accepts: [{
|
||||
arg: 'id',
|
||||
type: 'number',
|
||||
required: true,
|
||||
description: 'The invoiceOut id',
|
||||
http: {source: 'path'}
|
||||
}],
|
||||
returns: {
|
||||
type: 'object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: '/:id/regenerate',
|
||||
verb: 'POST'
|
||||
}
|
||||
});
|
||||
|
||||
Self.regenerate = async(ctx, id) => {
|
||||
const userId = ctx.req.accessToken.userId;
|
||||
const models = Self.app.models;
|
||||
const invoiceReportFk = 30; // Should be deprecated
|
||||
const worker = await models.Worker.findOne({where: {userFk: userId}});
|
||||
const tx = await Self.beginTransaction({});
|
||||
|
||||
try {
|
||||
let options = {transaction: tx};
|
||||
|
||||
// Remove all invoice references from tickets
|
||||
const invoiceOut = await models.InvoiceOut.findById(id, null, options);
|
||||
await invoiceOut.updateAttributes({
|
||||
hasPdf: false
|
||||
});
|
||||
|
||||
// Send to print queue
|
||||
await Self.rawSql(`
|
||||
INSERT INTO vn.printServerQueue (reportFk, param1, workerFk)
|
||||
VALUES (?, ?, ?)`, [invoiceReportFk, id, worker.id], options);
|
||||
|
||||
await tx.commit();
|
||||
|
||||
return invoiceOut;
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
const app = require('vn-loopback/server/server');
|
||||
const got = require('got');
|
||||
|
||||
describe('InvoiceOut createPdf()', () => {
|
||||
const userId = 1;
|
||||
const ctx = {
|
||||
req: {
|
||||
|
||||
accessToken: {userId: userId},
|
||||
headers: {origin: 'http://localhost:5000'},
|
||||
}
|
||||
};
|
||||
|
||||
it('should create a new PDF file and set true the hasPdf property', async() => {
|
||||
const invoiceId = 1;
|
||||
const response = {
|
||||
pipe: () => {},
|
||||
on: () => {},
|
||||
};
|
||||
spyOn(got, 'stream').and.returnValue(response);
|
||||
|
||||
let result = await app.models.InvoiceOut.createPdf(ctx, invoiceId);
|
||||
|
||||
expect(result.hasPdf).toBe(true);
|
||||
});
|
||||
});
|
|
@ -1,36 +0,0 @@
|
|||
const app = require('vn-loopback/server/server');
|
||||
|
||||
describe('invoiceOut regenerate()', () => {
|
||||
const invoiceReportFk = 30;
|
||||
const invoiceOutId = 1;
|
||||
|
||||
it('should check that the invoice has a PDF and is not in print generation queue', async() => {
|
||||
const invoiceOut = await app.models.InvoiceOut.findById(invoiceOutId);
|
||||
const [queue] = await app.models.InvoiceOut.rawSql(`
|
||||
SELECT COUNT(*) AS total
|
||||
FROM vn.printServerQueue
|
||||
WHERE reportFk = ?`, [invoiceReportFk]);
|
||||
|
||||
expect(invoiceOut.hasPdf).toBeTruthy();
|
||||
expect(queue.total).toEqual(0);
|
||||
});
|
||||
|
||||
it(`should mark the invoice as doesn't have PDF and add it to a print queue`, async() => {
|
||||
const ctx = {req: {accessToken: {userId: 5}}};
|
||||
const invoiceOut = await app.models.InvoiceOut.regenerate(ctx, invoiceOutId);
|
||||
const [queue] = await app.models.InvoiceOut.rawSql(`
|
||||
SELECT COUNT(*) AS total
|
||||
FROM vn.printServerQueue
|
||||
WHERE reportFk = ?`, [invoiceReportFk]);
|
||||
|
||||
expect(invoiceOut.hasPdf).toBeFalsy();
|
||||
expect(queue.total).toEqual(1);
|
||||
|
||||
// restores
|
||||
const invoiceOutToRestore = await app.models.InvoiceOut.findById(invoiceOutId);
|
||||
await invoiceOutToRestore.updateAttributes({hasPdf: true});
|
||||
await app.models.InvoiceOut.rawSql(`
|
||||
DELETE FROM vn.printServerQueue
|
||||
WHERE reportFk = ?`, [invoiceReportFk]);
|
||||
});
|
||||
});
|
|
@ -1,5 +1,8 @@
|
|||
{
|
||||
"InvoiceOut": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"InvoiceContainer": {
|
||||
"dataSource": "invoiceStorage"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "InvoiceContainer",
|
||||
"base": "Container",
|
||||
"acls": [{
|
||||
"accessType": "READ",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
}]
|
||||
}
|
|
@ -2,7 +2,7 @@ module.exports = Self => {
|
|||
require('../methods/invoiceOut/filter')(Self);
|
||||
require('../methods/invoiceOut/summary')(Self);
|
||||
require('../methods/invoiceOut/download')(Self);
|
||||
require('../methods/invoiceOut/regenerate')(Self);
|
||||
require('../methods/invoiceOut/delete')(Self);
|
||||
require('../methods/invoiceOut/book')(Self);
|
||||
require('../methods/invoiceOut/createPdf')(Self);
|
||||
};
|
||||
|
|
|
@ -25,6 +25,14 @@
|
|||
translate>
|
||||
Book invoice
|
||||
</vn-item>
|
||||
<vn-item
|
||||
ng-click="createInvoicePdfConfirmation.show()"
|
||||
vn-acl="invoicing"
|
||||
vn-acl-action="remove"
|
||||
name="regenerateInvoice"
|
||||
translate>
|
||||
Regenerate invoice PDF
|
||||
</vn-item>
|
||||
</slot-menu>
|
||||
<slot-body>
|
||||
<div class="attributes">
|
||||
|
@ -81,4 +89,12 @@
|
|||
</vn-confirm>
|
||||
<vn-client-descriptor-popover
|
||||
vn-id="clientDescriptor">
|
||||
</vn-client-descriptor-popover>
|
||||
</vn-client-descriptor-popover>
|
||||
|
||||
<!-- Create invoice PDF confirmation dialog -->
|
||||
<vn-confirm
|
||||
vn-id="createInvoicePdfConfirmation"
|
||||
on-accept="$ctrl.createInvoicePdf()"
|
||||
question="Are you sure you want to regenerate the invoice PDF document?"
|
||||
message="You are going to regenerate the invoice PDF document">
|
||||
</vn-confirm>
|
|
@ -22,6 +22,16 @@ class Controller extends Descriptor {
|
|||
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut booked')));
|
||||
}
|
||||
|
||||
createInvoicePdf() {
|
||||
const invoiceId = this.invoiceOut.id;
|
||||
return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`)
|
||||
.then(() => {
|
||||
const snackbarMessage = this.$t(
|
||||
`The invoice PDF document has been regenerated`);
|
||||
this.vnApp.showSuccess(snackbarMessage);
|
||||
});
|
||||
}
|
||||
|
||||
get filter() {
|
||||
if (this.invoiceOut)
|
||||
return JSON.stringify({refFk: this.invoiceOut.ref});
|
||||
|
|
|
@ -3,6 +3,7 @@ import './index';
|
|||
describe('vnInvoiceOutDescriptor', () => {
|
||||
let controller;
|
||||
let $httpBackend;
|
||||
const invoiceOut = {id: 1};
|
||||
|
||||
beforeEach(ngModule('invoiceOut'));
|
||||
|
||||
|
@ -11,6 +12,20 @@ describe('vnInvoiceOutDescriptor', () => {
|
|||
controller = $componentController('vnInvoiceOutDescriptor', {$element: null});
|
||||
}));
|
||||
|
||||
describe('createInvoicePdf()', () => {
|
||||
it('should make a query and show a success snackbar', () => {
|
||||
jest.spyOn(controller.vnApp, 'showSuccess');
|
||||
|
||||
controller.invoiceOut = invoiceOut;
|
||||
|
||||
$httpBackend.expectPOST(`InvoiceOuts/${invoiceOut.id}/createPdf`).respond();
|
||||
controller.createInvoicePdf();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadData()', () => {
|
||||
it(`should perform a get query to store the invoice in data into the controller`, () => {
|
||||
const id = 1;
|
||||
|
|
|
@ -8,4 +8,6 @@ InvoiceOut deleted: Factura eliminada
|
|||
Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura?
|
||||
Book invoice: Asentar factura
|
||||
InvoiceOut booked: Factura asentada
|
||||
Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura?
|
||||
Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura?
|
||||
Regenerate invoice PDF: Regenerar PDF factura
|
||||
The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado
|
|
@ -24,7 +24,7 @@
|
|||
class="clickable vn-tr search-result"
|
||||
ui-sref="invoiceOut.card.summary({id: {{::invoiceOut.id}}})">
|
||||
<vn-td>{{::invoiceOut.ref | dashIfEmpty}}</vn-td>
|
||||
<vn-td expand>{{::invoiceOut.issued | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td>
|
||||
<vn-td shrink>{{::invoiceOut.issued | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td>
|
||||
<vn-td number>{{::invoiceOut.amount | currency: 'EUR': 2 | dashIfEmpty}}</vn-td>
|
||||
<vn-td>
|
||||
<span
|
||||
|
@ -35,7 +35,7 @@
|
|||
</vn-td>
|
||||
<vn-td expand>{{::invoiceOut.created | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td>
|
||||
<vn-td>{{::invoiceOut.companyCode | dashIfEmpty}}</vn-td>
|
||||
<vn-td expand>{{::invoiceOut.dued | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td>
|
||||
<vn-td shrink>{{::invoiceOut.dued | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td>
|
||||
<vn-td>
|
||||
<vn-icon-button
|
||||
ng-show="invoiceOut.hasPdf"
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<vn-th field="clientFk">Client</vn-th>
|
||||
<vn-th field="isConfirmed" center>Confirmed</vn-th>
|
||||
<vn-th field="created" center expand>Created</vn-th>
|
||||
<vn-th field="landed" default-order="DESC" center expand>Landed</vn-th>
|
||||
<vn-th field="landed" default-order="DESC" shrink-date>Landed</vn-th>
|
||||
<vn-th field="created" center>Hour</vn-th>
|
||||
<vn-th field="agencyName" center>Agency</vn-th>
|
||||
<vn-th center>Total</vn-th>
|
||||
|
@ -46,8 +46,8 @@
|
|||
disabled="true">
|
||||
</vn-check>
|
||||
</vn-td>
|
||||
<vn-td center expand>{{::order.created | date: 'dd/MM/yyyy HH:mm'}}</vn-td>
|
||||
<vn-td center expand>
|
||||
<vn-td center>{{::order.created | date: 'dd/MM/yyyy HH:mm'}}</vn-td>
|
||||
<vn-td shrink-date>
|
||||
<span class="chip {{$ctrl.compareDate(order.landed)}}">
|
||||
{{::order.landed | date:'dd/MM/yyyy'}}
|
||||
</span>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<vn-th th-id="worker">Worker</vn-th>
|
||||
<vn-th th-id="agency">Agency</vn-th>
|
||||
<vn-th th-id="vehicle">Vehicle</vn-th>
|
||||
<vn-th th-id="created" expand>Date</vn-th>
|
||||
<vn-th th-id="created" shrink-date>Date</vn-th>
|
||||
<vn-th th-id="m3" number>m³</vn-th>
|
||||
<vn-th th-id="description">Description</vn-th>
|
||||
<vn-th shrink></vn-th>
|
||||
|
@ -44,7 +44,7 @@
|
|||
</vn-td>
|
||||
<vn-td>{{::route.agencyName | dashIfEmpty}}</vn-td>
|
||||
<vn-td>{{::route.vehiclePlateNumber | dashIfEmpty}}</vn-td>
|
||||
<vn-td expand>{{::route.created | dashIfEmpty | date:'dd/MM/yyyy'}}</vn-td>
|
||||
<vn-td shrink-date>{{::route.created | dashIfEmpty | date:'dd/MM/yyyy'}}</vn-td>
|
||||
<vn-td number>{{::route.m3 | dashIfEmpty}}</vn-td>
|
||||
<vn-td>{{::route.description | dashIfEmpty}}</vn-td>
|
||||
<vn-td>
|
||||
|
|
|
@ -303,13 +303,12 @@ module.exports = Self => {
|
|||
|
||||
stmt.merge(conn.makeOrderBy(filter.order));
|
||||
stmt.merge(conn.makeLimit(filter));
|
||||
let ticketsIndex = stmts.push(stmt);
|
||||
let ticketsIndex = stmts.push(stmt) - 1;
|
||||
|
||||
stmts.push(
|
||||
`DROP TEMPORARY TABLE
|
||||
tmp.filter,
|
||||
tmp.ticket,
|
||||
tmp.ticketTotal,
|
||||
tmp.ticketGetProblems`);
|
||||
|
||||
let sql = ParameterizedSQL.join(stmts, ';');
|
||||
|
|
|
@ -67,11 +67,7 @@ module.exports = function(Self) {
|
|||
|
||||
if (serial != 'R' && invoiceId) {
|
||||
await Self.rawSql('CALL invoiceOutBooking(?)', [invoiceId], options);
|
||||
await models.PrintServerQueue.create({
|
||||
reportFk: 3, // Tarea #2734 (Nueva): crear informe facturas
|
||||
param1: invoiceId,
|
||||
workerFk: userId
|
||||
}, options);
|
||||
await models.InvoiceOut.createPdf(ctx, invoiceId, options);
|
||||
}
|
||||
await tx.commit();
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ describe('ticket makeInvoice()', () => {
|
|||
const userId = 19;
|
||||
const activeCtx = {
|
||||
accessToken: {userId: userId},
|
||||
headers: {origin: 'http://localhost:5000'},
|
||||
};
|
||||
const ctx = {req: activeCtx};
|
||||
|
||||
|
@ -43,6 +44,9 @@ describe('ticket makeInvoice()', () => {
|
|||
});
|
||||
|
||||
it('should invoice a ticket, then try again to fail', async() => {
|
||||
const invoiceOutModel = app.models.InvoiceOut;
|
||||
spyOn(invoiceOutModel, 'createPdf');
|
||||
|
||||
invoice = await app.models.Ticket.makeInvoice(ctx, ticketId);
|
||||
|
||||
expect(invoice.invoiceFk).toBeDefined();
|
||||
|
|
|
@ -80,13 +80,13 @@
|
|||
Make invoice
|
||||
</vn-item>
|
||||
<vn-item
|
||||
ng-click="regenerateInvoiceConfirmation.show()"
|
||||
ng-click="createInvoicePdfConfirmation.show()"
|
||||
ng-show="$ctrl.isInvoiced"
|
||||
vn-acl="invoicing"
|
||||
vn-acl-action="remove"
|
||||
name="regenerateInvoice"
|
||||
translate>
|
||||
Regenerate invoice
|
||||
Regenerate invoice PDF
|
||||
</vn-item>
|
||||
<vn-item
|
||||
ng-click="recalculateComponentsConfirmation.show()"
|
||||
|
@ -207,12 +207,12 @@
|
|||
message="Are you sure you want to invoice this ticket?">
|
||||
</vn-confirm>
|
||||
|
||||
<!-- Regenerate invoice confirmation dialog -->
|
||||
<!-- Create invoice PDF confirmation dialog -->
|
||||
<vn-confirm
|
||||
vn-id="regenerateInvoiceConfirmation"
|
||||
on-accept="$ctrl.regenerateInvoice()"
|
||||
question="You are going to regenerate the invoice"
|
||||
message="Are you sure you want to regenerate the invoice?">
|
||||
vn-id="createInvoicePdfConfirmation"
|
||||
on-accept="$ctrl.createInvoicePdf()"
|
||||
question="Are you sure you want to regenerate the invoice PDF document?"
|
||||
message="You are going to regenerate the invoice PDF document">
|
||||
</vn-confirm>
|
||||
|
||||
<!-- Recalculate components confirmation dialog -->
|
||||
|
|
|
@ -219,12 +219,12 @@ class Controller extends Section {
|
|||
.then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced')));
|
||||
}
|
||||
|
||||
regenerateInvoice() {
|
||||
createInvoicePdf() {
|
||||
const invoiceId = this.ticket.invoiceOut.id;
|
||||
return this.$http.post(`InvoiceOuts/${invoiceId}/regenerate`)
|
||||
return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`)
|
||||
.then(() => {
|
||||
const snackbarMessage = this.$t(
|
||||
`Invoice sent for a regeneration, will be available in a few minutes`);
|
||||
`The invoice PDF document has been regenerated`);
|
||||
this.vnApp.showSuccess(snackbarMessage);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -148,12 +148,12 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('regenerateInvoice()', () => {
|
||||
describe('createInvoicePdf()', () => {
|
||||
it('should make a query and show a success snackbar', () => {
|
||||
jest.spyOn(controller.vnApp, 'showSuccess');
|
||||
|
||||
$httpBackend.expectPOST(`InvoiceOuts/${ticket.invoiceOut.id}/regenerate`).respond();
|
||||
controller.regenerateInvoice();
|
||||
$httpBackend.expectPOST(`InvoiceOuts/${ticket.invoiceOut.id}/createPdf`).respond();
|
||||
controller.createInvoicePdf();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
|
||||
|
|
|
@ -17,12 +17,12 @@ Make a payment: "Verdnatura le comunica:\rSu pedido está pendiente de pago.\rPo
|
|||
Minimum is needed: "Verdnatura le recuerda:\rEs necesario un importe mínimo de 50€ (Sin IVA) en su pedido {{ticketId}} del día {{created | date: 'dd/MM/yyyy'}} para recibirlo sin portes adicionales."
|
||||
Ticket invoiced: Ticket facturado
|
||||
Make invoice: Crear factura
|
||||
Regenerate invoice: Regenerar factura
|
||||
Regenerate invoice PDF: Regenerar PDF factura
|
||||
The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado
|
||||
You are going to invoice this ticket: Vas a facturar este ticket
|
||||
Are you sure you want to invoice this ticket?: ¿Seguro que quieres facturar este ticket?
|
||||
You are going to regenerate the invoice: Vas a regenerar la factura
|
||||
Are you sure you want to regenerate the invoice?: ¿Seguro que quieres regenerar la factura?
|
||||
Invoice sent for a regeneration, will be available in a few minutes: La factura ha sido enviada para ser regenerada, estará disponible en unos minutos
|
||||
You are going to regenerate the invoice PDF document: Vas a regenerar el documento PDF de la factura
|
||||
Are you sure you want to regenerate the invoice PDF document?: ¿Seguro que quieres regenerar el documento PDF de la factura?
|
||||
Shipped hour updated: Hora de envio modificada
|
||||
Deleted ticket: Ticket eliminado
|
||||
Recalculate components: Recalcular componentes
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<vn-th class="icon-field"></vn-th>
|
||||
<vn-th field="id">Id</vn-th>
|
||||
<vn-th field="salesPersonFk" class="expendable">Salesperson</vn-th>
|
||||
<vn-th field="shipped">Date</vn-th>
|
||||
<vn-th field="shipped" shrink-date>Date</vn-th>
|
||||
<vn-th>Hour</vn-th>
|
||||
<vn-th field="hour" shrink>Closure</vn-th>
|
||||
<vn-th field="nickname">Alias</vn-th>
|
||||
|
@ -80,12 +80,12 @@
|
|||
{{::ticket.userName | dashIfEmpty}}
|
||||
</span>
|
||||
</vn-td>
|
||||
<vn-td expand>
|
||||
<vn-td shrink-date>
|
||||
<span class="chip {{$ctrl.compareDate(ticket.shipped)}}">
|
||||
{{::ticket.shipped | date: 'dd/MM/yyyy'}}
|
||||
</span>
|
||||
</vn-td>
|
||||
<vn-td>{{::ticket.shipped | date: 'HH:mm'}}</vn-td>
|
||||
<vn-td shrink>{{::ticket.shipped | date: 'HH:mm'}}</vn-td>
|
||||
<vn-td shrink>{{::ticket.zoneLanding | date: 'HH:mm'}}</vn-td>
|
||||
<vn-td>
|
||||
<span
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
<vn-th field="ref">Reference</vn-th>
|
||||
<vn-th field="agencyFk">Agency</vn-th>
|
||||
<vn-th field="warehouseOutFk">Warehouse Out</vn-th>
|
||||
<vn-th field="shipped" center expand>Shipped</vn-th>
|
||||
<vn-th field="shipped" center shrink-date>Shipped</vn-th>
|
||||
<vn-th field="isDelivered" center>Delivered</vn-th>
|
||||
<vn-th field="warehouseInFk">Warehouse In</vn-th>
|
||||
<vn-th field="landed" center expand>Landed</vn-th>
|
||||
<vn-th field="landed" center shrink-date>Landed</vn-th>
|
||||
<vn-th field="isReceived" center>Received</vn-th>
|
||||
<vn-th shrink></vn-th>
|
||||
</vn-tr>
|
||||
|
@ -29,14 +29,14 @@
|
|||
<vn-td>{{::travel.ref}}</vn-td>
|
||||
<vn-td>{{::travel.agencyModeName}}</vn-td>
|
||||
<vn-td>{{::travel.warehouseOutName}}</vn-td>
|
||||
<vn-td center expand>
|
||||
<vn-td center shrink-date>
|
||||
<span class="chip {{$ctrl.compareDate(travel.shipped)}}">
|
||||
{{::travel.shipped | date:'dd/MM/yyyy'}}
|
||||
</span>
|
||||
</vn-td>
|
||||
<vn-td center><vn-check ng-model="travel.isDelivered" disabled="true"></vn-check></vn-td>
|
||||
<vn-td expand>{{::travel.warehouseInName}}</vn-td>
|
||||
<vn-td center expand>
|
||||
<vn-td center shrink-date>
|
||||
<span class="chip {{$ctrl.compareDate(travel.landed)}}">
|
||||
{{::travel.landed | date:'dd/MM/yyyy'}}
|
||||
</span>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
|
@ -15,15 +15,16 @@
|
|||
"bmp-js": "^0.1.0",
|
||||
"compression": "^1.7.3",
|
||||
"fs-extra": "^5.0.0",
|
||||
"got": "^6.7.1",
|
||||
"helmet": "^3.21.2",
|
||||
"i18n": "^0.8.4",
|
||||
"image-type": "^4.1.0",
|
||||
"imap": "^0.8.19",
|
||||
"ldapjs": "^2.2.0",
|
||||
"loopback": "^3.26.0",
|
||||
"loopback-boot": "^2.27.1",
|
||||
"loopback-boot": "3.3.1",
|
||||
"loopback-component-explorer": "^6.5.0",
|
||||
"loopback-component-storage": "^3.6.1",
|
||||
"loopback-component-storage": "3.6.1",
|
||||
"loopback-connector-mysql": "^5.4.3",
|
||||
"loopback-connector-remote": "^3.4.1",
|
||||
"loopback-context": "^3.4.0",
|
||||
|
@ -34,8 +35,6 @@
|
|||
"object.pick": "^1.3.0",
|
||||
"puppeteer": "^7.1.0",
|
||||
"read-chunk": "^3.2.0",
|
||||
"request": "^2.88.0",
|
||||
"request-promise-native": "^1.0.8",
|
||||
"require-yaml": "0.0.1",
|
||||
"sharp": "^0.27.1",
|
||||
"smbhash": "0.0.1",
|
||||
|
@ -48,13 +47,12 @@
|
|||
"devDependencies": {
|
||||
"@babel/core": "^7.7.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
||||
"@babel/polyfill": "^7.7.0",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@babel/register": "^7.7.7",
|
||||
"angular-mocks": "^1.7.9",
|
||||
"babel-jest": "^26.0.1",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"core-js": "^3.9.1",
|
||||
"css-loader": "^2.1.0",
|
||||
"del": "^2.2.2",
|
||||
"eslint": "^7.11.0",
|
||||
|
@ -90,6 +88,7 @@
|
|||
"nodemon": "^1.19.4",
|
||||
"plugin-error": "^1.0.1",
|
||||
"raw-loader": "^1.0.0",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"sass-loader": "^7.3.1",
|
||||
"style-loader": "^0.23.1",
|
||||
"webpack": "^4.41.5",
|
||||
|
|
|
@ -45,4 +45,8 @@
|
|||
.no-page-break {
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid
|
||||
}
|
||||
|
||||
.page-break-after {
|
||||
page-break-after: always;
|
||||
}
|
|
@ -9,6 +9,6 @@ body {
|
|||
.title {
|
||||
margin-bottom: 20px;
|
||||
font-weight: 100;
|
||||
font-size: 3em;
|
||||
font-size: 2.6rem;
|
||||
margin-top: 0
|
||||
}
|
|
@ -83,6 +83,11 @@ class Component {
|
|||
component.template = juice.inlineContent(this.template, this.stylesheet, {
|
||||
inlinePseudoElements: true
|
||||
});
|
||||
const tplPath = this.path;
|
||||
if (!component.computed) component.computed = {};
|
||||
component.computed.path = function() {
|
||||
return tplPath;
|
||||
};
|
||||
|
||||
return component;
|
||||
}
|
||||
|
@ -93,7 +98,7 @@ class Component {
|
|||
|
||||
const component = this.build();
|
||||
const i18n = new VueI18n(config.i18n);
|
||||
const props = {tplPath: this.path, ...this.args};
|
||||
const props = {...this.args};
|
||||
this._component = new Vue({
|
||||
i18n: i18n,
|
||||
render: h => h(component, {
|
||||
|
|
|
@ -36,13 +36,14 @@ module.exports = {
|
|||
* Makes a query from a SQL file
|
||||
* @param {String} queryName - The SQL file name
|
||||
* @param {Object} params - Parameterized values
|
||||
* @param {Object} connection - Optional pool connection
|
||||
*
|
||||
* @return {Object} - Result promise
|
||||
*/
|
||||
rawSqlFromDef(queryName, params) {
|
||||
rawSqlFromDef(queryName, params, connection) {
|
||||
const query = fs.readFileSync(`${queryName}.sql`, 'utf8');
|
||||
|
||||
return this.rawSql(query, params);
|
||||
return this.rawSql(query, params, connection);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -19,12 +19,13 @@ const dbHelper = {
|
|||
* Makes a query from a SQL file
|
||||
* @param {String} queryName - The SQL file name
|
||||
* @param {Object} params - Parameterized values
|
||||
* @param {Object} connection - Optional pool connection
|
||||
*
|
||||
* @return {Object} - Result promise
|
||||
*/
|
||||
rawSqlFromDef(queryName, params) {
|
||||
const absolutePath = path.join(__dirname, '../', this.tplPath, 'sql', queryName);
|
||||
return db.rawSqlFromDef(absolutePath, params);
|
||||
rawSqlFromDef(queryName, params, connection) {
|
||||
const absolutePath = path.join(__dirname, '../', this.path, 'sql', queryName);
|
||||
return db.rawSqlFromDef(absolutePath, params, connection);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -66,7 +67,7 @@ const dbHelper = {
|
|||
*/
|
||||
findValueFromDef(queryName, params) {
|
||||
return this.findOneFromDef(queryName, params).then(row => {
|
||||
return Object.values(row)[0];
|
||||
if (row) return Object.values(row)[0];
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -77,7 +78,7 @@ const dbHelper = {
|
|||
* @return {Object} - SQL
|
||||
*/
|
||||
getSqlFromDef(queryName) {
|
||||
const absolutePath = path.join(__dirname, '../', this.tplPath, 'sql', queryName);
|
||||
const absolutePath = path.join(__dirname, '../', this.path, 'sql', queryName);
|
||||
return db.getSqlFromDef(absolutePath);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -6,6 +6,10 @@ const config = require('../core/config');
|
|||
module.exports = app => {
|
||||
app.get('/api/closure/all', async function(req, res, next) {
|
||||
try {
|
||||
const reqArgs = req.args;
|
||||
if (!reqArgs.to)
|
||||
throw new Error('The argument to is required');
|
||||
|
||||
res.status(200).json({
|
||||
message: 'Task executed successfully'
|
||||
});
|
||||
|
@ -19,9 +23,12 @@ module.exports = app => {
|
|||
JOIN ticketState ts ON ts.ticketFk = t.id
|
||||
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
|
||||
WHERE al.code = 'PACKED'
|
||||
AND DATE(t.shipped) BETWEEN DATE_ADD(CURDATE(), INTERVAL -2 DAY) AND CURDATE()
|
||||
AND DATE(t.shipped) BETWEEN DATE_ADD(:to, INTERVAL -2 DAY)
|
||||
AND util.dayEnd(:to)
|
||||
AND t.refFk IS NULL
|
||||
GROUP BY e.ticketFk`);
|
||||
GROUP BY e.ticketFk`, {
|
||||
to: reqArgs.to
|
||||
});
|
||||
const ticketIds = tickets.map(ticket => ticket.id);
|
||||
|
||||
await closeAll(ticketIds, req.args);
|
||||
|
@ -33,10 +40,13 @@ module.exports = app => {
|
|||
JOIN deliveryMethod dm ON dm.id = am.deliveryMethodFk
|
||||
JOIN zone z ON z.id = t.zoneFk
|
||||
SET t.routeFk = NULL
|
||||
WHERE shipped BETWEEN CURDATE() AND util.dayEnd(CURDATE())
|
||||
WHERE DATE(t.shipped) BETWEEN DATE_ADD(:to, INTERVAL -2 DAY)
|
||||
AND util.dayEnd(:to)
|
||||
AND al.code NOT IN('DELIVERED','PACKED')
|
||||
AND t.routeFk
|
||||
AND z.name LIKE '%MADRID%'`);
|
||||
AND z.name LIKE '%MADRID%'`, {
|
||||
to: reqArgs.to
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
|
@ -100,7 +110,8 @@ module.exports = app => {
|
|||
WHERE al.code = 'PACKED'
|
||||
AND t.agencyModeFk IN(:agencyModeId)
|
||||
AND t.warehouseFk = :warehouseId
|
||||
AND DATE(t.shipped) BETWEEN DATE_ADD(:to, INTERVAL -2 DAY) AND :to
|
||||
AND DATE(t.shipped) BETWEEN DATE_ADD(:to, INTERVAL -2 DAY)
|
||||
AND util.dayEnd(:to)
|
||||
AND t.refFk IS NULL
|
||||
GROUP BY e.ticketFk`, {
|
||||
agencyModeId: agenciesId,
|
||||
|
|
|
@ -6,11 +6,16 @@ module.exports = app => {
|
|||
const reportName = req.params.name;
|
||||
const fileName = getFileName(reportName, req.args);
|
||||
const report = new Report(reportName, req.args);
|
||||
const stream = await report.toPdfStream();
|
||||
if (req.args.preview) {
|
||||
const template = await report.render();
|
||||
res.send(template);
|
||||
} else {
|
||||
const stream = await report.toPdfStream();
|
||||
|
||||
res.setHeader('Content-type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
|
||||
res.end(stream);
|
||||
res.setHeader('Content-type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
|
||||
res.end(stream);
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -14,7 +14,6 @@
|
|||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"fs-extra": "^7.0.1",
|
||||
"html-pdf": "^2.2.0",
|
||||
"intl": "^1.2.5",
|
||||
"js-yaml": "^3.13.1",
|
||||
"juice": "^5.2.0",
|
||||
|
|
|
@ -19,7 +19,7 @@ h2 {
|
|||
}
|
||||
|
||||
.ticket-info {
|
||||
font-size: 26px
|
||||
font-size: 22px
|
||||
}
|
||||
|
||||
#phytosanitary {
|
||||
|
|
|
@ -37,6 +37,7 @@ FROM vn.sale s
|
|||
LEFT JOIN taxClass tcl ON tcl.id = itc.taxClassFk
|
||||
LEFT JOIN itemBotanicalWithGenus ib ON ib.itemFk = i.id
|
||||
AND ic.code = 'plant'
|
||||
AND ib.ediBotanic IS NOT NULL
|
||||
WHERE s.ticketFk = ?
|
||||
GROUP BY s.id
|
||||
ORDER BY (it.isPackaging), s.concept, s.itemFk
|
|
@ -0,0 +1,9 @@
|
|||
const Stylesheet = require(`${appPath}/core/stylesheet`);
|
||||
|
||||
module.exports = new Stylesheet([
|
||||
`${appPath}/common/css/spacing.css`,
|
||||
`${appPath}/common/css/misc.css`,
|
||||
`${appPath}/common/css/layout.css`,
|
||||
`${appPath}/common/css/report.css`,
|
||||
`${__dirname}/style.css`])
|
||||
.mergeStyles();
|
|
@ -0,0 +1,29 @@
|
|||
h2 {
|
||||
font-weight: 100;
|
||||
color: #555
|
||||
}
|
||||
|
||||
.table-title {
|
||||
margin-bottom: 15px;
|
||||
font-size: 0.8rem
|
||||
}
|
||||
|
||||
.table-title h2 {
|
||||
margin: 0 15px 0 0
|
||||
}
|
||||
|
||||
.ticket-info {
|
||||
font-size: 22px
|
||||
}
|
||||
|
||||
#incoterms table {
|
||||
font-size: 1.2rem
|
||||
}
|
||||
|
||||
#incoterms table th {
|
||||
width: 10%
|
||||
}
|
||||
|
||||
#incoterms p {
|
||||
font-size: 1.2rem
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
<!DOCTYPE html>
|
||||
<html v-bind:lang="$i18n.locale">
|
||||
<body>
|
||||
<table class="grid no-page-break page-break-after">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<!-- Header block -->
|
||||
<report-header v-bind="$props"
|
||||
v-bind:company-code="invoice.companyCode">
|
||||
</report-header>
|
||||
<!-- Block -->
|
||||
<div class="grid-row">
|
||||
<div class="grid-block">
|
||||
<div class="columns vn-mb-lg">
|
||||
<div class="size50">
|
||||
<div class="size75 vn-mt-ml">
|
||||
<h1 class="title uppercase">{{$t('title')}}</h1>
|
||||
<table class="row-oriented ticket-info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="font gray uppercase">{{$t('clientId')}}</td>
|
||||
<th>{{client.id}}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font gray uppercase">{{$t('invoice')}}</td>
|
||||
<th>{{invoice.ref}}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font gray uppercase">{{$t('date')}}</td>
|
||||
<th>{{invoice.issued | date('%d-%m-%Y')}}</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="size50">
|
||||
<div class="panel">
|
||||
<div class="header">{{$t('invoiceData')}}</div>
|
||||
<div class="body">
|
||||
<h3 class="uppercase">{{client.socialName}}</h3>
|
||||
<div>
|
||||
{{client.postalAddress}}
|
||||
</div>
|
||||
<div>
|
||||
{{client.postcodeCity}}
|
||||
</div>
|
||||
<div>
|
||||
{{$t('fiscalId')}}: {{client.fi}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="incoterms" class="panel">
|
||||
<div class="header">{{$t('incotermsTitle')}}</div>
|
||||
<div class="body">
|
||||
|
||||
<table class="row-oriented">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
{{$t('incoterms')}}
|
||||
<div class="description">asd</div>
|
||||
</th>
|
||||
<td>{{incoterms.incotermsFk}} - {{incoterms.incotermsName}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
{{$t('productDescription')}}
|
||||
</th>
|
||||
<td>{{incoterms.intrastat}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{$t('expeditionDescription')}}</th>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{$t('packageNumber')}}</th>
|
||||
<td>{{incoterms.packages}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{$t('packageGrossWeight')}}</th>
|
||||
<td>{{incoterms.weight}} KG</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{$t('packageCubing')}}</th>
|
||||
<td>{{incoterms.volume}} m3</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
<div class="font bold">
|
||||
<span>{{$t('customsInfo')}}</span>
|
||||
<span>{{incoterms.customsAgentName}}</span>
|
||||
</div>
|
||||
<div class="font bold">
|
||||
<span>(</span>
|
||||
<span>{{incoterms.customsAgentNif}}</span>
|
||||
<span>{{incoterms.customsAgentStreet}}</span>
|
||||
<span v-if="incoterms.customsAgentPhone">
|
||||
☎ {{incoterms.customsAgentPhone}}
|
||||
</span>
|
||||
<span v-if="incoterms.customsAgentEmail">
|
||||
✉ {{incoterms.customsAgentEmail}}
|
||||
</span>
|
||||
<span>)</span>
|
||||
</div>
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{$t('productDisclaimer')}}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,39 @@
|
|||
const Component = require(`${appPath}/core/component`);
|
||||
const reportHeader = new Component('report-header');
|
||||
const reportFooter = new Component('report-footer');
|
||||
|
||||
module.exports = {
|
||||
name: 'invoice-incoterms',
|
||||
async serverPrefetch() {
|
||||
this.invoice = await this.fetchInvoice(this.invoiceId);
|
||||
this.client = await this.fetchClient(this.invoiceId);
|
||||
this.incoterms = await this.fetchIncoterms(this.invoiceId);
|
||||
|
||||
if (!this.invoice)
|
||||
throw new Error('Something went wrong');
|
||||
},
|
||||
computed: {
|
||||
|
||||
},
|
||||
methods: {
|
||||
fetchInvoice(invoiceId) {
|
||||
return this.findOneFromDef('invoice', [invoiceId]);
|
||||
},
|
||||
fetchClient(invoiceId) {
|
||||
return this.findOneFromDef('client', [invoiceId]);
|
||||
},
|
||||
fetchIncoterms(invoiceId) {
|
||||
return this.findOneFromDef('incoterms', {invoiceId});
|
||||
}
|
||||
},
|
||||
components: {
|
||||
'report-header': reportHeader.build(),
|
||||
'report-footer': reportFooter.build()
|
||||
},
|
||||
props: {
|
||||
invoiceId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
title: Factura
|
||||
invoice: Factura
|
||||
clientId: Cliente
|
||||
date: Fecha
|
||||
invoiceData: Datos de facturación
|
||||
fiscalId: CIF / NIF
|
||||
invoiceRef: Factura {0}
|
||||
incotermsTitle: Información para la exportación
|
||||
incoterms: Incoterms
|
||||
productDescription: Descripción de la mercancia
|
||||
expeditionDescription: INFORMACIÓN DE LA EXPEDICIÓN
|
||||
packageNumber: Número de bultos
|
||||
packageGrossWeight: Peso bruto
|
||||
packageCubing: Cubicaje
|
||||
customsInfo: A despachar por la agencia de aduanas
|
||||
productDisclaimer: Mercancía destinada a la exportación, EXENTA de IVA (Ley 37/1992 - Art. 21)
|
|
@ -0,0 +1,12 @@
|
|||
SELECT
|
||||
c.id,
|
||||
c.socialName,
|
||||
c.street AS postalAddress,
|
||||
IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi,
|
||||
CONCAT(c.postcode, ' - ', c.city) postcodeCity
|
||||
FROM vn.invoiceOut io
|
||||
JOIN vn.client c ON c.id = io.clientFk
|
||||
JOIN vn.country cty ON cty.id = c.countryFk
|
||||
LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial
|
||||
AND ios.taxAreaFk = 'CEE'
|
||||
WHERE io.id = ?
|
|
@ -0,0 +1,71 @@
|
|||
SELECT io.issued,
|
||||
c.socialName,
|
||||
c.street postalAddress,
|
||||
IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi,
|
||||
io.clientFk,
|
||||
c.postcode,
|
||||
c.city,
|
||||
io.companyFk,
|
||||
io.ref,
|
||||
tc.code,
|
||||
s.concept,
|
||||
s.quantity,
|
||||
s.price,
|
||||
s.discount,
|
||||
s.ticketFk,
|
||||
t.shipped,
|
||||
t.refFk,
|
||||
a.nickname,
|
||||
s.itemFk,
|
||||
s.id saleFk,
|
||||
pm.name AS pmname,
|
||||
sa.iban,
|
||||
c.phone,
|
||||
MAX(t.packages) packages,
|
||||
a.incotermsFk,
|
||||
ic.name incotermsName ,
|
||||
sub.description weight,
|
||||
t.observations,
|
||||
ca.fiscalName customsAgentName,
|
||||
ca.street customsAgentStreet,
|
||||
ca.nif customsAgentNif,
|
||||
ca.phone customsAgentPhone,
|
||||
ca.email customsAgentEmail,
|
||||
CAST(sub2.volume AS DECIMAL (10,2)) volume,
|
||||
sub3.intrastat
|
||||
FROM vn.invoiceOut io
|
||||
JOIN vn.supplier su ON su.id = io.companyFk
|
||||
JOIN vn.client c ON c.id = io.clientFk
|
||||
LEFT JOIN vn.province p ON p.id = c.provinceFk
|
||||
JOIN vn.ticket t ON t.refFk = io.ref
|
||||
LEFT JOIN (SELECT tob.ticketFk,tob.description
|
||||
FROM vn.ticketObservation tob
|
||||
LEFT JOIN vn.observationType ot ON ot.id = tob.observationTypeFk
|
||||
WHERE ot.description = "Peso Aduana"
|
||||
)sub ON sub.ticketFk = t.id
|
||||
JOIN vn.address a ON a.id = t.addressFk
|
||||
LEFT JOIN vn.incoterms ic ON ic.code = a.incotermsFk
|
||||
LEFT JOIN vn.customsAgent ca ON ca.id = a.customsAgentFk
|
||||
JOIN vn.sale s ON s.ticketFk = t.id
|
||||
JOIN (SELECT SUM(volume) volume
|
||||
FROM vn.invoiceOut io
|
||||
JOIN vn.ticket t ON t.refFk = io.ref
|
||||
JOIN vn.saleVolume sv ON sv.ticketFk = t.id
|
||||
WHERE io.id = :invoiceId
|
||||
)sub2 ON TRUE
|
||||
JOIN vn.itemTaxCountry itc ON itc.countryFk = su.countryFk AND itc.itemFk = s.itemFk
|
||||
JOIN vn.taxClass tc ON tc.id = itc.taxClassFk
|
||||
LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial AND ios.taxAreaFk = 'CEE'
|
||||
JOIN vn.country cty ON cty.id = c.countryFk
|
||||
JOIN vn.payMethod pm ON pm.id = c .payMethodFk
|
||||
JOIN vn.company co ON co.id=io.companyFk
|
||||
JOIN vn.supplierAccount sa ON sa.id=co.supplierAccountFk
|
||||
LEFT JOIN (SELECT GROUP_CONCAT(DISTINCT ir.description ORDER BY ir.description SEPARATOR '. ' ) as intrastat
|
||||
FROM vn.ticket t
|
||||
JOIN vn.invoiceOut io ON io.ref = t.refFk
|
||||
JOIN vn.sale s ON t.id = s.ticketFk
|
||||
JOIN vn.item i ON i.id = s.itemFk
|
||||
JOIN vn.intrastat ir ON ir.id = i.intrastatFk
|
||||
WHERE io.id = :invoiceId
|
||||
)sub3 ON TRUE
|
||||
WHERE io.id = :invoiceId
|
|
@ -0,0 +1,17 @@
|
|||
SELECT
|
||||
io.id,
|
||||
io.issued,
|
||||
io.clientFk,
|
||||
io.companyFk,
|
||||
io.ref,
|
||||
pm.code AS payMethodCode,
|
||||
cny.code companyCode,
|
||||
sa.iban,
|
||||
ios.footNotes
|
||||
FROM invoiceOut io
|
||||
JOIN client c ON c.id = io.clientFk
|
||||
JOIN payMethod pm ON pm.id = c.payMethodFk
|
||||
JOIN company cny ON cny.id = io.companyFk
|
||||
JOIN supplierAccount sa ON sa.id = cny.supplierAccountFk
|
||||
LEFT JOIN invoiceOutSerial ios ON ios.code = io.serial
|
||||
WHERE io.id = ?
|
|
@ -0,0 +1,9 @@
|
|||
const Stylesheet = require(`${appPath}/core/stylesheet`);
|
||||
|
||||
module.exports = new Stylesheet([
|
||||
`${appPath}/common/css/spacing.css`,
|
||||
`${appPath}/common/css/misc.css`,
|
||||
`${appPath}/common/css/layout.css`,
|
||||
`${appPath}/common/css/report.css`,
|
||||
`${__dirname}/style.css`])
|
||||
.mergeStyles();
|
|
@ -0,0 +1,39 @@
|
|||
h2 {
|
||||
font-weight: 100;
|
||||
color: #555
|
||||
}
|
||||
|
||||
.table-title {
|
||||
margin-bottom: 15px;
|
||||
font-size: 0.8rem
|
||||
}
|
||||
|
||||
.table-title h2 {
|
||||
margin: 0 15px 0 0
|
||||
}
|
||||
|
||||
.ticket-info {
|
||||
font-size: 22px
|
||||
}
|
||||
|
||||
#nickname h2 {
|
||||
max-width: 400px;
|
||||
word-wrap: break-word
|
||||
}
|
||||
|
||||
#phytosanitary {
|
||||
padding-right: 10px
|
||||
}
|
||||
|
||||
#phytosanitary .flag img {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
#phytosanitary .flag .flag-text {
|
||||
padding-left: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.phytosanitary-info {
|
||||
margin-top: 10px
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
|
@ -0,0 +1,319 @@
|
|||
<!DOCTYPE html>
|
||||
<html v-bind:lang="$i18n.locale">
|
||||
<body>
|
||||
<table class="grid">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<!-- Incoterms block -->
|
||||
<invoice-incoterms
|
||||
v-if="hasIncoterms"
|
||||
v-bind="$props">
|
||||
</invoice-incoterms>
|
||||
|
||||
<!-- Header block -->
|
||||
<report-header v-bind="$props"
|
||||
v-bind:company-code="invoice.companyCode">
|
||||
</report-header>
|
||||
<!-- Block -->
|
||||
<div class="grid-row">
|
||||
<div class="grid-block">
|
||||
<div class="columns vn-mb-lg">
|
||||
<div class="size50">
|
||||
<div class="size75 vn-mt-ml">
|
||||
<h1 class="title uppercase">{{$t('title')}}</h1>
|
||||
<table class="row-oriented ticket-info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="font gray uppercase">{{$t('clientId')}}</td>
|
||||
<th>{{client.id}}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font gray uppercase">{{$t('invoice')}}</td>
|
||||
<th>{{invoice.ref}}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font gray uppercase">{{$t('date')}}</td>
|
||||
<th>{{invoice.issued | date('%d-%m-%Y')}}</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="size50">
|
||||
<div class="panel">
|
||||
<div class="header">{{$t('invoiceData')}}</div>
|
||||
<div class="body">
|
||||
<h3 class="uppercase">{{client.socialName}}</h3>
|
||||
<div>
|
||||
{{client.postalAddress}}
|
||||
</div>
|
||||
<div>
|
||||
{{client.postcodeCity}}
|
||||
</div>
|
||||
<div>
|
||||
{{$t('fiscalId')}}: {{client.fi}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rectified invoices block -->
|
||||
<div class="size100 no-page-break" v-if="rectified.length > 0">
|
||||
<h2>{{$t('rectifiedInvoices')}}</h2>
|
||||
<table class="column-oriented">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{$t('invoice')}}</th>
|
||||
<th>{{$t('issued')}}</th>
|
||||
<th class="number">{{$t('amount')}}</th>
|
||||
<th width="50%">{{$t('description')}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in rectified">
|
||||
<td>{{row.ref}}</td>
|
||||
<td>{{row.issued | date}}</td>
|
||||
<td class="number">{{row.amount | currency('EUR', $i18n.locale)}}</td>
|
||||
<td width="50%">{{row.description}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- End of rectified invoices block -->
|
||||
|
||||
<!-- Sales block -->
|
||||
<div class="vn-mt-lg no-page-break" v-for="ticket in tickets">
|
||||
<div class="table-title clearfix">
|
||||
<div class="pull-left">
|
||||
<h2>{{$t('deliveryNote')}}</strong>
|
||||
</div>
|
||||
<div class="pull-left vn-mr-md">
|
||||
<div class="field rectangle">
|
||||
<span>{{ticket.id}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-left">
|
||||
<h2>Shipped</h2>
|
||||
</div>
|
||||
<div class="pull-left">
|
||||
<div class="field rectangle">
|
||||
<span>{{ticket.shipped | date}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span id="nickname" class="pull-right">
|
||||
<h2>{{ticket.nickname}}</h2>
|
||||
</span>
|
||||
</div>
|
||||
<table class="column-oriented">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="5%">{{$t('reference')}}</th>
|
||||
<th class="number">{{$t('quantity')}}</th>
|
||||
<th width="50%">{{$t('concept')}}</th>
|
||||
<th class="number">{{$t('price')}}</th>
|
||||
<th class="centered" width="5%">{{$t('discount')}}</th>
|
||||
<th class="centered">{{$t('vat')}}</th>
|
||||
<th class="number">{{$t('amount')}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-for="sale in ticket.sales" class="no-page-break">
|
||||
<tr>
|
||||
<td width="5%">{{sale.itemFk | zerofill('000000')}}</td>
|
||||
<td class="number">{{sale.quantity}}</td>
|
||||
<td width="50%">{{sale.concept}}</td>
|
||||
<td class="number">{{sale.price | currency('EUR', $i18n.locale)}}</td>
|
||||
<td class="centered" width="5%">{{(sale.discount / 100) | percentage}}</td>
|
||||
<td class="centered">{{sale.vatType}}</td>
|
||||
<td class="number">{{saleImport(sale) | currency('EUR', $i18n.locale)}}</td>
|
||||
</tr>
|
||||
<tr class="description font light-gray">
|
||||
<td colspan="7">
|
||||
<span v-if="sale.value5">
|
||||
<strong>{{sale.tag5}}</strong> {{sale.value5}}
|
||||
</span>
|
||||
<span v-if="sale.value6">
|
||||
<strong>{{sale.tag6}}</strong> {{sale.value6}}
|
||||
</span>
|
||||
<span v-if="sale.value7">
|
||||
<strong>{{sale.tag7}}</strong> {{sale.value7}}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="6" class="font bold">
|
||||
<span class="pull-right">{{$t('subtotal')}}</span>
|
||||
</td>
|
||||
<td class="number">{{ticketSubtotal(ticket) | currency('EUR', $i18n.locale)}}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<!-- End of sales block -->
|
||||
|
||||
<div class="columns vn-mt-xl">
|
||||
<!-- Taxes block -->
|
||||
<div id="taxes" class="size50 pull-right no-page-break" v-if="taxes">
|
||||
<table class="column-oriented">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="4">{{$t('taxBreakdown')}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<thead class="light">
|
||||
<tr>
|
||||
<th width="45%">{{$t('type')}}</th>
|
||||
<th width="25%" class="number">
|
||||
{{$t('taxBase')}}
|
||||
</th>
|
||||
<th>{{$t('tax')}}</th>
|
||||
<th class="number">{{$t('fee')}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="tax in taxes">
|
||||
<td width="45%">{{tax.name}}</td>
|
||||
<td width="25%" class="number">
|
||||
{{tax.base | currency('EUR', $i18n.locale)}}
|
||||
</td>
|
||||
<td>{{tax.vatPercent | percentage}}</td>
|
||||
<td class="number">{{tax.vat | currency('EUR', $i18n.locale)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="font bold">
|
||||
<td width="45%">{{$t('subtotal')}}</td>
|
||||
<td width="20%" class="number">
|
||||
{{sumTotal(taxes, 'base') | currency('EUR', $i18n.locale)}}
|
||||
</td>
|
||||
<td></td>
|
||||
<td class="number">{{sumTotal(taxes, 'vat') | currency('EUR', $i18n.locale)}}</td>
|
||||
</tr>
|
||||
<tr class="font bold">
|
||||
<td colspan="2">{{$t('total')}}</td>
|
||||
<td colspan="2" class="number">{{taxTotal | currency('EUR', $i18n.locale)}}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<div class="panel" v-if="invoice.footNotes">
|
||||
<div class="header">{{$t('notes')}}</div>
|
||||
<div class="body">
|
||||
<span>{{invoice.footNotes}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of taxes block -->
|
||||
|
||||
<!-- Phytosanitary block -->
|
||||
<div id="phytosanitary" class="size50 pull-left no-page-break">
|
||||
<div class="panel">
|
||||
<div class="body">
|
||||
<div class="flag">
|
||||
<div class="columns">
|
||||
<div class="size25">
|
||||
<img v-bind:src="getReportSrc('europe.png')"/>
|
||||
</div>
|
||||
<div class="size75 flag-text">
|
||||
<strong>{{$t('plantPassport')}}</strong><br/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="phytosanitary-info">
|
||||
<div>
|
||||
<strong>A</strong>
|
||||
<span>{{botanical}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>B</strong>
|
||||
<span>ES17462130</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>C</strong>
|
||||
<span>{{ticketsId}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>D</strong>
|
||||
<span>ES</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of phytosanitary block -->
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Intrastat block -->
|
||||
<div class="size100 no-page-break" v-if="intrastat.length > 0">
|
||||
<h2>{{$t('intrastat')}}</h2>
|
||||
<table class="column-oriented">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{$t('code')}}</th>
|
||||
<th width="50%">{{$t('description')}}</th>
|
||||
<th class="number">{{$t('stems')}}</th>
|
||||
<th class="number">{{$t('netKg')}}</th>
|
||||
<th class="number">{{$t('amount')}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in intrastat">
|
||||
<td>{{row.code}}</td>
|
||||
<td width="50%">{{row.description}}</td>
|
||||
<td class="number">{{row.stems | number($i18n.locale)}}</td>
|
||||
<td class="number">{{row.netKg | number($i18n.locale)}}</td>
|
||||
<td class="number">{{row.subtotal | currency('EUR', $i18n.locale)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="2"></td>
|
||||
<td class="number">
|
||||
<strong>{{sumTotal(intrastat, 'stems') | number($i18n.locale)}}</strong>
|
||||
</td>
|
||||
<td class="number">
|
||||
<strong>{{sumTotal(intrastat, 'netKg') | number($i18n.locale)}}</strong>
|
||||
</td>
|
||||
<td class="number">
|
||||
<strong>{{sumTotal(intrastat, 'subtotal') | currency('EUR', $i18n.locale)}}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<!-- End of intrastat block -->
|
||||
|
||||
<!-- Observations block -->
|
||||
<div class="columns vn-mt-xl" v-if="invoice.payMethodCode == 'wireTransfer'">
|
||||
<div class="size50 pull-left no-page-break" >
|
||||
<div class="panel" >
|
||||
<div class="header">{{$t('observations')}}</div>
|
||||
<div class="body">
|
||||
<div>{{$t('wireTransfer')}}</div>
|
||||
<div>{{$t('accountNumber', [invoice.iban])}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of observations block -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer block -->
|
||||
<report-footer id="pageFooter"
|
||||
v-bind:company-code="invoice.companyCode"
|
||||
v-bind:left-text="$t('invoiceRef', [invoice.ref])"
|
||||
v-bind:center-text="client.socialName"
|
||||
v-bind="$props">
|
||||
</report-footer>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,130 @@
|
|||
const Component = require(`${appPath}/core/component`);
|
||||
const Report = require(`${appPath}/core/report`);
|
||||
const reportHeader = new Component('report-header');
|
||||
const reportFooter = new Component('report-footer');
|
||||
const invoiceIncoterms = new Report('invoice-incoterms');
|
||||
const db = require(`${appPath}/core/database`);
|
||||
|
||||
module.exports = {
|
||||
name: 'invoice',
|
||||
async serverPrefetch() {
|
||||
this.invoice = await this.fetchInvoice(this.invoiceId);
|
||||
this.client = await this.fetchClient(this.invoiceId);
|
||||
this.taxes = await this.fetchTaxes(this.invoiceId);
|
||||
this.intrastat = await this.fetchIntrastat(this.invoiceId);
|
||||
this.rectified = await this.fetchRectified(this.invoiceId);
|
||||
this.hasIncoterms = await this.fetchHasIncoterms(this.invoiceId);
|
||||
|
||||
const tickets = await this.fetchTickets(this.invoiceId);
|
||||
const sales = await this.fetchSales(this.invoiceId);
|
||||
|
||||
const map = new Map();
|
||||
|
||||
for (let ticket of tickets)
|
||||
map.set(ticket.id, ticket);
|
||||
|
||||
for (let sale of sales) {
|
||||
const ticket = map.get(sale.ticketFk);
|
||||
if (!ticket.sales) ticket.sales = [];
|
||||
ticket.sales.push(sale);
|
||||
}
|
||||
|
||||
this.tickets = tickets;
|
||||
|
||||
if (!this.invoice)
|
||||
throw new Error('Something went wrong');
|
||||
},
|
||||
data() {
|
||||
return {totalBalance: 0.00};
|
||||
},
|
||||
computed: {
|
||||
ticketsId() {
|
||||
const tickets = this.tickets.map(ticket => ticket.id);
|
||||
|
||||
return tickets.join(', ');
|
||||
},
|
||||
botanical() {
|
||||
let phytosanitary = [];
|
||||
for (let ticket of this.tickets) {
|
||||
for (let sale of ticket.sales) {
|
||||
if (sale.botanical)
|
||||
phytosanitary.push(sale.botanical);
|
||||
}
|
||||
}
|
||||
|
||||
return phytosanitary.filter((item, index) =>
|
||||
phytosanitary.indexOf(item) == index
|
||||
).join(', ');
|
||||
},
|
||||
taxTotal() {
|
||||
const base = this.sumTotal(this.taxes, 'base');
|
||||
const vat = this.sumTotal(this.taxes, 'vat');
|
||||
return base + vat;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchInvoice(invoiceId) {
|
||||
return this.findOneFromDef('invoice', [invoiceId]);
|
||||
},
|
||||
fetchClient(invoiceId) {
|
||||
return this.findOneFromDef('client', [invoiceId]);
|
||||
},
|
||||
fetchTickets(invoiceId) {
|
||||
return this.rawSqlFromDef('tickets', [invoiceId]);
|
||||
},
|
||||
async fetchSales(invoiceId) {
|
||||
const connection = await db.pool.getConnection();
|
||||
await this.rawSql(`DROP TEMPORARY TABLE IF EXISTS tmp.invoiceTickets`, connection);
|
||||
await this.rawSqlFromDef('invoiceTickets', [invoiceId], connection);
|
||||
|
||||
const sales = this.rawSqlFromDef('sales', connection);
|
||||
|
||||
await this.rawSql(`DROP TEMPORARY TABLE tmp.invoiceTickets`, connection);
|
||||
await connection.release();
|
||||
|
||||
return sales;
|
||||
},
|
||||
fetchTaxes(invoiceId) {
|
||||
return this.rawSqlFromDef(`taxes`, [invoiceId]);
|
||||
},
|
||||
fetchIntrastat(invoiceId) {
|
||||
return this.rawSqlFromDef(`intrastat`, [invoiceId]);
|
||||
},
|
||||
fetchRectified(invoiceId) {
|
||||
return this.rawSqlFromDef(`rectified`, [invoiceId]);
|
||||
},
|
||||
fetchHasIncoterms(invoiceId) {
|
||||
return this.findValueFromDef(`hasIncoterms`, [invoiceId]);
|
||||
},
|
||||
saleImport(sale) {
|
||||
const price = sale.quantity * sale.price;
|
||||
|
||||
return price * (1 - sale.discount / 100);
|
||||
},
|
||||
ticketSubtotal(ticket) {
|
||||
let subTotal = 0.00;
|
||||
for (let sale of ticket.sales)
|
||||
subTotal += this.saleImport(sale);
|
||||
|
||||
return subTotal;
|
||||
},
|
||||
sumTotal(rows, prop) {
|
||||
let total = 0.00;
|
||||
for (let row of rows)
|
||||
total += parseFloat(row[prop]);
|
||||
|
||||
return total;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
'report-header': reportHeader.build(),
|
||||
'report-footer': reportFooter.build(),
|
||||
'invoice-incoterms': invoiceIncoterms.build()
|
||||
},
|
||||
props: {
|
||||
invoiceId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
title: Factura
|
||||
invoice: Factura
|
||||
clientId: Cliente
|
||||
invoiceData: Datos de facturación
|
||||
fiscalId: CIF / NIF
|
||||
invoiceRef: Factura {0}
|
||||
deliveryNote: Albarán
|
||||
shipped: F. envío
|
||||
date: Fecha
|
||||
reference: Ref.
|
||||
quantity: Cant.
|
||||
concept: Concepto
|
||||
price: PVP/u
|
||||
discount: Dto.
|
||||
vat: IVA
|
||||
amount: Importe
|
||||
type: Tipo
|
||||
taxBase: Base imp.
|
||||
tax: Tasa
|
||||
fee: Cuota
|
||||
total: Total
|
||||
subtotal: Subtotal
|
||||
taxBreakdown: Desglose impositivo
|
||||
notes: Notas
|
||||
intrastat: Intrastat
|
||||
code: Código
|
||||
description: Descripción
|
||||
stems: Tallos
|
||||
netKg: KG Neto
|
||||
rectifiedInvoices: Facturas rectificadas
|
||||
issued: F. emisión
|
||||
plantPassport: Pasaporte fitosanitario
|
||||
observations: Observaciones
|
||||
wireTransfer: "Forma de pago: Transferencia"
|
||||
accountNumber: "Número de cuenta: {0}"
|
|
@ -0,0 +1,12 @@
|
|||
SELECT
|
||||
c.id,
|
||||
c.socialName,
|
||||
c.street AS postalAddress,
|
||||
IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi,
|
||||
CONCAT(c.postcode, ' - ', c.city) postcodeCity
|
||||
FROM vn.invoiceOut io
|
||||
JOIN vn.client c ON c.id = io.clientFk
|
||||
JOIN vn.country cty ON cty.id = c.countryFk
|
||||
LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial
|
||||
AND ios.taxAreaFk = 'CEE'
|
||||
WHERE io.id = ?
|
|
@ -0,0 +1,8 @@
|
|||
SELECT IF(incotermsFk IS NULL, FALSE, TRUE) AS hasIncoterms
|
||||
FROM ticket t
|
||||
JOIN invoiceOut io ON io.ref = t.refFk
|
||||
JOIN client c ON c.id = t.clientFk
|
||||
JOIN address a ON a.id = t.addressFk
|
||||
WHERE io.id = ?
|
||||
AND IF(c.hasToinvoiceByAddress = FALSE, c.defaultAddressFk, TRUE)
|
||||
LIMIT 1
|
|
@ -0,0 +1,14 @@
|
|||
SELECT
|
||||
ir.id AS code,
|
||||
ir.description AS description,
|
||||
CAST(SUM(IFNULL(i.stems,1) * s.quantity) AS DECIMAL(10,2)) as stems,
|
||||
CAST(SUM( weight) AS DECIMAL(10,2)) as netKg,
|
||||
CAST(SUM((s.quantity * s.price * (100 - s.discount) / 100 )) AS DECIMAL(10,2)) AS subtotal
|
||||
FROM vn.sale s
|
||||
LEFT JOIN vn.saleVolume sv ON sv.saleFk = s.id
|
||||
LEFT JOIN vn.ticket t ON t.id = s.ticketFk
|
||||
LEFT JOIN vn.invoiceOut io ON io.ref = t.refFk
|
||||
LEFT JOIN vn.item i ON i.id = s.itemFk
|
||||
JOIN vn.intrastat ir ON ir.id = i.intrastatFk
|
||||
WHERE io.id = ?
|
||||
GROUP BY i.intrastatFk;
|
|
@ -0,0 +1,16 @@
|
|||
SELECT
|
||||
io.issued,
|
||||
io.clientFk,
|
||||
io.companyFk,
|
||||
io.ref,
|
||||
pm.code AS payMethodCode,
|
||||
cny.code companyCode,
|
||||
sa.iban,
|
||||
ios.footNotes
|
||||
FROM invoiceOut io
|
||||
JOIN client c ON c.id = io.clientFk
|
||||
JOIN payMethod pm ON pm.id = c.payMethodFk
|
||||
JOIN company cny ON cny.id = io.companyFk
|
||||
JOIN supplierAccount sa ON sa.id = cny.supplierAccountFk
|
||||
LEFT JOIN invoiceOutSerial ios ON ios.code = io.serial
|
||||
WHERE io.id = ?
|
|
@ -0,0 +1,20 @@
|
|||
CREATE TEMPORARY TABLE tmp.invoiceTickets
|
||||
ENGINE = MEMORY
|
||||
SELECT
|
||||
t.id AS ticketFk,
|
||||
t.clientFk,
|
||||
t.shipped,
|
||||
t.nickname,
|
||||
io.ref,
|
||||
c.socialName,
|
||||
sa.iban,
|
||||
pm.name AS payMethod,
|
||||
su.countryFk AS supplierCountryFk
|
||||
FROM vn.invoiceOut io
|
||||
JOIN vn.supplier su ON su.id = io.companyFk
|
||||
JOIN vn.ticket t ON t.refFk = io.ref
|
||||
JOIN vn.client c ON c.id = t.clientFk
|
||||
JOIN vn.payMethod pm ON pm.id = c.payMethodFk
|
||||
JOIN vn.company co ON co.id = io.companyFk
|
||||
JOIN vn.supplierAccount sa ON sa.id = co.supplierAccountFk
|
||||
WHERE io.id = ?
|
|
@ -0,0 +1,14 @@
|
|||
SELECT CONCAT( 'A ', GROUP_CONCAT(DISTINCT(ib.ediBotanic) SEPARATOR ', '), CHAR(13,10), CHAR(13,10),
|
||||
'B ES17462130', CHAR(13,10), CHAR(13,10),
|
||||
'C ', GROUP_CONCAT(DISTINCT(t.id) SEPARATOR ', '), CHAR(13,10), CHAR(13,10),
|
||||
'D ES' ) phytosanitary
|
||||
FROM vn.ticket t
|
||||
JOIN vn.sale s ON s.ticketFk = t.id
|
||||
JOIN vn.item i ON i.id = s.itemFk
|
||||
JOIN vn.itemType it ON it.id = i.typeFk
|
||||
JOIN vn.itemCategory ic ON ic.id = it.categoryFk
|
||||
JOIN vn.itemBotanicalWithGenus ib ON ib.itemfk = i.id
|
||||
WHERE t.refFk = # AND
|
||||
ic.`code` = 'plant' AND
|
||||
ib.ediBotanic IS NOT NULL
|
||||
ORDER BY ib.ediBotanic
|
|
@ -0,0 +1,9 @@
|
|||
SELECT
|
||||
io.amount,
|
||||
io.ref,
|
||||
io.issued,
|
||||
ict.description
|
||||
FROM vn.invoiceCorrection ic
|
||||
JOIN vn.invoiceOut io ON io.id = ic.correctedFk
|
||||
JOIN vn.invoiceCorrectionType ict ON ict.id = ic.invoiceCorrectionTypeFk
|
||||
where ic.correctingFk = ?
|
|
@ -0,0 +1,59 @@
|
|||
SELECT
|
||||
it.ref,
|
||||
it.socialName,
|
||||
it.iban,
|
||||
it.payMethod,
|
||||
it.clientFk,
|
||||
it.shipped,
|
||||
it.nickname,
|
||||
s.ticketFk,
|
||||
s.itemFk,
|
||||
s.concept,
|
||||
s.quantity,
|
||||
s.price,
|
||||
s.discount,
|
||||
i.tag5,
|
||||
i.value5,
|
||||
i.tag6,
|
||||
i.value6,
|
||||
i.tag7,
|
||||
i.value7,
|
||||
tc.code AS vatType,
|
||||
ib.ediBotanic botanical
|
||||
FROM tmp.invoiceTickets it
|
||||
JOIN vn.sale s ON s.ticketFk = it.ticketFk
|
||||
JOIN item i ON i.id = s.itemFk
|
||||
LEFT JOIN itemType it ON it.id = i.typeFk
|
||||
LEFT JOIN itemCategory ic ON ic.id = it.categoryFk
|
||||
LEFT JOIN itemBotanicalWithGenus ib ON ib.itemFk = i.id
|
||||
AND ic.code = 'plant'
|
||||
AND ib.ediBotanic IS NOT NULL
|
||||
JOIN vn.itemTaxCountry itc ON itc.countryFk = it.supplierCountryFk
|
||||
AND itc.itemFk = s.itemFk
|
||||
JOIN vn.taxClass tc ON tc.id = itc.taxClassFk
|
||||
UNION ALL
|
||||
SELECT
|
||||
it.ref,
|
||||
it.socialName,
|
||||
it.iban,
|
||||
it.payMethod,
|
||||
it.clientFk,
|
||||
it.shipped,
|
||||
it.nickname,
|
||||
it.ticketFk,
|
||||
'',
|
||||
ts.description concept,
|
||||
ts.quantity,
|
||||
ts.price,
|
||||
0 discount,
|
||||
NULL AS tag5,
|
||||
NULL AS value5,
|
||||
NULL AS tag6,
|
||||
NULL AS value6,
|
||||
NULL AS tag7,
|
||||
NULL AS value7,
|
||||
tc.code AS vatType,
|
||||
NULL AS botanical
|
||||
FROM tmp.invoiceTickets it
|
||||
JOIN vn.ticketService ts ON ts.ticketFk = it.ticketFk
|
||||
JOIN vn.taxClass tc ON tc.id = ts.taxClassFk
|
|
@ -0,0 +1,11 @@
|
|||
SELECT
|
||||
iot.vat,
|
||||
pgc.name,
|
||||
IF(pe.equFk IS NULL, taxableBase, 0) AS base,
|
||||
pgc.rate / 100 AS vatPercent
|
||||
FROM invoiceOutTax iot
|
||||
JOIN pgc ON pgc.code = iot.pgcFk
|
||||
LEFT JOIN pgcEqu pe ON pe.equFk = pgc.code
|
||||
JOIN invoiceOut io ON io.id = iot.invoiceOutFk
|
||||
WHERE invoiceOutFk = ?
|
||||
ORDER BY iot.id
|
|
@ -0,0 +1,7 @@
|
|||
SELECT
|
||||
t.id,
|
||||
t.shipped,
|
||||
t.nickname
|
||||
FROM invoiceOut io
|
||||
JOIN ticket t ON t.refFk = io.ref
|
||||
WHERE io.id = ?
|
Loading…
Reference in New Issue