diff --git a/Jenkinsfile b/Jenkinsfile index 1fa6a49ea4..9af09b3060 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -68,7 +68,7 @@ pipeline { stage('Backend') { steps { nodejs('node-lts') { - sh 'gulp backTestDockerOnce --junit --random' + sh 'gulp backTestOnce --ci' } } } diff --git a/README.md b/README.md index 52f854b6e9..82f7497659 100644 --- a/README.md +++ b/README.md @@ -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/) diff --git a/back/models/account.js b/back/models/account.js index 7cc1968523..a0b08dd57a 100644 --- a/back/models/account.js +++ b/back/models/account.js @@ -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', { diff --git a/db/docker.js b/db/docker.js new file mode 100644 index 0000000000..849cf182cc --- /dev/null +++ b/db/docker.js @@ -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 + }); + } + }); + }); + } +}; diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 13ac2bcb3c..e48a20ec65 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -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 diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 5cd24cda5f..98533b8c35 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -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"]', diff --git a/e2e/paths/05-ticket/14_create_ticket.spec.js b/e2e/paths/05-ticket/14_create_ticket.spec.js index 4ce2e51565..26c22ad3df 100644 --- a/e2e/paths/05-ticket/14_create_ticket.spec.js +++ b/e2e/paths/05-ticket/14_create_ticket.spec.js @@ -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); }); }); diff --git a/e2e/smokes-tests.js b/e2e/smokes-tests.js deleted file mode 100644 index 7b4e16edfb..0000000000 --- a/e2e/smokes-tests.js +++ /dev/null @@ -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(); diff --git a/e2e/smokes/01_client_path.spec.js b/e2e/smokes/01_client_path.spec.js deleted file mode 100644 index 6c106b2eb2..0000000000 --- a/e2e/smokes/01_client_path.spec.js +++ /dev/null @@ -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); - }); -}); diff --git a/front/core/components/searchbar/searchbar.html b/front/core/components/searchbar/searchbar.html index 1de40fa231..b41c050ebe 100644 --- a/front/core/components/searchbar/searchbar.html +++ b/front/core/components/searchbar/searchbar.html @@ -1,7 +1,7 @@
this.vnApp.showMessage(this.$t('Notification sent!'))); + } +} +Email.$inject = ['$http', '$translate', 'vnApp']; + +ngModule.service('vnEmail', Email); diff --git a/front/core/services/file.js b/front/core/services/file.js new file mode 100644 index 0000000000..25ace44706 --- /dev/null +++ b/front/core/services/file.js @@ -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); diff --git a/front/core/services/index.js b/front/core/services/index.js index 4573ab5c97..ff1d438ede 100644 --- a/front/core/services/index.js +++ b/front/core/services/index.js @@ -7,3 +7,6 @@ import './modules'; import './interceptor'; import './config'; import './week-days'; +import './report'; +import './email'; +import './file'; diff --git a/front/core/services/report.js b/front/core/services/report.js new file mode 100644 index 0000000000..32ccb52a37 --- /dev/null +++ b/front/core/services/report.js @@ -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); diff --git a/front/core/styles/font-family.scss b/front/core/styles/font-family.scss index 035d96bc98..24d9bbe439 100644 --- a/front/core/styles/font-family.scss +++ b/front/core/styles/font-family.scss @@ -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 { diff --git a/front/core/styles/icons/Material-Design-Icons.woff2 b/front/core/styles/icons/Material-Design-Icons.woff2 deleted file mode 100644 index 20f1f6746c..0000000000 Binary files a/front/core/styles/icons/Material-Design-Icons.woff2 and /dev/null differ diff --git a/front/core/styles/icons/MaterialIcons-Regular.woff2 b/front/core/styles/icons/MaterialIcons-Regular.woff2 new file mode 100644 index 0000000000..9fa2112520 Binary files /dev/null and b/front/core/styles/icons/MaterialIcons-Regular.woff2 differ diff --git a/front/salix/components/descriptor/index.js b/front/salix/components/descriptor/index.js index 6b10b1ff9c..d87a4c3957 100644 --- a/front/salix/components/descriptor/index.js +++ b/front/salix/components/descriptor/index.js @@ -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: { diff --git a/gulpfile.js b/gulpfile.js index ff6c71aa4e..678fda5271 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -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, }; diff --git a/modules/claim/front/descriptor/index.js b/modules/claim/front/descriptor/index.js index 5b09b18861..674ac91e19 100644 --- a/modules/claim/front/descriptor/index.js +++ b/modules/claim/front/descriptor/index.js @@ -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 diff --git a/modules/claim/front/descriptor/index.spec.js b/modules/claim/front/descriptor/index.spec.js index 7cdca1b828..bca47409c9 100644 --- a/modules/claim/front/descriptor/index.spec.js +++ b/modules/claim/front/descriptor/index.spec.js @@ -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); }); }); diff --git a/modules/claim/front/photos/index.html b/modules/claim/front/photos/index.html index cb3f55b65e..9cc6c649c0 100644 --- a/modules/claim/front/photos/index.html +++ b/modules/claim/front/photos/index.html @@ -13,8 +13,8 @@
+ ng-style="{'background': 'url(' + $ctrl.getImagePath(photo.dmsFk) + ')'}" + zoom-image="{{$ctrl.getImagePath(photo.dmsFk)}}">
+ ng-style="{'background': 'url(' + $ctrl.getImagePath(photo.dmsFk) + ')'}" + zoom-image="{{$ctrl.getImagePath(photo.dmsFk)}}">
diff --git a/modules/claim/front/summary/index.js b/modules/claim/front/summary/index.js index 0ccf6dcd2f..6e62252d5a 100644 --- a/modules/claim/front/summary/index.js +++ b/modules/claim/front/summary/index.js @@ -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, diff --git a/modules/client/back/methods/client/consumption.js b/modules/client/back/methods/client/consumption.js new file mode 100644 index 0000000000..cb5b39c295 --- /dev/null +++ b/modules/client/back/methods/client/consumption.js @@ -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); + }; +}; diff --git a/modules/client/back/methods/client/specs/consumption.spec.js b/modules/client/back/methods/client/specs/consumption.spec.js new file mode 100644 index 0000000000..e7a42a8da7 --- /dev/null +++ b/modules/client/back/methods/client/specs/consumption.spec.js @@ -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); + }); +}); diff --git a/modules/client/back/models/client.js b/modules/client/back/models/client.js index cf3ded2f8c..056b49d01a 100644 --- a/modules/client/back/models/client.js +++ b/modules/client/back/models/client.js @@ -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; diff --git a/modules/client/front/address/index/index.html b/modules/client/front/address/index/index.html index bdf4496ced..bd5d10fd26 100644 --- a/modules/client/front/address/index/index.html +++ b/modules/client/front/address/index/index.html @@ -6,6 +6,15 @@ data="$ctrl.addresses" auto-load="true"> + + + + @@ -35,7 +44,7 @@ -
{{::address.nickname}}
+
{{::address.nickname}} - #{{::address.id}}
{{::address.street}}
{{::address.city}}, {{::address.province.name}}
diff --git a/modules/client/front/address/index/index.js b/modules/client/front/address/index/index.js index 19f3524fbb..6a9d7507b9 100644 --- a/modules/client/front/address/index/index.js +++ b/modules/client/front/address/index/index.js @@ -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']; diff --git a/modules/client/front/address/index/index.spec.js b/modules/client/front/address/index/index.spec.js index d53abe8bea..4240e86292 100644 --- a/modules/client/front/address/index/index.spec.js +++ b/modules/client/front/address/index/index.spec.js @@ -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%'}}); + }); + }); }); }); diff --git a/modules/client/front/address/locale/es.yml b/modules/client/front/address/locale/es.yml index dc39175d60..06d9e76f7b 100644 --- a/modules/client/front/address/locale/es.yml +++ b/modules/client/front/address/locale/es.yml @@ -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 diff --git a/modules/client/front/consumption-search-panel/index.html b/modules/client/front/consumption-search-panel/index.html new file mode 100644 index 0000000000..e957c891b3 --- /dev/null +++ b/modules/client/front/consumption-search-panel/index.html @@ -0,0 +1,68 @@ +
+ + + + + + + + + + {{nickname}} + + + + + +
{{name}}
+
+ {{category.name}} +
+
+
+ + +
+ + + + + + + + + + +
diff --git a/modules/client/front/consumption-search-panel/index.js b/modules/client/front/consumption-search-panel/index.js new file mode 100644 index 0000000000..9b9354a935 --- /dev/null +++ b/modules/client/front/consumption-search-panel/index.js @@ -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 +}); diff --git a/modules/client/front/consumption-search-panel/locale/es.yml b/modules/client/front/consumption-search-panel/locale/es.yml new file mode 100644 index 0000000000..68de42b23a --- /dev/null +++ b/modules/client/front/consumption-search-panel/locale/es.yml @@ -0,0 +1,3 @@ +Item id: Id artículo +From: Desde +To: Hasta \ No newline at end of file diff --git a/modules/client/front/consumption/index.html b/modules/client/front/consumption/index.html new file mode 100644 index 0000000000..8ea65ecae4 --- /dev/null +++ b/modules/client/front/consumption/index.html @@ -0,0 +1,91 @@ + + + + + + + + +
+ + + + + + + + +
+ + + + Item + Ticket + Fecha + Description + Quantity + + + + + + + {{::sale.itemFk}} + + + + + {{::sale.ticketFk}} + + + {{::sale.shipped | date: 'dd/MM/yyyy'}} + + + + + {{::sale.quantity | dashIfEmpty}} + + + +
+
+ + + + + + diff --git a/modules/client/front/consumption/index.js b/modules/client/front/consumption/index.js new file mode 100644 index 0000000000..4b075abb94 --- /dev/null +++ b/modules/client/front/consumption/index.js @@ -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' + } +}); diff --git a/modules/client/front/consumption/index.spec.js b/modules/client/front/consumption/index.spec.js new file mode 100644 index 0000000000..d76bc1e76d --- /dev/null +++ b/modules/client/front/consumption/index.spec.js @@ -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(' { + 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(); + }); + }); + }); +}); + diff --git a/modules/client/front/consumption/locale/es.yml b/modules/client/front/consumption/locale/es.yml new file mode 100644 index 0000000000..adf0f060cc --- /dev/null +++ b/modules/client/front/consumption/locale/es.yml @@ -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 diff --git a/modules/client/front/descriptor/index.html b/modules/client/front/descriptor/index.html index 19fcba1280..aac5eb343b 100644 --- a/modules/client/front/descriptor/index.html +++ b/modules/client/front/descriptor/index.html @@ -13,11 +13,6 @@ translate> Send SMS - - View consumer report -
@@ -94,28 +89,4 @@ - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/modules/client/front/descriptor/index.js b/modules/client/front/descriptor/index.js index d079b38dad..89248ebedf 100644 --- a/modules/client/front/descriptor/index.js +++ b/modules/client/front/descriptor/index.js @@ -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', { diff --git a/modules/client/front/dms/index/index.html b/modules/client/front/dms/index/index.html index fd1527e233..3402074ccc 100644 --- a/modules/client/front/dms/index/index.html +++ b/modules/client/front/dms/index/index.html @@ -54,11 +54,10 @@ - + {{::document.dms.file}} - + - - - - + + { 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); }); }); }); diff --git a/modules/route/front/descriptor/index.js b/modules/route/front/descriptor/index.js index c140350155..32411aa58f 100644 --- a/modules/route/front/descriptor/index.js +++ b/modules/route/front/descriptor/index.js @@ -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 }); diff --git a/modules/ticket/back/methods/ticket/filter.js b/modules/ticket/back/methods/ticket/filter.js index 51979cf112..28de7b8ee9 100644 --- a/modules/ticket/back/methods/ticket/filter.js +++ b/modules/ticket/back/methods/ticket/filter.js @@ -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': diff --git a/modules/ticket/back/methods/ticket/getVolume.js b/modules/ticket/back/methods/ticket/getVolume.js index 9b4d28edc2..149f650ec6 100644 --- a/modules/ticket/back/methods/ticket/getVolume.js +++ b/modules/ticket/back/methods/ticket/getVolume.js @@ -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]); }; }; diff --git a/modules/ticket/back/methods/ticket/setDeleted.js b/modules/ticket/back/methods/ticket/setDeleted.js index 06e443e946..fe244becd8 100644 --- a/modules/ticket/back/methods/ticket/setDeleted.js +++ b/modules/ticket/back/methods/ticket/setDeleted.js @@ -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' diff --git a/modules/ticket/back/methods/ticket/specs/filter.spec.js b/modules/ticket/back/methods/ticket/specs/filter.spec.js index faaeb82c07..56d351f2ea 100644 --- a/modules/ticket/back/methods/ticket/specs/filter.spec.js +++ b/modules/ticket/back/methods/ticket/specs/filter.spec.js @@ -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); + }); }); diff --git a/modules/ticket/back/methods/ticket/specs/getVolume.spec.js b/modules/ticket/back/methods/ticket/specs/getVolume.spec.js index 8acf673afd..6807246611 100644 --- a/modules/ticket/back/methods/ticket/specs/getVolume.spec.js +++ b/modules/ticket/back/methods/ticket/specs/getVolume.spec.js @@ -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); }); }); }); diff --git a/modules/ticket/back/methods/ticket/specs/setDeleted.spec.js b/modules/ticket/back/methods/ticket/specs/setDeleted.spec.js index 7965b508cb..0a73ad3220 100644 --- a/modules/ticket/back/methods/ticket/specs/setDeleted.spec.js +++ b/modules/ticket/back/methods/ticket/specs/setDeleted.spec.js @@ -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(); }); }); diff --git a/modules/ticket/front/descriptor/index.js b/modules/ticket/front/descriptor/index.js index 546c5bec76..d6fe85372e 100644 --- a/modules/ticket/front/descriptor/index.js +++ b/modules/ticket/front/descriptor/index.js @@ -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 diff --git a/modules/ticket/front/descriptor/index.spec.js b/modules/ticket/front/descriptor/index.spec.js index f152f1f0f9..41a2cceea5 100644 --- a/modules/ticket/front/descriptor/index.spec.js +++ b/modules/ticket/front/descriptor/index.spec.js @@ -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); }); }); diff --git a/modules/ticket/front/dms/index/index.html b/modules/ticket/front/dms/index/index.html index 176eed7545..80ddbf8997 100644 --- a/modules/ticket/front/dms/index/index.html +++ b/modules/ticket/front/dms/index/index.html @@ -52,11 +52,10 @@ - + {{::document.dms.file}} - + - - - - + + {{::sale.quantity}} - {{::sale.volume.m3 | number:3}} + {{::sale.saleVolume.volume | number:3}} diff --git a/modules/ticket/front/volume/index.js b/modules/ticket/front/volume/index.js index 3d690764f2..74ff61c85b 100644 --- a/modules/ticket/front/volume/index.js +++ b/modules/ticket/front/volume/index.js @@ -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; }); }); } diff --git a/modules/ticket/front/volume/index.spec.js b/modules/ticket/front/volume/index.spec.js index 7807bfe10a..f7cb5a58d0 100644 --- a/modules/ticket/front/volume/index.spec.js +++ b/modules/ticket/front/volume/index.spec.js @@ -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); }); }); diff --git a/modules/ticket/front/weekly/index.html b/modules/ticket/front/weekly/index.html index 8411d4f723..92ee644784 100644 --- a/modules/ticket/front/weekly/index.html +++ b/modules/ticket/front/weekly/index.html @@ -10,6 +10,7 @@ diff --git a/modules/ticket/front/weekly/locale/es.yml b/modules/ticket/front/weekly/locale/es.yml index fa40fe96b3..7c7c6a1399 100644 --- a/modules/ticket/front/weekly/locale/es.yml +++ b/modules/ticket/front/weekly/locale/es.yml @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/modules/travel/front/thermograph/index/index.html b/modules/travel/front/thermograph/index/index.html index dae88d2a71..98a44bf795 100644 --- a/modules/travel/front/thermograph/index/index.html +++ b/modules/travel/front/thermograph/index/index.html @@ -29,13 +29,10 @@ {{::thermograph.warehouse.name}} {{::thermograph.created | date: 'dd/MM/yyyy'}} - - - - + + - {{::document.file}} - + + {{::document.file}} + {{::document.created | date:'dd/MM/yyyy HH:mm'}} - - - - + + =12" + }, "dependencies": { "compression": "^1.7.3", "fs-extra": "^5.0.0", diff --git a/print/core/components/email-footer/assets/css/style.css b/print/core/components/email-footer/assets/css/style.css index 4bc22fdfd1..29620a64ef 100644 --- a/print/core/components/email-footer/assets/css/style.css +++ b/print/core/components/email-footer/assets/css/style.css @@ -46,6 +46,7 @@ } .privacy { + text-align: center; padding: 20px 0; font-size: 10px; font-weight: 100 diff --git a/print/core/components/email-header/assets/css/style.css b/print/core/components/email-header/assets/css/style.css index 4db5e2b2ec..e6451ca5a3 100644 --- a/print/core/components/email-header/assets/css/style.css +++ b/print/core/components/email-header/assets/css/style.css @@ -1,5 +1,10 @@ +header { + text-align: center +} + header .logo { - margin-bottom: 15px; + margin-top: 25px; + margin-bottom: 25px } header .logo img { diff --git a/print/core/filters/date.js b/print/core/filters/date.js index 0988eda755..5d1bc0de53 100644 --- a/print/core/filters/date.js +++ b/print/core/filters/date.js @@ -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); }); diff --git a/print/templates/email/campaign-metrics/attachments.json b/print/templates/email/campaign-metrics/attachments.json index 3f6a93bb5e..d836d60403 100644 --- a/print/templates/email/campaign-metrics/attachments.json +++ b/print/templates/email/campaign-metrics/attachments.json @@ -1,6 +1,6 @@ [ { - "filename": "campaing-metrics", + "filename": "campaign-metrics.pdf", "component": "campaign-metrics" } ] \ No newline at end of file diff --git a/print/templates/email/campaign-metrics/campaign-metrics.html b/print/templates/email/campaign-metrics/campaign-metrics.html index 4ba95adb9d..9d7014f34e 100644 --- a/print/templates/email/campaign-metrics/campaign-metrics.html +++ b/print/templates/email/campaign-metrics/campaign-metrics.html @@ -25,7 +25,7 @@

