Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2275-claim_detail

This commit is contained in:
Bernat Exposito Domenech 2020-06-15 09:54:46 +02:00
commit 9d80ec11af
80 changed files with 1263 additions and 701 deletions

2
Jenkinsfile vendored
View File

@ -68,7 +68,7 @@ pipeline {
stage('Backend') { stage('Backend') {
steps { steps {
nodejs('node-lts') { nodejs('node-lts') {
sh 'gulp backTestDockerOnce --junit --random' sh 'gulp backTestOnce --ci'
} }
} }
} }

View File

@ -8,18 +8,12 @@ Salix is also the scientific name of a beautifull tree! :)
Required applications. Required applications.
* Visual Studio Code
* Node.js = 12.17.0 LTS * Node.js = 12.17.0 LTS
* Docker * Docker
In Visual Studio Code we use the ESLint extension. Open Visual Studio Code, press Ctrl+P and paste the following command.
```
ext install dbaeumer.vscode-eslint
```
You will need to install globally the following items. You will need to install globally the following items.
``` ```
# sudo npm install -g jest gulp-cli nodemon # sudo npm install -g jest gulp-cli
``` ```
## Linux Only Prerequisites ## Linux Only Prerequisites
@ -65,6 +59,15 @@ For end-to-end tests run from project's root.
$ gulp e2e $ gulp e2e
``` ```
## Recommended tools
* Visual Studio Code
In Visual Studio Code we use the ESLint extension. Open Visual Studio Code, press Ctrl+P and paste the following command.
```
ext install dbaeumer.vscode-eslint
```
## Built With ## Built With
* [angularjs](https://angularjs.org/) * [angularjs](https://angularjs.org/)
@ -75,4 +78,4 @@ $ gulp e2e
* [gulp.js](https://gulpjs.com/) * [gulp.js](https://gulpjs.com/)
* [jest](https://jestjs.io/) * [jest](https://jestjs.io/)
* [Jasmine](https://jasmine.github.io/) * [Jasmine](https://jasmine.github.io/)
* [Nightmare](http://www.nightmarejs.org/) * [Puppeteer](https://pptr.dev/)

View File

@ -15,20 +15,6 @@ module.exports = Self => {
Self.observe('before save', async function(ctx) { Self.observe('before save', async function(ctx) {
if (ctx.currentInstance && ctx.currentInstance.id && ctx.data && ctx.data.password) if (ctx.currentInstance && ctx.currentInstance.id && ctx.data && ctx.data.password)
ctx.data.password = md5(ctx.data.password); ctx.data.password = md5(ctx.data.password);
if (!ctx.isNewInstance && ctx.data && (ctx.data.name || ctx.data.active)) {
let instance = JSON.parse(JSON.stringify(ctx.currentInstance));
let userId = ctx.options.accessToken.userId;
let logRecord = {
originFk: ctx.currentInstance.id,
userFk: userId,
action: 'update',
changedModel: 'Account',
oldInstance: {name: instance.name, active: instance.active},
newInstance: ctx.data
};
await Self.app.models.ClientLog.create(logRecord);
}
}); });
Self.remoteMethod('getCurrentUserData', { Self.remoteMethod('getCurrentUserData', {

166
db/docker.js Normal file
View File

@ -0,0 +1,166 @@
const exec = require('child_process').exec;
const log = require('fancy-log');
const dataSources = require('../loopback/server/datasources.json');
module.exports = class Docker {
constructor(name) {
Object.assign(this, {
id: name,
name,
isRandom: name == null,
dbConf: Object.assign({}, dataSources.vn)
});
}
/**
* Builds the database image and runs a container. It only rebuilds the
* image when fixtures have been modified or when the day on which the
* image was built is different to today. Some workarounds have been used
* to avoid a bug with OverlayFS driver on MacOS.
*
* @param {Boolean} ci continuous integration environment argument
*/
async run(ci) {
let d = new Date();
let pad = v => v < 10 ? '0' + v : v;
let stamp = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
await this.execP(`docker build --build-arg STAMP=${stamp} -t salix-db ./db`);
let dockerArgs;
if (this.isRandom)
dockerArgs = '-p 3306';
else {
try {
await this.rm();
} catch (e) {}
dockerArgs = `--name ${this.name} -p 3306:${this.dbConf.port}`;
}
let runChown = process.platform != 'linux';
let container = await this.execP(`docker run --env RUN_CHOWN=${runChown} -d ${dockerArgs} salix-db`);
this.id = container.stdout;
try {
if (this.isRandom) {
let inspect = await this.execP(`docker inspect -f "{{json .NetworkSettings}}" ${this.id}`);
let netSettings = JSON.parse(inspect.stdout);
if (ci)
this.dbConf.host = netSettings.Gateway;
this.dbConf.port = netSettings.Ports['3306/tcp'][0]['HostPort'];
}
if (runChown) await this.wait();
} catch (err) {
if (this.isRandom)
await this.rm();
throw err;
}
}
/**
* Does the minium effort to start the database container, if it doesn't exists
* calls the 'docker' task, if it is started does nothing. Keep in mind that when
* you do not rebuild the docker you may be using an outdated version of it.
* See the 'docker' task for more info.
*/
async start() {
let state;
try {
let result = await this.execP(`docker inspect -f "{{json .State}}" ${this.id}`);
state = JSON.parse(result.stdout);
} catch (err) {
return await this.run();
}
switch (state.Status) {
case 'running':
return;
case 'exited':
await this.execP(`docker start ${this.id}`);
await this.wait();
return;
default:
throw new Error(`Unknown docker status: ${state.Status}`);
}
}
wait() {
return new Promise((resolve, reject) => {
const mysql = require('mysql2');
let interval = 100;
let elapsedTime = 0;
let maxInterval = 4 * 60 * 1000;
let myConf = {
user: this.dbConf.username,
password: this.dbConf.password,
host: this.dbConf.host,
port: this.dbConf.port
};
log('Waiting for MySQL init process...');
async function checker() {
elapsedTime += interval;
let state;
try {
let result = await this.execP(`docker container inspect -f "{{json .State}}" ${this.id}`);
state = JSON.parse(result.stdout);
} catch (err) {
return reject(new Error(err.message));
}
if (state.Status === 'exited')
return reject(new Error('Docker exited, please see the docker logs for more info'));
let conn = mysql.createConnection(myConf);
conn.on('error', () => {});
conn.connect(err => {
conn.destroy();
if (!err) {
log('MySQL process ready.');
return resolve();
}
if (elapsedTime >= maxInterval)
reject(new Error(`MySQL not initialized whithin ${elapsedTime / 1000} secs`));
else
setTimeout(bindedChecker, interval);
});
}
let bindedChecker = checker.bind(this);
bindedChecker();
});
}
rm() {
return this.execP(`docker rm -fv ${this.id}`);
}
/**
* Promisified version of exec().
*
* @param {String} command The exec command
* @return {Promise} The promise
*/
execP(command) {
return new Promise((resolve, reject) => {
exec(command, (err, stdout, stderr) => {
if (err)
reject(err);
else {
resolve({
stdout: stdout,
stderr: stderr
});
}
});
});
}
};

View File

@ -664,12 +664,12 @@ INSERT INTO `vn`.`itemCategory`(`id`, `name`, `display`, `color`, `icon`, `code`
INSERT INTO `vn`.`itemType`(`id`, `code`, `name`, `categoryFk`, `life`,`workerFk`, `isPackaging`) INSERT INTO `vn`.`itemType`(`id`, `code`, `name`, `categoryFk`, `life`,`workerFk`, `isPackaging`)
VALUES VALUES
(1, 'CRI', 'Crisantemo', 2, 31, 5, 0), (1, 'CRI', 'Crisantemo', 2, 31, 35, 0),
(2, 'ITG', 'Anthurium', 1, 31, 5, 0), (2, 'ITG', 'Anthurium', 1, 31, 35, 0),
(3, 'WPN', 'Paniculata', 2, 31, 5, 0), (3, 'WPN', 'Paniculata', 2, 31, 35, 0),
(4, 'PRT', 'Delivery ports', 3, NULL, 5, 1), (4, 'PRT', 'Delivery ports', 3, NULL, 35, 1),
(5, 'CON', 'Container', 3, NULL, 5, 1), (5, 'CON', 'Container', 3, NULL, 35, 1),
(6, 'ALS', 'Alstroemeria', 1, 31, 5, 0); (6, 'ALS', 'Alstroemeria', 1, 31, 35, 0);
INSERT INTO `vn`.`ink`(`id`, `name`, `picture`, `showOrder`) INSERT INTO `vn`.`ink`(`id`, `name`, `picture`, `showOrder`)
VALUES VALUES

View File

@ -10,7 +10,7 @@ export default {
ticketsButton: '.modules-menu [ui-sref="ticket.index"]', ticketsButton: '.modules-menu [ui-sref="ticket.index"]',
invoiceOutButton: '.modules-menu [ui-sref="invoiceOut.index"]', invoiceOutButton: '.modules-menu [ui-sref="invoiceOut.index"]',
claimsButton: '.modules-menu [ui-sref="claim.index"]', claimsButton: '.modules-menu [ui-sref="claim.index"]',
returnToModuleIndexButton: 'a[ui-sref="order.index"]', returnToModuleIndexButton: 'a[name="goToModuleIndex"]',
homeButton: 'vn-topbar > div.side.start > a', homeButton: 'vn-topbar > div.side.start > a',
userLocalWarehouse: '.user-popover vn-autocomplete[ng-model="$ctrl.localWarehouseFk"]', userLocalWarehouse: '.user-popover vn-autocomplete[ng-model="$ctrl.localWarehouseFk"]',
userLocalBank: '.user-popover vn-autocomplete[ng-model="$ctrl.localBankFk"]', userLocalBank: '.user-popover vn-autocomplete[ng-model="$ctrl.localBankFk"]',
@ -365,7 +365,8 @@ export default {
firstSaleQuantity: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(3)', firstSaleQuantity: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(3)',
firstSaleDiscount: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(6)', firstSaleDiscount: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(6)',
invoiceOutRef: 'vn-ticket-summary > vn-card > vn-horizontal > vn-one:nth-child(1) > vn-label-value:nth-child(7) > section > span', invoiceOutRef: 'vn-ticket-summary > vn-card > vn-horizontal > vn-one:nth-child(1) > vn-label-value:nth-child(7) > section > span',
setOk: 'vn-ticket-summary vn-button[label="SET OK"] > button' setOk: 'vn-ticket-summary vn-button[label="SET OK"] > button',
descriptorTicketId: 'vn-ticket-descriptor > vn-descriptor-content > div > div.body > div.top > div'
}, },
ticketsIndex: { ticketsIndex: {
openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]', openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]',

View File

@ -4,6 +4,9 @@ import getBrowser from '../../helpers/puppeteer';
describe('Ticket create path', () => { describe('Ticket create path', () => {
let browser; let browser;
let page; let page;
let nextMonth = new Date();
nextMonth.setMonth(nextMonth.getMonth() + 1);
let stowawayTicketId;
beforeAll(async() => { beforeAll(async() => {
browser = await getBrowser(); browser = await getBrowser();
@ -21,13 +24,9 @@ describe('Ticket create path', () => {
}); });
it('should succeed to create a ticket', async() => { it('should succeed to create a ticket', async() => {
const nextMonth = new Date(); await page.autocompleteSearch(selectors.createTicketView.client, 'Clark Kent');
nextMonth.setMonth(nextMonth.getMonth() + 1);
await page.autocompleteSearch(selectors.createTicketView.client, 'Tony Stark');
await page.autocompleteSearch(selectors.createTicketView.address, 'Tony Stark');
await page.pickDate(selectors.createTicketView.deliveryDate, nextMonth); await page.pickDate(selectors.createTicketView.deliveryDate, nextMonth);
await page.autocompleteSearch(selectors.createTicketView.warehouse, 'Warehouse One'); await page.autocompleteSearch(selectors.createTicketView.warehouse, 'Warehouse Two');
await page.autocompleteSearch(selectors.createTicketView.agency, 'Silla247'); await page.autocompleteSearch(selectors.createTicketView.agency, 'Silla247');
await page.waitToClick(selectors.createTicketView.createButton); await page.waitToClick(selectors.createTicketView.createButton);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
@ -37,5 +36,53 @@ describe('Ticket create path', () => {
it('should check the url is now the summary of the ticket', async() => { it('should check the url is now the summary of the ticket', async() => {
await page.waitForState('ticket.card.summary'); await page.waitForState('ticket.card.summary');
stowawayTicketId = await page.waitToGetProperty(selectors.ticketSummary.descriptorTicketId, 'innerText');
stowawayTicketId = stowawayTicketId.substring(1);
});
it('should again open the new ticket form', async() => {
await page.waitToClick(selectors.globalItems.returnToModuleIndexButton);
await page.waitToClick(selectors.ticketsIndex.newTicketButton);
await page.waitForState('ticket.create');
});
it('should succeed to create another ticket for the same client', async() => {
await page.autocompleteSearch(selectors.createTicketView.client, 'Clark Kent');
await page.pickDate(selectors.createTicketView.deliveryDate, nextMonth);
await page.autocompleteSearch(selectors.createTicketView.warehouse, 'Warehouse One');
await page.autocompleteSearch(selectors.createTicketView.agency, 'Silla247');
await page.waitToClick(selectors.createTicketView.createButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
});
it('should check the url is now the summary of the created ticket', async() => {
await page.waitForState('ticket.card.summary');
});
it('should make the previously created ticket the stowaway of the current ticket', async() => {
await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitToClick(selectors.ticketDescriptor.moreMenuAddStowaway);
await page.waitToClick(selectors.ticketDescriptor.addStowawayDialogFirstTicket);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
});
it('should delete the current ticket', async() => {
await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitToClick(selectors.ticketDescriptor.moreMenuDeleteTicket);
await page.waitToClick(selectors.ticketDescriptor.acceptDeleteButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
});
it('should search for the stowaway ticket of the previously deleted ticket', async() => {
await page.accessToSearchResult(stowawayTicketId);
const result = await page.countElement(selectors.ticketDescriptor.shipButton);
expect(result).toBe(0);
}); });
}); });

View File

@ -1,35 +0,0 @@
require('babel-core/register')({presets: ['es2015']});
process.on('warning', warning => {
console.log(warning.name);
console.log(warning.message);
console.log(warning.stack);
});
let verbose = false;
if (process.argv[2] === '--v')
verbose = true;
let Jasmine = require('jasmine');
let jasmine = new Jasmine();
let SpecReporter = require('jasmine-spec-reporter').SpecReporter;
jasmine.loadConfig({
spec_files: [
`${__dirname}/smokes/**/*[sS]pec.js`,
`${__dirname}/helpers/extensions.js`
],
helpers: []
});
jasmine.addReporter(new SpecReporter({
spec: {
// displayStacktrace: 'summary',
displaySuccessful: verbose,
displayFailedSpec: true,
displaySpecDuration: true
}
}));
jasmine.execute();

View File

@ -1,31 +0,0 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('create client path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'client');
});
afterAll(async() => {
await browser.close();
});
it('should access to the create client view by clicking the create-client floating button', async() => {
await page.waitToClick(selectors.clientsIndex.createClientButton);
let url = await page.expectURL('#!/client/create');
expect(url).toBe(true);
});
it('should cancel the client creation to go back to clients index', async() => {
await page.waitToClick(selectors.globalItems.applicationsMenuButton);
await page.waitToClick(selectors.globalItems.clientsButton);
let url = await page.expectURL('#!/client/index');
expect(url).toBe(true);
});
});

View File

@ -1,7 +1,7 @@
<form ng-submit="$ctrl.onSubmit()"> <form ng-submit="$ctrl.onSubmit()">
<vn-textfield <vn-textfield
class="dense standout" class="dense standout"
placeholder="{{::'Search by' | translate: {module: $ctrl.baseState} }}" placeholder="{{::$ctrl.placeholder | translate}}"
ng-model="$ctrl.searchString"> ng-model="$ctrl.searchString">
<prepend> <prepend>
<vn-icon <vn-icon

View File

@ -21,6 +21,7 @@ export default class Searchbar extends Component {
constructor($element, $) { constructor($element, $) {
super($element, $); super($element, $);
this.searchState = '.'; this.searchState = '.';
this.placeholder = 'Search';
this.autoState = true; this.autoState = true;
this.deregisterCallback = this.$transitions.onSuccess( this.deregisterCallback = this.$transitions.onSuccess(
@ -35,6 +36,9 @@ export default class Searchbar extends Component {
} }
this.searchState = `${this.baseState}.index`; this.searchState = `${this.baseState}.index`;
this.placeholder = this.$translate.instant('Search by', {
module: this.baseState
});
} }
this.fetchStateFilter(this.autoLoad); this.fetchStateFilter(this.autoLoad);
@ -293,7 +297,8 @@ ngModule.vnComponent('vnSearchbar', {
stateParams: '&?', stateParams: '&?',
model: '<?', model: '<?',
exprBuilder: '&?', exprBuilder: '&?',
fetchParams: '&?' fetchParams: '&?',
placeholder: '@?'
} }
}); });

View File

@ -0,0 +1,24 @@
import ngModule from '../module';
class Email {
constructor($http, $translate, vnApp) {
this.$http = $http;
this.vnApp = vnApp;
this.$t = $translate.instant;
}
/**
* Sends an email displaying a notification when it's sent.
*
* @param {String} template The email report name
* @param {Object} params The email parameters
* @return {Promise} Promise resolved when it's sent
*/
send(template, params) {
return this.$http.get(`email/${template}`, {params})
.then(() => this.vnApp.showMessage(this.$t('Notification sent!')));
}
}
Email.$inject = ['$http', '$translate', 'vnApp'];
ngModule.service('vnEmail', Email);

View File

@ -0,0 +1,35 @@
import ngModule from '../module';
class File {
constructor($httpParamSerializer, vnToken) {
this.$httpParamSerializer = $httpParamSerializer;
this.vnToken = vnToken;
}
/**
* Returns the full download path
*
* @param {String} dmsUrl The file download path
* @return {String} The full download path
*/
getPath(dmsUrl) {
const serializedParams = this.$httpParamSerializer({
access_token: this.vnToken.token
});
return `${dmsUrl}?${serializedParams}`;
}
/**
* Downloads a file in another window, automatically adds the authorization
* token to params.
*
* @param {String} dmsUrl The file download path
*/
download(dmsUrl) {
window.open(this.getPath(dmsUrl));
}
}
File.$inject = ['$httpParamSerializer', 'vnToken'];
ngModule.service('vnFile', File);

View File

@ -7,3 +7,6 @@ import './modules';
import './interceptor'; import './interceptor';
import './config'; import './config';
import './week-days'; import './week-days';
import './report';
import './email';
import './file';

View File

@ -0,0 +1,26 @@
import ngModule from '../module';
class Report {
constructor($httpParamSerializer, vnToken) {
this.$httpParamSerializer = $httpParamSerializer;
this.vnToken = vnToken;
}
/**
* Shows a report in another window, automatically adds the authorization
* token to params.
*
* @param {String} report The report name
* @param {Object} params The report parameters
*/
show(report, params) {
params = Object.assign({
authorization: this.vnToken.token
}, params);
const serializedParams = this.$httpParamSerializer(params);
window.open(`api/report/${report}?${serializedParams}`);
}
}
Report.$inject = ['$httpParamSerializer', 'vnToken'];
ngModule.service('vnReport', Report);

View File

@ -14,7 +14,7 @@
font-family: 'Material Icons'; font-family: 'Material Icons';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url('./icons/Material-Design-Icons.woff2') format('woff2'); src: url('./icons/MaterialIcons-Regular.woff2') format('woff2');
} }
.material-icons { .material-icons {

Binary file not shown.

View File

@ -7,6 +7,13 @@ import './quick-link';
* Small card with basing entity information and actions. * Small card with basing entity information and actions.
*/ */
export default class Descriptor extends Component { export default class Descriptor extends Component {
constructor($element, $, vnReport, vnEmail) {
super($element, $);
this.vnReport = vnReport;
this.vnEmail = vnEmail;
}
$postLink() { $postLink() {
const content = this.element.querySelector('vn-descriptor-content'); const content = this.element.querySelector('vn-descriptor-content');
if (!content) throw new Error('Directive vnDescriptorContent not found'); if (!content) throw new Error('Directive vnDescriptorContent not found');
@ -74,34 +81,9 @@ export default class Descriptor extends Component {
return this.$http.get(url, options) return this.$http.get(url, options)
.finally(() => this.canceler = null); .finally(() => this.canceler = null);
} }
/**
* Shows a report in another window, automatically adds the authorization
* token to params.
*
* @param {String} report The report name
* @param {Object} params The report parameters
*/
showReport(report, params) {
params = Object.assign({
authorization: this.vnToken.token
}, params);
const serializedParams = this.$httpParamSerializer(params);
window.open(`api/report/${report}?${serializedParams}`);
} }
/** Descriptor.$inject = ['$element', '$scope', 'vnReport', 'vnEmail'];
* Sends an email displaying a notification when it's sent.
*
* @param {String} report The email report name
* @param {Object} params The email parameters
* @return {Promise} Promise resolved when it's sent
*/
sendEmail(report, params) {
return this.$http.get(`email/${report}`, {params})
.then(() => this.vnApp.showMessage(this.$t('Notification sent!')));
}
}
ngModule.vnComponent('vnDescriptor', { ngModule.vnComponent('vnDescriptor', {
controller: Descriptor, controller: Descriptor,

View File

@ -1,11 +1,11 @@
require('require-yaml'); require('require-yaml');
const gulp = require('gulp'); const gulp = require('gulp');
const exec = require('child_process').exec;
const PluginError = require('plugin-error'); const PluginError = require('plugin-error');
const argv = require('minimist')(process.argv.slice(2)); const argv = require('minimist')(process.argv.slice(2));
const log = require('fancy-log'); const log = require('fancy-log');
const request = require('request'); const request = require('request');
const e2eConfig = require('./e2e/helpers/config.js'); const e2eConfig = require('./e2e/helpers/config.js');
const Docker = require('./db/docker.js');
// Configuration // Configuration
@ -18,10 +18,6 @@ let langs = ['es', 'en'];
let srcDir = './front'; let srcDir = './front';
let modulesDir = './modules'; let modulesDir = './modules';
let buildDir = 'dist'; let buildDir = 'dist';
let containerId = 'salix-db';
let dataSources = require('./loopback/server/datasources.json');
let dbConf = dataSources.vn;
let backSources = [ let backSources = [
'!node_modules', '!node_modules',
@ -63,7 +59,7 @@ function backWatch(done) {
done: done done: done
}); });
} }
backWatch.description = `Starts backend in waching mode`; backWatch.description = `Starts backend in watcher mode`;
const back = gulp.series(dockerStart, backWatch); const back = gulp.series(dockerStart, backWatch);
back.description = `Starts backend and database service`; back.description = `Starts backend and database service`;
@ -73,13 +69,25 @@ defaultTask.description = `Starts all application services`;
// Backend tests // Backend tests
async function backTestOnce() { async function backTestOnce(done) {
let bootOptions; let err;
let dataSources = require('./loopback/server/datasources.json');
if (argv['random']) const container = new Docker();
bootOptions = {dataSources}; await container.run(argv.ci);
dataSources = JSON.parse(JSON.stringify(dataSources));
Object.assign(dataSources.vn, {
host: container.dbConf.host,
port: container.dbConf.port
});
let bootOptions = {dataSources};
let app = require(`./loopback/server/server`); let app = require(`./loopback/server/server`);
try {
app.boot(bootOptions); app.boot(bootOptions);
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
@ -92,7 +100,7 @@ async function backTestOnce() {
} }
}; };
if (argv.junit) { if (argv.ci) {
const reporters = require('jasmine-reporters'); const reporters = require('jasmine-reporters');
options.reporter = new reporters.JUnitXmlReporter(); options.reporter = new reporters.JUnitXmlReporter();
} }
@ -109,42 +117,16 @@ async function backTestOnce() {
.on('error', reject) .on('error', reject)
.resume(); .resume();
}); });
} catch (e) {
err = e;
}
await app.disconnect(); await app.disconnect();
await container.rm();
done();
if (err)
throw err;
} }
backTestOnce.description = `Runs the backend tests once, can receive --junit arg to save reports on a xml file`; backTestOnce.description = `Runs the backend tests once using a random container, can receive --ci arg to save reports on a xml file`;
async function backTestDockerOnce() {
let containerId = await docker();
let err;
try {
await backTestOnce();
} catch (e) {
err = e;
}
if (argv['random'])
await execP(`docker rm -fv ${containerId}`);
if (err) throw err;
}
backTestDockerOnce.description = `Runs backend tests using in site container once`;
async function backTestDocker() {
let containerId = await docker();
let err;
try {
await backTest();
} catch (e) {
err = e;
}
if (argv['random'])
await execP(`docker rm -fv ${containerId}`);
if (err) throw err;
}
backTestDocker.description = `Runs backend tests restoring fixtures first`;
function backTest(done) { function backTest(done) {
const nodemon = require('gulp-nodemon'); const nodemon = require('gulp-nodemon');
@ -208,7 +190,14 @@ function e2eSingleRun() {
] ]
})); }));
} }
e2eSingleRun.description = `Runs the e2e tests just once`;
e2e = gulp.series(docker, async function isBackendReady() {
const attempts = await backendStatus();
log(`Backend ready after ${attempts} attempt(s)`);
return attempts;
}, e2eSingleRun);
e2e.description = `Restarts database and runs the e2e tests`;
async function backendStatus() { async function backendStatus() {
const milliseconds = 250; const milliseconds = 250;
@ -231,24 +220,6 @@ async function backendStatus() {
} }
backendStatus.description = `Performs a simple requests to check the backend status`; backendStatus.description = `Performs a simple requests to check the backend status`;
e2e = gulp.series(docker, async function isBackendReady() {
const attempts = await backendStatus();
log(`Backend ready after ${attempts} attempt(s)`);
return attempts;
}, e2eSingleRun);
e2e.description = `Restarts database and runs the e2e tests`;
function smokesOnly() {
const jasmine = require('gulp-jasmine');
return gulp.src('./e2e/smokes-tests.js')
.pipe(jasmine({reporter: 'none'}));
}
smokesOnly.description = `Runs the smokes tests only`;
smokes = gulp.series(docker, smokesOnly);
smokes.description = `Restarts database and runs the smokes tests`;
function install() { function install() {
const install = require('gulp-install'); const install = require('gulp-install');
const print = require('gulp-print'); const print = require('gulp-print');
@ -414,156 +385,17 @@ function watch(done) {
watch.description = `Watches for changes in routes and locale files`; watch.description = `Watches for changes in routes and locale files`;
// Docker // Docker
/**
* Builds the database image and runs a container. It only rebuilds the
* image when fixtures have been modified or when the day on which the
* image was built is different to today. Some workarounds have been used
* to avoid a bug with OverlayFS driver on MacOS.
*/
async function docker() {
let d = new Date();
let pad = v => v < 10 ? '0' + v : v;
let stamp = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
await execP(`docker build --build-arg STAMP=${stamp} -t salix-db ./db`);
let dockerArgs = `--name ${containerId} -p 3306:${dbConf.port}`;
if (argv['random'])
dockerArgs = '-p 3306';
else {
try {
await execP(`docker rm -fv ${containerId}`);
} catch (e) {}
}
let runChown = process.platform != 'linux';
if (argv['run-chown']) runChown = true;
let result = await execP(`docker run --env RUN_CHOWN=${runChown} -d ${dockerArgs} salix-db`);
containerId = result.stdout;
try {
if (argv['random']) {
let inspect = await execP(`docker inspect -f "{{json .NetworkSettings}}" ${containerId}`);
let netSettings = JSON.parse(inspect.stdout);
dbConf.host = netSettings.Gateway;
dbConf.port = netSettings.Ports['3306/tcp'][0]['HostPort'];
}
if (runChown) await dockerWait();
} catch (err) {
if (argv['random'])
await execP(`docker rm -fv ${containerId}`);
throw err;
}
return containerId;
}
docker.description = `Builds the database image and runs a container`;
/**
* Does the minium effort to start the database container, if it doesn't exists
* calls the 'docker' task, if it is started does nothing. Keep in mind that when
* you do not rebuild the docker you may be using an outdated version of it.
* See the 'docker' task for more info.
*/
async function dockerStart() { async function dockerStart() {
let state; const container = new Docker('salix-db');
try { await container.start();
let result = await execP(`docker inspect -f "{{json .State}}" ${containerId}`);
state = JSON.parse(result.stdout);
} catch (err) {
return await docker();
} }
dockerStart.description = `Starts the salix-db container`;
switch (state.Status) { async function docker() {
case 'running': const container = new Docker('salix-db');
return; await container.run();
case 'exited':
await execP(`docker start ${containerId}`);
await dockerWait();
return;
default:
throw new Error(`Unknown docker status: ${state.Status}`);
}
}
dockerStart.description = `Starts the database container`;
function dockerWait() {
return new Promise((resolve, reject) => {
const mysql = require('mysql2');
let interval = 100;
let elapsedTime = 0;
let maxInterval = 4 * 60 * 1000;
let myConf = {
user: dbConf.username,
password: dbConf.password,
host: dbConf.host,
port: dbConf.port
};
log('Waiting for MySQL init process...');
checker();
async function checker() {
elapsedTime += interval;
let state;
try {
let result = await execP(`docker container inspect -f "{{json .State}}" ${containerId}`);
state = JSON.parse(result.stdout);
} catch (err) {
return reject(new Error(err.message));
}
if (state.Status === 'exited')
return reject(new Error('Docker exited, please see the docker logs for more info'));
let conn = mysql.createConnection(myConf);
conn.on('error', () => {});
conn.connect(err => {
conn.destroy();
if (!err) {
log('MySQL process ready.');
return resolve();
}
if (elapsedTime >= maxInterval)
reject(new Error(`MySQL not initialized whithin ${elapsedTime / 1000} secs`));
else
setTimeout(checker, interval);
});
}
});
}
dockerWait.description = `Waits until database service is ready`;
// Helpers
/**
* Promisified version of exec().
*
* @param {String} command The exec command
* @return {Promise} The promise
*/
function execP(command) {
return new Promise((resolve, reject) => {
exec(command, (err, stdout, stderr) => {
if (err)
reject(err);
else {
resolve({
stdout: stdout,
stderr: stderr
});
}
});
});
} }
docker.description = `Runs the salix-db container`;
module.exports = { module.exports = {
default: defaultTask, default: defaultTask,
@ -572,13 +404,8 @@ module.exports = {
backOnly, backOnly,
backWatch, backWatch,
backTestOnce, backTestOnce,
backTestDockerOnce,
backTest, backTest,
backTestDocker,
e2e, e2e,
e2eSingleRun,
smokes,
smokesOnly,
i, i,
install, install,
build, build,
@ -590,7 +417,5 @@ module.exports = {
localesRoutes, localesRoutes,
watch, watch,
docker, docker,
dockerStart,
dockerWait,
backendStatus, backendStatus,
}; };

View File

@ -11,14 +11,14 @@ class Controller extends Descriptor {
} }
showPickupOrder() { showPickupOrder() {
this.showReport('claim-pickup-order', { this.vnReport.show('claim-pickup-order', {
recipientId: this.claim.clientFk, recipientId: this.claim.clientFk,
claimId: this.claim.id claimId: this.claim.id
}); });
} }
sendPickupOrder() { sendPickupOrder() {
return this.sendEmail('claim-pickup-order', { return this.vnEmail.send('claim-pickup-order', {
recipient: this.claim.client.email, recipient: this.claim.client.email,
recipientId: this.claim.clientFk, recipientId: this.claim.clientFk,
claimId: this.claim.id claimId: this.claim.id

View File

@ -20,21 +20,22 @@ describe('Item Component vnClaimDescriptor', () => {
describe('showPickupOrder()', () => { describe('showPickupOrder()', () => {
it('should open a new window showing a pickup order PDF document', () => { it('should open a new window showing a pickup order PDF document', () => {
controller.showReport = jest.fn(); jest.spyOn(controller.vnReport, 'show');
window.open = jasmine.createSpy('open');
const params = { const params = {
recipientId: claim.clientFk, recipientId: claim.clientFk,
claimId: claim.id claimId: claim.id
}; };
controller.showPickupOrder(); controller.showPickupOrder();
expect(controller.showReport).toHaveBeenCalledWith('claim-pickup-order', params); expect(controller.vnReport.show).toHaveBeenCalledWith('claim-pickup-order', params);
}); });
}); });
describe('sendPickupOrder()', () => { describe('sendPickupOrder()', () => {
it('should make a query and call vnApp.showMessage() if the response is accept', () => { it('should make a query and call vnApp.showMessage() if the response is accept', () => {
jest.spyOn(controller, 'sendEmail'); jest.spyOn(controller.vnEmail, 'send');
const params = { const params = {
recipient: claim.client.email, recipient: claim.client.email,
@ -43,7 +44,7 @@ describe('Item Component vnClaimDescriptor', () => {
}; };
controller.sendPickupOrder(); controller.sendPickupOrder();
expect(controller.sendEmail).toHaveBeenCalledWith('claim-pickup-order', params); expect(controller.vnEmail.send).toHaveBeenCalledWith('claim-pickup-order', params);
}); });
}); });

View File

@ -13,8 +13,8 @@
</section> </section>
<section class="photo" ng-repeat="photo in $ctrl.photos"> <section class="photo" ng-repeat="photo in $ctrl.photos">
<section class="image vn-shadow" on-error-src <section class="image vn-shadow" on-error-src
ng-style="{'background': 'url(/api/dms/' + photo.dmsFk + '/downloadFile?access_token=' + $ctrl.vnToken.token + ')'}" ng-style="{'background': 'url(' + $ctrl.getImagePath(photo.dmsFk) + ')'}"
zoom-image="/api/dms/{{::photo.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}"> zoom-image="{{$ctrl.getImagePath(photo.dmsFk)}}">
</section> </section>
<section class="actions"> <section class="actions">
<vn-button <vn-button

View File

@ -3,6 +3,11 @@ import Section from 'salix/components/section';
import './style.scss'; import './style.scss';
class Controller extends Section { class Controller extends Section {
constructor($element, $, vnFile) {
super($element, $);
this.vnFile = vnFile;
}
deleteDms(index) { deleteDms(index) {
const dmsFk = this.photos[index].dmsFk; const dmsFk = this.photos[index].dmsFk;
return this.$http.post(`ClaimDms/${dmsFk}/removeFile`) return this.$http.post(`ClaimDms/${dmsFk}/removeFile`)
@ -80,7 +85,13 @@ class Controller extends Section {
this.$.model.refresh(); this.$.model.refresh();
}); });
} }
getImagePath(dmsId) {
return this.vnFile.getPath(`/api/dms/${dmsId}/downloadFile`);
} }
}
Controller.$inject = ['$element', '$scope', 'vnFile'];
ngModule.component('vnClaimPhotos', { ngModule.component('vnClaimPhotos', {
template: require('./index.html'), template: require('./index.html'),

View File

@ -90,8 +90,8 @@
<vn-horizontal class="photo-list"> <vn-horizontal class="photo-list">
<section class="photo" ng-repeat="photo in photos"> <section class="photo" ng-repeat="photo in photos">
<section class="image" on-error-src <section class="image" on-error-src
ng-style="{'background': 'url(/api/dms/' + photo.dmsFk + '/downloadFile?access_token=' + $ctrl.vnToken.token + ')'}" ng-style="{'background': 'url(' + $ctrl.getImagePath(photo.dmsFk) + ')'}"
zoom-image="/api/dms/{{::photo.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}"> zoom-image="{{$ctrl.getImagePath(photo.dmsFk)}}">
</section> </section>
</section> </section>
</vn-horizontal> </vn-horizontal>

View File

@ -3,6 +3,11 @@ import Section from 'salix/components/section';
import './style.scss'; import './style.scss';
class Controller extends Section { class Controller extends Section {
constructor($element, $, vnFile) {
super($element, $);
this.vnFile = vnFile;
}
$onChanges() { $onChanges() {
if (this.claim && this.claim.id) if (this.claim && this.claim.id)
this.getSummary(); this.getSummary();
@ -32,7 +37,13 @@ class Controller extends Section {
this.summary = response.data; this.summary = response.data;
}); });
} }
getImagePath(dmsId) {
return this.vnFile.getPath(`/api/dms/${dmsId}/downloadFile`);
} }
}
Controller.$inject = ['$element', '$scope', 'vnFile'];
ngModule.component('vnClaimSummary', { ngModule.component('vnClaimSummary', {
template: require('./index.html'), template: require('./index.html'),

View File

@ -0,0 +1,122 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const buildFilter = require('vn-loopback/util/filter').buildFilter;
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
module.exports = Self => {
Self.remoteMethodCtx('consumption', {
description: 'Find all instances of the model matched by filter from the data source.',
accessType: 'READ',
accepts: [
{
arg: 'filter',
type: 'Object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string'
}, {
arg: 'search',
type: 'String',
description: `If it's and integer searchs by id, otherwise it searchs by name`
}, {
arg: 'itemFk',
type: 'Integer',
description: 'Item id'
}, {
arg: 'categoryFk',
type: 'Integer',
description: 'Category id'
}, {
arg: 'typeFk',
type: 'Integer',
description: 'Item type id',
}, {
arg: 'buyerFk',
type: 'Integer',
description: 'Buyer id'
}, {
arg: 'from',
type: 'Date',
description: `The from date filter`
}, {
arg: 'to',
type: 'Date',
description: `The to date filter`
}, {
arg: 'grouped',
type: 'Boolean',
description: 'Group by item'
}
],
returns: {
type: ['Object'],
root: true
},
http: {
path: `/consumption`,
verb: 'GET'
}
});
Self.consumption = async(ctx, filter) => {
const conn = Self.dataSource.connector;
const args = ctx.args;
const where = buildFilter(ctx.args, (param, value) => {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? {'i.id': value}
: {'i.name': {like: `%${value}%`}};
case 'itemId':
return {'i.id': value};
case 'description':
return {'i.description': {like: `%${value}%`}};
case 'categoryId':
return {'it.categoryFk': value};
case 'typeId':
return {'it.id': value};
case 'buyerId':
return {'it.workerFk': value};
}
});
filter = mergeFilters(filter, {where});
let stmt = new ParameterizedSQL('SELECT');
if (args.grouped)
stmt.merge(`SUM(s.quantity) AS quantity,`);
else
stmt.merge(`s.quantity,`);
stmt.merge(`s.itemFk,
s.concept,
s.ticketFk,
t.shipped,
i.name AS itemName,
i.size AS itemSize,
i.typeFk AS itemTypeFk,
i.subName,
i.tag5,
i.value5,
i.tag6,
i.value6,
i.tag7,
i.value7,
i.tag8,
i.value8,
i.tag9,
i.value9,
i.tag10,
i.value10
FROM sale s
JOIN ticket t ON t.id = s.ticketFk
JOIN item i ON i.id = s.itemFk
JOIN itemType it ON it.id = i.typeFk`, [args.grouped]);
stmt.merge(conn.makeWhere(filter.where));
if (args.grouped)
stmt.merge(`GROUP BY s.itemFk`);
stmt.merge(conn.makePagination(filter));
return conn.executeStmt(stmt);
};
};

View File

@ -0,0 +1,40 @@
const app = require('vn-loopback/server/server');
describe('client consumption() filter', () => {
it('should return a list of buyed items by ticket', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {}};
const filter = {
where: {
clientFk: 101
},
order: 'itemTypeFk, itemName, itemSize'
};
const result = await app.models.Client.consumption(ctx, filter);
expect(result.length).toEqual(10);
});
it('should return a list of tickets grouped by item', async() => {
const ctx = {req: {accessToken: {userId: 9}},
args: {
grouped: true
}
};
const filter = {
where: {
clientFk: 101
},
order: 'itemTypeFk, itemName, itemSize'
};
const result = await app.models.Client.consumption(ctx, filter);
const firstRow = result[0];
const secondRow = result[1];
const thirdRow = result[2];
expect(result.length).toEqual(3);
expect(firstRow.quantity).toEqual(10);
expect(secondRow.quantity).toEqual(15);
expect(thirdRow.quantity).toEqual(20);
});
});

View File

@ -2,7 +2,6 @@ let request = require('request-promise-native');
let UserError = require('vn-loopback/util/user-error'); let UserError = require('vn-loopback/util/user-error');
let getFinalState = require('vn-loopback/util/hook').getFinalState; let getFinalState = require('vn-loopback/util/hook').getFinalState;
let isMultiple = require('vn-loopback/util/hook').isMultiple; let isMultiple = require('vn-loopback/util/hook').isMultiple;
const httpParamSerializer = require('vn-loopback/util/http').httpParamSerializer;
const LoopBackContext = require('loopback-context'); const LoopBackContext = require('loopback-context');
module.exports = Self => { module.exports = Self => {
@ -27,6 +26,7 @@ module.exports = Self => {
require('../methods/client/sendSms')(Self); require('../methods/client/sendSms')(Self);
require('../methods/client/createAddress')(Self); require('../methods/client/createAddress')(Self);
require('../methods/client/updateAddress')(Self); require('../methods/client/updateAddress')(Self);
require('../methods/client/consumption')(Self);
// Validations // Validations
@ -218,6 +218,36 @@ module.exports = Self => {
await Self.app.models.ClientCredit.create(newCredit); await Self.app.models.ClientCredit.create(newCredit);
} }
}); });
const app = require('vn-loopback/server/server');
app.on('started', function() {
let account = app.models.Account;
account.observe('before save', async ctx => {
if (ctx.isNewInstance) return;
ctx.hookState.oldInstance = JSON.parse(JSON.stringify(ctx.currentInstance));
});
account.observe('after save', async ctx => {
let changes = ctx.data || ctx.instance;
if (!ctx.isNewInstance && changes) {
let oldData = ctx.hookState.oldInstance;
let hasChanges = oldData.name != changes.name || oldData.active != changes.active;
if (!hasChanges) return;
let userId = ctx.options.accessToken.userId;
let logRecord = {
originFk: oldData.id,
userFk: userId,
action: 'update',
changedModel: 'Account',
oldInstance: {name: oldData.name, active: oldData.active},
newInstance: {name: changes.name, active: changes.active}
};
await Self.app.models.ClientLog.create(logRecord);
}
});
});
Self.observe('after save', async ctx => { Self.observe('after save', async ctx => {
if (ctx.isNewInstance) return; if (ctx.isNewInstance) return;

View File

@ -6,6 +6,15 @@
data="$ctrl.addresses" data="$ctrl.addresses"
auto-load="true"> auto-load="true">
</vn-crud-model> </vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar
placeholder="Search by address"
info="You can search by address id or name"
model="model"
expr-builder="$ctrl.exprBuilder(param, value)"
auto-state="false">
</vn-searchbar>
</vn-portal>
<vn-data-viewer <vn-data-viewer
model="model" model="model"
class="vn-w-md"> class="vn-w-md">
@ -35,7 +44,7 @@
</vn-none> </vn-none>
<vn-one <vn-one
style="overflow: hidden; min-width: 14em;"> style="overflow: hidden; min-width: 14em;">
<div class="ellipsize"><b>{{::address.nickname}}</b></div> <div class="ellipsize"><b>{{::address.nickname}} - #{{::address.id}}</b></div>
<div class="ellipsize" name="street">{{::address.street}}</div> <div class="ellipsize" name="street">{{::address.street}}</div>
<div class="ellipsize">{{::address.city}}, {{::address.province.name}}</div> <div class="ellipsize">{{::address.city}}, {{::address.province.name}}</div>
<div class="ellipsize"> <div class="ellipsize">

View File

@ -68,6 +68,15 @@ class Controller extends Section {
return this.isDefaultAddress(b) - this.isDefaultAddress(a); return this.isDefaultAddress(b) - this.isDefaultAddress(a);
}); });
} }
exprBuilder(param, value) {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? {id: value}
: {nickname: {like: `%${value}%`}};
}
}
} }
Controller.$inject = ['$element', '$scope']; Controller.$inject = ['$element', '$scope'];

View File

@ -67,5 +67,19 @@ describe('Client', () => {
expect(controller.addresses[0].id).toEqual(123); expect(controller.addresses[0].id).toEqual(123);
}); });
}); });
describe('exprBuilder()', () => {
it('should return a filter based on a search by id', () => {
const filter = controller.exprBuilder('search', '123');
expect(filter).toEqual({id: '123'});
});
it('should return a filter based on a search by name', () => {
const filter = controller.exprBuilder('search', 'Bruce Wayne');
expect(filter).toEqual({nickname: {like: '%Bruce Wayne%'}});
});
});
}); });
}); });

