Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2290-client_consumption

This commit is contained in:
Joan Sanchez 2020-06-15 09:00:24 +02:00
commit ed83fd5f73
54 changed files with 731 additions and 636 deletions

2
Jenkinsfile vendored
View File

@ -68,7 +68,7 @@ pipeline {
stage('Backend') {
steps {
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.
* Visual Studio Code
* Node.js = 12.17.0 LTS
* 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.
```
# sudo npm install -g jest gulp-cli nodemon
# sudo npm install -g jest gulp-cli
```
## Linux Only Prerequisites
@ -65,6 +59,15 @@ For end-to-end tests run from project's root.
$ 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
* [angularjs](https://angularjs.org/)
@ -75,4 +78,4 @@ $ gulp e2e
* [gulp.js](https://gulpjs.com/)
* [jest](https://jestjs.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) {
if (ctx.currentInstance && ctx.currentInstance.id && ctx.data && 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', {

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

@ -10,7 +10,7 @@ export default {
ticketsButton: '.modules-menu [ui-sref="ticket.index"]',
invoiceOutButton: '.modules-menu [ui-sref="invoiceOut.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',
userLocalWarehouse: '.user-popover vn-autocomplete[ng-model="$ctrl.localWarehouseFk"]',
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)',
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',
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: {
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', () => {
let browser;
let page;
let nextMonth = new Date();
nextMonth.setMonth(nextMonth.getMonth() + 1);
let stowawayTicketId;
beforeAll(async() => {
browser = await getBrowser();
@ -21,13 +24,9 @@ describe('Ticket create path', () => {
});
it('should succeed to create a ticket', async() => {
const nextMonth = new Date();
nextMonth.setMonth(nextMonth.getMonth() + 1);
await page.autocompleteSearch(selectors.createTicketView.client, 'Tony Stark');
await page.autocompleteSearch(selectors.createTicketView.address, 'Tony Stark');
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.warehouse, 'Warehouse Two');
await page.autocompleteSearch(selectors.createTicketView.agency, 'Silla247');
await page.waitToClick(selectors.createTicketView.createButton);
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() => {
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()">
<vn-textfield
class="dense standout"
placeholder="{{::'Search by' | translate: {module: $ctrl.baseState} }}"
placeholder="{{::$ctrl.placeholder | translate}}"
ng-model="$ctrl.searchString">
<prepend>
<vn-icon

View File

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

@ -7,6 +7,13 @@ import './quick-link';
* Small card with basing entity information and actions.
*/
export default class Descriptor extends Component {
constructor($element, $, vnReport, vnEmail) {
super($element, $);
this.vnReport = vnReport;
this.vnEmail = vnEmail;
}
$postLink() {
const content = this.element.querySelector('vn-descriptor-content');
if (!content) throw new Error('Directive vnDescriptorContent not found');
@ -74,35 +81,10 @@ export default class Descriptor extends Component {
return this.$http.get(url, options)
.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}`);
}
/**
* 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!')));
}
}
Descriptor.$inject = ['$element', '$scope', 'vnReport', 'vnEmail'];
ngModule.vnComponent('vnDescriptor', {
controller: Descriptor,
bindings: {

View File

@ -1,11 +1,11 @@
require('require-yaml');
const gulp = require('gulp');
const exec = require('child_process').exec;
const PluginError = require('plugin-error');
const argv = require('minimist')(process.argv.slice(2));
const log = require('fancy-log');
const request = require('request');
const e2eConfig = require('./e2e/helpers/config.js');
const Docker = require('./db/docker.js');
// Configuration
@ -18,10 +18,6 @@ let langs = ['es', 'en'];
let srcDir = './front';
let modulesDir = './modules';
let buildDir = 'dist';
let containerId = 'salix-db';
let dataSources = require('./loopback/server/datasources.json');
let dbConf = dataSources.vn;
let backSources = [
'!node_modules',
@ -63,7 +59,7 @@ function backWatch(done) {
done: done
});
}
backWatch.description = `Starts backend in waching mode`;
backWatch.description = `Starts backend in watcher mode`;
const back = gulp.series(dockerStart, backWatch);
back.description = `Starts backend and database service`;
@ -73,78 +69,64 @@ defaultTask.description = `Starts all application services`;
// Backend tests
async function backTestOnce() {
let bootOptions;
async function backTestOnce(done) {
let err;
let dataSources = require('./loopback/server/datasources.json');
if (argv['random'])
bootOptions = {dataSources};
const container = new Docker();
await container.run(argv.ci);
let app = require(`./loopback/server/server`);
app.boot(bootOptions);
dataSources = JSON.parse(JSON.stringify(dataSources));
await new Promise((resolve, reject) => {
const jasmine = require('gulp-jasmine');
let options = {
errorOnFail: false,
config: {
random: false
}
};
if (argv.junit) {
const reporters = require('jasmine-reporters');
options.reporter = new reporters.JUnitXmlReporter();
}
let backSpecFiles = [
'back/**/*.spec.js',
'loopback/**/*.spec.js',
'modules/*/back/**/*.spec.js'
];
gulp.src(backSpecFiles)
.pipe(jasmine(options))
.on('end', resolve)
.on('error', reject)
.resume();
Object.assign(dataSources.vn, {
host: container.dbConf.host,
port: container.dbConf.port
});
let bootOptions = {dataSources};
let app = require(`./loopback/server/server`);
try {
app.boot(bootOptions);
await new Promise((resolve, reject) => {
const jasmine = require('gulp-jasmine');
let options = {
errorOnFail: false,
config: {
random: false
}
};
if (argv.ci) {
const reporters = require('jasmine-reporters');
options.reporter = new reporters.JUnitXmlReporter();
}
let backSpecFiles = [
'back/**/*.spec.js',
'loopback/**/*.spec.js',
'modules/*/back/**/*.spec.js'
];
gulp.src(backSpecFiles)
.pipe(jasmine(options))
.on('end', resolve)
.on('error', reject)
.resume();
});
} catch (e) {
err = e;
}
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`;
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`;
backTestOnce.description = `Runs the backend tests once using a random container, can receive --ci arg to save reports on a xml file`;
function backTest(done) {
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() {
const milliseconds = 250;
@ -231,24 +220,6 @@ async function backendStatus() {
}
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() {
const install = require('gulp-install');
const print = require('gulp-print');
@ -414,156 +385,17 @@ function watch(done) {
watch.description = `Watches for changes in routes and locale files`;
// 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() {
let state;
try {
let result = await execP(`docker inspect -f "{{json .State}}" ${containerId}`);
state = JSON.parse(result.stdout);
} catch (err) {
return await docker();
}
switch (state.Status) {
case 'running':
return;
case 'exited':
await execP(`docker start ${containerId}`);
await dockerWait();
return;
default:
throw new Error(`Unknown docker status: ${state.Status}`);
}
const container = new Docker('salix-db');
await container.start();
}
dockerStart.description = `Starts the database container`;
dockerStart.description = `Starts the salix-db 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
});
}
});
});
async function docker() {
const container = new Docker('salix-db');
await container.run();
}
docker.description = `Runs the salix-db container`;
module.exports = {
default: defaultTask,
@ -572,13 +404,8 @@ module.exports = {
backOnly,
backWatch,
backTestOnce,
backTestDockerOnce,
backTest,
backTestDocker,
e2e,
e2eSingleRun,
smokes,
smokesOnly,
i,
install,
build,
@ -590,7 +417,5 @@ module.exports = {
localesRoutes,
watch,
docker,
dockerStart,
dockerWait,
backendStatus,
};

View File

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

View File

@ -20,21 +20,22 @@ describe('Item Component vnClaimDescriptor', () => {
describe('showPickupOrder()', () => {
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 = {
recipientId: claim.clientFk,
claimId: claim.id
};
controller.showPickupOrder();
expect(controller.showReport).toHaveBeenCalledWith('claim-pickup-order', params);
expect(controller.vnReport.show).toHaveBeenCalledWith('claim-pickup-order', params);
});
});
describe('sendPickupOrder()', () => {
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 = {
recipient: claim.client.email,
@ -43,7 +44,7 @@ describe('Item Component vnClaimDescriptor', () => {
};
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 class="photo" ng-repeat="photo in $ctrl.photos">
<section class="image vn-shadow" on-error-src
ng-style="{'background': 'url(/api/dms/' + photo.dmsFk + '/downloadFile?access_token=' + $ctrl.vnToken.token + ')'}"
zoom-image="/api/dms/{{::photo.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">
ng-style="{'background': 'url(' + $ctrl.getImagePath(photo.dmsFk) + ')'}"
zoom-image="{{$ctrl.getImagePath(photo.dmsFk)}}">
</section>
<section class="actions">
<vn-button

View File

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

View File

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

View File

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

View File

@ -218,6 +218,36 @@ module.exports = Self => {
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 => {
if (ctx.isNewInstance) return;

View File

@ -6,6 +6,15 @@
data="$ctrl.addresses"
auto-load="true">
</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
model="model"
class="vn-w-md">
@ -35,7 +44,7 @@
</vn-none>
<vn-one
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">{{::address.city}}, {{::address.province.name}}</div>
<div class="ellipsize">

View File

@ -68,6 +68,15 @@ class Controller extends Section {
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'];

View File

@ -67,5 +67,19 @@ describe('Client', () => {
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
Set as default: Establecer 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
Enabled: Activo
Is equalizated: Recargo de equivalencia

View File

@ -41,7 +41,7 @@ class Controller extends Descriptor {
}
onConsumerReportAccept() {
this.showReport('campaign-metrics', {
this.vnReport.show('campaign-metrics', {
recipientId: this.id,
from: this.from,
to: this.to,

View File

@ -54,11 +54,10 @@
</span>
</vn-td>
<vn-td shrink>
<a target="_blank"
title="{{'Download file' | translate}}"
href="api/dms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">
<span title="{{'Download file' | translate}}" class="link"
ng-click="$ctrl.downloadFile(document.dmsFk)">
{{::document.dms.file}}
</a>
</span>
</vn-td>
<vn-td shrink>
<span class="link"
@ -69,13 +68,10 @@
{{::document.dms.created | date:'dd/MM/yyyy HH:mm'}}
</vn-td>
<vn-td shrink>
<a target="_blank"
href="api/dms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">
<vn-icon-button
icon="cloud_download"
title="{{'Download file' | translate}}">
</vn-icon-button>
</a>
<vn-icon-button title="{{'Download file' | translate}}"
icon="cloud_download"
ng-click="$ctrl.downloadFile(document.dmsFk)">
</vn-icon-button>
</vn-td>
<vn-td shrink>
<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';
class Controller extends Section {
constructor($element, $) {
super($element, $);
constructor($element, $, vnFile) {
super($element, $, vnFile);
this.vnFile = vnFile;
this.filter = {
include: {
relation: 'dms',
@ -49,9 +50,13 @@ class Controller extends Section {
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', {
template: require('./index.html'),

View File

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

View File

@ -12,15 +12,16 @@ describe('Entry Component vnEntryDescriptor', () => {
describe('showEntryReport()', () => {
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 = {
clientId: controller.vnConfig.storage.currentUserWorkerId,
entryId: entry.id
};
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() {
this.showReport('driver-route', {
this.vnReport.show('driver-route', {
routeId: this.id
});
}
sendRouteReport() {
const workerUser = this.route.worker.user;
this.sendEmail('driver-route', {
this.vnEmail.send('driver-route', {
recipient: workerUser.emailUser.email,
routeId: this.id
});

View File

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

View File

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

View File

@ -43,18 +43,6 @@ module.exports = Self => {
if (hasItemShelvingSales && !isSalesAssistant)
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
const claimOfATicket = await models.Claim.findOne({where: {ticketFk: id}});
if (claimOfATicket)
@ -69,10 +57,23 @@ module.exports = Self => {
if (hasPurchaseRequests)
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
const ticketGreuges = await models.Greuge.find({where: {ticketFk: id}});
const ownGreuges = ticketGreuges.every(greuge => {
return greuge.ticketFk = id;
return greuge.ticketFk == id;
});
if (ownGreuges) {
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;
if (ticket.stowaway())
otherTicketId = ticket.stowaway().shipFk;
@ -112,6 +113,7 @@ module.exports = Self => {
otherTicketId = ticket.ship().id;
if (otherTicketId) {
await models.Ticket.deleteStowaway(ctx, otherTicketId);
await models.TicketTracking.changeState(ctx, {
ticketFk: otherTicketId,
code: 'FIXING'

View File

@ -71,4 +71,20 @@ describe('ticket filter()', () => {
expect(secondRow.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;
await app.models.Ticket.getVolume(ticketFk)
.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 models = app.models;
// 2301 Failing tests
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);
});
describe('ticket setDeleted()', () => {
it('should throw an error if the given ticket has a claim', async() => {
const ticketId = 16;
const ctx = {
@ -134,13 +25,11 @@ xdescribe('ticket deleted()', () => {
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() => {
const ticketId = 16;
const claimIdToRemove = 2;
const stowawayTicketId = 17;
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 employeeUser = 110;
const ctx = {
req: {
accessToken: {userId: 106},
accessToken: {userId: employeeUser},
headers: {
origin: 'http://localhost:5000'
},
@ -148,20 +37,66 @@ xdescribe('ticket deleted()', () => {
}
};
await app.models.Stowaway.rawSql(`
INSERT INTO vn.stowaway(id, shipFk)
VALUES (?, ?)`, [stowawayTicketId, ticketId]);
let sampleTicket = await models.Ticket.findById(12);
let sampleStowaway = await models.Ticket.findById(13);
deletedClaim = await app.models.Claim.findById(claimIdToRemove);
await app.models.Claim.destroyById(claimIdToRemove);
await app.models.Ticket.setDeleted(ctx, ticketId);
sampleTicket.id = undefined;
let shipTicket = await models.Ticket.create(sampleTicket);
const stowawayTicket = await app.models.TicketState.findOne({
sampleStowaway.id = undefined;
let stowawayTicket = await models.Ticket.create(sampleStowaway);
await models.Stowaway.rawSql(`
INSERT INTO vn.stowaway(id, shipFk)
VALUES (?, ?)`, [stowawayTicket.id, shipTicket.id]);
const boardingState = await models.State.findOne({
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() {
this.showReport('delivery-note', {
this.vnReport.show('delivery-note', {
recipientId: this.ticket.client.id,
ticketId: this.id,
});
}
sendDeliveryNote() {
return this.sendEmail('delivery-note', {
return this.vnEmail.send('delivery-note', {
recipientId: this.ticket.client.id,
recipient: this.ticket.client.email,
ticketId: this.id

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@
sub-name="::sale.item.subName"/>
</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-tbody>
</vn-table>

View File

@ -39,7 +39,7 @@ class Controller extends Section {
this.sales.forEach(sale => {
this.volumes.forEach(volume => {
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`, () => {
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[1].volume.m3).toEqual(0.015);
expect(controller.sales[0].saleVolume.volume).toEqual(0.012);
expect(controller.sales[1].saleVolume.volume).toEqual(0.015);
});
});

View File

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

View File

@ -2,4 +2,5 @@ Ticket ID: ID Ticket
Weekly tickets: Tickets programados
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?
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.created | date: 'dd/MM/yyyy'}}</vn-td>
<vn-td shrink>
<a target="_blank"
href="api/dms/{{::thermograph.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">
<vn-icon-button
icon="cloud_download"
title="{{'Download file' | translate}}">
</vn-icon-button>
</a>
<vn-icon-button title="{{'Download file' | translate}}"
icon="cloud_download"
ng-click="$ctrl.downloadFile(thermograph.dmsFk)">
</vn-icon-button>
</vn-td>
<vn-td shrink>
<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';
class Controller extends Section {
$onInit() {
constructor($element, $, vnFile) {
super($element, $);
this.vnFile = vnFile;
this.filter = {
include:
{relation: 'warehouse',
@ -29,8 +31,14 @@ class Controller extends Section {
this.thermographIndex = null;
});
}
downloadFile(dmsId) {
this.vnFile.download(`api/dms/${dmsId}/downloadFile`);
}
}
Controller.$inject = ['$element', '$scope', 'vnFile'];
ngModule.component('vnTravelThermographIndex', {
template: require('./index.html'),
controller: Controller,

View File

@ -38,22 +38,19 @@
</span>
</vn-td >
<vn-td shrink>
<a target="_blank"
title="{{'Download file' | translate}}"
href="api/workerDms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">{{::document.file}}
</a>
<span title="{{'Download file' | translate}}" class="link"
ng-click="$ctrl.downloadFile(document.dmsFk)">
{{::document.file}}
</span>
</vn-td>
<vn-td>
{{::document.created | date:'dd/MM/yyyy HH:mm'}}
</vn-td>
<vn-td shrink>
<a target="_blank"
href="api/workerDms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">
<vn-icon-button
icon="cloud_download"
title="{{'Download file' | translate}}">
</vn-icon-button>
</a>
<vn-icon-button title="{{'Download file' | translate}}"
icon="cloud_download"
ng-click="$ctrl.downloadFile(document.dmsFk)">
</vn-icon-button>
</vn-td>
<vn-td shrink>
<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';
class Controller extends Component {
constructor($element, $) {
constructor($element, $, vnFile) {
super($element, $);
this.vnFile = vnFile;
this.filter = {
include: {
relation: 'dms',
@ -51,8 +52,14 @@ class Controller extends Component {
this.vnApp.showSuccess(this.$t('Data saved!'));
});
}
downloadFile(dmsId) {
this.vnFile.download(`api/workerDms/${dmsId}/downloadFile`);
}
}
Controller.$inject = ['$element', '$scope', 'vnFile'];
ngModule.component('vnWorkerDmsIndex', {
template: require('./index.html'),
controller: Controller,

57
package-lock.json generated
View File

@ -3265,7 +3265,7 @@
},
"util": {
"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=",
"dev": true,
"requires": {
@ -4081,7 +4081,7 @@
"base": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
"integrity": "sha1-e95c7RRbbVUakNuH+DxVi060io8=",
"integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
"dev": true,
"requires": {
"cache-base": "^1.0.1",
@ -4532,7 +4532,7 @@
},
"readable-stream": {
"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=",
"dev": true,
"requires": {
@ -4544,7 +4544,7 @@
},
"string_decoder": {
"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=",
"dev": true
}
@ -4604,7 +4604,7 @@
"cache-base": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
"integrity": "sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=",
"integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
"dev": true,
"requires": {
"collection-visit": "^1.0.0",
@ -4674,7 +4674,7 @@
},
"camelcase-keys": {
"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=",
"dev": true,
"requires": {
@ -4813,7 +4813,7 @@
"class-utils": {
"version": "0.3.6",
"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,
"requires": {
"arr-union": "^3.1.0",
@ -5899,7 +5899,7 @@
"dot-prop": {
"version": "4.2.0",
"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": {
"is-obj": "^1.0.0"
}
@ -6856,7 +6856,7 @@
},
"file-loader": {
"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==",
"dev": true,
"requires": {
@ -7813,7 +7813,7 @@
},
"string-width": {
"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=",
"dev": true,
"requires": {
@ -8023,7 +8023,7 @@
"global-modules": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
"integrity": "sha1-bXcPDrUjrHgWTXK15xqIdyZcw+o=",
"integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
"dev": true,
"requires": {
"global-prefix": "^1.0.1",
@ -10201,7 +10201,7 @@
"is-plain-object": {
"version": "2.0.4",
"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,
"requires": {
"isobject": "^3.0.1"
@ -10563,7 +10563,7 @@
"jasmine-spec-reporter": {
"version": "4.2.1",
"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,
"requires": {
"colors": "1.1.2"
@ -10740,7 +10740,8 @@
},
"yargs-parser": {
"version": "13.1.1",
"resolved": "",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz",
"integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
@ -12568,7 +12569,7 @@
},
"meow": {
"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=",
"dev": true,
"requires": {
@ -13209,7 +13210,7 @@
"dependencies": {
"semver": {
"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=",
"dev": true
}
@ -13359,7 +13360,7 @@
},
"chalk": {
"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=",
"dev": true,
"requires": {
@ -13826,7 +13827,7 @@
},
"os-homedir": {
"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=",
"dev": true
},
@ -14999,7 +15000,7 @@
"dependencies": {
"jsesc": {
"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=",
"dev": true
}
@ -15386,7 +15387,7 @@
},
"safe-regex": {
"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=",
"dev": true,
"requires": {
@ -15578,7 +15579,7 @@
"dependencies": {
"source-map": {
"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=",
"dev": true,
"requires": {
@ -15930,7 +15931,7 @@
"snapdragon-node": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
"integrity": "sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=",
"integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
"dev": true,
"requires": {
"define-property": "^1.0.0",
@ -15981,7 +15982,7 @@
"snapdragon-util": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
"integrity": "sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=",
"integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
"dev": true,
"requires": {
"kind-of": "^3.2.0"
@ -16265,7 +16266,7 @@
"split-string": {
"version": "3.1.0",
"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,
"requires": {
"extend-shallow": "^3.0.0"
@ -17509,7 +17510,7 @@
"touch": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
"integrity": "sha1-/jZfX3XsntTlaCXgu3bSSrdK+Ds=",
"integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
"dev": true,
"requires": {
"nopt": "~1.0.10"
@ -18753,7 +18754,7 @@
},
"globby": {
"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=",
"dev": true,
"requires": {
@ -18766,7 +18767,7 @@
"dependencies": {
"pify": {
"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=",
"dev": true
}
@ -19227,7 +19228,7 @@
},
"xmlbuilder": {
"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="
},
"xmlcreate": {

View File

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