Merge branch 'dev' into 2817-translate_ticket_request_message
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Joan Sanchez 2021-03-17 06:39:24 +00:00
commit d40da2a630
79 changed files with 50290 additions and 3720 deletions

View File

@ -1,5 +1,5 @@
module.exports = {
presets: [
'@babel/preset-env',
'@babel/env',
],
};

View File

@ -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) {

View File

@ -7,7 +7,8 @@ module.exports = Self => {
type: 'Number',
required: true,
description: 'The worker id of the destinatary'
}, {
},
{
arg: 'message',
type: 'String',
required: true,

View File

@ -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);

View File

@ -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');

View File

@ -67138,7 +67138,7 @@ BEGIN
isTaxDataChecked = FALSE;
SELECT * FROM tmp.ticketProblems;
-- SELECT * FROM tmp.ticketProblems;
DROP TEMPORARY TABLE
tmp.clientGetDebt,

View File

@ -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();

View File

@ -81,6 +81,9 @@ vn-table {
width: 1px;
text-align: center;
}
&[shrink-date] {
width: 100px;
}
&[expand] {
max-width: 400px;
min-width: 0;

View File

@ -1,4 +1,5 @@
import '@babel/polyfill';
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import * as ng from 'angular';
export {ng};

3297
front/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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);
});
}

View File

@ -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>",

View File

@ -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"
]
}
}

View File

@ -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)"

View File

@ -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>

View File

@ -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
});
}
});

View File

@ -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]);
};
};

View File

@ -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;
}
};
};

View File

@ -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;
}
};
};

View File

@ -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);
});
});

View File

@ -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]);
});
});

View File

@ -1,5 +1,8 @@
{
"InvoiceOut": {
"dataSource": "vn"
},
"InvoiceContainer": {
"dataSource": "invoiceStorage"
}
}

View File

@ -0,0 +1,10 @@
{
"name": "InvoiceContainer",
"base": "Container",
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -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);
};

View File

@ -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>

View File

@ -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});

View File

@ -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;

View File

@ -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

View File

@ -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"

View File

@ -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>

View File

@ -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></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>

View File

@ -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, ';');

View File

@ -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();

View File

@ -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();

View File

@ -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 -->

View File

@ -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);
});
}

View File

@ -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();

View File

@ -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

View File

@ -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

View File

@ -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>

46249
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -45,4 +45,8 @@
.no-page-break {
page-break-inside: avoid;
break-inside: avoid
}
.page-break-after {
page-break-after: always;
}

View File

@ -9,6 +9,6 @@ body {
.title {
margin-bottom: 20px;
font-weight: 100;
font-size: 3em;
font-size: 2.6rem;
margin-top: 0
}

View File

@ -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, {

View File

@ -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);
},
/**

View File

@ -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);
},
},

View File

@ -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,

View File

@ -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);
}

2896
print/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -19,7 +19,7 @@ h2 {
}
.ticket-info {
font-size: 26px
font-size: 22px
}
#phytosanitary {

View File

@ -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

View File

@ -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();

View File

@ -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
}

View File

@ -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">
&#9742; {{incoterms.customsAgentPhone}}
</span>
<span v-if="incoterms.customsAgentEmail">
&#9993; {{incoterms.customsAgentEmail}}
</span>
<span>)</span>
</div>
</p>
<p>
<strong>{{$t('productDisclaimer')}}</strong>
</p>
</div>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -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
}
}
};

View File

@ -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)

View File

@ -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 = ?

View File

@ -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

View File

@ -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 = ?

View File

@ -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();

View File

@ -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

View File

@ -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>

View File

@ -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
}
}
};

View File

@ -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}"

View File

@ -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 = ?

View File

@ -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

View File

@ -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;

View File

@ -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 = ?

View File

@ -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 = ?

View File

@ -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

View File

@ -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 = ?

View File

@ -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

View File

@ -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

View File

@ -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 = ?

View File