View File

@ -1,6 +1,8 @@
# Index # Index
Set as default: Establecer como predeterminado Set as default: Establecer como predeterminado
Active first to set as default: Active primero para marcar como predeterminado Active first to set as default: Active primero para marcar como predeterminado
Search by address: Buscar por consignatario
You can search by address id or name: Puedes buscar por el id o nombre del consignatario
# Edit # Edit
Enabled: Activo Enabled: Activo
Is equalizated: Recargo de equivalencia Is equalizated: Recargo de equivalencia

View File

@ -0,0 +1,68 @@
<div class="search-panel">
<form class="vn-pa-lg" ng-submit="$ctrl.onSearch()">
<vn-horizontal>
<vn-textfield vn-focus
vn-one
label="General search"
ng-model="filter.search"
vn-focus>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="Item id"
ng-model="filter.itemId">
</vn-textfield>
<vn-autocomplete
vn-one
ng-model="filter.buyerId"
url="Clients/activeWorkersWithRole"
search-function="{firstName: $search}"
value-field="id"
where="{role: 'employee'}"
label="Buyer">
<tpl-item>{{nickname}}</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
ng-model="filter.typeId"
url="ItemTypes"
show-field="name"
value-field="id"
label="Type"
fields="['categoryFk']"
include="'category'">
<tpl-item>
<div>{{name}}</div>
<div class="text-caption text-secondary">
{{category.name}}
</div>
</tpl-item>
</vn-autocomplete>
<vn-autocomplete vn-one
url="ItemCategories"
label="Category"
show-field="name"
value-field="id"
ng-model="filter.categoryId">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-date-picker
vn-one
label="From"
ng-model="filter.from">
</vn-date-picker>
<vn-date-picker
vn-one
label="To"
ng-model="filter.to">
</vn-date-picker>
</vn-horizontal>
<vn-horizontal class="vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>
</div>

