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

@ -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`)
VALUES
(1, 'CRI', 'Crisantemo', 2, 31, 5, 0),
(2, 'ITG', 'Anthurium', 1, 31, 5, 0),
(3, 'WPN', 'Paniculata', 2, 31, 5, 0),
(4, 'PRT', 'Delivery ports', 3, NULL, 5, 1),
(5, 'CON', 'Container', 3, NULL, 5, 1),
(6, 'ALS', 'Alstroemeria', 1, 31, 5, 0);
(1, 'CRI', 'Crisantemo', 2, 31, 35, 0),
(2, 'ITG', 'Anthurium', 1, 31, 35, 0),
(3, 'WPN', 'Paniculata', 2, 31, 35, 0),
(4, 'PRT', 'Delivery ports', 3, NULL, 35, 1),
(5, 'CON', 'Container', 3, NULL, 35, 1),
(6, 'ALS', 'Alstroemeria', 1, 31, 35, 0);
INSERT INTO `vn`.`ink`(`id`, `name`, `picture`, `showOrder`)
VALUES

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

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

Binary file not shown.

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

@ -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 getFinalState = require('vn-loopback/util/hook').getFinalState;
let isMultiple = require('vn-loopback/util/hook').isMultiple;
const httpParamSerializer = require('vn-loopback/util/http').httpParamSerializer;
const LoopBackContext = require('loopback-context');
module.exports = Self => {
@ -27,6 +26,7 @@ module.exports = Self => {
require('../methods/client/sendSms')(Self);
require('../methods/client/createAddress')(Self);
require('../methods/client/updateAddress')(Self);
require('../methods/client/consumption')(Self);
// Validations
@ -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

@ -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>
Send SMS
</vn-item>
<vn-item
ng-click="consumerReportDialog.show()"
translate>
View consumer report
</vn-item>
</slot-menu>
<slot-body>
<div class="attributes">
@ -94,28 +89,4 @@
<vn-client-sms
vn-id="sms"
sms="$ctrl.newSMS">
</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>
</vn-client-sms>

View File

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

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

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

View File

@ -56,4 +56,5 @@ Requested credits: Créditos solicitados
Contacts: Contactos
Samples: Plantillas
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.balance.index", "icon": "icon-invoices"},
{"state": "client.card.recovery.index", "icon": "icon-recovery"},
{"state": "client.card.webAccess", "icon": "cloud"},
{"state": "client.card.log", "icon": "history"},
{
"description": "Others",
"icon": "more",
"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.creditInsurance.index", "icon": "icon-solunion"},
{"state": "client.card.contact", "icon": "contact_phone"},
{"state": "client.card.sample.index", "icon": "mail"},
{"state": "client.card.webPayment", "icon": "icon-onlinepayment"},
{"state": "client.card.dms.index", "icon": "cloud_upload"}
]
@ -350,6 +351,15 @@
"params": {
"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() {
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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,17 @@ const emailFooter = new Component('email-footer');
module.exports = {
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: {
'email-header': emailHeader.build(),
'email-footer': emailFooter.build()

View File

@ -1,7 +1,8 @@
subject: Informe consumo campaña
title: Informe consumo campaña
subject: Informe de consumo
title: Informe de consumo
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
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.

View File

@ -6,9 +6,6 @@ const reportFooter = new Component('report-footer');
module.exports = {
name: 'campaign-metrics',
async serverPrefetch() {
this.to = new Date(this.to);
this.from = new Date(this.from);
this.client = await this.fetchClient(this.recipientId);
this.sales = await this.fetchSales(this.recipientId, this.from, this.to);
@ -54,7 +51,7 @@ module.exports = {
t.clientFk = ? AND it.isPackaging = FALSE
AND DATE(t.shipped) BETWEEN ? AND ?
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: {
@ -66,12 +63,10 @@ module.exports = {
required: true
},
from: {
required: true,
type: Date
required: true
},
to: {
required: true,
type: Date
required: true
}
}
};

View File

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