{{ $t('title') }}

{{$t('dear')}},

-

{{$t('description')}}

+

diff --git a/print/templates/email/campaign-metrics/campaign-metrics.js b/print/templates/email/campaign-metrics/campaign-metrics.js index 51d2ebb444..0ace0fc256 100755 --- a/print/templates/email/campaign-metrics/campaign-metrics.js +++ b/print/templates/email/campaign-metrics/campaign-metrics.js @@ -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() diff --git a/print/templates/email/campaign-metrics/locale/es.yml b/print/templates/email/campaign-metrics/locale/es.yml index e662ca6140..d1c1182a2d 100644 --- a/print/templates/email/campaign-metrics/locale/es.yml +++ b/print/templates/email/campaign-metrics/locale/es.yml @@ -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 {0} y {1}. + Espero le sea de utilidad para preparar su pedido.

Al mismo tiempo aprovecho la ocasión para saludarle cordialmente. diff --git a/print/templates/reports/campaign-metrics/campaign-metrics.js b/print/templates/reports/campaign-metrics/campaign-metrics.js index ef1d735dea..420c1ffb1b 100755 --- a/print/templates/reports/campaign-metrics/campaign-metrics.js +++ b/print/templates/reports/campaign-metrics/campaign-metrics.js @@ -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 } } }; diff --git a/print/templates/reports/campaign-metrics/locale/es.yml b/print/templates/reports/campaign-metrics/locale/es.yml index df0c91971a..8a4cc46376 100644 --- a/print/templates/reports/campaign-metrics/locale/es.yml +++ b/print/templates/reports/campaign-metrics/locale/es.yml @@ -1,4 +1,4 @@ -title: Consumo de campaña +title: Consumo Client: Cliente clientData: Datos del cliente dated: Fecha