View File

@ -0,0 +1,7 @@
import ngModule from '../module';
import SearchPanel from 'core/components/searchbar/search-panel';
ngModule.component('vnConsumptionSearchPanel', {
template: require('./index.html'),
controller: SearchPanel
});

View File

@ -0,0 +1,3 @@
Item id: Id artículo
From: Desde
To: Hasta

View File

@ -0,0 +1,91 @@
<vn-crud-model vn-id="model"
url="Clients/consumption"
link="{clientFk: $ctrl.$params.id}"
filter="::$ctrl.filter"
user-params="::$ctrl.filterParams"
data="sales"
order="itemTypeFk, itemName, itemSize">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar
panel="vn-consumption-search-panel"
suggested-filter="$ctrl.filterParams"
info="Search by item id or name"
model="model"
auto-state="false">
</vn-searchbar>
</vn-portal>
<vn-data-viewer model="model">
<vn-card class="vn-pa-lg vn-w-lg">
<section class="header">
<vn-tool-bar class="vn-mb-md">
<vn-button disabled="!model.userParams.from || !model.userParams.to"
icon="picture_as_pdf"
ng-click="$ctrl.showReport()"
vn-tooltip="Open as PDF">
</vn-button>
<vn-button disabled="!model.userParams.from || !model.userParams.to"
icon="email"
ng-click="confirm.show()"
vn-tooltip="Send to email">
</vn-button>
<vn-check
label="Group by item"
on-change="$ctrl.changeGrouped(value)">
</vn-check>
</vn-tool-bar>
</section>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th field="itemFk" number>Item</vn-th>
<vn-th field="ticketFk" number>Ticket</vn-th>
<vn-th field="shipped">Fecha</vn-th>
<vn-th expand>Description</vn-th>
<vn-th field="quantity" number>Quantity</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr
ng-repeat="sale in sales">
<vn-td number>
<span
ng-click="itemDescriptor.show($event, sale.itemFk)"
class="link">
{{::sale.itemFk}}
</span>
</vn-td>
<vn-td number>
<span
ng-click="ticketDescriptor.show($event, sale.ticketFk)"
class="link">
{{::sale.ticketFk}}
</span>
</vn-td>
<vn-td>{{::sale.shipped | date: 'dd/MM/yyyy'}}</vn-td>
<vn-td expand>
<vn-fetched-tags
max-length="6"
item="::sale"
name="::sale.concept"
sub-name="::sale.subName">
</vn-fetched-tags>
</vn-td>
<vn-td number>{{::sale.quantity | dashIfEmpty}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
<vn-item-descriptor-popover
vn-id="item-descriptor">
</vn-item-descriptor-popover>
<vn-ticket-descriptor-popover
vn-id="ticket-descriptor">
</vn-ticket-descriptor-popover>
<vn-confirm
vn-id="confirm"
question="Please, confirm"
message="The consumption report will be sent"
on-accept="$ctrl.sendEmail()">
</vn-confirm>

View File

@ -0,0 +1,69 @@
import ngModule from '../module';
import Section from 'salix/components/section';
class Controller extends Section {
constructor($element, $, vnReport, vnEmail) {
super($element, $);
this.vnReport = vnReport;
this.vnEmail = vnEmail;
this.filter = {
where: {
isPackaging: false
}
};
const minDate = new Date();
minDate.setHours(0, 0, 0, 0);
minDate.setMonth(minDate.getMonth() - 2);
const maxDate = new Date();
maxDate.setHours(23, 59, 59, 59);
this.filterParams = {
from: minDate,
to: maxDate
};
}
get reportParams() {
const userParams = this.$.model.userParams;
return Object.assign({
authorization: this.vnToken.token,
recipientId: this.client.id
}, userParams);
}
showTicketDescriptor(event, sale) {
if (!sale.isTicket) return;
this.$.ticketDescriptor.show(event.target, sale.origin);
}
showReport() {
this.vnReport.show('campaign-metrics', this.reportParams);
}
sendEmail() {
this.vnEmail.send('campaign-metrics', this.reportParams);
}
changeGrouped(value) {
const model = this.$.model;
model.addFilter({}, {grouped: value});
}
}
Controller.$inject = ['$element', '$scope', 'vnReport', 'vnEmail'];
ngModule.component('vnClientConsumption', {
template: require('./index.html'),
controller: Controller,
bindings: {
client: '<'
},
require: {
card: '^vnClientCard'
}
});

View File

@ -0,0 +1,72 @@
import './index.js';
import crudModel from 'core/mocks/crud-model';
describe('Client', () => {
describe('Component vnClientConsumption', () => {
let $scope;
let controller;
let $httpParamSerializer;
let $httpBackend;
beforeEach(ngModule('client'));
beforeEach(angular.mock.inject(($componentController, $rootScope, _$httpParamSerializer_, _$httpBackend_) => {
$scope = $rootScope.$new();
$httpParamSerializer = _$httpParamSerializer_;
$httpBackend = _$httpBackend_;
const $element = angular.element('<vn-client-consumption></vn-client-consumption');
controller = $componentController('vnClientConsumption', {$element, $scope});
controller.$.model = crudModel;
controller.client = {
id: 101
};
}));
describe('showReport()', () => {
it('should call the window.open function', () => {
jest.spyOn(window, 'open').mockReturnThis();
const now = new Date();
controller.$.model.userParams = {
from: now,
to: now
};
controller.showReport();
const expectedParams = {
recipientId: 101,
from: now,
to: now
};
const serializedParams = $httpParamSerializer(expectedParams);
const path = `api/report/campaign-metrics?${serializedParams}`;
expect(window.open).toHaveBeenCalledWith(path);
});
});
describe('sendEmail()', () => {
it('should make a GET query sending the report', () => {
const now = new Date();
controller.$.model.userParams = {
from: now,
to: now
};
const expectedParams = {
recipientId: 101,
from: now,
to: now
};
const serializedParams = $httpParamSerializer(expectedParams);
const path = `email/campaign-metrics?${serializedParams}`;
$httpBackend.expect('GET', path).respond({});
controller.sendEmail();
$httpBackend.flush();
});
});
});
});

View File

@ -0,0 +1,6 @@
Group by item: Agrupar por artículo
Open as PDF: Abrir como PDF
Send to email: Enviar por email
Search by item id or name: Buscar por id de artículo o nombre
The consumption report will be sent: Se enviará el informe de consumo
Please, confirm: Por favor, confirma

View File

@ -13,11 +13,6 @@
translate> translate>
Send SMS Send SMS
</vn-item> </vn-item>
<vn-item
ng-click="consumerReportDialog.show()"
translate>
View consumer report
</vn-item>
</slot-menu> </slot-menu>
<slot-body> <slot-body>
<div class="attributes"> <div class="attributes">
@ -95,27 +90,3 @@
vn-id="sms" vn-id="sms"
sms="$ctrl.newSMS"> sms="$ctrl.newSMS">
</vn-client-sms> </vn-client-sms>
<vn-dialog
vn-id="consumerReportDialog"
on-accept="$ctrl.onConsumerReportAccept()"
message="Send consumer report">
<tpl-body>
<vn-date-picker
vn-id="from"
vn-one
ng-model="$ctrl.from"
label="From date"
vn-focus>
</vn-date-picker>
<vn-date-picker
vn-id="to"
vn-one
ng-model="$ctrl.to"
label="To date">
</vn-date-picker>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Accept</button>
</tpl-buttons>
</vn-dialog>

View File

@ -39,14 +39,6 @@ class Controller extends Descriptor {
}; };
this.$.sms.open(); this.$.sms.open();
} }
onConsumerReportAccept() {
this.showReport('campaign-metrics', {
recipientId: this.id,
from: this.from,
to: this.to,
});
}
} }
ngModule.vnComponent('vnClientDescriptor', { ngModule.vnComponent('vnClientDescriptor', {

View File

@ -54,11 +54,10 @@
</span> </span>
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<a target="_blank" <span title="{{'Download file' | translate}}" class="link"
title="{{'Download file' | translate}}" ng-click="$ctrl.downloadFile(document.dmsFk)">
href="api/dms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">
{{::document.dms.file}} {{::document.dms.file}}
</a> </span>
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<span class="link" <span class="link"
@ -69,13 +68,10 @@
{{::document.dms.created | date:'dd/MM/yyyy HH:mm'}} {{::document.dms.created | date:'dd/MM/yyyy HH:mm'}}
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<a target="_blank" <vn-icon-button title="{{'Download file' | translate}}"
href="api/dms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">
<vn-icon-button
icon="cloud_download" icon="cloud_download"
title="{{'Download file' | translate}}"> ng-click="$ctrl.downloadFile(document.dmsFk)">
</vn-icon-button> </vn-icon-button>
</a>
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<vn-icon-button ui-sref="client.card.dms.edit({dmsId: {{::document.dmsFk}}})" <vn-icon-button ui-sref="client.card.dms.edit({dmsId: {{::document.dmsFk}}})"

View File

@ -3,8 +3,9 @@ import Section from 'salix/components/section';
import './style.scss'; import './style.scss';
class Controller extends Section { class Controller extends Section {
constructor($element, $) { constructor($element, $, vnFile) {
super($element, $); super($element, $, vnFile);
this.vnFile = vnFile;
this.filter = { this.filter = {
include: { include: {
relation: 'dms', relation: 'dms',
@ -49,9 +50,13 @@ class Controller extends Section {
this.vnApp.showSuccess(this.$t('Data saved!')); this.vnApp.showSuccess(this.$t('Data saved!'));
}); });
} }
downloadFile(dmsId) {
this.vnFile.download(`api/dms/${dmsId}/downloadFile`);
}
} }
Controller.$inject = ['$element', '$scope']; Controller.$inject = ['$element', '$scope', 'vnFile'];
ngModule.component('vnClientDmsIndex', { ngModule.component('vnClientDmsIndex', {
template: require('./index.html'), template: require('./index.html'),

View File

@ -40,3 +40,5 @@ import './postcode';
import './dms/index'; import './dms/index';
import './dms/create'; import './dms/create';
import './dms/edit'; import './dms/edit';
import './consumption';
import './consumption-search-panel';

View File

@ -57,3 +57,4 @@ Contacts: Contactos
Samples: Plantillas Samples: Plantillas
Send sample: Enviar plantilla Send sample: Enviar plantilla
Log: Historial Log: Historial
Consumption: Consumo

View File

@ -18,16 +18,17 @@
{"state": "client.card.greuge.index", "icon": "work"}, {"state": "client.card.greuge.index", "icon": "work"},
{"state": "client.card.balance.index", "icon": "icon-invoices"}, {"state": "client.card.balance.index", "icon": "icon-invoices"},
{"state": "client.card.recovery.index", "icon": "icon-recovery"}, {"state": "client.card.recovery.index", "icon": "icon-recovery"},
{"state": "client.card.webAccess", "icon": "cloud"},
{"state": "client.card.log", "icon": "history"}, {"state": "client.card.log", "icon": "history"},
{ {
"description": "Others", "description": "Others",
"icon": "more", "icon": "more",
"childs": [ "childs": [
{"state": "client.card.webAccess", "icon": "cloud"}, {"state": "client.card.sample.index", "icon": "mail"},
{"state": "client.card.consumption", "icon": "show_chart"},
{"state": "client.card.mandate", "icon": "pan_tool"}, {"state": "client.card.mandate", "icon": "pan_tool"},
{"state": "client.card.creditInsurance.index", "icon": "icon-solunion"}, {"state": "client.card.creditInsurance.index", "icon": "icon-solunion"},
{"state": "client.card.contact", "icon": "contact_phone"}, {"state": "client.card.contact", "icon": "contact_phone"},
{"state": "client.card.sample.index", "icon": "mail"},
{"state": "client.card.webPayment", "icon": "icon-onlinepayment"}, {"state": "client.card.webPayment", "icon": "icon-onlinepayment"},
{"state": "client.card.dms.index", "icon": "cloud_upload"} {"state": "client.card.dms.index", "icon": "cloud_upload"}
] ]
@ -350,6 +351,15 @@
"params": { "params": {
"client": "$ctrl.client" "client": "$ctrl.client"
} }
},
{
"url": "/consumption",
"state": "client.card.consumption",
"component": "vn-client-consumption",
"description": "Consumption",
"params": {
"client": "$ctrl.client"
}
} }
] ]
} }

View File

@ -36,7 +36,7 @@ class Controller extends Descriptor {
} }
showEntryReport() { showEntryReport() {
this.showReport('entry-order', { this.vnReport.show('entry-order', {
entryId: this.entry.id entryId: this.entry.id
}); });
} }

View File

@ -12,15 +12,16 @@ describe('Entry Component vnEntryDescriptor', () => {
describe('showEntryReport()', () => { describe('showEntryReport()', () => {
it('should open a new window showing a delivery note PDF document', () => { it('should open a new window showing a delivery note PDF document', () => {
controller.showReport = jest.fn(); jest.spyOn(controller.vnReport, 'show');
window.open = jasmine.createSpy('open');
const params = { const params = {
clientId: controller.vnConfig.storage.currentUserWorkerId, clientId: controller.vnConfig.storage.currentUserWorkerId,
entryId: entry.id entryId: entry.id
}; };
controller.showEntryReport(); controller.showEntryReport();
expect(controller.showReport).toHaveBeenCalledWith('entry-order', params); expect(controller.vnReport.show).toHaveBeenCalledWith('entry-order', params);
}); });
}); });
}); });

View File

@ -11,14 +11,14 @@ class Controller extends Descriptor {
} }
showRouteReport() { showRouteReport() {
this.showReport('driver-route', { this.vnReport.show('driver-route', {
routeId: this.id routeId: this.id
}); });
} }
sendRouteReport() { sendRouteReport() {
const workerUser = this.route.worker.user; const workerUser = this.route.worker.user;
this.sendEmail('driver-route', { this.vnEmail.send('driver-route', {
recipient: workerUser.emailUser.email, recipient: workerUser.emailUser.email,
routeId: this.id routeId: this.id
}); });

View File

@ -100,34 +100,27 @@ module.exports = Self => {
}); });
Self.filter = async(ctx, filter) => { Self.filter = async(ctx, filter) => {
const userId = ctx.req.accessToken.userId;
const conn = Self.dataSource.connector; const conn = Self.dataSource.connector;
const models = Self.app.models;
const args = ctx.args; const args = ctx.args;
let worker = await Self.app.models.Worker.findOne({ // Apply filter by team
where: {userFk: ctx.req.accessToken.userId}, const teamMembersId = [];
include: [ if (args.myTeam != null) {
{relation: 'collegues'} const worker = await models.Worker.findById(userId, {
] include: {
}); relation: 'collegues'
let teamIds = [];
if (worker.collegues().length && args.myTeam) {
worker.collegues().forEach(collegue => {
teamIds.push(collegue.collegueFk);
});
} }
if (args.mine || (worker.collegues().length === 0 && args.myTeam)) {
worker = await Self.app.models.Worker.findOne({
fields: ['id'],
where: {userFk: ctx.req.accessToken.userId}
}); });
teamIds = [worker && worker.id]; const collegues = worker.collegues() || [];
} collegues.forEach(collegue => {
teamMembersId.push(collegue.collegueFk);
});
if (ctx.args && (args.mine || args.myTeam)) if (teamMembersId.length == 0)
args.teamIds = teamIds; teamMembersId.push(userId);
}
if (ctx.args && args.to) { if (ctx.args && args.to) {
const dateTo = args.to; const dateTo = args.to;
@ -156,7 +149,11 @@ module.exports = Self => {
return {'ts.stateFk': value}; return {'ts.stateFk': value};
case 'mine': case 'mine':
case 'myTeam': case 'myTeam':
return {'c.salesPersonFk': {inq: teamIds}}; if (value)
return {'c.salesPersonFk': {inq: teamMembersId}};
else
return {'c.salesPersonFk': {nin: teamMembersId}};
case 'alertLevel': case 'alertLevel':
return {'ts.alertLevel': value}; return {'ts.alertLevel': value};
case 'pending': case 'pending':

View File

@ -20,7 +20,7 @@ module.exports = Self => {
}); });
Self.getVolume = async ticketFk => { Self.getVolume = async ticketFk => {
let [volume] = await Self.rawSql(`CALL vn.ticketListVolume(?)`, [ticketFk]); return Self.rawSql(`SELECT * FROM vn.saleVolume
return volume; WHERE ticketFk = ?`, [ticketFk]);
}; };
}; };

