diff --git a/back/methods/chat/sendCheckingPresence.js b/back/methods/chat/sendCheckingPresence.js index 5c27d72fd..6560240c6 100644 --- a/back/methods/chat/sendCheckingPresence.js +++ b/back/methods/chat/sendCheckingPresence.js @@ -46,7 +46,7 @@ module.exports = Self => { const {data} = await Self.getUserStatus(recipient.name); if (data) { - if (data.status === 'offline') { + if (data.status === 'offline' || data.status === 'busy') { // Send message to department room const workerDepartment = await models.WorkerDepartment.findById(recipientId, { include: { @@ -58,6 +58,8 @@ module.exports = Self => { if (channelName) return Self.send(ctx, `#${channelName}`, `@${recipient.name} ➔ ${message}`); + else + return Self.send(ctx, `@${recipient.name}`, message); } else return Self.send(ctx, `@${recipient.name}`, message); } diff --git a/back/methods/collection/setSaleQuantity.js b/back/methods/collection/setSaleQuantity.js index 82451b8be..95145e9a0 100644 --- a/back/methods/collection/setSaleQuantity.js +++ b/back/methods/collection/setSaleQuantity.js @@ -28,7 +28,7 @@ module.exports = Self => { const args = ctx.args; const models = Self.app.models; - const sale = await models.Sale.findById(args.saleId,); + const sale = await models.Sale.findById(args.saleId); return await sale.updateAttribute('quantity', args.quantity); }; }; diff --git a/db/changes/10451-april/00-invoiceOut_queue.sql b/db/changes/10451-april/00-invoiceOut_queue.sql new file mode 100644 index 000000000..f60bcab77 --- /dev/null +++ b/db/changes/10451-april/00-invoiceOut_queue.sql @@ -0,0 +1,14 @@ +create table `vn`.`invoiceOut_queue` +( + invoiceFk int(10) unsigned not null, + queued datetime default now() not null, + printed datetime null, + `status` VARCHAR(50) default '' null, + constraint invoiceOut_queue_pk + primary key (invoiceFk), + constraint invoiceOut_queue_invoiceOut_id_fk + foreign key (invoiceFk) references invoiceOut (id) + on update cascade on delete cascade +) + comment 'Queue for PDF invoicing'; + diff --git a/db/changes/10460-mothersDay/delete.keep b/db/changes/10460-mothersDay/delete.keep new file mode 100644 index 000000000..0e7498f40 --- /dev/null +++ b/db/changes/10460-mothersDay/delete.keep @@ -0,0 +1 @@ +Delete file \ No newline at end of file diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 0849e6708..e03027b36 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -2266,13 +2266,19 @@ INSERT INTO `vn`.`dmsType`(`id`, `name`, `path`, `readRoleFk`, `writeRoleFk`, `c INSERT INTO `vn`.`dms`(`id`, `dmsTypeFk`, `file`, `contentType`, `workerFk`, `warehouseFk`, `companyFk`, `hardCopyNumber`, `hasFile`, `reference`, `description`, `created`) VALUES - (1, 14, '1.txt', 'text/plain', 5, 1, 442, NULL, FALSE, 'Ticket:11', 'Ticket:11 dms for the ticket', CURDATE()), - (2, 5, '2.txt', 'text/plain', 5, 1, 442, 1, TRUE, 'Client:104', 'Client:104 dms for the client', CURDATE()), - (3, 5, '3.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'Client: 104', 'Client:104 readme', CURDATE()), - (4, 3, '4.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'Worker: 106', 'Worker:106 readme', CURDATE()), - (5, 5, '5.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'travel: 1', 'dmsForThermograph', CURDATE()), - (6, 5, '6.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'NotExists', 'DoesNotExists', CURDATE()); + (1, 14, '1.txt', 'text/plain', 5, 1, 442, NULL, FALSE, 'Ticket:11', 'Ticket:11 dms for the ticket', CURDATE()), + (2, 5, '2.txt', 'text/plain', 5, 1, 442, 1, TRUE, 'Client:104', 'Client:104 dms for the client', CURDATE()), + (3, 5, '3.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'Client: 104', 'Client:104 readme', CURDATE()), + (4, 3, '4.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'Worker: 106', 'Worker:106 readme', CURDATE()), + (5, 5, '5.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'travel: 1', 'dmsForThermograph', CURDATE()), + (6, 5, '6.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'NotExists', 'DoesNotExists', CURDATE()), + (7, 20, '7.jpg', 'image/jpeg', 9, 1, 442, NULL, FALSE, '1', 'TICKET ID DEL CLIENTE BRUCE WAYNE ID 1101', CURDATE()), + (8, 20, '8.mp4', 'video/mp4', 9, 1, 442, NULL, FALSE, '1', 'TICKET ID DEL CLIENTE BRUCE WAYNE ID 1101', CURDATE()); +INSERT INTO `vn`.`claimDms`(`claimFk`, `dmsFk`) + VALUES + (1, 7), + (1, 8); INSERT INTO `vn`.`ticketDms`(`ticketFk`, `dmsFk`) VALUES diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 189b7be67..48f483450 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -731,7 +731,7 @@ export default { claimAction: { importClaimButton: 'vn-claim-action vn-button[label="Import claim"]', anyLine: 'vn-claim-action vn-tbody > vn-tr', - firstDeleteLine: 'vn-claim-action vn-tr:nth-child(1) vn-icon-button[icon="delete"]', + firstDeleteLine: 'vn-claim-action tr:nth-child(1) vn-icon-button[icon="delete"]', isPaidWithManaCheckbox: 'vn-claim-action vn-check[ng-model="$ctrl.claim.isChargedToMana"]' }, ordersIndex: { @@ -1133,7 +1133,7 @@ export default { entryLatestBuys: { firstBuy: 'vn-entry-latest-buys tbody > tr:nth-child(1)', allBuysCheckBox: 'vn-entry-latest-buys thead vn-check', - secondBuyCheckBox: 'vn-entry-latest-buys tbody tr:nth-child(2) vn-check[ng-model="buy.$checked"]', + secondBuyCheckBox: 'vn-entry-latest-buys tbody tr:nth-child(2) vn-check[ng-model="buy.checked"]', editBuysButton: 'vn-entry-latest-buys vn-button[icon="edit"]', fieldAutocomplete: 'vn-autocomplete[ng-model="$ctrl.editedColumn.field"]', newValueInput: 'vn-textfield[ng-model="$ctrl.editedColumn.newValue"]', diff --git a/e2e/paths/02-client/12_lock_of_verified_data.spec.js b/e2e/paths/02-client/12_lock_of_verified_data.spec.js index 701531c76..af42e2a4b 100644 --- a/e2e/paths/02-client/12_lock_of_verified_data.spec.js +++ b/e2e/paths/02-client/12_lock_of_verified_data.spec.js @@ -111,7 +111,7 @@ describe('Client lock verified data path', () => { await page.waitToClick(selectors.clientFiscalData.saveButton); const message = await page.waitForSnackbar(); - expect(message.text).toContain(`You can't make changes on a client with verified data`); + expect(message.text).toContain(`Not enough privileges to edit a client with verified data`); }); }); @@ -123,19 +123,19 @@ describe('Client lock verified data path', () => { await page.accessToSection('client.card.fiscalData'); }, 20000); - it('should confirm verified data button is enabled for salesAssistant', async() => { + it('should confirm verified data button is disabled for salesAssistant', async() => { const isDisabled = await page.isDisabled(selectors.clientFiscalData.verifiedDataCheckbox); - expect(isDisabled).toBeFalsy(); + expect(isDisabled).toBeTrue(); }); - it('should now edit the social name', async() => { + it('should return error when edit the social name', async() => { await page.clearInput(selectors.clientFiscalData.socialName); await page.write(selectors.clientFiscalData.socialName, 'new social name edition'); await page.waitToClick(selectors.clientFiscalData.saveButton); const message = await page.waitForSnackbar(); - expect(message.text).toContain('Data saved!'); + expect(message.text).toContain(`Not enough privileges to edit a client with verified data`); }); it('should now confirm the social name have been edited once and for all', async() => { diff --git a/front/core/components/multi-check/locale/en.yml b/front/core/components/multi-check/locale/en.yml new file mode 100644 index 000000000..ea52dcf8f --- /dev/null +++ b/front/core/components/multi-check/locale/en.yml @@ -0,0 +1 @@ +SelectAllRows: Select the {{rows}} row(s) \ No newline at end of file diff --git a/front/core/components/multi-check/locale/es.yml b/front/core/components/multi-check/locale/es.yml new file mode 100644 index 000000000..5365c3392 --- /dev/null +++ b/front/core/components/multi-check/locale/es.yml @@ -0,0 +1,3 @@ +SelectAllRows: Seleccionar las {{rows}} fila(s) +All: Se han seleccionado +row(s) have been selected.: fila(s). \ No newline at end of file diff --git a/front/core/components/multi-check/multi-check.html b/front/core/components/multi-check/multi-check.html index fb950aaff..eaa4577f0 100644 --- a/front/core/components/multi-check/multi-check.html +++ b/front/core/components/multi-check/multi-check.html @@ -2,4 +2,23 @@ ng-model="$ctrl.checked" indeterminate="$ctrl.isIndeterminate" translate-attr="{title: 'Check all'}"> - \ No newline at end of file + + + + + + All + + {{$ctrl.rows}} + + row(s) have been selected. + + {{$ctrl.allRowsText}} + + + \ No newline at end of file diff --git a/front/core/components/multi-check/multi-check.js b/front/core/components/multi-check/multi-check.js index afa1bc3c4..e60d16519 100644 --- a/front/core/components/multi-check/multi-check.js +++ b/front/core/components/multi-check/multi-check.js @@ -106,6 +106,9 @@ export default class MultiCheck extends FormInput { this.toggle(); this.emit('change', value); + + if (!value) + this.checkedDummyCount = null; } /** @@ -132,12 +135,43 @@ export default class MultiCheck extends FormInput { areAllUnchecked() { if (!this.model || !this.model.data) return; + this.checkedDummyCount = null; const data = this.model.data; return data.every(item => { return item[this.checkField] === false; }); } + countRows() { + if (!this.model || !this.model.data) return; + + const data = this.model.data; + const modelParams = this.model.userParams; + const params = { + filter: { + modelParams: modelParams, + limit: null + } + }; + + this.rows = data.length; + + this.$http.get(this.model.url, {params}) + .then(res => { + this.allRowsCount = res.data.length; + this.allRowsText = this.$t('SelectAllRows', { + rows: this.allRowsCount + }); + }); + } + + checkDummy() { + if (this.checkedDummyCount) + return this.checkedDummyCount = null; + + this.checkedDummyCount = this.allRowsCount; + } + /** * Toggles checked property on * all instances @@ -158,7 +192,9 @@ ngModule.vnComponent('vnMultiCheck', { checkField: '@?', checkAll: '=?', checked: '=?', - disabled: ' { let controller; let $element; + let $httpBackend; + let $httpParamSerializer; beforeEach(ngModule('vnCore')); - beforeEach(inject($componentController => { + beforeEach(inject(($componentController, _$httpBackend_, _$httpParamSerializer_) => { + $httpBackend = _$httpBackend_; + $httpParamSerializer = _$httpParamSerializer_; $element = angular.element(`
`); controller = $componentController('vnMultiCheck', {$element: $element}); controller._model = crudModel; @@ -26,6 +30,14 @@ describe('Component vnMultiCheck', () => { expect(controller._checked).toEqual(crudModel); expect(controller.toggle).toHaveBeenCalledWith(); }); + + it(`should set checkedDummyCount to null`, () => { + jest.spyOn(controller, 'toggle'); + controller.checkedDummyCount = 12; + controller.checked = null; + + expect(controller.checkedDummyCount).toBeNull(); + }); }); describe('toggle()', () => { @@ -132,4 +144,50 @@ describe('Component vnMultiCheck', () => { expect(thirdRow.checked).toBeTruthy(); }); }); + + describe('countRows()', () => { + it(`should count visible rows and all rows of model`, () => { + controller.model.url = 'modelUrl/filter'; + const data = controller.model.data; + const filter = { + limit: null + }; + const serializedParams = $httpParamSerializer({filter}); + + const response = [ + {id: 1, name: 'My item 1'}, + {id: 2, name: 'My item 2'}, + {id: 3, name: 'My item 3'}, + {id: 4, name: 'My item 4'}, + {id: 5, name: 'My item 5'}, + {id: 6, name: 'My item 6'} + ]; + + controller.countRows(); + $httpBackend.expectGET(`modelUrl/filter?${serializedParams}`).respond(response); + $httpBackend.flush(); + + expect(controller.rows).toEqual(data.length); + expect(controller.allRowsCount).toEqual(response.length); + expect(controller.allRowsCount).toBeGreaterThan(controller.rows); + }); + }); + + describe('checkDummy()', () => { + const allRows = 1234; + it(`should set the checked dummy count to all rows count if there was no count yet`, () => { + controller.checkedDummyCount = null; + controller.allRowsCount = allRows; + controller.checkDummy(); + + expect(controller.checkedDummyCount).toEqual(controller.allRowsCount); + }); + + it(`should remove the dummy count if there was an existing one`, () => { + controller.checkedDummyCount = allRows; + controller.checkDummy(); + + expect(controller.checkedDummyCount).toBeNull(); + }); + }); }); diff --git a/front/core/components/multi-check/style.scss b/front/core/components/multi-check/style.scss index 79c2397bc..7a4d10675 100644 --- a/front/core/components/multi-check/style.scss +++ b/front/core/components/multi-check/style.scss @@ -1,5 +1,17 @@ +@import "variables"; vn-multi-check { .vn-check { margin-bottom: 12px } + + vn-list{ + padding: 50px; + } + vn-menu{ + padding: 50px; + } + +} +.bold{ + font-weight: bold; } \ No newline at end of file diff --git a/front/core/styles/icons/salixfont.css b/front/core/styles/icons/salixfont.css index 530772246..6b2482bee 100644 --- a/front/core/styles/icons/salixfont.css +++ b/front/core/styles/icons/salixfont.css @@ -26,7 +26,7 @@ .icon-agency-term:before { content: "\e950"; } -.icon-deaulter:before { +.icon-defaulter:before { content: "\e94b"; } .icon-100:before { diff --git a/loopback/locale/en.json b/loopback/locale/en.json index b7242befb..b7e9b43d3 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -12,7 +12,6 @@ "That payment method requires an IBAN": "That payment method requires an IBAN", "That payment method requires a BIC": "That payment method requires a BIC", "The default consignee can not be unchecked": "The default consignee can not be unchecked", - "You can't make changes on a client with verified data": "You can't make changes on a client with verified data", "Enter an integer different to zero": "Enter an integer different to zero", "Package cannot be blank": "Package cannot be blank", "The new quantity should be smaller than the old one": "The new quantity should be smaller than the old one", @@ -123,5 +122,6 @@ "The type of business must be filled in basic data": "The type of business must be filled in basic data", "The worker has hours recorded that day": "The worker has hours recorded that day", "isWithoutNegatives": "isWithoutNegatives", - "routeFk": "routeFk" + "routeFk": "routeFk", + "Not enough privileges to edit a client with verified data": "Not enough privileges to edit a client with verified data" } \ No newline at end of file diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 77bd21780..a44ba2da8 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -50,7 +50,7 @@ "You don't have enough privileges to change that field": "No tienes permisos para cambiar ese campo", "Warehouse cannot be blank": "El almacén no puede quedar en blanco", "Agency cannot be blank": "La agencia no puede quedar en blanco", - "You can't make changes on a client with verified data": "No puedes hacer cambios en un cliente con datos comprobados", + "Not enough privileges to edit a client with verified data": "No tienes permisos para hacer cambios en un cliente con datos comprobados", "This address doesn't exist": "Este consignatario no existe", "You must delete the claim id %d first": "Antes debes borrar la reclamación %d", "You don't have enough privileges": "No tienes suficientes permisos", diff --git a/loopback/server/datasources.json b/loopback/server/datasources.json index 8b741ec97..f51beeb19 100644 --- a/loopback/server/datasources.json +++ b/loopback/server/datasources.json @@ -39,7 +39,8 @@ "multipart/x-zip", "image/png", "image/jpeg", - "image/jpg" + "image/jpg", + "video/mp4" ] }, "dmsStorage": { @@ -84,5 +85,18 @@ "application/octet-stream", "application/pdf" ] + }, + "claimStorage": { + "name": "claimStorage", + "connector": "loopback-component-storage", + "provider": "filesystem", + "root": "./storage/dms", + "maxFileSize": "31457280", + "allowedContentTypes": [ + "image/png", + "image/jpeg", + "image/jpg", + "video/mp4" + ] } } \ No newline at end of file diff --git a/modules/claim/back/methods/claim-end/deleteClamedSales.js b/modules/claim/back/methods/claim-end/deleteClamedSales.js new file mode 100644 index 000000000..a6142c0fd --- /dev/null +++ b/modules/claim/back/methods/claim-end/deleteClamedSales.js @@ -0,0 +1,49 @@ +module.exports = Self => { + Self.remoteMethodCtx('deleteClamedSales', { + description: 'Deletes the claimed sales', + accessType: 'WRITE', + accepts: [{ + arg: 'sales', + type: ['object'], + required: true, + description: 'The sales to remove' + }], + returns: { + type: ['object'], + root: true + }, + http: { + path: `/deleteClamedSales`, + verb: 'POST' + } + }); + + Self.deleteClamedSales = async(ctx, sales, options) => { + const models = Self.app.models; + const myOptions = {}; + const tx = await Self.beginTransaction({}); + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) + myOptions.transaction = tx; + + try { + const promises = []; + for (let sale of sales) { + const deletedSale = models.ClaimEnd.destroyById(sale.id, myOptions); + promises.push(deletedSale); + } + + const deletedSales = await Promise.all(promises); + + if (tx) await tx.commit(); + + return deletedSales; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/claim/back/methods/claim-end/filter.js b/modules/claim/back/methods/claim-end/filter.js new file mode 100644 index 000000000..0dda16a3a --- /dev/null +++ b/modules/claim/back/methods/claim-end/filter.js @@ -0,0 +1,69 @@ + +const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; + +module.exports = Self => { + Self.remoteMethodCtx('filter', { + 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', + http: {source: 'query'} + }, { + arg: 'search', + type: 'string', + description: `If it's and integer searchs by id, otherwise it searchs by client id`, + http: {source: 'query'} + } + ], + returns: { + type: ['object'], + root: true + }, + http: { + path: `/filter`, + verb: 'GET' + } + }); + + Self.filter = async(ctx, filter, options) => { + const conn = Self.dataSource.connector; + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + const stmts = []; + + const stmt = new ParameterizedSQL( + `SELECT * + FROM ( + SELECT + ce.id, + ce.claimFk, + s.itemFk, + s.ticketFk, + ce.claimDestinationFk, + t.landed, + s.quantity, + s.concept, + s.price, + s.discount, + s.quantity * s.price * ((100 - s.discount) / 100) total + FROM vn.claimEnd ce + LEFT JOIN vn.sale s ON s.id = ce.saleFk + LEFT JOIN vn.ticket t ON t.id = s.ticketFk + ) ce` + ); + + stmt.merge(conn.makeSuffix(filter)); + const itemsIndex = stmts.push(stmt) - 1; + + const sql = ParameterizedSQL.join(stmts, ';'); + const result = await conn.executeStmt(sql, myOptions); + + return itemsIndex === 0 ? result : result[itemsIndex]; + }; +}; diff --git a/modules/claim/back/methods/claim/downloadFile.js b/modules/claim/back/methods/claim/downloadFile.js new file mode 100644 index 000000000..750356b0b --- /dev/null +++ b/modules/claim/back/methods/claim/downloadFile.js @@ -0,0 +1,59 @@ +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.remoteMethodCtx('downloadFile', { + description: 'Get the claim file', + accessType: 'READ', + accepts: [ + { + arg: 'id', + type: 'Number', + description: 'The document id', + http: {source: 'path'} + } + ], + returns: [ + { + arg: 'body', + type: 'file', + root: true + }, + { + arg: 'Content-Type', + type: 'String', + http: {target: 'header'} + }, + { + arg: 'Content-Disposition', + type: 'String', + http: {target: 'header'} + } + ], + http: { + path: `/:id/downloadFile`, + verb: 'GET' + } + }); + + Self.downloadFile = async function(ctx, id) { + const models = Self.app.models; + const ClaimContainer = models.ClaimContainer; + const dms = await models.Dms.findById(id); + const pathHash = ClaimContainer.getHash(dms.id); + try { + await ClaimContainer.getFile(pathHash, dms.file); + } catch (e) { + if (e.code != 'ENOENT') + throw e; + + const error = new UserError(`File doesn't exists`); + error.statusCode = 404; + + throw error; + } + + const stream = ClaimContainer.downloadStream(pathHash, dms.file); + + return [stream, dms.contentType, `filename="${dms.file}"`]; + }; +}; diff --git a/modules/claim/back/methods/claim/specs/downloadFile.spec.js b/modules/claim/back/methods/claim/specs/downloadFile.spec.js new file mode 100644 index 000000000..3e46a9bc3 --- /dev/null +++ b/modules/claim/back/methods/claim/specs/downloadFile.spec.js @@ -0,0 +1,13 @@ +const app = require('vn-loopback/server/server'); + +describe('claim downloadFile()', () => { + const dmsId = 7; + + it('should return a response for an employee with image content-type', async() => { + const workerId = 1107; + const ctx = {req: {accessToken: {userId: workerId}}}; + const result = await app.models.Claim.downloadFile(ctx, dmsId); + + expect(result[1]).toEqual('image/jpeg'); + }); +}); diff --git a/modules/claim/back/methods/claim/specs/uploadFile.spec.js b/modules/claim/back/methods/claim/specs/uploadFile.spec.js new file mode 100644 index 000000000..952d80d27 --- /dev/null +++ b/modules/claim/back/methods/claim/specs/uploadFile.spec.js @@ -0,0 +1,18 @@ +const app = require('vn-loopback/server/server'); + +describe('claim uploadFile()', () => { + it(`should return an error for a user without enough privileges`, async() => { + const clientId = 1101; + const ticketDmsTypeId = 14; + const ctx = {req: {accessToken: {userId: clientId}}, args: {dmsTypeId: ticketDmsTypeId}}; + + let error; + await app.models.Claim.uploadFile(ctx).catch(e => { + error = e; + }).finally(() => { + expect(error.message).toEqual(`You don't have enough privileges`); + }); + + expect(error).toBeDefined(); + }); +}); diff --git a/modules/claim/back/methods/claim/updateClaimDestination.js b/modules/claim/back/methods/claim/updateClaimDestination.js new file mode 100644 index 000000000..190883c6d --- /dev/null +++ b/modules/claim/back/methods/claim/updateClaimDestination.js @@ -0,0 +1,55 @@ + +module.exports = Self => { + Self.remoteMethod('updateClaimDestination', { + description: 'Update a claim with privileges', + accessType: 'WRITE', + accepts: [{ + arg: 'rows', + type: ['object'], + required: true, + description: `the sales which will be modified the claimDestinationFk` + }, { + arg: 'claimDestinationFk', + type: 'number', + required: true + }], + returns: { + type: 'object', + root: true + }, + http: { + path: `/updateClaimDestination`, + verb: 'post' + } + }); + + Self.updateClaimDestination = async(rows, claimDestinationFk, options) => { + const tx = await Self.beginTransaction({}); + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) + myOptions.transaction = tx; + + try { + const models = Self.app.models; + const promises = []; + for (let row of rows) { + const claimEnd = await models.ClaimEnd.findById(row.id, null, myOptions); + const updatedClaimEnd = claimEnd.updateAttribute('claimDestinationFk', claimDestinationFk, myOptions); + promises.push(updatedClaimEnd); + } + + const updatedSales = await Promise.all(promises); + + if (tx) await tx.commit(); + + return updatedSales; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/claim/back/methods/claim/uploadFile.js b/modules/claim/back/methods/claim/uploadFile.js index 81ad40219..3d0737cf8 100644 --- a/modules/claim/back/methods/claim/uploadFile.js +++ b/modules/claim/back/methods/claim/uploadFile.js @@ -1,6 +1,10 @@ +const UserError = require('vn-loopback/util/user-error'); +const fs = require('fs-extra'); +const path = require('path'); + module.exports = Self => { Self.remoteMethodCtx('uploadFile', { - description: 'Upload and attach a document', + description: 'Upload and attach a file', accessType: 'WRITE', accepts: [{ arg: 'id', @@ -53,22 +57,54 @@ module.exports = Self => { }); Self.uploadFile = async(ctx, id, options) => { - let tx; + const tx = await Self.beginTransaction({}); const myOptions = {}; if (typeof options == 'object') Object.assign(myOptions, options); - if (!myOptions.transaction) { - tx = await Self.beginTransaction({}); + if (!myOptions.transaction) myOptions.transaction = tx; - } + const models = Self.app.models; const promises = []; + const TempContainer = models.TempContainer; + const ClaimContainer = models.ClaimContainer; + const fileOptions = {}; + const args = ctx.args; + let srcFile; try { - const uploadedFiles = await models.Dms.uploadFile(ctx, myOptions); - uploadedFiles.forEach(dms => { + const hasWriteRole = await models.DmsType.hasWriteRole(ctx, args.dmsTypeId, myOptions); + if (!hasWriteRole) + throw new UserError(`You don't have enough privileges`); + + // Upload file to temporary path + const tempContainer = await TempContainer.container('dms'); + const uploaded = await TempContainer.upload(tempContainer.name, ctx.req, ctx.result, fileOptions); + const files = Object.values(uploaded.files).map(file => { + return file[0]; + }); + + const addedDms = []; + for (const uploadedFile of files) { + const newDms = await createDms(ctx, uploadedFile, myOptions); + const pathHash = ClaimContainer.getHash(newDms.id); + + const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name); + srcFile = path.join(file.client.root, file.container, file.name); + + const claimContainer = await ClaimContainer.container(pathHash); + const dstFile = path.join(claimContainer.client.root, pathHash, newDms.file); + + await fs.move(srcFile, dstFile, { + overwrite: true + }); + + addedDms.push(newDms); + } + + addedDms.forEach(dms => { const newClaimDms = models.ClaimDms.create({ claimFk: id, dmsFk: dms.id @@ -83,7 +119,34 @@ module.exports = Self => { return resolvedPromises; } catch (e) { if (tx) await tx.rollback(); + + if (fs.existsSync(srcFile)) + await fs.unlink(srcFile); + throw e; } }; + + async function createDms(ctx, file, myOptions) { + const models = Self.app.models; + const myUserId = ctx.req.accessToken.userId; + const args = ctx.args; + + const newDms = await models.Dms.create({ + workerFk: myUserId, + dmsTypeFk: args.dmsTypeId, + companyFk: args.companyId, + warehouseFk: args.warehouseId, + reference: args.reference, + description: args.description, + contentType: file.type, + hasFile: args.hasFile + }, myOptions); + + let fileName = file.name; + const extension = models.DmsContainer.getFileExtension(fileName); + fileName = `${newDms.id}.${extension}`; + + return newDms.updateAttribute('file', fileName, myOptions); + } }; diff --git a/modules/claim/back/model-config.json b/modules/claim/back/model-config.json index 16d34543c..d4d772b58 100644 --- a/modules/claim/back/model-config.json +++ b/modules/claim/back/model-config.json @@ -37,5 +37,8 @@ }, "ClaimLog": { "dataSource": "vn" + }, + "ClaimContainer": { + "dataSource": "claimStorage" } } diff --git a/modules/claim/back/models/claim-container.json b/modules/claim/back/models/claim-container.json new file mode 100644 index 000000000..446be77b9 --- /dev/null +++ b/modules/claim/back/models/claim-container.json @@ -0,0 +1,10 @@ +{ + "name": "ClaimContainer", + "base": "Container", + "acls": [{ + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + }] +} \ No newline at end of file diff --git a/modules/claim/back/models/claim-end.js b/modules/claim/back/models/claim-end.js new file mode 100644 index 000000000..75ecda59a --- /dev/null +++ b/modules/claim/back/models/claim-end.js @@ -0,0 +1,4 @@ +module.exports = Self => { + require('../methods/claim-end/filter')(Self); + require('../methods/claim-end/deleteClamedSales')(Self); +}; diff --git a/modules/claim/back/models/claim.js b/modules/claim/back/models/claim.js index fba11cfb6..e6dc50073 100644 --- a/modules/claim/back/models/claim.js +++ b/modules/claim/back/models/claim.js @@ -7,4 +7,6 @@ module.exports = Self => { require('../methods/claim/uploadFile')(Self); require('../methods/claim/updateClaimAction')(Self); require('../methods/claim/isEditable')(Self); + require('../methods/claim/updateClaimDestination')(Self); + require('../methods/claim/downloadFile')(Self); }; diff --git a/modules/claim/front/action/index.html b/modules/claim/front/action/index.html index 6c0e2402d..059764ae2 100644 --- a/modules/claim/front/action/index.html +++ b/modules/claim/front/action/index.html @@ -1,10 +1,8 @@ -
- - - - - - - - -
- - - - - Id - Ticket - Destination - Landed - Quantity - Description - Price - Disc. - Total - - - - + +
+ + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + +
+ + + IdTicketDestinationLandedQuantityDescriptionPriceDisc.Total
+ - {{::saleClaimed.sale.itemFk | zeroFill:6}} - - - - + + - {{::saleClaimed.sale.ticketFk}} - - - + ng-click="ticketDescriptor.show($event, saleClaimed.ticketFk)"> + {{::saleClaimed.ticketFk}} + + - - {{::saleClaimed.sale.ticket.landed | date: 'dd/MM/yyyy'}} - {{::saleClaimed.sale.quantity}} - {{::saleClaimed.sale.concept}} - {{::saleClaimed.sale.price | currency: 'EUR':2}} - {{::saleClaimed.sale.discount}} % - - {{saleClaimed.sale.quantity * saleClaimed.sale.price * - ((100 - saleClaimed.sale.discount) / 100) | currency: 'EUR':2}} - - + {{::saleClaimed.landed | date: 'dd/MM/yyyy'}}{{::saleClaimed.quantity}}{{::saleClaimed.concept}}{{::saleClaimed.price | currency: 'EUR':2}}{{::saleClaimed.discount}} %{{saleClaimed.total | currency: 'EUR':2}} - - - - - - - - - +
+
+ + + + +
- \ No newline at end of file + + + + + +
+
{{$ctrl.$t('Change destination to all selected rows', {total: $ctrl.checked.length})}}
+ + + + +
+
+ + + + +
\ No newline at end of file diff --git a/modules/claim/front/action/index.js b/modules/claim/front/action/index.js index 4b3214211..10b629f27 100644 --- a/modules/claim/front/action/index.js +++ b/modules/claim/front/action/index.js @@ -5,6 +5,7 @@ import './style.scss'; export default class Controller extends Section { constructor($element, $) { super($element, $); + this.newDestination; this.filter = { include: [ {relation: 'sale', @@ -21,6 +22,81 @@ export default class Controller extends Section { }; this.getResolvedState(); this.maxResponsibility = 5; + this.smartTableOptions = { + activeButtons: { + search: true + }, + columns: [ + { + field: 'claimDestinationFk', + autocomplete: { + url: 'ClaimDestinations', + showField: 'description', + valueField: 'id' + } + }, + { + field: 'landed', + searchable: false + } + ] + }; + } + + exprBuilder(param, value) { + switch (param) { + case 'itemFk': + case 'ticketFk': + case 'claimDestinationFk': + case 'quantity': + case 'price': + case 'discount': + case 'total': + return {[param]: value}; + case 'concept': + return {[param]: {like: `%${value}%`}}; + case 'landed': + return {[param]: {between: this.dateRange(value)}}; + } + } + + dateRange(value) { + const minHour = new Date(value); + minHour.setHours(0, 0, 0, 0); + const maxHour = new Date(value); + maxHour.setHours(23, 59, 59, 59); + + return [minHour, maxHour]; + } + + get checked() { + const salesClaimed = this.$.model.data || []; + + const checkedSalesClaimed = []; + for (let saleClaimed of salesClaimed) { + if (saleClaimed.$checked) + checkedSalesClaimed.push(saleClaimed); + } + + return checkedSalesClaimed; + } + + updateDestination(saleClaimed, claimDestinationFk) { + const data = {rows: [saleClaimed], claimDestinationFk: claimDestinationFk}; + this.$http.post(`Claims/updateClaimDestination`, data).then(() => { + this.vnApp.showSuccess(this.$t('Data saved!')); + }).catch(e => { + this.$.model.refresh(); + throw e; + }); + } + + removeSales(saleClaimed) { + const params = {sales: [saleClaimed]}; + this.$http.post(`ClaimEnds/deleteClamedSales`, params).then(() => { + this.$.model.refresh(); + this.vnApp.showSuccess(this.$t('Data saved!')); + }); } getResolvedState() { @@ -54,8 +130,8 @@ export default class Controller extends Section { calculateTotals() { this.claimedTotal = 0; this.salesClaimed.forEach(sale => { - const price = sale.sale.quantity * sale.sale.price; - const discount = (sale.sale.discount * (sale.sale.quantity * sale.sale.price)) / 100; + const price = sale.quantity * sale.price; + const discount = (sale.discount * (sale.quantity * sale.price)) / 100; this.claimedTotal += price - discount; }); } @@ -125,6 +201,24 @@ export default class Controller extends Section { onSave() { this.vnApp.showSuccess(this.$t('Data saved!')); } + + onResponse() { + const rowsToEdit = []; + for (let row of this.checked) + rowsToEdit.push({id: row.id}); + + const data = { + rows: rowsToEdit, + claimDestinationFk: this.newDestination + }; + + const query = `Claims/updateClaimDestination`; + this.$http.post(query, data) + .then(() => { + this.$.model.refresh(); + this.vnApp.showSuccess(this.$t('Data saved!')); + }); + } } ngModule.vnComponent('vnClaimAction', { diff --git a/modules/claim/front/action/index.spec.js b/modules/claim/front/action/index.spec.js index f25f2eb3c..458d5e831 100644 --- a/modules/claim/front/action/index.spec.js +++ b/modules/claim/front/action/index.spec.js @@ -43,9 +43,9 @@ describe('claim', () => { describe('calculateTotals()', () => { it('should calculate the total price of the items claimed', () => { controller.salesClaimed = [ - {sale: {quantity: 5, price: 2, discount: 0}}, - {sale: {quantity: 10, price: 2, discount: 0}}, - {sale: {quantity: 10, price: 2, discount: 0}} + {quantity: 5, price: 2, discount: 0}, + {quantity: 10, price: 2, discount: 0}, + {quantity: 10, price: 2, discount: 0} ]; controller.calculateTotals(); @@ -151,5 +151,17 @@ describe('claim', () => { expect(controller.vnApp.showMessage).toHaveBeenCalledWith('Greuge added'); }); }); + + describe('onResponse()', () => { + it('should perform a post query and show a success message', () => { + jest.spyOn(controller.vnApp, 'showSuccess'); + + $httpBackend.expect('POST', `Claims/updateClaimDestination`).respond({}); + controller.onResponse(); + $httpBackend.flush(); + + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + }); + }); }); }); diff --git a/modules/claim/front/action/locale/es.yml b/modules/claim/front/action/locale/es.yml index 22b2740b3..97640d9dc 100644 --- a/modules/claim/front/action/locale/es.yml +++ b/modules/claim/front/action/locale/es.yml @@ -7,4 +7,7 @@ Regularize: Regularizar Do you want to insert greuges?: Desea insertar greuges? Insert greuges on client card: Insertar greuges en la ficha del cliente Greuge added: Greuge añadido -ClaimGreugeDescription: Reclamación id {{claimId}} \ No newline at end of file +ClaimGreugeDescription: Reclamación id {{claimId}} +Change destination: Cambiar destino +Change destination to all selected rows: Cambiar destino a {{total}} fila(s) seleccionada(s) +Add observation to all selected clients: Añadir observación a {{total}} cliente(s) seleccionado(s) diff --git a/modules/claim/front/action/style.scss b/modules/claim/front/action/style.scss index bac316287..cda6779c8 100644 --- a/modules/claim/front/action/style.scss +++ b/modules/claim/front/action/style.scss @@ -6,7 +6,7 @@ vn-claim-action { align-content: center; vn-tool-bar { - flex: 1 + flex: none } .vn-check { @@ -39,4 +39,8 @@ vn-claim-action { max-height: 350px; } } + + .right { + margin-left: 370px; + } } \ No newline at end of file diff --git a/modules/claim/front/photos/index.html b/modules/claim/front/photos/index.html index 9cc6c649c..9e00ee02f 100644 --- a/modules/claim/front/photos/index.html +++ b/modules/claim/front/photos/index.html @@ -1,6 +1,7 @@
+ zoom-image="{{$ctrl.getImagePath(photo.dmsFk)}}" + ng-if="photo.dms.contentType != 'video/mp4'">
+
diff --git a/modules/claim/front/photos/index.js b/modules/claim/front/photos/index.js index 2b77c6abc..62e439a91 100644 --- a/modules/claim/front/photos/index.js +++ b/modules/claim/front/photos/index.js @@ -6,6 +6,13 @@ class Controller extends Section { constructor($element, $, vnFile) { super($element, $); this.vnFile = vnFile; + this.filter = { + include: [ + { + relation: 'dms' + } + ] + }; } deleteDms(index) { @@ -13,7 +20,7 @@ class Controller extends Section { return this.$http.post(`ClaimDms/${dmsFk}/removeFile`) .then(() => { this.$.model.remove(index); - this.vnApp.showSuccess(this.$t('Photo deleted')); + this.vnApp.showSuccess(this.$t('File deleted')); }); } @@ -81,13 +88,13 @@ class Controller extends Section { data: this.dms.files }; this.$http(options).then(() => { - this.vnApp.showSuccess(this.$t('Photo uploaded!')); + this.vnApp.showSuccess(this.$t('File uploaded!')); this.$.model.refresh(); }); } getImagePath(dmsId) { - return this.vnFile.getPath(`/api/dms/${dmsId}/downloadFile`); + return this.vnFile.getPath(`/api/Claims/${dmsId}/downloadFile`); } } diff --git a/modules/claim/front/photos/locale/es.yml b/modules/claim/front/photos/locale/es.yml index 8ccc1dba4..d2ee9ffbd 100644 --- a/modules/claim/front/photos/locale/es.yml +++ b/modules/claim/front/photos/locale/es.yml @@ -1,5 +1,5 @@ Are you sure you want to continue?: ¿Seguro que quieres continuar? Drag & Drop photos here...: Arrastra y suelta fotos aquí... -Photo deleted: Foto eliminada -Photo uploaded!: Foto subida! -Select photo: Seleccionar foto \ No newline at end of file +File deleted: Archivo eliminado +File uploaded!: Archivo subido! +Select file: Seleccionar fichero \ No newline at end of file diff --git a/modules/claim/front/photos/style.scss b/modules/claim/front/photos/style.scss index d747dd9b8..101cb0da2 100644 --- a/modules/claim/front/photos/style.scss +++ b/modules/claim/front/photos/style.scss @@ -29,4 +29,19 @@ vn-claim-photos { height: 288px; } } -} \ No newline at end of file + + .video { + width: 100%; + height: 100%; + object-fit: cover; + cursor: pointer; + box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), + 0 3px 1px -2px rgba(0,0,0,.2), + 0 1px 5px 0 rgba(0,0,0,.12); + border: 2px solid transparent; + + } + .video:hover { + border: 2px solid $color-primary + } +} diff --git a/modules/client/back/methods/client/specs/addressesPropagateRe.spec.js b/modules/client/back/methods/client/specs/addressesPropagateRe.spec.js index 069c7bd25..74d80b964 100644 --- a/modules/client/back/methods/client/specs/addressesPropagateRe.spec.js +++ b/modules/client/back/methods/client/specs/addressesPropagateRe.spec.js @@ -1,6 +1,22 @@ const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); describe('Client addressesPropagateRe', () => { + beforeAll(async() => { + const activeCtx = { + accessToken: {userId: 9}, + http: { + req: { + headers: {origin: 'http://localhost'} + } + } + }; + + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + }); + it('should propagate the isEqualizated on both addresses of Mr Wayne and set hasToInvoiceByAddress to false', async() => { const tx = await models.Client.beginTransaction({}); diff --git a/modules/client/back/methods/client/specs/createAddress.spec.js b/modules/client/back/methods/client/specs/createAddress.spec.js index d6178ae0d..0841ad98c 100644 --- a/modules/client/back/methods/client/specs/createAddress.spec.js +++ b/modules/client/back/methods/client/specs/createAddress.spec.js @@ -1,4 +1,5 @@ const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); describe('Address createAddress', () => { const clientFk = 1101; @@ -6,6 +7,21 @@ describe('Address createAddress', () => { const incotermsFk = 'FAS'; const customAgentOneId = 1; + beforeAll(async() => { + const activeCtx = { + accessToken: {userId: 9}, + http: { + req: { + headers: {origin: 'http://localhost'} + } + } + }; + + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + }); + it('should throw a non uee member error if no incoterms is defined', async() => { const tx = await models.Client.beginTransaction({}); diff --git a/modules/client/back/methods/client/specs/createWithUser.spec.js b/modules/client/back/methods/client/specs/createWithUser.spec.js index 4c827c745..7d4261aee 100644 --- a/modules/client/back/methods/client/specs/createWithUser.spec.js +++ b/modules/client/back/methods/client/specs/createWithUser.spec.js @@ -1,4 +1,5 @@ const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); describe('Client Create', () => { const newAccount = { @@ -12,6 +13,21 @@ describe('Client Create', () => { businessTypeFk: 'florist' }; + beforeAll(async() => { + const activeCtx = { + accessToken: {userId: 9}, + http: { + req: { + headers: {origin: 'http://localhost'} + } + } + }; + + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + }); + it(`should not find Deadpool as he's not created yet`, async() => { const tx = await models.Client.beginTransaction({}); diff --git a/modules/client/back/methods/client/specs/updateAddress.spec.js b/modules/client/back/methods/client/specs/updateAddress.spec.js index 2ad564cc5..6f02323c5 100644 --- a/modules/client/back/methods/client/specs/updateAddress.spec.js +++ b/modules/client/back/methods/client/specs/updateAddress.spec.js @@ -1,4 +1,5 @@ const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); describe('Address updateAddress', () => { const clientId = 1101; @@ -13,6 +14,21 @@ describe('Address updateAddress', () => { } }; + beforeAll(async() => { + const activeCtx = { + accessToken: {userId: 9}, + http: { + req: { + headers: {origin: 'http://localhost'} + } + } + }; + + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + }); + it('should throw the non uee member error if no incoterms is defined', async() => { const tx = await models.Client.beginTransaction({}); @@ -93,6 +109,7 @@ describe('Address updateAddress', () => { try { const options = {transaction: tx}; + ctx.req.accessToken.userId = employeeId; ctx.args = { isLogifloraAllowed: true }; diff --git a/modules/client/back/methods/client/specs/updateFiscalData.spec.js b/modules/client/back/methods/client/specs/updateFiscalData.spec.js index 7c0bc0599..a08f97faf 100644 --- a/modules/client/back/methods/client/specs/updateFiscalData.spec.js +++ b/modules/client/back/methods/client/specs/updateFiscalData.spec.js @@ -1,10 +1,25 @@ const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); describe('Client updateFiscalData', () => { const clientId = 1101; const employeeId = 1; const salesAssistantId = 21; const administrativeId = 5; + const activeCtx = { + accessToken: {userId: employeeId}, + http: { + req: { + headers: {origin: 'http://localhost'} + } + } + }; + + beforeEach(() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + }); it('should return an error if the user is not salesAssistant and the isTaxDataChecked value is true', async() => { const tx = await models.Client.beginTransaction({}); @@ -24,7 +39,7 @@ describe('Client updateFiscalData', () => { error = e; } - expect(error.message).toEqual(`You can't make changes on a client with verified data`); + expect(error.message).toEqual(`Not enough privileges to edit a client with verified data`); }); it('should return an error if the salesAssistant did not fill the sage data before checking verified data', async() => { diff --git a/modules/client/back/methods/client/updateFiscalData.js b/modules/client/back/methods/client/updateFiscalData.js index 539d89d3a..c2de8f927 100644 --- a/modules/client/back/methods/client/updateFiscalData.js +++ b/modules/client/back/methods/client/updateFiscalData.js @@ -125,11 +125,11 @@ module.exports = Self => { } try { - const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant', myOptions); + const isAdministrative = await models.Account.hasRole(userId, 'administrative', myOptions); const client = await models.Client.findById(clientId, null, myOptions); - if (!isSalesAssistant && client.isTaxDataChecked) - throw new UserError(`You can't make changes on a client with verified data`); + if (!isAdministrative && client.isTaxDataChecked) + throw new UserError(`Not enough privileges to edit a client with verified data`); // Sage data validation const taxDataChecked = args.isTaxDataChecked; diff --git a/modules/client/back/models/client.js b/modules/client/back/models/client.js index 5ccc1ec64..5ed777ab5 100644 --- a/modules/client/back/models/client.js +++ b/modules/client/back/models/client.js @@ -46,10 +46,6 @@ module.exports = Self => { message: 'TIN must be unique' }); - Self.validatesUniquenessOf('socialName', { - message: 'The company name must be unique' - }); - Self.validatesFormatOf('email', { message: 'Invalid email', allowNull: true, @@ -63,17 +59,37 @@ module.exports = Self => { min: 3, max: 10 }); + Self.validateAsync('socialName', socialNameIsUnique, { + message: 'The company name must be unique' + }); + + async function socialNameIsUnique(err, done) { + const filter = { + where: { + and: [ + {socialName: this.socialName}, + {isActive: true}, + {id: {neq: this.id}} + ] + } + }; + const client = await Self.app.models.Client.findOne(filter); + if (client) + err(); + done(); + } + Self.validateAsync('iban', ibanNeedsValidation, { message: 'The IBAN does not have the correct format' }); async function ibanNeedsValidation(err, done) { - let filter = { + const filter = { fields: ['code'], where: {id: this.countryFk} }; - let country = await Self.app.models.Country.findOne(filter); - let code = country ? country.code.toLowerCase() : null; + const country = await Self.app.models.Country.findOne(filter); + const code = country ? country.code.toLowerCase() : null; if (code != 'es') return done(); @@ -90,12 +106,12 @@ module.exports = Self => { if (!this.isTaxDataChecked) return done(); - let filter = { + const filter = { fields: ['code'], where: {id: this.countryFk} }; - let country = await Self.app.models.Country.findOne(filter); - let code = country ? country.code.toLowerCase() : null; + const country = await Self.app.models.Country.findOne(filter); + const code = country ? country.code.toLowerCase() : null; if (!this.fi || !validateTin(this.fi, code)) err(); @@ -118,8 +134,8 @@ module.exports = Self => { function cannotHaveET(err) { if (!this.fi) return; - let tin = this.fi.toUpperCase(); - let cannotHaveET = /^[A-B]/.test(tin); + const tin = this.fi.toUpperCase(); + const cannotHaveET = /^[A-B]/.test(tin); if (cannotHaveET && this.isEqualizated) err(); @@ -208,6 +224,34 @@ module.exports = Self => { throw new UserError(`The type of business must be filled in basic data`); }); + Self.observe('before save', async ctx => { + const changes = ctx.data || ctx.instance; + const orgData = ctx.currentInstance; + const models = Self.app.models; + + const loopBackContext = LoopBackContext.getCurrentContext(); + const userId = loopBackContext.active.accessToken.userId; + + const isAdministrative = await models.Account.hasRole(userId, 'administrative', ctx.options); + const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant', ctx.options); + const hasChanges = orgData && changes; + + const isTaxDataChecked = hasChanges && (changes.isTaxDataChecked || orgData.isTaxDataChecked); + const isTaxDataCheckedChanged = hasChanges && orgData.isTaxDataChecked != isTaxDataChecked; + + const sageTaxType = hasChanges && (changes.sageTaxTypeFk || orgData.sageTaxTypeFk); + const sageTaxTypeChanged = hasChanges && orgData.sageTaxTypeFk != sageTaxType; + + const sageTransactionType = hasChanges && (changes.sageTransactionTypeFk || orgData.sageTransactionTypeFk); + const sageTransactionTypeChanged = hasChanges && orgData.sageTransactionTypeFk != sageTransactionType; + + const cantEditVerifiedData = isTaxDataCheckedChanged && !isAdministrative; + const cantChangeSageData = (sageTaxTypeChanged || sageTransactionTypeChanged) && !isSalesAssistant; + + if (cantEditVerifiedData || cantChangeSageData) + throw new UserError(`You don't have enough privileges`); + }); + Self.observe('before save', async function(ctx) { const changes = ctx.data || ctx.instance; const orgData = ctx.currentInstance; @@ -221,10 +265,10 @@ module.exports = Self => { const socialNameChanged = hasChanges && orgData.socialName != socialName; - const dataCheckedChanged = hasChanges + const isTaxDataCheckedChanged = hasChanges && orgData.isTaxDataChecked != isTaxDataChecked; - if ((socialNameChanged || dataCheckedChanged) && !isAlpha(socialName)) + if ((socialNameChanged || isTaxDataCheckedChanged) && !isAlpha(socialName)) throw new UserError(`The social name has an invalid format`); if (changes.salesPerson === null) { diff --git a/modules/client/back/models/specs/address.spec.js b/modules/client/back/models/specs/address.spec.js index cae18258f..81af6ee28 100644 --- a/modules/client/back/models/specs/address.spec.js +++ b/modules/client/back/models/specs/address.spec.js @@ -1,20 +1,36 @@ -const app = require('vn-loopback/server/server'); +const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); describe('loopback model address', () => { let createdAddressId; const clientId = 1101; - afterAll(async() => { - let client = await app.models.Client.findById(clientId); + const activeCtx = { + accessToken: {userId: 9}, + http: { + req: { + headers: {origin: 'http://localhost'} + } + } + }; - await app.models.Address.destroyById(createdAddressId); + beforeEach(() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + }); + + afterAll(async() => { + const client = await models.Client.findById(clientId); + + await models.Address.destroyById(createdAddressId); await client.updateAttribute('isEqualizated', false); }); describe('observe()', () => { it('should throw an error when deactivating a consignee if its the default address', async() => { let error; - let address = await app.models.Address.findById(1); + const address = await models.Address.findById(1); await address.updateAttribute('isActive', false) .catch(e => { @@ -27,13 +43,13 @@ describe('loopback model address', () => { }); it('should set isEqualizated to true of a given Client to trigger any new address to have it', async() => { - let client = await app.models.Client.findById(clientId); + const client = await models.Client.findById(clientId); expect(client.isEqualizated).toBeFalsy(); await client.updateAttribute('isEqualizated', true); - let newAddress = await app.models.Address.create({ + const newAddress = await models.Address.create({ clientFk: clientId, agencyModeFk: 5, city: 'here', diff --git a/modules/client/front/descriptor/index.html b/modules/client/front/descriptor/index.html index de7a86d3b..cad226416 100644 --- a/modules/client/front/descriptor/index.html +++ b/modules/client/front/descriptor/index.html @@ -105,7 +105,7 @@
diff --git a/modules/client/front/descriptor/locale/es.yml b/modules/client/front/descriptor/locale/es.yml index e6aca9665..71723e654 100644 --- a/modules/client/front/descriptor/locale/es.yml +++ b/modules/client/front/descriptor/locale/es.yml @@ -3,5 +3,6 @@ View consumer report: Ver informe de consumo From date: Fecha desde To date: Fecha hasta Go to user: Ir al usuario +Go to supplier: Ir al proveedor Client invoices list: Listado de facturas del cliente Pay method: Forma de pago \ No newline at end of file diff --git a/modules/client/front/fiscal-data/index.html b/modules/client/front/fiscal-data/index.html index f3fd42e76..2249127c5 100644 --- a/modules/client/front/fiscal-data/index.html +++ b/modules/client/front/fiscal-data/index.html @@ -33,7 +33,7 @@ label="Social name" ng-model="$ctrl.client.socialName" rule - info="You can use letters and spaces" + info="Only letters, numbers and spaces can be used" required="true"> + vn-acl="administrative"> diff --git a/modules/client/front/fiscal-data/locale/es.yml b/modules/client/front/fiscal-data/locale/es.yml index 18c24604a..ed107ef4b 100644 --- a/modules/client/front/fiscal-data/locale/es.yml +++ b/modules/client/front/fiscal-data/locale/es.yml @@ -3,7 +3,7 @@ You changed the equalization tax: Has cambiado el recargo de equivalencia Do you want to spread the change?: ¿Deseas propagar el cambio a sus consignatarios? Frozen: Congelado In order to invoice, this field is not consulted, but the consignee's ET. When modifying this field if the invoice by address option is not checked, the change will be automatically propagated to all addresses, otherwise the user will be asked if he wants to propagate it or not.: Para facturar no se consulta este campo, sino el RE de consignatario. Al modificar este campo si no esta marcada la casilla Facturar por consignatario, se propagará automáticamente el cambio a todos los consignatarios, en caso contrario preguntará al usuario si quiere o no propagar. -You can use letters and spaces: Se pueden utilizar letras y espacios +Only letters, numbers and spaces can be used: Sólo se pueden usar letras, numeros y espacios Found a client with this data: Se ha encontrado un cliente con estos datos Found a client with this phone or email: El cliente con id {{clientId}} ya tiene este teléfono o email.
¿Quieres continuar? Sage tax type: Tipo de impuesto Sage diff --git a/modules/client/front/notification/index.js b/modules/client/front/notification/index.js index 12a1a4acb..0506ea4ba 100644 --- a/modules/client/front/notification/index.js +++ b/modules/client/front/notification/index.js @@ -81,7 +81,7 @@ export default class Controller extends Section { clientIds: clientIds }, this.campaign); - this.$http.post('notify/consumption', params) + this.$http.post('schedule/consumption', params) .then(() => this.$.filters.hide()) .then(() => this.vnApp.showSuccess(this.$t('Notifications sent!'))); } diff --git a/modules/client/front/notification/index.spec.js b/modules/client/front/notification/index.spec.js index 13c6bc513..8847357f7 100644 --- a/modules/client/front/notification/index.spec.js +++ b/modules/client/front/notification/index.spec.js @@ -74,7 +74,7 @@ describe('Client notification', () => { clientIds: [1101, 1102] }, controller.campaign); - $httpBackend.expect('POST', `notify/consumption`, params).respond(200, params); + $httpBackend.expect('POST', `schedule/consumption`, params).respond(200, params); controller.onSendClientConsumption(); $httpBackend.flush(); diff --git a/modules/entry/back/methods/entry/editLatestBuys.js b/modules/entry/back/methods/entry/editLatestBuys.js index fb0397d2b..72bee98ae 100644 --- a/modules/entry/back/methods/entry/editLatestBuys.js +++ b/modules/entry/back/methods/entry/editLatestBuys.js @@ -1,27 +1,32 @@ module.exports = Self => { - Self.remoteMethod('editLatestBuys', { + Self.remoteMethodCtx('editLatestBuys', { description: 'Updates a column for one or more buys', accessType: 'WRITE', accepts: [{ arg: 'field', - type: 'String', + type: 'string', required: true, description: `the column to edit` }, { arg: 'newValue', - type: 'Any', + type: 'any', required: true, description: `The new value to save` }, { arg: 'lines', - type: ['Object'], + type: ['object'], required: true, description: `the buys which will be modified` + }, + { + arg: 'filter', + type: 'object', + description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string' }], returns: { - type: 'Object', + type: 'object', root: true }, http: { @@ -30,7 +35,7 @@ module.exports = Self => { } }); - Self.editLatestBuys = async(field, newValue, lines, options) => { + Self.editLatestBuys = async(ctx, field, newValue, lines, filter, options) => { let tx; const myOptions = {}; @@ -64,17 +69,19 @@ module.exports = Self => { const models = Self.app.models; const model = models[modelName]; - try { const promises = []; + const value = {}; + value[field] = newValue; + + if (filter) { + ctx.args.filter = {where: filter, limit: null}; + lines = await models.Buy.latestBuysFilter(ctx, null, myOptions); + } const targets = lines.map(line => { return line[identifier]; }); - - const value = {}; - value[field] = newValue; - for (let target of targets) promises.push(model.upsertWithWhere({id: target}, value, myOptions)); diff --git a/modules/entry/back/methods/entry/latestBuysFilter.js b/modules/entry/back/methods/entry/latestBuysFilter.js index 04570533c..9693670c8 100644 --- a/modules/entry/back/methods/entry/latestBuysFilter.js +++ b/modules/entry/back/methods/entry/latestBuysFilter.js @@ -86,7 +86,7 @@ module.exports = Self => { } ], returns: { - type: ['Object'], + type: ['object'], root: true }, http: { @@ -98,6 +98,9 @@ module.exports = Self => { Self.latestBuysFilter = async(ctx, filter, options) => { const myOptions = {}; + if (filter && filter.modelParams) + ctx.args = filter.modelParams; + if (typeof options == 'object') Object.assign(myOptions, options); diff --git a/modules/entry/back/methods/entry/specs/editLatestBuys.spec.js b/modules/entry/back/methods/entry/specs/editLatestBuys.spec.js index b17b0ab21..2413c18f6 100644 --- a/modules/entry/back/methods/entry/specs/editLatestBuys.spec.js +++ b/modules/entry/back/methods/entry/specs/editLatestBuys.spec.js @@ -6,7 +6,7 @@ describe('Buy editLatestsBuys()', () => { const options = {transaction: tx}; try { - let ctx = { + const ctx = { args: { search: 'Ranged weapon longbow 2m' } @@ -18,7 +18,35 @@ describe('Buy editLatestsBuys()', () => { const newValue = 99; const lines = [{itemFk: original.itemFk, id: original.id}]; - await models.Buy.editLatestBuys(field, newValue, lines, options); + await models.Buy.editLatestBuys(ctx, field, newValue, lines, null, options); + + const [result] = await models.Buy.latestBuysFilter(ctx, null, options); + + expect(result[field]).toEqual(newValue); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should change the value of a given column for filter', async() => { + const tx = await models.Buy.beginTransaction({}); + const options = {transaction: tx}; + + try { + const filter = {typeFk: 1}; + const ctx = { + args: { + filter: filter + } + }; + + const field = 'size'; + const newValue = 88; + + await models.Buy.editLatestBuys(ctx, field, newValue, null, filter, options); const [result] = await models.Buy.latestBuysFilter(ctx, null, options); diff --git a/modules/entry/front/latest-buys/index.html b/modules/entry/front/latest-buys/index.html index 4eeeeedce..a4d6f7e83 100644 --- a/modules/entry/front/latest-buys/index.html +++ b/modules/entry/front/latest-buys/index.html @@ -29,9 +29,11 @@ + checked="$ctrl.checkAll" + check-field="checked" + check-dummy-enabled="true" + checked-dummy-count="$ctrl.checkedDummyCount"> Picture @@ -126,7 +128,7 @@ }"> diff --git a/modules/entry/front/latest-buys/index.js b/modules/entry/front/latest-buys/index.js index 385e7e4b6..44c29cb11 100644 --- a/modules/entry/front/latest-buys/index.js +++ b/modules/entry/front/latest-buys/index.js @@ -6,7 +6,7 @@ export default class Controller extends Section { constructor($element, $) { super($element, $); this.editedColumn; - this.$checkAll = false; + this.checkAll = false; this.smartTableOptions = { activeButtons: { @@ -91,7 +91,7 @@ export default class Controller extends Section { const buys = this.$.model.data || []; const checkedBuys = []; for (let buy of buys) { - if (buy.$checked) + if (buy.checked) checkedBuys.push(buy); } @@ -142,6 +142,9 @@ export default class Controller extends Section { } get totalChecked() { + if (this.checkedDummyCount) + return this.checkedDummyCount; + return this.checked.length; } @@ -156,6 +159,9 @@ export default class Controller extends Section { lines: rowsToEdit }; + if (this.checkedDummyCount && this.checkedDummyCount > 0) + data.filter = this.$.model.userParams; + return this.$http.post('Buys/editLatestBuys', data) .then(() => { this.uncheck(); diff --git a/modules/entry/front/latest-buys/index.spec.js b/modules/entry/front/latest-buys/index.spec.js index e9392a797..76e122d80 100644 --- a/modules/entry/front/latest-buys/index.spec.js +++ b/modules/entry/front/latest-buys/index.spec.js @@ -31,10 +31,10 @@ describe('Entry', () => { describe('get checked', () => { it(`should return a set of checked lines`, () => { controller.$.model.data = [ - {$checked: true, id: 1}, - {$checked: true, id: 2}, - {$checked: true, id: 3}, - {$checked: false, id: 4}, + {checked: true, id: 1}, + {checked: true, id: 2}, + {checked: true, id: 3}, + {checked: false, id: 4}, ]; let result = controller.checked; diff --git a/modules/entry/front/summary/index.html b/modules/entry/front/summary/index.html index f443a94fe..a95b2f18a 100644 --- a/modules/entry/front/summary/index.html +++ b/modules/entry/front/summary/index.html @@ -1,6 +1,6 @@ diff --git a/modules/entry/front/summary/index.js b/modules/entry/front/summary/index.js index c949dba64..6e18bc959 100644 --- a/modules/entry/front/summary/index.js +++ b/modules/entry/front/summary/index.js @@ -4,6 +4,9 @@ import Summary from 'salix/components/summary'; class Controller extends Summary { get entry() { + if (!this._entry) + return this.$params; + return this._entry; } diff --git a/modules/invoiceIn/back/methods/invoice-in/filter.js b/modules/invoiceIn/back/methods/invoice-in/filter.js index 7e497d39a..f5eab9099 100644 --- a/modules/invoiceIn/back/methods/invoice-in/filter.js +++ b/modules/invoiceIn/back/methods/invoice-in/filter.js @@ -144,6 +144,7 @@ module.exports = Self => { ii.isBooked, ii.supplierRef, ii.docFk AS dmsFk, + dm.file, ii.supplierFk, ii.expenceFkDeductible deductibleExpenseFk, s.name AS supplierName, @@ -156,7 +157,8 @@ module.exports = Self => { LEFT JOIN duaInvoiceIn dii ON dii.invoiceInFk = ii.id LEFT JOIN dua d ON d.id = dii.duaFk LEFT JOIN awb ON awb.id = d.awbFk - LEFT JOIN company co ON co.id = ii.companyFk` + LEFT JOIN company co ON co.id = ii.companyFk + LEFT JOIN dms dm ON dm.id = ii.docFk` ); const sqlWhere = conn.makeWhere(filter.where); diff --git a/modules/invoiceIn/back/methods/invoice-in/getTotals.js b/modules/invoiceIn/back/methods/invoice-in/getTotals.js index 918467010..f35c10617 100644 --- a/modules/invoiceIn/back/methods/invoice-in/getTotals.js +++ b/modules/invoiceIn/back/methods/invoice-in/getTotals.js @@ -30,7 +30,7 @@ module.exports = Self => { SUM(iidd.amount) totalDueDay FROM vn.invoiceIn ii LEFT JOIN (SELECT SUM(iit.taxableBase) totalTaxableBase, - SUM(iit.taxableBase * (1 + (ti.PorcentajeIva / 100))) totalVat + CAST(SUM(iit.taxableBase * (1 + (ti.PorcentajeIva / 100))) AS DECIMAL(10,2)) totalVat FROM vn.invoiceInTax iit LEFT JOIN sage.TiposIva ti ON ti.CodigoIva = iit.taxTypeSageFk WHERE iit.invoiceInFk = ?) iit ON TRUE diff --git a/modules/invoiceIn/front/create/index.html b/modules/invoiceIn/front/create/index.html index 4ec724854..16ecf26cf 100644 --- a/modules/invoiceIn/front/create/index.html +++ b/modules/invoiceIn/front/create/index.html @@ -13,13 +13,14 @@ vn-id="supplier" url="Suppliers" label="Supplier" - search-function="{or: [{id: $search}, {name: {like: '%'+ $search +'%'}}]}" + search-function="{or: [{id: $search}, {name: {like: '%'+ $search +'%'}}, {nif: {like: '%'+ $search +'%'}}]}" + fields="['nif']" show-field="name" value-field="id" ng-model="$ctrl.invoiceIn.supplierFk" order="id" vn-focus> - {{id}}: {{name}} + {{id}}: {{nif}}: {{name}} - - Supplier ref. Serial number Serial - Account + File Issued Is booked AWB @@ -27,7 +27,7 @@ class="clickable vn-tr search-result" ui-sref="invoiceIn.card.summary({id: {{::invoiceIn.id}}})"> {{::invoiceIn.id}} - + @@ -36,8 +36,13 @@ {{::invoiceIn.supplierRef | dashIfEmpty}} {{::invoiceIn.serialNumber}} - {{::invoiceIn.serial}} - {{::invoiceIn.account}} + {{::invoiceIn.serial}} + + + {{::invoiceIn.file}} + + {{::invoiceIn.issued | date:'dd/MM/yyyy' | dashIfEmpty}} { Self.remoteMethodCtx('globalInvoicing', { description: 'Make a global invoice', @@ -140,6 +138,9 @@ module.exports = Self => { if (newInvoice.id) { await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], myOptions); + query = `INSERT IGNORE INTO invoiceOut_queue(invoiceFk) VALUES(?)`; + await Self.rawSql(query, [newInvoice.id], myOptions); + invoicesIds.push(newInvoice.id); } } catch (e) { @@ -160,10 +161,6 @@ module.exports = Self => { throw e; } - // Print invoices PDF - for (let invoiceId of invoicesIds) - await Self.createPdf(ctx, invoiceId); - return invoicesIds; }; @@ -212,7 +209,13 @@ module.exports = Self => { const query = `SELECT c.id, - SUM(IFNULL(s.quantity * s.price * (100-s.discount)/100, 0) + IFNULL(ts.quantity * ts.price,0)) AS sumAmount, + SUM(IFNULL + ( + s.quantity * + s.price * (100-s.discount)/100, + 0) + + IFNULL(ts.quantity * ts.price,0) + ) AS sumAmount, c.hasToInvoiceByAddress, c.email, c.isToBeMailed, diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/createManualInvoice.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/createManualInvoice.spec.js index f1c1bbab8..b166caf78 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/specs/createManualInvoice.spec.js +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/createManualInvoice.spec.js @@ -93,6 +93,9 @@ describe('InvoiceOut createManualInvoice()', () => { it('should throw an error for a non-invoiceable client', async() => { spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true))); + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); const tx = await models.InvoiceOut.beginTransaction({}); const options = {transaction: tx}; diff --git a/modules/invoiceOut/front/index/global-invoicing/index.html b/modules/invoiceOut/front/index/global-invoicing/index.html index 9fd412d0e..3d245b8d8 100644 --- a/modules/invoiceOut/front/index/global-invoicing/index.html +++ b/modules/invoiceOut/front/index/global-invoicing/index.html @@ -19,7 +19,7 @@ ng-if="$ctrl.isInvoicing"> - Invoicing in progress... + Adding invoices to queue...
@@ -66,5 +66,5 @@ - + \ No newline at end of file diff --git a/modules/invoiceOut/front/index/global-invoicing/locale/es.yml b/modules/invoiceOut/front/index/global-invoicing/locale/es.yml index 51e165e2a..1a6e15656 100644 --- a/modules/invoiceOut/front/index/global-invoicing/locale/es.yml +++ b/modules/invoiceOut/front/index/global-invoicing/locale/es.yml @@ -1,7 +1,7 @@ Create global invoice: Crear factura global Some fields are required: Algunos campos son obligatorios Max date: Fecha límite -Invoicing in progress...: Facturación en progreso... +Adding invoices to queue...: Añadiendo facturas a la cola... Invoice date: Fecha de factura From client: Desde el cliente To client: Hasta el cliente diff --git a/modules/monitor/front/index/tickets/index.html b/modules/monitor/front/index/tickets/index.html index 968215e67..a504301a5 100644 --- a/modules/monitor/front/index/tickets/index.html +++ b/modules/monitor/front/index/tickets/index.html @@ -50,7 +50,7 @@ Salesperson - + Date @@ -153,8 +153,8 @@ - - {{::ticket.shipped | date: 'dd/MM/yyyy'}} + + {{::ticket.shippedDate | date: 'dd/MM/yyyy'}} {{::ticket.zoneLanding | date: 'HH:mm'}} diff --git a/modules/monitor/front/index/tickets/index.js b/modules/monitor/front/index/tickets/index.js index 58b56bbc5..0770d8634 100644 --- a/modules/monitor/front/index/tickets/index.js +++ b/modules/monitor/front/index/tickets/index.js @@ -52,7 +52,7 @@ export default class Controller extends Section { } }, { - field: 'shipped', + field: 'shippedDate', searchable: false }, { @@ -136,7 +136,7 @@ export default class Controller extends Section { return {'z.hour': value}; case 'practicalHour': return {'zed.etc': value}; - case 'shipped': + case 'shippedDate': return {'t.shipped': { between: this.dateRange(value)} }; diff --git a/modules/order/front/index/index.html b/modules/order/front/index/index.html index a2a4a5226..c4bed7307 100644 --- a/modules/order/front/index/index.html +++ b/modules/order/front/index/index.html @@ -33,7 +33,7 @@ {{::order.name | dashIfEmpty}} - + @@ -46,7 +46,7 @@ disabled="true"> - {{::order.created | date: 'dd/MM/yyyy HH:mm'}} + {{::order.created | date: 'dd/MM/yyyy HH:mm'}} {{::order.landed | date:'dd/MM/yyyy'}} diff --git a/modules/supplier/back/methods/supplier/getSummary.js b/modules/supplier/back/methods/supplier/getSummary.js index 67f5267b6..c29a2a058 100644 --- a/modules/supplier/back/methods/supplier/getSummary.js +++ b/modules/supplier/back/methods/supplier/getSummary.js @@ -100,12 +100,7 @@ module.exports = Self => { }, ] }; - let supplier = await Self.app.models.Supplier.findOne(filter); - const farmerCode = 2; - if (supplier.sageWithholdingFk == farmerCode) - supplier.isFarmer = true; - - return supplier; + return Self.app.models.Supplier.findOne(filter); }; }; diff --git a/modules/supplier/back/methods/supplier/specs/getSummary.spec.js b/modules/supplier/back/methods/supplier/specs/getSummary.spec.js index 21e56882f..30713f517 100644 --- a/modules/supplier/back/methods/supplier/specs/getSummary.spec.js +++ b/modules/supplier/back/methods/supplier/specs/getSummary.spec.js @@ -25,12 +25,4 @@ describe('Supplier getSummary()', () => { expect(payMethod.name).toEqual('PayMethod one'); }); - - it(`should get if supplier is farmer by sageWithholdingFk`, async() => { - const supplier = await app.models.Supplier.findById(2); - const supplierSummary = await app.models.Supplier.getSummary(2); - - expect(supplier.isFarmer).toBeUndefined(); - expect(supplierSummary.isFarmer).toEqual(true); - }); }); diff --git a/modules/supplier/front/agency-term/index/index.html b/modules/supplier/front/agency-term/index/index.html index ac3b9b0d5..9d53226c5 100644 --- a/modules/supplier/front/agency-term/index/index.html +++ b/modules/supplier/front/agency-term/index/index.html @@ -78,7 +78,7 @@ diff --git a/modules/supplier/front/fiscal-data/index.html b/modules/supplier/front/fiscal-data/index.html index fc44468f4..4f34528f2 100644 --- a/modules/supplier/front/fiscal-data/index.html +++ b/modules/supplier/front/fiscal-data/index.html @@ -38,7 +38,7 @@ vn-focus label="Social name" ng-model="$ctrl.supplier.name" - info="You can use letters and spaces" + info="Only letters, numbers and spaces can be used" required="true" rule> diff --git a/modules/supplier/front/summary/index.html b/modules/supplier/front/summary/index.html index 51ebde695..086cd844d 100644 --- a/modules/supplier/front/summary/index.html +++ b/modules/supplier/front/summary/index.html @@ -83,11 +83,6 @@ label="Account" value="{{::$ctrl.summary.account}}"> - - diff --git a/modules/zone/front/delivery-days/index.js b/modules/zone/front/delivery-days/index.js index e4d5e72e9..71e8c8ab7 100644 --- a/modules/zone/front/delivery-days/index.js +++ b/modules/zone/front/delivery-days/index.js @@ -73,15 +73,14 @@ class Controller extends Section { for (let event of $events) zoneIds.push(event.zoneFk); - this.$.zoneEvents.show($event.target); - const params = { zoneIds: zoneIds, date: day }; this.$http.post(`Zones/getZoneClosing`, params) - .then(res => this.zoneClosing = res.data); + .then(res => this.zoneClosing = res.data) + .then(() => this.$.zoneEvents.show($event.target)); } preview(zone) { diff --git a/print/methods/routes.js b/print/methods/routes.js index ea40e0743..28671b3da 100644 --- a/print/methods/routes.js +++ b/print/methods/routes.js @@ -16,7 +16,7 @@ module.exports = [ cb: require('./closure') }, { - url: '/api/notify', - cb: require('./notify') + url: '/api/schedule', + cb: require('./schedule') } ]; diff --git a/print/methods/notify/consumption.js b/print/methods/schedule/consumption.js similarity index 100% rename from print/methods/notify/consumption.js rename to print/methods/schedule/consumption.js diff --git a/print/methods/notify/index.js b/print/methods/schedule/index.js similarity index 76% rename from print/methods/notify/index.js rename to print/methods/schedule/index.js index df4705d1e..05d54b2ed 100644 --- a/print/methods/notify/index.js +++ b/print/methods/schedule/index.js @@ -2,5 +2,6 @@ const express = require('express'); const router = new express.Router(); router.post('/consumption', require('./consumption')); +router.post('/invoice', require('./invoice')); module.exports = router; diff --git a/print/methods/schedule/invoice.js b/print/methods/schedule/invoice.js new file mode 100644 index 000000000..c76ca85b5 --- /dev/null +++ b/print/methods/schedule/invoice.js @@ -0,0 +1,114 @@ +const db = require('vn-print/core/database'); +const Email = require('vn-print/core/email'); +const Report = require('vn-print/core/report'); +const storage = require('vn-print/core/storage'); + +module.exports = async function(request, response, next) { + try { + response.status(200).json({ + message: 'Success' + }); + + const invoices = await db.rawSql(` + SELECT + io.id, + io.clientFk, + io.issued, + io.ref, + c.email recipient, + c.salesPersonFk, + c.isToBeMailed, + c.hasToInvoice, + co.hasDailyInvoice, + eu.email salesPersonEmail + FROM invoiceOut_queue ioq + JOIN invoiceOut io ON io.id = ioq.invoiceFk + JOIN client c ON c.id = io.clientFk + JOIN province p ON p.id = c.provinceFk + JOIN country co ON co.id = p.countryFk + LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk + WHERE status = ''`); + + let connection; + let invoiceId; + for (const invoiceOut of invoices) { + try { + invoiceId = invoiceOut.id; + connection = await db.getConnection(); + connection.query('START TRANSACTION'); + + const args = Object.assign({ + invoiceId: invoiceOut.id, + recipientId: invoiceOut.clientFk, + recipient: invoiceOut.recipient, + replyTo: invoiceOut.salesPersonEmail + }, response.locals); + + const invoiceReport = new Report('invoice', args); + const stream = await invoiceReport.toPdfStream(); + + const issued = invoiceOut.issued; + const year = issued.getFullYear().toString(); + const month = (issued.getMonth() + 1).toString(); + const day = issued.getDate().toString(); + + const fileName = `${year}${invoiceOut.ref}.pdf`; + + // Store invoice + storage.write(stream, { + type: 'invoice', + path: `${year}/${month}/${day}`, + fileName: fileName + }); + + connection.query('UPDATE invoiceOut SET hasPdf = true WHERE id = ?', [invoiceOut.id]); + + const isToBeMailed = invoiceOut.recipient && invoiceOut.salesPersonFk && invoiceOut.isToBeMailed; + + if (isToBeMailed) { + const mailOptions = { + overrideAttachments: true, + attachments: [] + }; + + const invoiceAttachment = { + filename: fileName, + content: stream + }; + + if (invoiceOut.serial == 'E' && invoiceOut.companyCode == 'VNL') { + const exportation = new Report('exportation', args); + const stream = await exportation.toPdfStream(); + const fileName = `CITES-${invoiceOut.ref}.pdf`; + + mailOptions.attachments.push({ + filename: fileName, + content: stream + }); + } + + mailOptions.attachments.push(invoiceAttachment); + + const email = new Email('invoice', args); + await email.send(mailOptions); + } + // Update queue status + sql = `UPDATE invoiceOut_queue + SET status = "printed", + printed = NOW() + WHERE invoiceFk = ?`; + connection.query(sql, [invoiceOut.id]); + connection.query('COMMIT'); + } catch (error) { + connection.query('ROLLBACK'); + connection.release(); + sql = `UPDATE invoiceOut_queue + SET status = ? + WHERE invoiceFk = ?`; + await db.rawSql(sql, [error.message, invoiceId]); + } + } + } catch (error) { + next(error); + } +}; diff --git a/print/templates/reports/delivery-note/delivery-note.js b/print/templates/reports/delivery-note/delivery-note.js index adda6e756..f9dba0578 100755 --- a/print/templates/reports/delivery-note/delivery-note.js +++ b/print/templates/reports/delivery-note/delivery-note.js @@ -7,6 +7,10 @@ const fs = require('fs-extra'); module.exports = { name: 'delivery-note', + created() { + if (!this.type) + this.type = 'deliveryNote'; + }, async serverPrefetch() { this.client = await this.fetchClient(this.ticketId); this.ticket = await this.fetchTicket(this.ticketId); @@ -129,7 +133,7 @@ module.exports = { }, type: { type: String, - required: true + required: false } } }; diff --git a/storage/dms/8f1/7.jpg b/storage/dms/8f1/7.jpg new file mode 100644 index 000000000..83052dea6 Binary files /dev/null and b/storage/dms/8f1/7.jpg differ diff --git a/storage/dms/c9f/8.mp4 b/storage/dms/c9f/8.mp4 new file mode 100644 index 000000000..b11552f9c Binary files /dev/null and b/storage/dms/c9f/8.mp4 differ