View File

@ -43,18 +43,6 @@ module.exports = Self => {
if (hasItemShelvingSales && !isSalesAssistant) if (hasItemShelvingSales && !isSalesAssistant)
throw new UserError(`You cannot delete a ticket that part of it is being prepared`); throw new UserError(`You cannot delete a ticket that part of it is being prepared`);
if (hasItemShelvingSales && isSalesAssistant) {
const promises = [];
for (let sale of sales) {
if (sale.itemShelvingSale()) {
const itemShelvingSale = sale.itemShelvingSale();
const destroyedShelving = models.ItemShelvingSale.destroyById(itemShelvingSale.id);
promises.push(destroyedShelving);
}
}
await Promise.all(promises);
}
// Check for existing claim // Check for existing claim
const claimOfATicket = await models.Claim.findOne({where: {ticketFk: id}}); const claimOfATicket = await models.Claim.findOne({where: {ticketFk: id}});
if (claimOfATicket) if (claimOfATicket)
@ -69,10 +57,23 @@ module.exports = Self => {
if (hasPurchaseRequests) if (hasPurchaseRequests)
throw new UserError('You must delete all the buy requests first'); throw new UserError('You must delete all the buy requests first');
// removes item shelvings
if (hasItemShelvingSales && isSalesAssistant) {
const promises = [];
for (let sale of sales) {
if (sale.itemShelvingSale()) {
const itemShelvingSale = sale.itemShelvingSale();
const destroyedShelving = models.ItemShelvingSale.destroyById(itemShelvingSale.id);
promises.push(destroyedShelving);
}
}
await Promise.all(promises);
}
// Remove ticket greuges // Remove ticket greuges
const ticketGreuges = await models.Greuge.find({where: {ticketFk: id}}); const ticketGreuges = await models.Greuge.find({where: {ticketFk: id}});
const ownGreuges = ticketGreuges.every(greuge => { const ownGreuges = ticketGreuges.every(greuge => {
return greuge.ticketFk = id; return greuge.ticketFk == id;
}); });
if (ownGreuges) { if (ownGreuges) {
for (const greuge of ticketGreuges) { for (const greuge of ticketGreuges) {
@ -104,7 +105,7 @@ module.exports = Self => {
}] }]
}); });
// Change state to "fixing" if contains an stowaway // Change state to "fixing" if contains an stowaway and removed the link between them
let otherTicketId; let otherTicketId;
if (ticket.stowaway()) if (ticket.stowaway())
otherTicketId = ticket.stowaway().shipFk; otherTicketId = ticket.stowaway().shipFk;
@ -112,6 +113,7 @@ module.exports = Self => {
otherTicketId = ticket.ship().id; otherTicketId = ticket.ship().id;
if (otherTicketId) { if (otherTicketId) {
await models.Ticket.deleteStowaway(ctx, otherTicketId);
await models.TicketTracking.changeState(ctx, { await models.TicketTracking.changeState(ctx, {
ticketFk: otherTicketId, ticketFk: otherTicketId,
code: 'FIXING' code: 'FIXING'

View File

@ -71,4 +71,20 @@ describe('ticket filter()', () => {
expect(secondRow.state).toEqual('Entregado'); expect(secondRow.state).toEqual('Entregado');
expect(thirdRow.state).toEqual('Entregado'); expect(thirdRow.state).toEqual('Entregado');
}); });
it('should return the tickets from the worker team', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {myTeam: true}};
const filter = {};
const result = await app.models.Ticket.filter(ctx, filter);
expect(result.length).toEqual(17);
});
it('should return the tickets that are not from the worker team', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {myTeam: false}};
const filter = {};
const result = await app.models.Ticket.filter(ctx, filter);
expect(result.length).toEqual(7);
});
}); });

View File

@ -5,7 +5,7 @@ describe('ticket getVolume()', () => {
let ticketFk = 1; let ticketFk = 1;
await app.models.Ticket.getVolume(ticketFk) await app.models.Ticket.getVolume(ticketFk)
.then(response => { .then(response => {
expect(response[0].m3).toEqual(1.09); expect(response[0].volume).toEqual(1.09);
}); });
}); });
}); });

View File

@ -1,116 +1,7 @@
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
const models = app.models; const models = app.models;
// 2301 Failing tests describe('ticket setDeleted()', () => {
xdescribe('ticket deleted()', () => {
let ticket;
let sale;
let deletedClaim;
beforeAll(async done => {
let originalTicket = await models.Ticket.findOne({where: {id: 16}});
originalTicket.id = null;
ticket = await models.Ticket.create(originalTicket);
sale = await models.Sale.create({
ticketFk: ticket.id,
itemFk: 4,
concept: 'Melee weapon',
quantity: 10
});
await models.ItemShelvingSale.create({
itemShelvingFk: 1,
saleFk: sale.id,
quantity: 10,
userFk: 106
});
done();
});
afterAll(async done => {
const ticketId = 16;
const stowawayTicketId = 17;
const ctx = {
req: {
accessToken: {userId: 106},
headers: {
origin: 'http://localhost:5000'
},
__: () => {}
}
};
await models.Ticket.destroyById(ticket.id);
const stowaway = await models.Stowaway.findOne({
where: {
id: stowawayTicketId,
shipFk: ticketId
}
});
await stowaway.destroy();
await models.Claim.create(deletedClaim);
await models.TicketTracking.changeState(ctx, {
ticketFk: ticketId,
code: 'OK'
});
await models.TicketTracking.changeState(ctx, {
ticketFk: stowawayTicketId,
code: 'OK'
});
const orgTicket = await models.Ticket.findById(ticketId);
await orgTicket.updateAttribute('isDeleted', false);
done();
});
it('should make sure the ticket is not deleted yet', async() => {
expect(ticket.isDeleted).toEqual(false);
});
it('should make sure the ticket sale has an item shelving', async() => {
const sales = await models.Sale.find({
include: {relation: 'itemShelvingSale'},
where: {ticketFk: ticket.id}
});
const hasItemShelvingSales = sales.some(sale => {
return sale.itemShelvingSale();
});
expect(hasItemShelvingSales).toEqual(true);
});
it('should set a ticket to deleted and remove all item shelvings', async() => {
const salesAssistantId = 21;
const ctx = {
req: {
accessToken: {userId: salesAssistantId},
headers: {
origin: 'http://localhost:5000'
},
__: () => {}
}
};
await app.models.Ticket.setDeleted(ctx, ticket.id);
let deletedTicket = await app.models.Ticket.findOne({
where: {id: ticket.id},
fields: ['isDeleted']
});
expect(deletedTicket.isDeleted).toEqual(true);
});
it('should not have any item shelving', async() => {
const sales = await models.Sale.find({
include: {relation: 'itemShelvingSale'},
where: {ticketFk: ticket.id}
});
const hasItemShelvingSales = sales.some(sale => {
return sale.itemShelvingSale();
});
expect(hasItemShelvingSales).toEqual(false);
});
it('should throw an error if the given ticket has a claim', async() => { it('should throw an error if the given ticket has a claim', async() => {
const ticketId = 16; const ticketId = 16;
const ctx = { const ctx = {
@ -134,13 +25,11 @@ xdescribe('ticket deleted()', () => {
expect(error.message).toEqual('You must delete the claim id %d first'); expect(error.message).toEqual('You must delete the claim id %d first');
}); });
it('should delete the ticket and change the state to "FIXING" to the stowaway ticket', async() => { it('should delete the ticket, remove the stowaway link and change the stowaway ticket state to "FIXING" and get ride of the itemshelving', async() => {
const ticketId = 16; const employeeUser = 110;
const claimIdToRemove = 2;
const stowawayTicketId = 17;
const ctx = { const ctx = {
req: { req: {
accessToken: {userId: 106}, accessToken: {userId: employeeUser},
headers: { headers: {
origin: 'http://localhost:5000' origin: 'http://localhost:5000'
}, },
@ -148,20 +37,66 @@ xdescribe('ticket deleted()', () => {
} }
}; };
await app.models.Stowaway.rawSql(` let sampleTicket = await models.Ticket.findById(12);
let sampleStowaway = await models.Ticket.findById(13);
sampleTicket.id = undefined;
let shipTicket = await models.Ticket.create(sampleTicket);
sampleStowaway.id = undefined;
let stowawayTicket = await models.Ticket.create(sampleStowaway);
await models.Stowaway.rawSql(`
INSERT INTO vn.stowaway(id, shipFk) INSERT INTO vn.stowaway(id, shipFk)
VALUES (?, ?)`, [stowawayTicketId, ticketId]); VALUES (?, ?)`, [stowawayTicket.id, shipTicket.id]);
deletedClaim = await app.models.Claim.findById(claimIdToRemove); const boardingState = await models.State.findOne({
await app.models.Claim.destroyById(claimIdToRemove);
await app.models.Ticket.setDeleted(ctx, ticketId);
const stowawayTicket = await app.models.TicketState.findOne({
where: { where: {
ticketFk: stowawayTicketId code: 'BOARDING'
}
});
await models.TicketTracking.create({
ticketFk: stowawayTicket.id,
stateFk: boardingState.id,
workerFk: ctx.req.accessToken.userId
});
const okState = await models.State.findOne({
where: {
code: 'OK'
}
});
await models.TicketTracking.create({
ticketFk: shipTicket.id,
stateFk: okState.id,
workerFk: ctx.req.accessToken.userId
});
let stowawayTicketState = await models.TicketState.findOne({
where: {
ticketFk: stowawayTicket.id
} }
}); });
expect(stowawayTicket.code).toEqual('FIXING'); let stowaway = await models.Stowaway.findById(shipTicket.id);
expect(stowaway).toBeDefined();
expect(stowawayTicketState.code).toEqual('BOARDING');
await models.Ticket.setDeleted(ctx, shipTicket.id);
stowawayTicketState = await models.TicketState.findOne({
where: {
ticketFk: stowawayTicket.id
}
});
stowaway = await models.Stowaway.findById(shipTicket.id);
expect(stowaway).toBeNull();
expect(stowawayTicketState.code).toEqual('FIXING');
await shipTicket.destroy();
await stowawayTicket.destroy();
}); });
}); });

View File

@ -101,14 +101,14 @@ class Controller extends Descriptor {
} }
showDeliveryNote() { showDeliveryNote() {
this.showReport('delivery-note', { this.vnReport.show('delivery-note', {
recipientId: this.ticket.client.id, recipientId: this.ticket.client.id,
ticketId: this.id, ticketId: this.id,
}); });
} }
sendDeliveryNote() { sendDeliveryNote() {
return this.sendEmail('delivery-note', { return this.vnEmail.send('delivery-note', {
recipientId: this.ticket.client.id, recipientId: this.ticket.client.id,
recipient: this.ticket.client.email, recipient: this.ticket.client.email,
ticketId: this.id ticketId: this.id

View File

@ -64,21 +64,22 @@ describe('Ticket Component vnTicketDescriptor', () => {
describe('showDeliveryNote()', () => { describe('showDeliveryNote()', () => {
it('should open a new window showing a delivery note PDF document', () => { it('should open a new window showing a delivery note PDF document', () => {
jest.spyOn(controller, 'showReport'); jest.spyOn(controller.vnReport, 'show');
window.open = jasmine.createSpy('open');
const params = { const params = {
clientId: ticket.client.id, clientId: ticket.client.id,
ticketId: ticket.id ticketId: ticket.id
}; };
controller.showDeliveryNote(); controller.showDeliveryNote();
expect(controller.showReport).toHaveBeenCalledWith('delivery-note', params); expect(controller.vnReport.show).toHaveBeenCalledWith('delivery-note', params);
}); });
}); });
describe('sendDeliveryNote()', () => { describe('sendDeliveryNote()', () => {
it('should make a query and call vnApp.showMessage()', () => { it('should make a query and call vnApp.showMessage()', () => {
jest.spyOn(controller, 'sendEmail'); jest.spyOn(controller.vnEmail, 'send');
const params = { const params = {
recipient: ticket.client.email, recipient: ticket.client.email,
@ -87,7 +88,7 @@ describe('Ticket Component vnTicketDescriptor', () => {
}; };
controller.sendDeliveryNote(); controller.sendDeliveryNote();
expect(controller.sendEmail).toHaveBeenCalledWith('delivery-note', params); expect(controller.vnEmail.send).toHaveBeenCalledWith('delivery-note', params);
}); });
}); });

View File

@ -52,11 +52,10 @@
</span> </span>
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<a target="_blank" <span title="{{'Download file' | translate}}" class="link"
title="{{'Download file' | translate}}" ng-click="$ctrl.downloadFile(document.dmsFk)">
href="api/dms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">
{{::document.dms.file}} {{::document.dms.file}}
</a> </span>
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<span class="link" <span class="link"
@ -67,13 +66,10 @@
{{::document.dms.created | date:'dd/MM/yyyy HH:mm'}} {{::document.dms.created | date:'dd/MM/yyyy HH:mm'}}
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<a target="_blank" <vn-icon-button title="{{'Download file' | translate}}"
href="api/dms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">
<vn-icon-button
icon="cloud_download" icon="cloud_download"
title="{{'Download file' | translate}}"> ng-click="$ctrl.downloadFile(document.dmsFk)">
</vn-icon-button> </vn-icon-button>
</a>
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<vn-icon-button icon="edit" <vn-icon-button icon="edit"

View File

@ -3,8 +3,9 @@ import Section from 'salix/components/section';
import './style.scss'; import './style.scss';
class Controller extends Section { class Controller extends Section {
constructor($element, $) { constructor($element, $, vnFile) {
super($element, $); super($element, $);
this.vnFile = vnFile;
this.filter = { this.filter = {
include: { include: {
relation: 'dms', relation: 'dms',
@ -50,7 +51,13 @@ class Controller extends Section {
this.vnApp.showSuccess(this.$t('Data saved!')); this.vnApp.showSuccess(this.$t('Data saved!'));
}); });
} }
downloadFile(dmsId) {
this.vnFile.download(`api/dms/${dmsId}/downloadFile`);
} }
}
Controller.$inject = ['$element', '$scope', 'vnFile'];
ngModule.component('vnTicketDmsIndex', { ngModule.component('vnTicketDmsIndex', {
template: require('./index.html'), template: require('./index.html'),

View File

@ -50,7 +50,7 @@
sub-name="::sale.item.subName"/> sub-name="::sale.item.subName"/>
</vn-td> </vn-td>
<vn-td number>{{::sale.quantity}}</vn-td> <vn-td number>{{::sale.quantity}}</vn-td>
<vn-td number>{{::sale.volume.m3 | number:3}}</vn-td> <vn-td number>{{::sale.saleVolume.volume | number:3}}</vn-td>
</vn-tr> </vn-tr>
</vn-tbody> </vn-tbody>
</vn-table> </vn-table>

View File

@ -39,7 +39,7 @@ class Controller extends Section {
this.sales.forEach(sale => { this.sales.forEach(sale => {
this.volumes.forEach(volume => { this.volumes.forEach(volume => {
if (sale.id === volume.saleFk) if (sale.id === volume.saleFk)
sale.volume = volume; sale.saleVolume = volume;
}); });
}); });
} }

View File

@ -59,10 +59,10 @@ describe('ticket', () => {
it(`should apply volumes to the sales if sales and volumes properties are defined on controller`, () => { it(`should apply volumes to the sales if sales and volumes properties are defined on controller`, () => {
controller.sales = [{id: 1, name: 'Sale one'}, {id: 2, name: 'Sale two'}]; controller.sales = [{id: 1, name: 'Sale one'}, {id: 2, name: 'Sale two'}];
controller.volumes = [{saleFk: 1, m3: 0.012}, {saleFk: 2, m3: 0.015}]; controller.volumes = [{saleFk: 1, volume: 0.012}, {saleFk: 2, volume: 0.015}];
expect(controller.sales[0].volume.m3).toEqual(0.012); expect(controller.sales[0].saleVolume.volume).toEqual(0.012);
expect(controller.sales[1].volume.m3).toEqual(0.015); expect(controller.sales[1].saleVolume.volume).toEqual(0.015);
}); });
}); });

View File

@ -10,6 +10,7 @@
</vn-crud-model> </vn-crud-model>
<vn-portal slot="topbar"> <vn-portal slot="topbar">
<vn-searchbar <vn-searchbar
placeholder="Search by weekly ticket"
info="Search weekly ticket by id or client id" info="Search weekly ticket by id or client id"
auto-state="false" auto-state="false"
model="model"> model="model">

View File

@ -3,3 +3,4 @@ Weekly tickets: Tickets programados
You are going to delete this weekly ticket: Vas a eliminar este ticket programado You are going to delete this weekly ticket: Vas a eliminar este ticket programado
This ticket will be removed from weekly tickets! Continue anyway?: Este ticket se eliminará de tickets programados! ¿Continuar de todas formas? This ticket will be removed from weekly tickets! Continue anyway?: Este ticket se eliminará de tickets programados! ¿Continuar de todas formas?
Search weekly ticket by id or client id: Busca tickets programados por el identificador o el identificador del cliente Search weekly ticket by id or client id: Busca tickets programados por el identificador o el identificador del cliente
Search by weekly ticket: Buscar por tickets programados

View File

@ -29,13 +29,10 @@
<vn-td>{{::thermograph.warehouse.name}}</vn-td> <vn-td>{{::thermograph.warehouse.name}}</vn-td>
<vn-td>{{::thermograph.created | date: 'dd/MM/yyyy'}}</vn-td> <vn-td>{{::thermograph.created | date: 'dd/MM/yyyy'}}</vn-td>
<vn-td shrink> <vn-td shrink>
<a target="_blank" <vn-icon-button title="{{'Download file' | translate}}"
href="api/dms/{{::thermograph.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">
<vn-icon-button
icon="cloud_download" icon="cloud_download"
title="{{'Download file' | translate}}"> ng-click="$ctrl.downloadFile(thermograph.dmsFk)">
</vn-icon-button> </vn-icon-button>
</a>
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<vn-icon-button ui-sref="travel.card.thermograph.edit({thermographId: {{::thermograph.id}}})" <vn-icon-button ui-sref="travel.card.thermograph.edit({thermographId: {{::thermograph.id}}})"

View File

@ -3,7 +3,9 @@ import Section from 'salix/components/section';
import './style.scss'; import './style.scss';
class Controller extends Section { class Controller extends Section {
$onInit() { constructor($element, $, vnFile) {
super($element, $);
this.vnFile = vnFile;
this.filter = { this.filter = {
include: include:
{relation: 'warehouse', {relation: 'warehouse',
@ -29,7 +31,13 @@ class Controller extends Section {
this.thermographIndex = null; this.thermographIndex = null;
}); });
} }
downloadFile(dmsId) {
this.vnFile.download(`api/dms/${dmsId}/downloadFile`);
} }
}
Controller.$inject = ['$element', '$scope', 'vnFile'];
ngModule.component('vnTravelThermographIndex', { ngModule.component('vnTravelThermographIndex', {
template: require('./index.html'), template: require('./index.html'),

View File

@ -38,22 +38,19 @@
</span> </span>
</vn-td > </vn-td >
<vn-td shrink> <vn-td shrink>
<a target="_blank" <span title="{{'Download file' | translate}}" class="link"
title="{{'Download file' | translate}}" ng-click="$ctrl.downloadFile(document.dmsFk)">
href="api/workerDms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">{{::document.file}} {{::document.file}}
</a> </span>
</vn-td> </vn-td>
<vn-td> <vn-td>
{{::document.created | date:'dd/MM/yyyy HH:mm'}} {{::document.created | date:'dd/MM/yyyy HH:mm'}}
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<a target="_blank" <vn-icon-button title="{{'Download file' | translate}}"
href="api/workerDms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">
<vn-icon-button
icon="cloud_download" icon="cloud_download"
title="{{'Download file' | translate}}"> ng-click="$ctrl.downloadFile(document.dmsFk)">
</vn-icon-button> </vn-icon-button>
</a>
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<vn-icon-button ui-sref="worker.card.edit({dmsId: {{::document.dmsFk}}})" <vn-icon-button ui-sref="worker.card.edit({dmsId: {{::document.dmsFk}}})"

View File

@ -3,8 +3,9 @@ import Component from 'core/lib/component';
import './style.scss'; import './style.scss';
class Controller extends Component { class Controller extends Component {
constructor($element, $) { constructor($element, $, vnFile) {
super($element, $); super($element, $);
this.vnFile = vnFile;
this.filter = { this.filter = {
include: { include: {
relation: 'dms', relation: 'dms',
@ -51,7 +52,13 @@ class Controller extends Component {
this.vnApp.showSuccess(this.$t('Data saved!')); this.vnApp.showSuccess(this.$t('Data saved!'));
}); });
} }
downloadFile(dmsId) {
this.vnFile.download(`api/workerDms/${dmsId}/downloadFile`);
} }
}
Controller.$inject = ['$element', '$scope', 'vnFile'];
ngModule.component('vnWorkerDmsIndex', { ngModule.component('vnWorkerDmsIndex', {
template: require('./index.html'), template: require('./index.html'),

57
package-lock.json generated
View File

@ -3265,7 +3265,7 @@
}, },
"util": { "util": {
"version": "0.10.3", "version": "0.10.3",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -4081,7 +4081,7 @@
"base": { "base": {
"version": "0.11.2", "version": "0.11.2",
"resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
"integrity": "sha1-e95c7RRbbVUakNuH+DxVi060io8=", "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
"dev": true, "dev": true,
"requires": { "requires": {
"cache-base": "^1.0.1", "cache-base": "^1.0.1",
@ -4532,7 +4532,7 @@
}, },
"readable-stream": { "readable-stream": {
"version": "1.1.14", "version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -4544,7 +4544,7 @@
}, },
"string_decoder": { "string_decoder": {
"version": "0.10.31", "version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
"dev": true "dev": true
} }
@ -4604,7 +4604,7 @@
"cache-base": { "cache-base": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
"integrity": "sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=", "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"collection-visit": "^1.0.0", "collection-visit": "^1.0.0",
@ -4674,7 +4674,7 @@
}, },
"camelcase-keys": { "camelcase-keys": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
"integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -4813,7 +4813,7 @@
"class-utils": { "class-utils": {
"version": "0.3.6", "version": "0.3.6",
"resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
"integrity": "sha1-+TNprouafOAv1B+q0MqDAzGQxGM=", "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
"dev": true, "dev": true,
"requires": { "requires": {
"arr-union": "^3.1.0", "arr-union": "^3.1.0",
@ -5899,7 +5899,7 @@
"dot-prop": { "dot-prop": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
"integrity": "sha1-HxngwuGqDjJ5fEl5nyg3rGr2nFc=", "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==",
"requires": { "requires": {
"is-obj": "^1.0.0" "is-obj": "^1.0.0"
} }
@ -6856,7 +6856,7 @@
}, },
"file-loader": { "file-loader": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz", "resolved": "http://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz",
"integrity": "sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==", "integrity": "sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==",
"dev": true, "dev": true,
"requires": { "requires": {
@ -7813,7 +7813,7 @@
}, },
"string-width": { "string-width": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "resolved": "http://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -8023,7 +8023,7 @@
"global-modules": { "global-modules": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
"integrity": "sha1-bXcPDrUjrHgWTXK15xqIdyZcw+o=", "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
"dev": true, "dev": true,
"requires": { "requires": {
"global-prefix": "^1.0.1", "global-prefix": "^1.0.1",
@ -10201,7 +10201,7 @@
"is-plain-object": { "is-plain-object": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
"integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
"dev": true, "dev": true,
"requires": { "requires": {
"isobject": "^3.0.1" "isobject": "^3.0.1"
@ -10563,7 +10563,7 @@
"jasmine-spec-reporter": { "jasmine-spec-reporter": {
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz", "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz",
"integrity": "sha1-HWMq7ANBZwrTJPkrqEtLMrNeniI=", "integrity": "sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg==",
"dev": true, "dev": true,
"requires": { "requires": {
"colors": "1.1.2" "colors": "1.1.2"
@ -10740,7 +10740,8 @@
}, },
"yargs-parser": { "yargs-parser": {
"version": "13.1.1", "version": "13.1.1",
"resolved": "", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz",
"integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"camelcase": "^5.0.0", "camelcase": "^5.0.0",
@ -12568,7 +12569,7 @@
}, },
"meow": { "meow": {
"version": "3.7.0", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
"integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -13209,7 +13210,7 @@
"dependencies": { "dependencies": {
"semver": { "semver": {
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", "resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
"dev": true "dev": true
} }
@ -13359,7 +13360,7 @@
}, },
"chalk": { "chalk": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -13826,7 +13827,7 @@
}, },
"os-homedir": { "os-homedir": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"dev": true "dev": true
}, },
@ -14999,7 +15000,7 @@
"dependencies": { "dependencies": {
"jsesc": { "jsesc": {
"version": "0.5.0", "version": "0.5.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
"dev": true "dev": true
} }
@ -15386,7 +15387,7 @@
}, },
"safe-regex": { "safe-regex": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
"integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -15578,7 +15579,7 @@
"dependencies": { "dependencies": {
"source-map": { "source-map": {
"version": "0.4.4", "version": "0.4.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
"integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -15930,7 +15931,7 @@
"snapdragon-node": { "snapdragon-node": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
"integrity": "sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=", "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
"dev": true, "dev": true,
"requires": { "requires": {
"define-property": "^1.0.0", "define-property": "^1.0.0",
@ -15981,7 +15982,7 @@
"snapdragon-util": { "snapdragon-util": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
"integrity": "sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=", "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"kind-of": "^3.2.0" "kind-of": "^3.2.0"
@ -16265,7 +16266,7 @@
"split-string": { "split-string": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
"integrity": "sha1-fLCd2jqGWFcFxks5pkZgOGguj+I=", "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
"dev": true, "dev": true,
"requires": { "requires": {
"extend-shallow": "^3.0.0" "extend-shallow": "^3.0.0"
@ -17509,7 +17510,7 @@
"touch": { "touch": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
"integrity": "sha1-/jZfX3XsntTlaCXgu3bSSrdK+Ds=", "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
"dev": true, "dev": true,
"requires": { "requires": {
"nopt": "~1.0.10" "nopt": "~1.0.10"
@ -18753,7 +18754,7 @@
}, },
"globby": { "globby": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", "resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
"integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -18766,7 +18767,7 @@
"dependencies": { "dependencies": {
"pify": { "pify": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true "dev": true
} }
@ -19227,7 +19228,7 @@
}, },
"xmlbuilder": { "xmlbuilder": {
"version": "9.0.7", "version": "9.0.7",
"resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0="
}, },
"xmlcreate": { "xmlcreate": {

View File

@ -8,6 +8,9 @@
"type": "git", "type": "git",
"url": "https://gitea.verdnatura.es/verdnatura/salix" "url": "https://gitea.verdnatura.es/verdnatura/salix"
}, },
"engines": {
"node": ">=12"
},
"dependencies": { "dependencies": {
"compression": "^1.7.3", "compression": "^1.7.3",
"fs-extra": "^5.0.0", "fs-extra": "^5.0.0",

View File

@ -46,6 +46,7 @@
} }
.privacy { .privacy {
text-align: center;
padding: 20px 0; padding: 20px 0;
font-size: 10px; font-size: 10px;
font-weight: 100 font-weight: 100

View File

@ -1,5 +1,10 @@
header {
text-align: center
}
header .logo { header .logo {
margin-bottom: 15px; margin-top: 25px;
margin-bottom: 25px
} }
header .logo img { header .logo img {

View File

@ -2,5 +2,6 @@ const Vue = require('vue');
const strftime = require('strftime'); const strftime = require('strftime');
Vue.filter('date', function(value, specifiers = '%d-%m-%Y') { Vue.filter('date', function(value, specifiers = '%d-%m-%Y') {
if (!(value instanceof Date)) value = new Date(value);
return strftime(specifiers, value); return strftime(specifiers, value);
}); });

View File

@ -1,6 +1,6 @@
[ [
{ {
"filename": "campaing-metrics", "filename": "campaign-metrics.pdf",
"component": "campaign-metrics" "component": "campaign-metrics"
} }
] ]

View File

@ -25,7 +25,7 @@
<div class="grid-block vn-pa-lg"> <div class="grid-block vn-pa-lg">
<h1>{{ $t('title') }}</h1> <h1>{{ $t('title') }}</h1>
<p>{{$t('dear')}},</p> <p>{{$t('dear')}},</p>
<p>{{$t('description')}}</p> <p v-html="$t('description', [minDate, maxDate])"></p>
</div> </div>
</div> </div>
<!-- Footer block --> <!-- Footer block -->

View File

@ -4,7 +4,17 @@ const emailFooter = new Component('email-footer');
module.exports = { module.exports = {
name: 'campaign-metrics', name: 'campaign-metrics',
created() {
this.filters = this.$options.filters;
},
computed: {
minDate: function() {
return this.filters.date(this.from, '%d-%m-%Y');
},
maxDate: function() {
return this.filters.date(this.to, '%d-%m-%Y');
}
},
components: { components: {
'email-header': emailHeader.build(), 'email-header': emailHeader.build(),
'email-footer': emailFooter.build() 'email-footer': emailFooter.build()

View File

@ -1,7 +1,8 @@
subject: Informe consumo campaña subject: Informe de consumo
title: Informe consumo campaña title: Informe de consumo
dear: Estimado cliente dear: Estimado cliente
description: Con motivo de esta próxima campaña, me complace description: Tal y como nos ha solicitado nos complace
relacionarle a continuación el consumo que nos consta en su cuenta para las relacionarle a continuación el consumo que nos consta en su cuenta para las
mismas fechas del año pasado. Espero le sea de utilidad para preparar su pedido. fechas comprendidas entre <strong>{0}</strong> y <strong>{1}</strong>.
Espero le sea de utilidad para preparar su pedido.<br/><br/>
Al mismo tiempo aprovecho la ocasión para saludarle cordialmente. Al mismo tiempo aprovecho la ocasión para saludarle cordialmente.

View File

@ -6,9 +6,6 @@ const reportFooter = new Component('report-footer');
module.exports = { module.exports = {
name: 'campaign-metrics', name: 'campaign-metrics',
async serverPrefetch() { async serverPrefetch() {
this.to = new Date(this.to);
this.from = new Date(this.from);
this.client = await this.fetchClient(this.recipientId); this.client = await this.fetchClient(this.recipientId);
this.sales = await this.fetchSales(this.recipientId, this.from, this.to); this.sales = await this.fetchSales(this.recipientId, this.from, this.to);
@ -54,7 +51,7 @@ module.exports = {
t.clientFk = ? AND it.isPackaging = FALSE t.clientFk = ? AND it.isPackaging = FALSE
AND DATE(t.shipped) BETWEEN ? AND ? AND DATE(t.shipped) BETWEEN ? AND ?
GROUP BY s.itemFk GROUP BY s.itemFk
ORDER BY i.typeFk , i.name , i.size`, [clientId, from, to]); ORDER BY i.typeFk , i.name`, [clientId, from, to]);
}, },
}, },
components: { components: {
@ -66,12 +63,10 @@ module.exports = {
required: true required: true
}, },
from: { from: {
required: true, required: true
type: Date
}, },
to: { to: {
required: true, required: true
type: Date
} }
} }
}; };

View File

@ -1,4 +1,4 @@
title: Consumo de campaña title: Consumo
Client: Cliente Client: Cliente
clientData: Datos del cliente clientData: Datos del cliente
dated: Fecha dated: Fecha