diff --git a/.vscode/settings.json b/.vscode/settings.json index 159cecdc9..05d23f3bb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,9 @@ "source.fixAll.eslint": true }, "search.useIgnoreFiles": false, - "editor.defaultFormatter": "dbaeumer.vscode-eslint" + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "eslint.format.enable": true, + "[javascript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c993b657..a346591d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2314.01] - 2023-04-20 + +### Added +- (Clientes -> Morosos) Ahora se puede filtrar por las columnas "Desde" y "Fecha Ú. O.". También se envia un email al comercial cuando se añade una nota. +- (Monitor tickets) Muestra un icono al lado de la zona, si el ticket es frágil y se envía por agencia +- (Facturas recibidas -> Bases negativas) Nueva sección + +### Changed +- + +### Fixed +- (Clientes -> Morosos) Ahora se mantienen los elementos seleccionados al hacer sroll. + ## [2312.01] - 2023-04-06 ### Added @@ -15,9 +28,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - (Envíos -> Extra comunitarios) Se agrupan las entradas del mismo travel. Añadidos campos Referencia y Importe. - (Envíos -> Índice) Cambiado el buscador superior por uno lateral -### Fixed -- - ## [2310.01] - 2023-03-23 ### Added diff --git a/db/changes/231201/00-mailACL.sql b/db/changes/231201/00-mailACL.sql new file mode 100644 index 000000000..ac687818d --- /dev/null +++ b/db/changes/231201/00-mailACL.sql @@ -0,0 +1,2 @@ +INSERT INTO `salix`.`ACL` ( model, property, accessType, permission, principalType, principalId) +VALUES('Mail', '*', '*', 'ALLOW', 'ROLE', 'employee'); diff --git a/db/changes/231401/00-claimBeginningAfterInsert.sql b/db/changes/231401/00-claimBeginningAfterInsert.sql new file mode 100644 index 000000000..230b6defb --- /dev/null +++ b/db/changes/231401/00-claimBeginningAfterInsert.sql @@ -0,0 +1 @@ +DROP TRIGGER IF EXISTS `vn`.`claimBeginning_afterInsert`; diff --git a/db/changes/231401/00-clientBeforeUpdate.sql b/db/changes/231401/00-clientBeforeUpdate.sql new file mode 100644 index 000000000..8f9f70dd5 --- /dev/null +++ b/db/changes/231401/00-clientBeforeUpdate.sql @@ -0,0 +1,72 @@ +DROP TRIGGER IF EXISTS `vn`.`client_beforeUpdate`; +USE `vn`; + +DELIMITER $$ +$$ +CREATE DEFINER=`root`@`localhost` TRIGGER `vn`.`client_beforeUpdate` + BEFORE UPDATE ON `client` + FOR EACH ROW +BEGIN + DECLARE vText VARCHAR(255) DEFAULT NULL; + DECLARE vPayMethodFk INT; + -- Comprueba que el formato de los teléfonos es válido + + IF !(NEW.phone <=> OLD.phone) AND (NEW.phone <> '') THEN + CALL pbx.phone_isValid(NEW.phone); + END IF; + + IF !(NEW.mobile <=> OLD.mobile) AND (NEW.mobile <> '')THEN + CALL pbx.phone_isValid(NEW.mobile); + END IF; + + SELECT id INTO vPayMethodFk + FROM vn.payMethod + WHERE code = 'bankDraft'; + + IF NEW.payMethodFk = vPayMethodFk AND NEW.dueDay = 0 THEN + SET NEW.dueDay = 5; + END IF; + + -- Avisar al comercial si ha llegado la documentación sepa/core + + IF NEW.hasSepaVnl AND !OLD.hasSepaVnl THEN + SET vText = 'Sepa de VNL'; + END IF; + + IF NEW.hasCoreVnl AND !OLD.hasCoreVnl THEN + SET vText = 'Core de VNL'; + END IF; + + IF vText IS NOT NULL + THEN + INSERT INTO mail(receiver, replyTo, `subject`, body) + SELECT + CONCAT(IF(ac.id,u.name, 'jgallego'), '@verdnatura.es'), + 'administracion@verdnatura.es', + CONCAT('Cliente ', NEW.id), + CONCAT('Recibida la documentación: ', vText) + FROM worker w + LEFT JOIN account.user u ON w.userFk = u.id AND u.active + LEFT JOIN account.account ac ON ac.id = u.id + WHERE w.id = NEW.salesPersonFk; + END IF; + + IF NEW.salespersonFk IS NULL AND OLD.salespersonFk IS NOT NULL THEN + IF (SELECT COUNT(clientFk) + FROM clientProtected + WHERE clientFk = NEW.id + ) > 0 THEN + CALL util.throw("HAS_CLIENT_PROTECTED"); + END IF; + END IF; + + IF !(NEW.salesPersonFk <=> OLD.salesPersonFk) THEN + SET NEW.lastSalesPersonFk = IFNULL(NEW.salesPersonFk, OLD.salesPersonFk); + END IF; + + IF !(NEW.businessTypeFk <=> OLD.businessTypeFk) AND (NEW.businessTypeFk = 'individual' OR OLD.businessTypeFk = 'individual') THEN + SET NEW.isTaxDataChecked = 0; + END IF; + +END$$ +DELIMITER ; diff --git a/db/changes/231401/00-hotfixDelivery.sql b/db/changes/231401/00-hotfixDelivery.sql new file mode 100644 index 000000000..9f7080764 --- /dev/null +++ b/db/changes/231401/00-hotfixDelivery.sql @@ -0,0 +1,76 @@ +DROP TABLE IF EXISTS `vn`.`dmsRecover`; +DROP PROCEDURE IF EXISTS `vn`.`route_getTickets`; + +DELIMITER $$ +$$ +CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`route_getTickets`(vRouteFk INT) +BEGIN +/** + * Pasado un RouteFk devuelve la información + * de sus tickets. + * + * @param vRouteFk + * @select Información de los tickets + */ +SELECT * + FROM ( + SELECT t.id Id, + t.clientFk Client, + a.id Address, + a.nickname ClientName, + t.packages Packages, + a.street AddressName, + a.postalCode PostalCode, + a.city City, + sub2.itemPackingTypeFk PackingType, + c.phone ClientPhone, + c.mobile ClientMobile, + a.phone AddressPhone, + a.mobile AddressMobile, + d.longitude Longitude, + d.latitude Latitude, + wm.mediaValue SalePersonPhone, + tob.description Note, + t.isSigned Signed, + t.priority + FROM ticket t + JOIN client c ON t.clientFk = c.id + JOIN address a ON t.addressFk = a.id + LEFT JOIN delivery d ON d.ticketFk = t.id + LEFT JOIN workerMedia wm ON wm.workerFk = c.salesPersonFk + LEFT JOIN( + SELECT tob.description, t.id + FROM ticketObservation tob + JOIN ticket t ON tob.ticketFk = t.id + JOIN observationType ot ON ot.id = tob.observationTypeFk + WHERE t.routeFk = vRouteFk + AND ot.code = 'delivery' + )tob ON tob.id = t.id + LEFT JOIN( + SELECT sub.ticketFk, + CONCAT('(', + GROUP_CONCAT(DISTINCT sub.itemPackingTypeFk + ORDER BY sub.items DESC SEPARATOR ','), + ') ') itemPackingTypeFk + FROM ( + SELECT s.ticketFk, i.itemPackingTypeFk, COUNT(*) items + FROM ticket t + JOIN sale s ON s.ticketFk = t.id + JOIN item i ON i.id = s.itemFk + WHERE t.routeFk = vRouteFk + GROUP BY t.id, i.itemPackingTypeFk + )sub + GROUP BY sub.ticketFk + )sub2 ON sub2.ticketFk = t.id + WHERE t.routeFk = vRouteFk + ORDER BY d.id DESC + LIMIT 10000000000000000000 + )sub3 + GROUP BY sub3.id + ORDER BY sub3.priority; +END$$ +DELIMITER ; + +ALTER TABLE `vn`.`delivery` DROP COLUMN ticketFk; +ALTER TABLE `vn`.`delivery` ADD ticketFk INT DEFAULT NULL; +ALTER TABLE `vn`.`delivery` ADD CONSTRAINT delivery_ticketFk_FK FOREIGN KEY (`ticketFk`) REFERENCES `vn`.`ticket`(`id`); diff --git a/db/changes/231401/00-invoiceOutAfterInsert.sql b/db/changes/231401/00-invoiceOutAfterInsert.sql new file mode 100644 index 000000000..adeaf9834 --- /dev/null +++ b/db/changes/231401/00-invoiceOutAfterInsert.sql @@ -0,0 +1,13 @@ +DROP TRIGGER IF EXISTS `vn`.`invoiceOut_afterInsert`; +USE vn; + +DELIMITER $$ +$$ +CREATE DEFINER=`root`@`localhost` TRIGGER `vn`.`invoiceOut_afterInsert` + AFTER INSERT ON `invoiceOut` + FOR EACH ROW +BEGIN + CALL clientRisk_update(NEW.clientFk, NEW.companyFk, NEW.amount); +END$$ +DELIMITER ; + diff --git a/db/changes/231401/00-negativeBases.sql b/db/changes/231401/00-negativeBases.sql new file mode 100644 index 000000000..0bdc6f2dc --- /dev/null +++ b/db/changes/231401/00-negativeBases.sql @@ -0,0 +1,4 @@ +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES + ('InvoiceIn', 'negativeBases', 'READ', 'ALLOW', 'ROLE', 'administrative'), + ('InvoiceIn', 'negativeBasesCsv', 'READ', 'ALLOW', 'ROLE', 'administrative'); diff --git a/db/changes/231401/00-workerNotes.sql b/db/changes/231401/00-workerNotes.sql new file mode 100644 index 000000000..0d9eaae7e --- /dev/null +++ b/db/changes/231401/00-workerNotes.sql @@ -0,0 +1,14 @@ +CREATE TABLE `vn`.`workerObservation` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `workerFk` int(10) unsigned DEFAULT NULL, + `userFk` int(10) unsigned DEFAULT NULL, + `text` text COLLATE utf8mb3_unicode_ci NOT NULL, + `created` timestamp NOT NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + CONSTRAINT `workerFk_workerObservation_FK` FOREIGN KEY (`workerFk`) REFERENCES `vn`.`worker` (`id`) ON UPDATE CASCADE, + CONSTRAINT `userFk_workerObservation_FK` FOREIGN KEY (`userFk`) REFERENCES `account`.`user`(`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci COMMENT='Todas las observaciones referentes a un trabajador'; + +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES + ('WorkerObservation', '*', '*', 'ALLOW', 'ROLE', 'hr'); diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 9006c6676..59d0a5eaa 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -1774,12 +1774,12 @@ INSERT INTO `vn`.`claimState`(`id`, `code`, `description`, `roleFk`, `priority`, ( 6, 'mana', 'Mana', 72, 4, 0), ( 7, 'lack', 'Faltas', 72, 2, 0); -INSERT INTO `vn`.`claim`(`id`, `ticketCreated`, `claimStateFk`, `clientFk`, `workerFk`, `responsibility`, `isChargedToMana`, `created`, `packages`, `rma`) +INSERT INTO `vn`.`claim`(`id`, `ticketCreated`, `claimStateFk`, `clientFk`, `workerFk`, `responsibility`, `isChargedToMana`, `created`, `packages`, `rma`, `ticketFk`) VALUES - (1, util.VN_CURDATE(), 1, 1101, 18, 3, 0, util.VN_CURDATE(), 0, '02676A049183'), - (2, util.VN_CURDATE(), 2, 1101, 18, 3, 0, util.VN_CURDATE(), 1, NULL), - (3, util.VN_CURDATE(), 3, 1101, 18, 1, 1, util.VN_CURDATE(), 5, NULL), - (4, util.VN_CURDATE(), 3, 1104, 18, 5, 0, util.VN_CURDATE(), 10, NULL); + (1, util.VN_CURDATE(), 1, 1101, 18, 3, 0, util.VN_CURDATE(), 0, '02676A049183', 11), + (2, util.VN_CURDATE(), 2, 1101, 18, 3, 0, util.VN_CURDATE(), 1, NULL, 16), + (3, util.VN_CURDATE(), 3, 1101, 18, 1, 1, util.VN_CURDATE(), 5, NULL, 7), + (4, util.VN_CURDATE(), 3, 1104, 18, 5, 0, util.VN_CURDATE(), 10, NULL, 8); INSERT INTO `vn`.`claimObservation` (`claimFk`, `workerFk`, `text`, `created`) VALUES diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 32a60a4e2..b504244ea 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -426,7 +426,8 @@ export default { fourthStarted: 'vn-fixed-price tr:nth-child(5) vn-date-picker[ng-model="price.started"]', fourthEnded: 'vn-fixed-price tr:nth-child(5) vn-date-picker[ng-model="price.ended"]', fourthDeleteIcon: 'vn-fixed-price tr:nth-child(5) > td:nth-child(9) > vn-icon-button[icon="delete"]', - orderColumnId: 'vn-fixed-price th[field="itemFk"]' + orderColumnId: 'vn-fixed-price th[field="itemFk"]', + removeWarehouseFilter: 'vn-searchbar > form > vn-textfield > div.container > div.prepend > prepend > div > span:nth-child(1) > vn-icon > i' }, itemCreateView: { temporalName: 'vn-item-create vn-textfield[ng-model="$ctrl.item.provisionalName"]', @@ -987,6 +988,12 @@ export default { locker: 'vn-worker-basic-data vn-input-number[ng-model="$ctrl.worker.locker"]', saveButton: 'vn-worker-basic-data button[type=submit]' }, + workerNotes: { + addNoteFloatButton: 'vn-float-button', + note: 'vn-textarea[ng-model="$ctrl.note.text"]', + saveButton: 'button[type=submit]', + firstNoteText: 'vn-worker-note .text' + }, workerPbx: { extension: 'vn-worker-pbx vn-textfield[ng-model="$ctrl.worker.sip.extension"]', saveButton: 'vn-worker-pbx button[type=submit]' diff --git a/e2e/paths/01-salix/03_smartTable_searchBar_integrations.spec.js b/e2e/paths/01-salix/03_smartTable_searchBar_integrations.spec.js index ad558ace2..a3d747f1c 100644 --- a/e2e/paths/01-salix/03_smartTable_searchBar_integrations.spec.js +++ b/e2e/paths/01-salix/03_smartTable_searchBar_integrations.spec.js @@ -90,7 +90,7 @@ describe('SmartTable SearchBar integration', () => { await page.waitToClick(selectors.itemFixedPrice.orderColumnId); const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value'); - expect(result).toEqual('13'); + expect(result).toEqual('3'); }); it('should reload page and have same order', async() => { @@ -99,7 +99,7 @@ describe('SmartTable SearchBar integration', () => { }); const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value'); - expect(result).toEqual('13'); + expect(result).toEqual('3'); }); }); }); diff --git a/e2e/paths/03-worker/08_add_notes.spec.js b/e2e/paths/03-worker/08_add_notes.spec.js new file mode 100644 index 000000000..eb2e4c041 --- /dev/null +++ b/e2e/paths/03-worker/08_add_notes.spec.js @@ -0,0 +1,42 @@ +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Worker Add notes path', () => { + let browser; + let page; + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('employee', 'worker'); + await page.accessToSearchResult('Bruce Banner'); + await page.accessToSection('worker.card.note.index'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it(`should reach the notes index`, async() => { + await page.waitForState('worker.card.note.index'); + }); + + it(`should click on the add note button`, async() => { + await page.waitToClick(selectors.workerNotes.addNoteFloatButton); + await page.waitForState('worker.card.note.create'); + }); + + it(`should create a note`, async() => { + await page.waitForSelector(selectors.workerNotes.note); + await page.type(`${selectors.workerNotes.note} textarea`, 'Meeting with Black Widow 21st 9am'); + await page.waitToClick(selectors.workerNotes.saveButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should confirm the note was created', async() => { + const result = await page.waitToGetProperty(selectors.workerNotes.firstNoteText, 'innerText'); + + expect(result).toEqual('Meeting with Black Widow 21st 9am'); + }); +}); diff --git a/e2e/paths/04-item/13_fixedPrice.spec.js b/e2e/paths/04-item/13_fixedPrice.spec.js index 1b0f82d83..ec8238b87 100644 --- a/e2e/paths/04-item/13_fixedPrice.spec.js +++ b/e2e/paths/04-item/13_fixedPrice.spec.js @@ -15,8 +15,9 @@ describe('Item fixed prices path', () => { await browser.close(); }); - it('should click on the add new foxed price button', async() => { - await page.doSearch(); + it('should click on the add new fixed price button', async() => { + await page.waitToClick(selectors.itemFixedPrice.removeWarehouseFilter); + await page.waitForSpinnerLoad(); await page.waitToClick(selectors.itemFixedPrice.add); await page.waitForSelector(selectors.itemFixedPrice.fourthFixedPrice); }); @@ -37,7 +38,8 @@ describe('Item fixed prices path', () => { it('should reload the section and check the created price has the expected ID', async() => { await page.accessToSection('item.index'); await page.accessToSection('item.fixedPrice'); - await page.doSearch(); + await page.waitToClick(selectors.itemFixedPrice.removeWarehouseFilter); + await page.waitForSpinnerLoad(); const result = await page.waitToGetProperty(selectors.itemFixedPrice.fourthItemID, 'value'); diff --git a/e2e/paths/09-invoice-in/05_negative_bases.spec.js b/e2e/paths/09-invoice-in/05_negative_bases.spec.js new file mode 100644 index 000000000..4c9fe651f --- /dev/null +++ b/e2e/paths/09-invoice-in/05_negative_bases.spec.js @@ -0,0 +1,29 @@ +import getBrowser from '../../helpers/puppeteer'; + +describe('InvoiceIn negative bases path', () => { + let browser; + let page; + const httpRequests = []; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + page.on('request', req => { + if (req.url().includes(`InvoiceIns/negativeBases`)) + httpRequests.push(req.url()); + }); + await page.loginAndModule('administrative', 'invoiceIn'); + await page.accessToSection('invoiceIn.negative-bases'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should show negative bases in a date range', async() => { + const request = httpRequests.find(req => + req.includes(`from`) && req.includes(`to`)); + + expect(request).toBeDefined(); + }); +}); diff --git a/loopback/locale/es.json b/loopback/locale/es.json index e4dc74aa3..d52eacff6 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -273,5 +273,9 @@ "Not exist this branch": "La rama no existe", "This ticket cannot be signed because it has not been boxed": "Este ticket no puede firmarse porque no ha sido encajado", "Collection does not exist": "La colección no existe", - "Cannot obtain exclusive lock": "No se puede obtener un bloqueo exclusivo" + "Cannot obtain exclusive lock": "No se puede obtener un bloqueo exclusivo", + "Insert a date range": "Inserte un rango de fechas", + "Added observation": "{{user}} añadió esta observacion: {{text}}", + "Comment added to client": "Observación añadida al cliente {{clientFk}}", + "Cannot create a new claimBeginning from a different ticket": "No se puede crear una línea de reclamación de un ticket diferente al origen" } diff --git a/loopback/server/connectors/vn-mysql.js b/loopback/server/connectors/vn-mysql.js index 728454d86..a6fd3351a 100644 --- a/loopback/server/connectors/vn-mysql.js +++ b/loopback/server/connectors/vn-mysql.js @@ -311,7 +311,7 @@ class VnMySQL extends MySQL { return super[method].apply(this, args); this.invokeMethodP(method, [...args], model, ctx, opts) - .then(res => cb(...res), cb); + .then(res => cb(...[null].concat(res)), cb); } async invokeMethodP(method, args, model, ctx, opts) { @@ -331,8 +331,7 @@ class VnMySQL extends MySQL { const userId = opts.httpCtx && opts.httpCtx.active.accessToken.userId; const user = await Model.app.models.Account.findById(userId, { fields: ['name'] }, opts); await this.executeP(`CALL account.myUser_loginWithName(?)`, [user.name], opts); - } - else { + } else { where = ctx.where; id = ctx.id; data = ctx.data; @@ -358,9 +357,12 @@ class VnMySQL extends MySQL { } } - const res = await new Promise(resolve => { + const res = await new Promise((resolve, reject) => { const fnArgs = args.slice(0, -2); - fnArgs.push(opts, (...args) => resolve(args)); + fnArgs.push(opts, (err, ...args) => { + if (err) return reject(err); + resolve(args); + }); super[method].apply(this, fnArgs); }); @@ -375,11 +377,11 @@ class VnMySQL extends MySQL { case 'update': { switch (method) { case 'createAll': - for (const row of res[1]) + for (const row of res[0]) ids.push(row[idName]); break; case 'create': - ids.push(res[1]); + ids.push(res[0]); break; case 'update': if (data[idName] != null) @@ -387,7 +389,7 @@ class VnMySQL extends MySQL { break; } - const newWhere = ids.length ? { [idName]: ids } : where; + const newWhere = ids.length ? {[idName]: {inq: ids}} : where; const stmt = this.buildSelectStmt(op, data, idName, model, newWhere, limit); newInstances = await this.executeStmt(stmt, opts); @@ -671,4 +673,4 @@ SQLConnector.prototype.all = function find(model, filter, options, cb) { cb(error, []) } }); -}; \ No newline at end of file +}; diff --git a/modules/claim/back/methods/claim/specs/createFromSales.spec.js b/modules/claim/back/methods/claim/specs/createFromSales.spec.js index 7cf663caf..fe009c1c3 100644 --- a/modules/claim/back/methods/claim/specs/createFromSales.spec.js +++ b/modules/claim/back/methods/claim/specs/createFromSales.spec.js @@ -2,9 +2,9 @@ const models = require('vn-loopback/server/server').models; const LoopBackContext = require('loopback-context'); describe('Claim createFromSales()', () => { - const ticketId = 16; + const ticketId = 23; const newSale = [{ - id: 3, + id: 31, instance: 0, quantity: 10 }]; diff --git a/modules/claim/back/models/claim-beginning.js b/modules/claim/back/models/claim-beginning.js index 4c4b59737..4b870e5ea 100644 --- a/modules/claim/back/models/claim-beginning.js +++ b/modules/claim/back/models/claim-beginning.js @@ -10,8 +10,16 @@ module.exports = Self => { }); Self.observe('before save', async ctx => { - if (ctx.isNewInstance) return; - //await claimIsEditable(ctx); + if (ctx.isNewInstance) { + const models = Self.app.models; + const options = ctx.options; + const instance = ctx.instance; + const ticket = await models.Sale.findById(instance.saleFk, {fields: ['ticketFk']}, options); + const claim = await models.Claim.findById(instance.claimFk, {fields: ['ticketFk']}, options); + if (ticket.ticketFk != claim.ticketFk) + throw new UserError(`Cannot create a new claimBeginning from a different ticket`); + } + // await claimIsEditable(ctx); }); Self.observe('before delete', async ctx => { diff --git a/modules/client/back/methods/client/checkDuplicated.js b/modules/client/back/methods/client/checkDuplicated.js deleted file mode 100644 index 522cd088f..000000000 --- a/modules/client/back/methods/client/checkDuplicated.js +++ /dev/null @@ -1,63 +0,0 @@ -module.exports = Self => { - Self.remoteMethod('checkDuplicatedData', { - description: 'Checks if a client has same email, mobile or phone than other client and send an email', - accepts: [{ - arg: 'id', - type: 'number', - required: true, - description: 'The client id' - }], - returns: { - type: 'object', - root: true - }, - http: { - verb: 'GET', - path: '/:id/checkDuplicatedData' - } - }); - - Self.checkDuplicatedData = async function(id, options) { - const myOptions = {}; - - if (typeof options == 'object') - Object.assign(myOptions, options); - - const client = await Self.app.models.Client.findById(id, myOptions); - - const findParams = []; - if (client.email) { - const emails = client.email.split(','); - for (let email of emails) - findParams.push({email: email}); - } - - if (client.phone) - findParams.push({phone: client.phone}); - - if (client.mobile) - findParams.push({mobile: client.mobile}); - - const filterObj = { - where: { - and: [ - {or: findParams}, - {id: {neq: client.id}} - ] - } - }; - - const clientSameData = await Self.findOne(filterObj, myOptions); - - if (clientSameData) { - await Self.app.models.Mail.create({ - receiver: 'direccioncomercial@verdnatura.es', - subject: `Cliente con email/teléfono/móvil duplicados`, - body: 'El cliente ' + client.id + ' comparte alguno de estos datos con el cliente ' + clientSameData.id + - '\n- Email: ' + client.email + - '\n- Teléfono: ' + client.phone + - '\n- Móvil: ' + client.mobile - }, myOptions); - } - }; -}; diff --git a/modules/client/back/methods/client/specs/checkDuplicated.spec.js b/modules/client/back/methods/client/specs/checkDuplicated.spec.js deleted file mode 100644 index 1b682ca35..000000000 --- a/modules/client/back/methods/client/specs/checkDuplicated.spec.js +++ /dev/null @@ -1,24 +0,0 @@ -const models = require('vn-loopback/server/server').models; - -describe('client checkDuplicated()', () => { - it('should send an mail if mobile/phone/email is duplicated', async() => { - const tx = await models.Client.beginTransaction({}); - - try { - const options = {transaction: tx}; - - const id = 1110; - const mailModel = models.Mail; - spyOn(mailModel, 'create'); - - await models.Client.checkDuplicatedData(id, options); - - expect(mailModel.create).toHaveBeenCalled(); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } - }); -}); diff --git a/modules/client/back/methods/defaulter/observationEmail.js b/modules/client/back/methods/defaulter/observationEmail.js new file mode 100644 index 000000000..c3c96010e --- /dev/null +++ b/modules/client/back/methods/defaulter/observationEmail.js @@ -0,0 +1,52 @@ +module.exports = Self => { + Self.remoteMethodCtx('observationEmail', { + description: 'Send an email with the observation', + accessType: 'WRITE', + accepts: [ + { + arg: 'defaulters', + type: ['object'], + required: true, + description: 'The defaulters to send the email' + }, + { + arg: 'observation', + type: 'string', + required: true, + description: 'The observation' + }], + returns: { + arg: 'observationEmail' + }, + http: { + path: `/observationEmail`, + verb: 'POST' + } + }); + + Self.observationEmail = async(ctx, defaulters, observation, options) => { + const models = Self.app.models; + const $t = ctx.req.__; // $translate + const myOptions = {}; + const userId = ctx.req.accessToken.userId; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + for (const defaulter of defaulters) { + const user = await models.Account.findById(userId, {fields: ['name']}, myOptions); + + const body = $t('Added observation', { + user: user.name, + text: observation + }); + + await models.Mail.create({ + subject: $t('Comment added to client', {clientFk: defaulter.clientFk}), + body: body, + receiver: `${defaulter.salesPersonName}@verdnatura.es`, + replyTo: `${user.name}@verdnatura.es` + }, myOptions); + } + }; +}; diff --git a/modules/client/back/models/client-methods.js b/modules/client/back/models/client-methods.js index fc77fc090..3538dbeb8 100644 --- a/modules/client/back/models/client-methods.js +++ b/modules/client/back/models/client-methods.js @@ -2,7 +2,6 @@ module.exports = Self => { require('../methods/client/addressesPropagateRe')(Self); require('../methods/client/canBeInvoiced')(Self); require('../methods/client/canCreateTicket')(Self); - require('../methods/client/checkDuplicated')(Self); require('../methods/client/confirmTransaction')(Self); require('../methods/client/consumption')(Self); require('../methods/client/createAddress')(Self); diff --git a/modules/client/back/models/defaulter.js b/modules/client/back/models/defaulter.js index 13bb1a614..868d6cd0a 100644 --- a/modules/client/back/models/defaulter.js +++ b/modules/client/back/models/defaulter.js @@ -1,3 +1,4 @@ module.exports = Self => { require('../methods/defaulter/filter')(Self); + require('../methods/defaulter/observationEmail')(Self); }; diff --git a/modules/client/front/basic-data/index.js b/modules/client/front/basic-data/index.js index b08d642d1..ed34eefc4 100644 --- a/modules/client/front/basic-data/index.js +++ b/modules/client/front/basic-data/index.js @@ -9,9 +9,7 @@ export default class Controller extends Section { } onSubmit() { - return this.$.watcher.submit().then(() => { - this.$http.get(`Clients/${this.$params.id}/checkDuplicatedData`); - }); + return this.$.watcher.submit(); } } diff --git a/modules/client/front/create/index.js b/modules/client/front/create/index.js index 9ca58ed10..631029802 100644 --- a/modules/client/front/create/index.js +++ b/modules/client/front/create/index.js @@ -12,7 +12,6 @@ export default class Controller extends Section { onSubmit() { return this.$.watcher.submit().then(json => { this.$state.go('client.card.basicData', {id: json.data.id}); - this.$http.get(`Clients/${this.client.id}/checkDuplicatedData`); }); } diff --git a/modules/client/front/defaulter/index.html b/modules/client/front/defaulter/index.html index 22b78594a..8f22629a9 100644 --- a/modules/client/front/defaulter/index.html +++ b/modules/client/front/defaulter/index.html @@ -5,6 +5,7 @@ limit="20" order="amount DESC" data="defaulters" + on-data-change="$ctrl.reCheck()" auto-load="true"> @@ -17,22 +18,22 @@ -
Total
-
- - @@ -56,25 +57,25 @@ Comercial - Balance D. - Author Last observation - L. O. Date - @@ -88,8 +89,9 @@ - @@ -150,7 +152,7 @@ - + @@ -160,7 +162,7 @@ id !== clientId) : [...this.checkedDefaulers, clientId]; + } + + reCheck() { + if (!this.$.model.data || !this.checkedDefaulers.length) return; + + this.$.model.data.forEach(defaulter => { + defaulter.checked = this.checkedDefaulers.includes(defaulter.clientFk); + }); + } + getBalanceDueTotal() { this.$http.get('Defaulters/filter') .then(res => { @@ -109,11 +123,20 @@ export default class Controller extends Section { } this.$http.post(`ClientObservations`, params) .then(() => { - this.vnApp.showMessage(this.$t('Observation saved!')); + this.vnApp.showSuccess(this.$t('Observation saved!')); + this.sendMail(); this.$state.reload(); }); } + sendMail() { + const params = { + defaulters: this.checked, + observation: this.defaulter.observation + }; + this.$http.post(`Defaulters/observationEmail`, params); + } + exprBuilder(param, value) { switch (param) { case 'creditInsurance': @@ -122,8 +145,25 @@ export default class Controller extends Section { case 'workerFk': case 'salesPersonFk': return {[`d.${param}`]: value}; + case 'created': + return {'d.created': { + between: this.dateRange(value)} + }; + case 'defaulterSinced': + return {'d.defaulterSinced': { + 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]; + } } ngModule.vnComponent('vnClientDefaulter', { diff --git a/modules/client/front/defaulter/index.spec.js b/modules/client/front/defaulter/index.spec.js index f92378d08..b4a9df184 100644 --- a/modules/client/front/defaulter/index.spec.js +++ b/modules/client/front/defaulter/index.spec.js @@ -81,14 +81,15 @@ describe('client defaulter', () => { const params = [{text: controller.defaulter.observation, clientFk: data[1].clientFk}]; - jest.spyOn(controller.vnApp, 'showMessage'); + jest.spyOn(controller.vnApp, 'showSuccess'); $httpBackend.expect('GET', `Defaulters/filter`).respond(200); $httpBackend.expect('POST', `ClientObservations`, params).respond(200, params); + $httpBackend.expect('POST', `Defaulters/observationEmail`).respond(200); controller.onResponse(); $httpBackend.flush(); - expect(controller.vnApp.showMessage).toHaveBeenCalledWith('Observation saved!'); + expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Observation saved!'); }); }); @@ -117,5 +118,62 @@ describe('client defaulter', () => { expect(controller.balanceDueTotal).toEqual(875); }); }); + + describe('dateRange()', () => { + it('should return two dates with the hours at the start and end of the given date', () => { + const now = Date.vnNew(); + + const today = now.getDate(); + + const dateRange = controller.dateRange(now); + const start = dateRange[0].toString(); + const end = dateRange[1].toString(); + + expect(start).toContain(today); + expect(start).toContain('00:00:00'); + + expect(end).toContain(today); + expect(end).toContain('23:59:59'); + }); + }); + + describe('reCheck()', () => { + it(`should recheck buys`, () => { + controller.$.model.data = [ + {checked: false, clientFk: 1}, + {checked: false, clientFk: 2}, + {checked: false, clientFk: 3}, + {checked: false, clientFk: 4}, + ]; + controller.checkedDefaulers = [1, 2]; + + controller.reCheck(); + + expect(controller.$.model.data[0].checked).toEqual(true); + expect(controller.$.model.data[1].checked).toEqual(true); + expect(controller.$.model.data[2].checked).toEqual(false); + expect(controller.$.model.data[3].checked).toEqual(false); + }); + }); + + describe('saveChecked()', () => { + it(`should check buy`, () => { + const buyCheck = 3; + controller.checkedDefaulers = [1, 2]; + + controller.saveChecked(buyCheck); + + expect(controller.checkedDefaulers[2]).toEqual(buyCheck); + }); + + it(`should uncheck buy`, () => { + const buyUncheck = 3; + controller.checkedDefaulers = [1, 2, 3]; + + controller.saveChecked(buyUncheck); + + expect(controller.checkedDefaulers[2]).toEqual(undefined); + }); + }); }); }); diff --git a/modules/client/front/defaulter/locale/es.yml b/modules/client/front/defaulter/locale/es.yml index c3e1d4e19..fe06a15a1 100644 --- a/modules/client/front/defaulter/locale/es.yml +++ b/modules/client/front/defaulter/locale/es.yml @@ -6,4 +6,6 @@ Last observation: Última observación L. O. Date: Fecha Ú. O. Last observation date: Fecha última observación Search client: Buscar clientes -Worker who made the last observation: Trabajador que ha realizado la última observación \ No newline at end of file +Worker who made the last observation: Trabajador que ha realizado la última observación +Email sended!: Email enviado! +Observation saved!: Observación añadida! diff --git a/modules/client/front/fiscal-data/index.js b/modules/client/front/fiscal-data/index.js index d76944c42..acad38185 100644 --- a/modules/client/front/fiscal-data/index.js +++ b/modules/client/front/fiscal-data/index.js @@ -2,6 +2,10 @@ import ngModule from '../module'; import Section from 'salix/components/section'; export default class Controller extends Section { + $onInit() { + this.card.reload(); + } + onSubmit() { const orgData = this.$.watcher.orgData; delete this.client.despiteOfClient; diff --git a/modules/client/front/locale/es.yml b/modules/client/front/locale/es.yml index de4b91e0b..adbca8dbf 100644 --- a/modules/client/front/locale/es.yml +++ b/modules/client/front/locale/es.yml @@ -63,4 +63,4 @@ Consumption: Consumo Compensation Account: Cuenta para compensar Amount to return: Cantidad a devolver Delivered amount: Cantidad entregada -Unpaid: Impagado \ No newline at end of file +Unpaid: Impagado diff --git a/modules/invoiceIn/back/methods/invoice-in/negativeBases.js b/modules/invoiceIn/back/methods/invoice-in/negativeBases.js new file mode 100644 index 000000000..4c9a8984b --- /dev/null +++ b/modules/invoiceIn/back/methods/invoice-in/negativeBases.js @@ -0,0 +1,113 @@ +const UserError = require('vn-loopback/util/user-error'); +const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; + +module.exports = Self => { + Self.remoteMethodCtx('negativeBases', { + description: 'Find all negative bases', + accessType: 'READ', + accepts: [ + { + arg: 'from', + type: 'date', + description: 'From date' + }, + { + arg: 'to', + type: 'date', + description: 'To date' + }, + { + arg: 'filter', + type: 'object', + description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string' + }, + ], + returns: { + type: ['object'], + root: true + }, + http: { + path: `/negativeBases`, + verb: 'GET' + } + }); + + Self.negativeBases = async(ctx, options) => { + const conn = Self.dataSource.connector; + const args = ctx.args; + + if (!args.from || !args.to) + throw new UserError(`Insert a date range`); + + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + const stmts = []; + let stmt; + stmts.push(`DROP TEMPORARY TABLE IF EXISTS tmp.ticket`); + + stmts.push(new ParameterizedSQL( + `CREATE TEMPORARY TABLE tmp.ticket + (KEY (ticketFk)) + ENGINE = MEMORY + SELECT id ticketFk + FROM ticket t + WHERE shipped BETWEEN ? AND ? + AND refFk IS NULL`, [args.from, args.to])); + stmts.push(`CALL vn.ticket_getTax(NULL)`); + stmts.push(`DROP TEMPORARY TABLE IF EXISTS tmp.filter`); + stmts.push(new ParameterizedSQL( + `CREATE TEMPORARY TABLE tmp.filter + ENGINE = MEMORY + SELECT + co.code company, + cou.country, + c.id clientId, + c.socialName clientSocialName, + SUM(s.quantity * s.price * ( 100 - s.discount ) / 100) amount, + negativeBase.taxableBase, + negativeBase.ticketFk, + c.isActive, + c.hasToInvoice, + c.isTaxDataChecked, + w.id comercialId, + CONCAT(w.firstName, ' ', w.lastName) comercialName + FROM vn.ticket t + JOIN vn.company co ON co.id = t.companyFk + JOIN vn.sale s ON s.ticketFk = t.id + JOIN vn.client c ON c.id = t.clientFk + JOIN vn.country cou ON cou.id = c.countryFk + LEFT JOIN vn.worker w ON w.id = c.salesPersonFk + LEFT JOIN ( + SELECT ticketFk, taxableBase + FROM tmp.ticketAmount + GROUP BY ticketFk + HAVING taxableBase < 0 + ) negativeBase ON negativeBase.ticketFk = t.id + WHERE t.shipped BETWEEN ? AND ? + AND t.refFk IS NULL + AND c.typeFk IN ('normal','trust') + GROUP BY t.clientFk, negativeBase.taxableBase + HAVING amount <> 0`, [args.from, args.to])); + + stmt = new ParameterizedSQL(` + SELECT f.* + FROM tmp.filter f`); + + stmt.merge(conn.makeWhere(args.filter.where)); + stmt.merge(conn.makeOrderBy(args.filter.order)); + stmt.merge(conn.makeLimit(args.filter)); + + const negativeBasesIndex = stmts.push(stmt) - 1; + + stmts.push(`DROP TEMPORARY TABLE tmp.filter, tmp.ticket, tmp.ticketTax, tmp.ticketAmount`); + + const sql = ParameterizedSQL.join(stmts, ';'); + const result = await conn.executeStmt(sql, myOptions); + + return negativeBasesIndex === 0 ? result : result[negativeBasesIndex]; + }; +}; + diff --git a/modules/invoiceIn/back/methods/invoice-in/negativeBasesCsv.js b/modules/invoiceIn/back/methods/invoice-in/negativeBasesCsv.js new file mode 100644 index 000000000..963151b7d --- /dev/null +++ b/modules/invoiceIn/back/methods/invoice-in/negativeBasesCsv.js @@ -0,0 +1,53 @@ +const {toCSV} = require('vn-loopback/util/csv'); + +module.exports = Self => { + Self.remoteMethodCtx('negativeBasesCsv', { + description: 'Returns the negative bases as .csv', + accessType: 'READ', + accepts: [{ + arg: 'negativeBases', + type: ['object'], + required: true + }, + { + arg: 'from', + type: 'date', + description: 'From date' + }, + { + arg: 'to', + type: 'date', + description: 'To date' + }], + 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: '/negativeBasesCsv', + verb: 'GET' + } + }); + + Self.negativeBasesCsv = async ctx => { + const args = ctx.args; + const content = toCSV(args.negativeBases); + + return [ + content, + 'text/csv', + `attachment; filename="negative-bases-${new Date(args.from).toLocaleDateString()}-${new Date(args.to).toLocaleDateString()}.csv"` + ]; + }; +}; diff --git a/modules/invoiceIn/back/methods/invoice-in/specs/negativeBases.spec.js b/modules/invoiceIn/back/methods/invoice-in/specs/negativeBases.spec.js new file mode 100644 index 000000000..a5c6e3102 --- /dev/null +++ b/modules/invoiceIn/back/methods/invoice-in/specs/negativeBases.spec.js @@ -0,0 +1,47 @@ +const models = require('vn-loopback/server/server').models; + +describe('invoiceIn negativeBases()', () => { + it('should return all negative bases in a date range', async() => { + const tx = await models.InvoiceIn.beginTransaction({}); + const options = {transaction: tx}; + const ctx = { + args: { + from: new Date().setMonth(new Date().getMonth() - 12), + to: new Date(), + filter: {} + } + }; + + try { + const result = await models.InvoiceIn.negativeBases(ctx, options); + + expect(result.length).toBeGreaterThan(0); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should throw an error if a date range is not in args', async() => { + let error; + const tx = await models.InvoiceIn.beginTransaction({}); + const options = {transaction: tx}; + const ctx = { + args: { + filter: {} + } + }; + + try { + await models.InvoiceIn.negativeBases(ctx, options); + await tx.rollback(); + } catch (e) { + error = e; + await tx.rollback(); + } + + expect(error.message).toEqual(`Insert a date range`); + }); +}); diff --git a/modules/invoiceIn/back/models/invoice-in.js b/modules/invoiceIn/back/models/invoice-in.js index 51905ccb8..167f2ac34 100644 --- a/modules/invoiceIn/back/models/invoice-in.js +++ b/modules/invoiceIn/back/models/invoice-in.js @@ -7,4 +7,6 @@ module.exports = Self => { require('../methods/invoice-in/invoiceInPdf')(Self); require('../methods/invoice-in/invoiceInEmail')(Self); require('../methods/invoice-in/getSerial')(Self); + require('../methods/invoice-in/negativeBases')(Self); + require('../methods/invoice-in/negativeBasesCsv')(Self); }; diff --git a/modules/invoiceIn/front/index.js b/modules/invoiceIn/front/index.js index e257cfee3..c0374e996 100644 --- a/modules/invoiceIn/front/index.js +++ b/modules/invoiceIn/front/index.js @@ -15,3 +15,4 @@ import './create'; import './log'; import './serial'; import './serial-search-panel'; +import './negative-bases'; diff --git a/modules/invoiceIn/front/locale/es.yml b/modules/invoiceIn/front/locale/es.yml index a2d658519..017e89dd4 100644 --- a/modules/invoiceIn/front/locale/es.yml +++ b/modules/invoiceIn/front/locale/es.yml @@ -24,3 +24,4 @@ Show agricultural receipt as PDF: Ver recibo agrícola como PDF Send agricultural receipt as PDF: Enviar recibo agrícola como PDF New InvoiceIn: Nueva Factura Days ago: Últimos días +Negative bases: Bases negativas diff --git a/modules/invoiceIn/front/negative-bases/index.html b/modules/invoiceIn/front/negative-bases/index.html new file mode 100644 index 000000000..5da8e7aad --- /dev/null +++ b/modules/invoiceIn/front/negative-bases/index.html @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Company + + Country + + Id Client + + Client + + Amount + + Base + + Id Ticket + + Active + + Has To Invoice + + Verified data + + Comercial +
{{client.company | dashIfEmpty}}{{client.country | dashIfEmpty}} + + {{::client.clientId | dashIfEmpty}} + + {{client.clientSocialName | dashIfEmpty}}{{client.amount | currency: 'EUR':2 | dashIfEmpty}}{{client.taxableBase | dashIfEmpty}} + + {{::client.ticketFk | dashIfEmpty}} + + + + + + + + + + + + + {{::client.comercialName | dashIfEmpty}} + +
+
+
+
+ + + + + + diff --git a/modules/invoiceIn/front/negative-bases/index.js b/modules/invoiceIn/front/negative-bases/index.js new file mode 100644 index 000000000..0f6f04692 --- /dev/null +++ b/modules/invoiceIn/front/negative-bases/index.js @@ -0,0 +1,84 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; +import './style.scss'; + +export default class Controller extends Section { + constructor($element, $, vnReport) { + super($element, $); + + this.vnReport = vnReport; + const now = new Date(); + const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0); + this.params = { + from: firstDayOfMonth, + to: lastDayOfMonth + }; + this.$checkAll = false; + + this.smartTableOptions = { + activeButtons: { + search: true, + }, columns: [ + { + field: 'isActive', + searchable: false + }, + { + field: 'hasToInvoice', + searchable: false + }, + { + field: 'isTaxDataChecked', + searchable: false + }, + ] + }; + } + + exprBuilder(param, value) { + switch (param) { + case 'company': + return {'company': value}; + case 'country': + return {'country': value}; + case 'clientId': + return {'clientId': value}; + case 'clientSocialName': + return {'clientSocialName': value}; + case 'amount': + return {'amount': value}; + case 'taxableBase': + return {'taxableBase': value}; + case 'ticketFk': + return {'ticketFk': value}; + case 'comercialName': + return {'comercialName': value}; + } + } + + downloadCSV() { + const data = []; + this.$.model._orgData.forEach(element => { + data.push(Object.keys(element).map(key => { + return {newName: this.$t(key), value: element[key]}; + }).filter(item => item !== null) + .reduce((result, item) => { + result[item.newName] = item.value; + return result; + }, {})); + }); + this.vnReport.show('InvoiceIns/negativeBasesCsv', { + negativeBases: data, + from: this.params.from, + to: this.params.to + }); + } +} + +Controller.$inject = ['$element', '$scope', 'vnReport']; + +ngModule.vnComponent('vnNegativeBases', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/invoiceIn/front/negative-bases/locale/es.yml b/modules/invoiceIn/front/negative-bases/locale/es.yml new file mode 100644 index 000000000..9095eee22 --- /dev/null +++ b/modules/invoiceIn/front/negative-bases/locale/es.yml @@ -0,0 +1,14 @@ +Has To Invoice: Facturar +Download as CSV: Descargar como CSV +company: Compañía +country: País +clientId: Id Cliente +clientSocialName: Cliente +amount: Importe +taxableBase: Base +ticketFk: Id Ticket +isActive: Activo +hasToInvoice: Facturar +isTaxDataChecked: Datos comprobados +comercialId: Id Comercial +comercialName: Comercial diff --git a/modules/invoiceIn/front/negative-bases/style.scss b/modules/invoiceIn/front/negative-bases/style.scss new file mode 100644 index 000000000..2d628cb94 --- /dev/null +++ b/modules/invoiceIn/front/negative-bases/style.scss @@ -0,0 +1,10 @@ +@import "./variables"; + +vn-negative-bases { + vn-date-picker{ + padding-right: 5%; + } + slot-actions{ + align-items: center; + } +} diff --git a/modules/invoiceIn/front/routes.json b/modules/invoiceIn/front/routes.json index 90c4f8472..40d061d1b 100644 --- a/modules/invoiceIn/front/routes.json +++ b/modules/invoiceIn/front/routes.json @@ -9,14 +9,9 @@ ], "menus": { "main": [ - { - "state": "invoiceIn.index", - "icon": "icon-invoice-in" - }, - { - "state": "invoiceIn.serial", - "icon": "icon-invoice-in" - } + { "state": "invoiceIn.index", "icon": "icon-invoice-in"}, + { "state": "invoiceIn.serial", "icon": "icon-invoice-in"}, + { "state": "invoiceIn.negative-bases", "icon": "icon-ticket"} ], "card": [ { @@ -58,6 +53,15 @@ "administrative" ] }, + { + "url": "/negative-bases", + "state": "invoiceIn.negative-bases", + "component": "vn-negative-bases", + "description": "Negative bases", + "acl": [ + "administrative" + ] + }, { "url": "/serial", "state": "invoiceIn.serial", diff --git a/modules/item/back/methods/fixed-price/editFixedPrice.js b/modules/item/back/methods/fixed-price/editFixedPrice.js new file mode 100644 index 000000000..13e0fc41b --- /dev/null +++ b/modules/item/back/methods/fixed-price/editFixedPrice.js @@ -0,0 +1,96 @@ +module.exports = Self => { + Self.remoteMethodCtx('editFixedPrice', { + description: 'Updates a column for one or more fixed price', + accessType: 'WRITE', + accepts: [{ + arg: 'field', + type: 'string', + required: true, + description: `the column to edit` + }, + { + arg: 'newValue', + type: 'any', + required: true, + description: `The new value to save` + }, + { + arg: 'lines', + 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', + root: true + }, + http: { + path: `/editFixedPrice`, + verb: 'POST' + } + }); + + Self.editFixedPrice = async(ctx, field, newValue, lines, filter, options) => { + let tx; + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + let modelName; + let identifier; + + switch (field) { + case 'hasMinPrice': + case 'minPrice': + modelName = 'Item'; + identifier = 'itemFk'; + break; + case 'rate2': + case 'rate3': + case 'started': + case 'ended': + case 'warehouseFk': + modelName = 'FixedPrice'; + identifier = 'id'; + } + + const models = Self.app.models; + const model = models[modelName]; + try { + const promises = []; + const value = {}; + value[field] = newValue; + + if (filter) { + filter = {where: filter}; + lines = await models.FixedPrice.filter(ctx, filter, myOptions); + } + + const targets = lines.map(line => { + return line[identifier]; + }); + for (let target of targets) + promises.push(model.upsertWithWhere({id: target}, value, myOptions)); + + const result = await Promise.all(promises); + + if (tx) await tx.commit(); + + return result; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/item/back/methods/fixed-price/filter.js b/modules/item/back/methods/fixed-price/filter.js index c15ae67f0..9c91886c1 100644 --- a/modules/item/back/methods/fixed-price/filter.js +++ b/modules/item/back/methods/fixed-price/filter.js @@ -184,8 +184,7 @@ module.exports = Self => { } } - stmt.merge(conn.makeWhere(filter.where)); - stmt.merge(conn.makePagination(filter)); + stmt.merge(conn.makeSuffix(filter)); const fixedPriceIndex = stmts.push(stmt) - 1; const sql = ParameterizedSQL.join(stmts, ';'); diff --git a/modules/item/back/methods/fixed-price/specs/editFixedPrice.spec.js b/modules/item/back/methods/fixed-price/specs/editFixedPrice.spec.js new file mode 100644 index 000000000..db255a91e --- /dev/null +++ b/modules/item/back/methods/fixed-price/specs/editFixedPrice.spec.js @@ -0,0 +1,63 @@ +const models = require('vn-loopback/server/server').models; + +describe('Item editFixedPrice()', () => { + it('should change the value of a given column for the selected buys', async() => { + const tx = await models.FixedPrice.beginTransaction({}); + const options = {transaction: tx}; + + try { + const ctx = { + args: { + search: '1' + }, + req: {accessToken: {userId: 1}} + }; + + const [original] = await models.FixedPrice.filter(ctx, null, options); + + const field = 'rate2'; + const newValue = 99; + const lines = [{itemFk: original.itemFk, id: original.id}]; + + await models.FixedPrice.editFixedPrice(ctx, field, newValue, lines, null, options); + + const [result] = await models.FixedPrice.filter(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.FixedPrice.beginTransaction({}); + const options = {transaction: tx}; + + try { + const filter = {where: {'it.categoryFk': 1}}; + const ctx = { + args: { + filter: filter + }, + req: {accessToken: {userId: 1}} + }; + + const field = 'rate2'; + const newValue = 88; + + await models.FixedPrice.editFixedPrice(ctx, field, newValue, null, filter.where, options); + + const [result] = await models.FixedPrice.filter(ctx, filter, options); + + expect(result[field]).toEqual(newValue); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); +}); diff --git a/modules/item/back/methods/fixed-price/specs/upsertFixedPrice.spec.js b/modules/item/back/methods/fixed-price/specs/upsertFixedPrice.spec.js index 86f73122d..823406500 100644 --- a/modules/item/back/methods/fixed-price/specs/upsertFixedPrice.spec.js +++ b/modules/item/back/methods/fixed-price/specs/upsertFixedPrice.spec.js @@ -42,7 +42,7 @@ describe('upsertFixedPrice()', () => { delete ctx.args.started; delete ctx.args.ended; - ctx.args.hasMinPrice = true; + ctx.args.hasMinPrice = false; expect(result).toEqual(jasmine.objectContaining(ctx.args)); @@ -74,7 +74,7 @@ describe('upsertFixedPrice()', () => { delete ctx.args.started; delete ctx.args.ended; - ctx.args.hasMinPrice = false; + ctx.args.hasMinPrice = true; expect(result).toEqual(jasmine.objectContaining(ctx.args)); @@ -105,7 +105,7 @@ describe('upsertFixedPrice()', () => { rate2: rate2, rate3: firstRate3, minPrice: 0, - hasMinPrice: false + hasMinPrice: true }}; // create new fixed price diff --git a/modules/item/back/methods/fixed-price/upsertFixedPrice.js b/modules/item/back/methods/fixed-price/upsertFixedPrice.js index eb3eec1bd..edbd23604 100644 --- a/modules/item/back/methods/fixed-price/upsertFixedPrice.js +++ b/modules/item/back/methods/fixed-price/upsertFixedPrice.js @@ -87,7 +87,7 @@ module.exports = Self => { await targetItem.updateAttributes({ minPrice: args.minPrice, - hasMinPrice: args.minPrice ? true : false + hasMinPrice: args.hasMinPrice }, myOptions); const itemFields = [ diff --git a/modules/item/back/methods/item-image-queue/downloadImages.js b/modules/item/back/methods/item-image-queue/downloadImages.js deleted file mode 100644 index 7f53df95a..000000000 --- a/modules/item/back/methods/item-image-queue/downloadImages.js +++ /dev/null @@ -1,105 +0,0 @@ -const https = require('https'); -const fs = require('fs-extra'); -const path = require('path'); -const uuid = require('uuid'); - -module.exports = Self => { - Self.remoteMethod('downloadImages', { - description: 'Returns last entries', - accessType: 'WRITE', - returns: { - type: ['Object'], - root: true - }, - http: { - path: `/downloadImages`, - verb: 'POST' - } - }); - - Self.downloadImages = async() => { - const models = Self.app.models; - const container = await models.TempContainer.container('salix-image'); - const tempPath = path.join(container.client.root, container.name); - const maxAttempts = 3; - - const images = await Self.find({ - where: {attempts: {eq: maxAttempts}} - }); - - for (let image of images) { - const currentStamp = Date.vnNew().getTime(); - const updatedStamp = image.updated.getTime(); - const graceTime = Math.abs(currentStamp - updatedStamp); - const maxTTL = 3600 * 48 * 1000; // 48 hours in ms; - - if (graceTime >= maxTTL) - await Self.destroyById(image.itemFk); - } - - download(); - - async function download() { - const image = await Self.findOne({ - where: {url: {neq: null}, attempts: {lt: maxAttempts}}, - order: 'priority, attempts, updated' - }); - - if (!image) return; - - const fileName = `${uuid.v4()}.png`; - const filePath = path.join(tempPath, fileName); - const imageUrl = image.url.replace('http://', 'https://'); - - https.get(imageUrl, async response => { - if (response.statusCode != 200) { - const error = new Error(`Could not download the image. Status code ${response.statusCode}`); - - return await errorHandler(image.itemFk, error, filePath); - } - - const writeStream = fs.createWriteStream(filePath); - writeStream.on('open', () => response.pipe(writeStream)); - writeStream.on('error', async error => - await errorHandler(image.itemFk, error, filePath)); - writeStream.on('finish', () => writeStream.end()); - - writeStream.on('close', async function() { - try { - await models.Image.registerImage('catalog', filePath, fileName, image.itemFk); - await image.destroy(); - - download(); - } catch (error) { - await errorHandler(image.itemFk, error, filePath); - } - }); - }).on('error', async error => { - await errorHandler(image.itemFk, error, filePath); - }); - } - - async function errorHandler(rowId, error, filePath) { - try { - const row = await Self.findById(rowId); - - if (!row) return; - - if (row.attempts < maxAttempts) { - await row.updateAttributes({ - error: error, - attempts: row.attempts + 1, - updated: Date.vnNew() - }); - } - - if (filePath && fs.existsSync(filePath)) - await fs.unlink(filePath); - - download(); - } catch (err) { - throw new Error(`Image download failed: ${err}`); - } - } - }; -}; diff --git a/modules/item/back/models/fixed-price.js b/modules/item/back/models/fixed-price.js index 91010805f..45f8d79ef 100644 --- a/modules/item/back/models/fixed-price.js +++ b/modules/item/back/models/fixed-price.js @@ -2,4 +2,5 @@ module.exports = Self => { require('../methods/fixed-price/filter')(Self); require('../methods/fixed-price/upsertFixedPrice')(Self); require('../methods/fixed-price/getRate2')(Self); + require('../methods/fixed-price/editFixedPrice')(Self); }; diff --git a/modules/item/back/models/item-shelving.json b/modules/item/back/models/item-shelving.json index 0890350da..339b9ab6e 100644 --- a/modules/item/back/models/item-shelving.json +++ b/modules/item/back/models/item-shelving.json @@ -20,6 +20,9 @@ }, "created": { "type": "date" + }, + "isChecked": { + "type": "boolean" } }, "relations": { diff --git a/modules/item/front/fixed-price/index.html b/modules/item/front/fixed-price/index.html index ce7cefe7a..a82fd2742 100644 --- a/modules/item/front/fixed-price/index.html +++ b/modules/item/front/fixed-price/index.html @@ -1,6 +1,7 @@ @@ -31,15 +34,21 @@ + - + + - +
+ + + Item ID Description - Warehouse - Grouping price @@ -57,13 +66,24 @@ Ended + Warehouse +
+ + + - {{price.name}} + {{itemFk.selection.name}}

{{price.subName}}

@@ -100,18 +120,11 @@ tabindex="-1">
- - - - {{price.rate2 | currency: 'EUR':2}} + + {{price.rate2 | currency: 'EUR':2}} + - {{price.rate3 | currency: 'EUR':2}} + + {{price.rate3 | currency: 'EUR':2}} + + ng-model="price.hasMinPrice" + on-change="$ctrl.upsertPrice(price)"> - - + + + + - + + + + + - + ng-model="price.warehouseFk" + data="warehouses" + on-change="$ctrl.upsertPrice(price)" + tabindex="2"> + + +
+ + + + +
+ + + Edit + + {{::$ctrl.totalChecked}} + + buy(s) + + + + + + + + + + + + + + + + + + + diff --git a/modules/item/front/fixed-price/index.js b/modules/item/front/fixed-price/index.js index df2989043..a39cd6602 100644 --- a/modules/item/front/fixed-price/index.js +++ b/modules/item/front/fixed-price/index.js @@ -5,6 +5,9 @@ import './style.scss'; export default class Controller extends Section { constructor($element, $) { super($element, $); + this.editedColumn; + this.checkAll = false; + this.checkedFixedPrices = []; this.smartTableOptions = { activeButtons: { @@ -30,13 +33,146 @@ export default class Controller extends Section { } ] }; + + this.filterParams = { + warehouseFk: this.vnConfig.warehouseFk + }; + } + + getFilterParams() { + return { + warehouseFk: this.vnConfig.warehouseFk + }; + } + + get columns() { + if (this._columns) return this._columns; + + this._columns = [ + {field: 'rate2', displayName: this.$t('Grouping price')}, + {field: 'rate3', displayName: this.$t('Packing price')}, + {field: 'hasMinPrice', displayName: this.$t('Has min price')}, + {field: 'minPrice', displayName: this.$t('Min price')}, + {field: 'started', displayName: this.$t('Started')}, + {field: 'ended', displayName: this.$t('Ended')}, + {field: 'warehouseFk', displayName: this.$t('Warehouse')} + ]; + + return this._columns; + } + + get checked() { + const fixedPrices = this.$.model.data || []; + const checkedBuys = []; + for (let fixedPrice of fixedPrices) { + if (fixedPrice.checked) + checkedBuys.push(fixedPrice); + } + + return checkedBuys; + } + + uncheck() { + this.checkAll = false; + this.checkedFixedPrices = []; + } + + get totalChecked() { + if (this.checkedDummyCount) + return this.checkedDummyCount; + + return this.checked.length; + } + + saveChecked(fixedPriceId) { + const index = this.checkedFixedPrices.indexOf(fixedPriceId); + if (index !== -1) + return this.checkedFixedPrices.splice(index, 1); + return this.checkedFixedPrices.push(fixedPriceId); + } + + reCheck() { + if (!this.$.model.data) return; + if (!this.checkedFixedPrices.length) return; + + this.$.model.data.forEach(fixedPrice => { + if (this.checkedFixedPrices.includes(fixedPrice.id)) + fixedPrice.checked = true; + }); + } + + onEditAccept() { + const rowsToEdit = []; + for (let row of this.checked) + rowsToEdit.push({id: row.id, itemFk: row.itemFk}); + + const data = { + field: this.editedColumn.field, + newValue: this.editedColumn.newValue, + lines: rowsToEdit + }; + + if (this.checkedDummyCount && this.checkedDummyCount > 0) { + const params = {}; + if (this.$.model.userParams) { + const userParams = this.$.model.userParams; + for (let param in userParams) { + let newParam = this.exprBuilder(param, userParams[param]); + if (!newParam) + newParam = {[param]: userParams[param]}; + Object.assign(params, newParam); + } + } + if (this.$.model.userFilter) + Object.assign(params, this.$.model.userFilter.where); + + data.filter = params; + } + + return this.$http.post('FixedPrices/editFixedPrice', data) + .then(() => { + this.uncheck(); + this.$.model.refresh(); + }); + } + + isBigger(date) { + let today = Date.vnNew(); + today.setHours(0, 0, 0, 0); + + date = new Date(date); + date.setHours(0, 0, 0, 0); + + const timeDifference = today - date; + if (timeDifference < 0) return 'warning'; + } + + isLower(date) { + let today = Date.vnNew(); + today.setHours(0, 0, 0, 0); + + date = new Date(date); + date.setHours(0, 0, 0, 0); + + const timeDifference = today - date; + if (timeDifference > 0) return 'warning'; } add() { if (!this.$.model.data || this.$.model.data.length == 0) { this.$.model.data = []; this.$.model.proxiedData = []; - this.$.model.insert({}); + + const today = Date.vnNew(); + + const millisecsInDay = 86400000; + const daysInWeek = 7; + const nextWeek = new Date(today.getTime() + daysInWeek * millisecsInDay); + + this.$.model.insert({ + started: today, + ended: nextWeek + }); return; } @@ -66,10 +202,8 @@ export default class Controller extends Section { if (resetMinPrice) delete price['minPrice']; - price.hasMinPrice = price.minPrice ? true : false; - - let requiredFields = ['itemFk', 'started', 'ended', 'rate2', 'rate3']; - for (let field of requiredFields) + const requiredFields = ['itemFk', 'started', 'ended', 'rate2', 'rate3']; + for (const field of requiredFields) if (price[field] == undefined) return; const query = 'FixedPrices/upsertFixedPrice'; diff --git a/modules/item/front/fixed-price/index.spec.js b/modules/item/front/fixed-price/index.spec.js index 42dd898b3..ae24da60b 100644 --- a/modules/item/front/fixed-price/index.spec.js +++ b/modules/item/front/fixed-price/index.spec.js @@ -12,8 +12,92 @@ describe('fixed price', () => { const $scope = $rootScope.$new(); const $element = angular.element(''); controller = $componentController('vnFixedPrice', {$element, $scope}); + controller.$ = { + model: {refresh: () => {}}, + edit: {hide: () => {}} + }; })); + describe('get columns', () => { + it(`should return a set of columns`, () => { + let result = controller.columns; + + let length = result.length; + let anyColumn = Object.keys(result[Math.floor(Math.random() * Math.floor(length))]); + + expect(anyColumn).toContain('field', 'displayName'); + }); + }); + + 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}, + ]; + + let result = controller.checked; + + expect(result.length).toEqual(3); + }); + }); + + describe('reCheck()', () => { + it(`should recheck buys`, () => { + controller.$.model.data = [ + {checked: false, id: 1}, + {checked: false, id: 2}, + {checked: false, id: 3}, + {checked: false, id: 4}, + ]; + controller.checkedFixedPrices = [1, 2]; + + controller.reCheck(); + + expect(controller.$.model.data[0].checked).toEqual(true); + expect(controller.$.model.data[1].checked).toEqual(true); + expect(controller.$.model.data[2].checked).toEqual(false); + expect(controller.$.model.data[3].checked).toEqual(false); + }); + }); + + describe('saveChecked()', () => { + it(`should check buy`, () => { + const buyCheck = 3; + controller.checkedFixedPrices = [1, 2]; + + controller.saveChecked(buyCheck); + + expect(controller.checkedFixedPrices[2]).toEqual(buyCheck); + }); + + it(`should uncheck buy`, () => { + const buyUncheck = 3; + controller.checkedFixedPrices = [1, 2, 3]; + + controller.saveChecked(buyUncheck); + + expect(controller.checkedFixedPrices[2]).toEqual(undefined); + }); + }); + + describe('onEditAccept()', () => { + it(`should perform a query to update columns`, () => { + controller.editedColumn = {field: 'my field', newValue: 'the new value'}; + const query = 'FixedPrices/editFixedPrice'; + + $httpBackend.expectPOST(query).respond(); + controller.onEditAccept(); + $httpBackend.flush(); + + const result = controller.checked; + + expect(result.length).toEqual(0); + }); + }); + describe('upsertPrice()', () => { it('should do nothing if one or more required arguments are missing', () => { jest.spyOn(controller.vnApp, 'showSuccess'); diff --git a/modules/item/front/fixed-price/locale/es.yml b/modules/item/front/fixed-price/locale/es.yml index 6bdfcb678..6dacf96c9 100644 --- a/modules/item/front/fixed-price/locale/es.yml +++ b/modules/item/front/fixed-price/locale/es.yml @@ -3,3 +3,5 @@ Search prices by item ID or code: Buscar por ID de artículo o código Search fixed prices: Buscar precios fijados Add fixed price: Añadir precio fijado This row will be removed: Esta linea se eliminará +Edit fixed price(s): Editar precio(s) fijado(s) +Has min price: Tiene precio mínimo diff --git a/modules/item/front/fixed-price/style.scss b/modules/item/front/fixed-price/style.scss index ba3878dba..97ceaf7cd 100644 --- a/modules/item/front/fixed-price/style.scss +++ b/modules/item/front/fixed-price/style.scss @@ -1,20 +1,46 @@ @import "variables"; -smart-table table{ - [shrink-field]{ - width: 80px; - max-width: 80px; +vn-fixed-price{ + smart-table table{ + [shrink-field]{ + width: 80px; + max-width: 80px; + } + [shrink-field-expand]{ + width: 150px; + max-width: 150px; + } } - [shrink-field-expand]{ - width: 150px; - max-width: 150px; + + .minPrice { + align-items: center; + text-align: center; + vn-input-number { + width: 90px; + max-width: 90px; + } + } + + smart-table table tbody > * > td .chip { + padding: 0px; + } + + smart-table table tbody > * > td{ + padding: 0px; + padding-left: 5px; + padding-right: 5px; + } + + smart-table table tbody > * > td .chip.warning { + color: $color-font-bg + } + + .vn-field > .container > .infix > .control > input { + color: inherit; + } + + vn-input-number.inactive{ + input { + color: $color-font-light !important; + } } } - -.minPrice { - align-items: center; - text-align: center; - vn-input-number { - width: 90px; - max-width: 90px; - } -} \ No newline at end of file diff --git a/modules/item/front/request-search-panel/index.html b/modules/item/front/request-search-panel/index.html index 8c9d04b64..a431d4fd6 100644 --- a/modules/item/front/request-search-panel/index.html +++ b/modules/item/front/request-search-panel/index.html @@ -38,6 +38,19 @@ url="Warehouses"> + + + {{firstName}} {{lastName}} + + +
{ type: 'number', description: `Search requests attended by a given worker id` }, + { + arg: 'requesterFk', + type: 'number' + }, { arg: 'mine', type: 'boolean', @@ -89,6 +93,8 @@ module.exports = Self => { return {'t.id': value}; case 'attenderFk': return {'tr.attenderFk': value}; + case 'requesterFk': + return {'tr.requesterFk': value}; case 'state': switch (value) { case 'pending': @@ -125,6 +131,7 @@ module.exports = Self => { tr.description, tr.response, tr.saleFk, + tr.requesterFk, tr.isOk, s.quantity AS saleQuantity, s.itemFk, diff --git a/modules/ticket/back/methods/ticket/saveSign.js b/modules/ticket/back/methods/ticket/saveSign.js index ab1c32d1b..39347f418 100644 --- a/modules/ticket/back/methods/ticket/saveSign.js +++ b/modules/ticket/back/methods/ticket/saveSign.js @@ -34,6 +34,8 @@ module.exports = Self => { const models = Self.app.models; const myOptions = {}; let tx; + let dms; + let gestDocCreated = false; if (typeof options == 'object') Object.assign(myOptions, options); @@ -96,11 +98,12 @@ module.exports = Self => { warehouseId: ticket.warehouseFk, companyId: ticket.companyFk, dmsTypeId: dmsType.id, - reference: id, - description: `Ticket ${id} Cliente ${ticket.client().name} Ruta ${ticket.route().id}`, + reference: '', + description: `Firma del cliente - Ruta ${ticket.route().id}`, hasFile: true }; - await models.Ticket.uploadFile(ctxUploadFile, id, myOptions); + dms = await models.Dms.uploadFile(ctxUploadFile, myOptions); + gestDocCreated = true; } try { @@ -118,12 +121,16 @@ module.exports = Self => { throw new UserError('This ticket cannot be signed because it has not been boxed'); else if (!await gestDocExists(args.tickets[i])) { if (args.location) setLocation(args.tickets[i]); - await createGestDoc(args.tickets[i]); + if (!gestDocCreated) await createGestDoc(args.tickets[i]); + await models.TicketDms.create({ticketFk: args.tickets[i], dmsFk: dms[0].id}, myOptions); + const ticket = await models.Ticket.findById(args.tickets[i], null, myOptions); + await ticket.updateAttribute('isSigned', true, myOptions); await Self.rawSql(`CALL vn.ticket_setState(?, ?)`, [args.tickets[i], 'DELIVERED'], myOptions); } } if (tx) await tx.commit(); + return; } catch (e) { if (tx) await tx.rollback(); throw e; diff --git a/modules/worker/back/methods/worker-time-control/addTimeEntry.js b/modules/worker/back/methods/worker-time-control/addTimeEntry.js index fef3cf223..c8c08d9b1 100644 --- a/modules/worker/back/methods/worker-time-control/addTimeEntry.js +++ b/modules/worker/back/methods/worker-time-control/addTimeEntry.js @@ -51,6 +51,8 @@ module.exports = Self => { if (response[0] && response[0].error) throw new UserError(response[0].error); + await models.WorkerTimeControl.resendWeeklyHourEmail(ctx, workerId, args.timed, myOptions); + return response; }; }; diff --git a/modules/worker/back/methods/worker-time-control/deleteTimeEntry.js b/modules/worker/back/methods/worker-time-control/deleteTimeEntry.js index c80dcab81..e33d6b790 100644 --- a/modules/worker/back/methods/worker-time-control/deleteTimeEntry.js +++ b/modules/worker/back/methods/worker-time-control/deleteTimeEntry.js @@ -38,7 +38,11 @@ module.exports = Self => { if (isSubordinate === false || (isSubordinate && isHimself && !isTeamBoss)) throw new UserError(`You don't have enough privileges`); - return Self.rawSql('CALL vn.workerTimeControl_remove(?, ?)', [ + const response = await Self.rawSql('CALL vn.workerTimeControl_remove(?, ?)', [ targetTimeEntry.userFk, targetTimeEntry.timed], myOptions); + + await models.WorkerTimeControl.resendWeeklyHourEmail(ctx, targetTimeEntry.userFk, targetTimeEntry.timed, myOptions); + + return response; }; }; diff --git a/modules/worker/back/methods/worker-time-control/resendWeeklyHourEmail.js b/modules/worker/back/methods/worker-time-control/resendWeeklyHourEmail.js new file mode 100644 index 000000000..2452a29f9 --- /dev/null +++ b/modules/worker/back/methods/worker-time-control/resendWeeklyHourEmail.js @@ -0,0 +1,68 @@ +module.exports = Self => { + Self.remoteMethodCtx('resendWeeklyHourEmail', { + description: 'Adds a new hour registry', + accessType: 'WRITE', + accepts: [{ + arg: 'id', + type: 'number', + description: 'The worker id', + http: {source: 'path'} + }, + { + arg: 'dated', + type: 'date', + required: true + }], + returns: [{ + type: 'Object', + root: true + }], + http: { + path: `/:id/resendWeeklyHourEmail`, + verb: 'POST' + } + }); + + Self.resendWeeklyHourEmail = async(ctx, workerId, dated, options) => { + const models = Self.app.models; + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + const yearNumber = dated.getFullYear(); + const weekNumber = getWeekNumber(dated); + const workerTimeControlMail = await models.WorkerTimeControlMail.findOne({ + where: { + workerFk: workerId, + year: yearNumber, + week: weekNumber + } + }, myOptions); + + if (workerTimeControlMail && workerTimeControlMail.state != 'SENDED') { + const worker = await models.EmailUser.findById(workerId, null, myOptions); + ctx.args = { + recipient: worker.email, + year: yearNumber, + week: weekNumber, + workerId: workerId, + state: 'SENDED' + }; + return models.WorkerTimeControl.weeklyHourRecordEmail(ctx, myOptions); + } + + return false; + }; + + function getWeekNumber(date) { + const tempDate = new Date(date); + let dayOfWeek = tempDate.getDay(); + dayOfWeek = (dayOfWeek === 0) ? 7 : dayOfWeek; + const firstDayOfWeek = new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate() - (dayOfWeek - 1)); + const firstDayOfYear = new Date(tempDate.getFullYear(), 0, 1); + const differenceInMilliseconds = firstDayOfWeek.getTime() - firstDayOfYear.getTime(); + const weekNumber = Math.floor(differenceInMilliseconds / (1000 * 60 * 60 * 24 * 7)) + 1; + return weekNumber; + } +}; diff --git a/modules/worker/back/methods/worker-time-control/sendMail.js b/modules/worker/back/methods/worker-time-control/sendMail.js index 4962e96f8..2c827e320 100644 --- a/modules/worker/back/methods/worker-time-control/sendMail.js +++ b/modules/worker/back/methods/worker-time-control/sendMail.js @@ -82,14 +82,9 @@ module.exports = Self => { updated: Date.vnNew(), state: 'SENDED' }, myOptions); - stmt = new ParameterizedSQL( - `CALL vn.timeControl_calculateByUser(?, ?, ?) - `, [args.workerId, started, ended]); + stmt = new ParameterizedSQL('DROP TEMPORARY TABLE IF EXISTS tmp.`user`'); stmts.push(stmt); - - stmt = new ParameterizedSQL( - `CALL vn.timeBusiness_calculateByUser(?, ?, ?) - `, [args.workerId, started, ended]); + stmt = new ParameterizedSQL('CREATE TEMPORARY TABLE tmp.`user` SELECT id userFk FROM account.user WHERE id = ?', [args.workerId]); stmts.push(stmt); } else { await models.WorkerTimeControl.destroyAll({ @@ -105,13 +100,38 @@ module.exports = Self => { updated: Date.vnNew(), state: 'SENDED' }, myOptions); - stmt = new ParameterizedSQL(`CALL vn.timeControl_calculateAll(?, ?)`, [started, ended]); + stmt = new ParameterizedSQL('DROP TEMPORARY TABLE IF EXISTS tmp.`user`'); stmts.push(stmt); - - stmt = new ParameterizedSQL(`CALL vn.timeBusiness_calculateAll(?, ?)`, [started, ended]); + stmt = new ParameterizedSQL('CREATE TEMPORARY TABLE IF NOT EXISTS tmp.`user` SELECT userFk FROM vn.worker w JOIN account.`user` u ON u.id = w.userFk WHERE userFk IS NOT NULL'); stmts.push(stmt); } + stmt = new ParameterizedSQL( + `CALL vn.timeControl_calculate(?, ?) + `, [started, ended]); + stmts.push(stmt); + + stmt = new ParameterizedSQL( + `CALL vn.timeBusiness_calculate(?, ?) + `, [started, ended]); + stmts.push(stmt); + + stmt = new ParameterizedSQL( + `CALL vn.timeControl_getError(?, ?) + `, [started, ended]); + stmts.push(stmt); + + stmt = new ParameterizedSQL(`INSERT INTO mail (receiver, subject, body) + SELECT CONCAT(u.name, '@verdnatura.es'), + CONCAT('Error registro de horas semana ', ?, ' año ', ?) , + CONCAT('No se ha podido enviar el registro de horas al empleado/s: ', GROUP_CONCAT(DISTINCT CONCAT('
', w.id, ' ', w.firstName, ' ', w.lastName))) + FROM tmp.timeControlError tce + JOIN vn.workerTimeControl wtc ON wtc.id = tce.id + JOIN worker w ON w.id = wtc.userFK + JOIN account.user u ON u.id = w.bossFk + GROUP BY w.bossFk`, [args.week, args.year]); + stmts.push(stmt); + stmt = new ParameterizedSQL(` SELECT CONCAT(u.name, '@verdnatura.es') receiver, u.id workerFk, @@ -131,20 +151,16 @@ module.exports = Self => { JOIN business b ON b.id = tb.businessFk LEFT JOIN tmp.timeControlCalculate tc ON tc.userFk = tb.userFk AND tc.dated = tb.dated LEFT JOIN worker w ON w.id = u.id - JOIN (SELECT tb.userFk, - SUM(IF(tb.type IS NULL, - IF(tc.timeWorkDecimal > 0, FALSE, IF(tb.timeWorkDecimal > 0, TRUE, FALSE)), - TRUE))isTeleworkingWeek - FROM tmp.timeBusinessCalculate tb - LEFT JOIN tmp.timeControlCalculate tc ON tc.userFk = tb.userFk - AND tc.dated = tb.dated - GROUP BY tb.userFk - HAVING isTeleworkingWeek > 0 - )sub ON sub.userFk = u.id - WHERE d.hasToRefill + LEFT JOIN ( + SELECT DISTINCT wtc.userFk + FROM tmp.timeControlError tce + JOIN vn.workerTimeControl wtc ON wtc.id = tce.id + )sub ON sub.userFk = tb.userFk + WHERE sub.userFK IS NULL AND IFNULL(?, u.id) = u.id AND b.companyCodeFk = 'VNL' AND w.businessFk + AND d.isTeleworking ORDER BY u.id, tb.dated `, [args.workerId]); const index = stmts.push(stmt) - 1; @@ -332,23 +348,18 @@ module.exports = Self => { const lastDay = days[index][days[index].length - 1]; if (day.workerFk != previousWorkerFk || day == lastDay) { - await models.WorkerTimeControlMail.create({ - workerFk: previousWorkerFk, + const query = `INSERT IGNORE INTO workerTimeControlMail (workerFk, year, week) + VALUES(?, ?, ?);`; + await Self.rawSql(query, [previousWorkerFk, args.year, args.week]); + + ctx.args = { + recipient: previousReceiver, year: args.year, - week: args.week - }, myOptions); - - const salix = await models.Url.findOne({ - where: { - appName: 'salix', - environment: process.env.NODE_ENV || 'dev' - } - }, myOptions); - - const timestamp = started.getTime() / 1000; - const url = `${salix.url}worker/${previousWorkerFk}/time-control?timestamp=${timestamp}`; - - await models.WorkerTimeControl.weeklyHourRecordEmail(ctx, previousReceiver, args.week, args.year, url); + week: args.week, + workerId: previousWorkerFk, + state: 'SENDED' + }; + await models.WorkerTimeControl.weeklyHourRecordEmail(ctx, myOptions); previousWorkerFk = day.workerFk; previousReceiver = day.receiver; diff --git a/modules/worker/back/methods/worker-time-control/specs/sendMail.spec.js b/modules/worker/back/methods/worker-time-control/specs/sendMail.spec.js deleted file mode 100644 index 24bfd6904..000000000 --- a/modules/worker/back/methods/worker-time-control/specs/sendMail.spec.js +++ /dev/null @@ -1,120 +0,0 @@ -const models = require('vn-loopback/server/server').models; - -describe('workerTimeControl sendMail()', () => { - const workerId = 18; - const activeCtx = { - getLocale: () => { - return 'en'; - } - }; - const ctx = {req: activeCtx, args: {}}; - - it('should fill time control of a worker without records in Journey and with rest', async() => { - const tx = await models.WorkerTimeControl.beginTransaction({}); - - try { - const options = {transaction: tx}; - - await models.WorkerTimeControl.sendMail(ctx, options); - - const workerTimeControl = await models.WorkerTimeControl.find({ - where: {userFk: workerId} - }, options); - - expect(workerTimeControl[0].timed.getHours()).toEqual(8); - expect(workerTimeControl[1].timed.getHours()).toEqual(9); - expect(`${workerTimeControl[2].timed.getHours()}:${workerTimeControl[2].timed.getMinutes()}`).toEqual('9:20'); - expect(workerTimeControl[3].timed.getHours()).toEqual(16); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } - }); - - it('should fill time control of a worker without records in Journey and without rest', async() => { - const workdayOf20Hours = 3; - const tx = await models.WorkerTimeControl.beginTransaction({}); - - try { - const options = {transaction: tx}; - query = `UPDATE business b - SET b.calendarTypeFk = ? - WHERE b.workerFk = ?; `; - await models.WorkerTimeControl.rawSql(query, [workdayOf20Hours, workerId], options); - - await models.WorkerTimeControl.sendMail(ctx, options); - - const workerTimeControl = await models.WorkerTimeControl.find({ - where: {userFk: workerId} - }, options); - - expect(workerTimeControl[0].timed.getHours()).toEqual(8); - expect(workerTimeControl[1].timed.getHours()).toEqual(12); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } - }); - - it('should fill time control of a worker with records in Journey and with rest', async() => { - const tx = await models.WorkerTimeControl.beginTransaction({}); - - try { - const options = {transaction: tx}; - query = `INSERT INTO postgresql.journey(journey_id, day_id, start, end, business_id) - VALUES - (1, 1, '09:00:00', '13:00:00', ?), - (2, 1, '14:00:00', '19:00:00', ?);`; - await models.WorkerTimeControl.rawSql(query, [workerId, workerId, workerId], options); - - await models.WorkerTimeControl.sendMail(ctx, options); - - const workerTimeControl = await models.WorkerTimeControl.find({ - where: {userFk: workerId} - }, options); - - expect(workerTimeControl[0].timed.getHours()).toEqual(9); - expect(workerTimeControl[2].timed.getHours()).toEqual(10); - expect(`${workerTimeControl[3].timed.getHours()}:${workerTimeControl[3].timed.getMinutes()}`).toEqual('10:20'); - expect(workerTimeControl[1].timed.getHours()).toEqual(13); - expect(workerTimeControl[4].timed.getHours()).toEqual(14); - expect(workerTimeControl[5].timed.getHours()).toEqual(19); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } - }); - - it('should fill time control of a worker with records in Journey and without rest', async() => { - const tx = await models.WorkerTimeControl.beginTransaction({}); - - try { - const options = {transaction: tx}; - query = `INSERT INTO postgresql.journey(journey_id, day_id, start, end, business_id) - VALUES - (1, 1, '12:30:00', '14:00:00', ?);`; - await models.WorkerTimeControl.rawSql(query, [workerId, workerId, workerId], options); - - await models.WorkerTimeControl.sendMail(ctx, options); - - const workerTimeControl = await models.WorkerTimeControl.find({ - where: {userFk: workerId} - }, options); - - expect(`${workerTimeControl[0].timed.getHours()}:${workerTimeControl[0].timed.getMinutes()}`).toEqual('12:30'); - expect(workerTimeControl[1].timed.getHours()).toEqual(14); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } - }); -}); - diff --git a/modules/worker/back/methods/worker-time-control/updateTimeEntry.js b/modules/worker/back/methods/worker-time-control/updateTimeEntry.js index a99a61770..83349ea63 100644 --- a/modules/worker/back/methods/worker-time-control/updateTimeEntry.js +++ b/modules/worker/back/methods/worker-time-control/updateTimeEntry.js @@ -46,8 +46,12 @@ module.exports = Self => { if (notAllowed) throw new UserError(`You don't have enough privileges`); - return targetTimeEntry.updateAttributes({ + const timeEntryUpdated = await targetTimeEntry.updateAttributes({ direction: args.direction }, myOptions); + + await models.WorkerTimeControl.resendWeeklyHourEmail(ctx, targetTimeEntry.userFk, targetTimeEntry.timed, myOptions); + + return timeEntryUpdated; }; }; diff --git a/modules/worker/back/methods/worker-time-control/updateWorkerTimeControlMail.js b/modules/worker/back/methods/worker-time-control/updateWorkerTimeControlMail.js index 642ff90d2..6f794511f 100644 --- a/modules/worker/back/methods/worker-time-control/updateWorkerTimeControlMail.js +++ b/modules/worker/back/methods/worker-time-control/updateWorkerTimeControlMail.js @@ -47,10 +47,6 @@ module.exports = Self => { if (typeof options == 'object') Object.assign(myOptions, options); - const isHimself = userId == args.workerId; - if (!isHimself) - throw new UserError(`You don't have enough privileges`); - const workerTimeControlMail = await models.WorkerTimeControlMail.findOne({ where: { workerFk: args.workerId, @@ -69,6 +65,12 @@ module.exports = Self => { reason: args.reason || null }, myOptions); + if (args.state == 'SENDED') { + await workerTimeControlMail.updateAttributes({ + sendedCounter: workerTimeControlMail.sendedCounter + 1 + }, myOptions); + } + const logRecord = { originFk: args.workerId, userFk: userId, diff --git a/modules/worker/back/methods/worker-time-control/weeklyHourRecordEmail.js b/modules/worker/back/methods/worker-time-control/weeklyHourRecordEmail.js index 6feadb936..f44080559 100644 --- a/modules/worker/back/methods/worker-time-control/weeklyHourRecordEmail.js +++ b/modules/worker/back/methods/worker-time-control/weeklyHourRecordEmail.js @@ -1,5 +1,3 @@ -const {Email} = require('vn-print'); - module.exports = Self => { Self.remoteMethodCtx('weeklyHourRecordEmail', { description: 'Sends the weekly hour record', @@ -22,7 +20,12 @@ module.exports = Self => { required: true }, { - arg: 'url', + arg: 'workerId', + type: 'number', + required: true + }, + { + arg: 'state', type: 'string', required: true } @@ -37,17 +40,48 @@ module.exports = Self => { } }); - Self.weeklyHourRecordEmail = async(ctx, recipient, week, year, url) => { - const params = { - recipient: recipient, - lang: ctx.req.getLocale(), - week: week, - year: year, - url: url - }; + Self.weeklyHourRecordEmail = async(ctx, options) => { + const models = Self.app.models; + const args = ctx.args; + const myOptions = {}; - const email = new Email('weekly-hour-record', params); + if (typeof options == 'object') + Object.assign(myOptions, options); - return email.send(); + const salix = await models.Url.findOne({ + where: { + appName: 'salix', + environment: process.env.NODE_ENV || 'dev' + } + }, myOptions); + + const dated = getMondayDateFromYearWeek(args.year, args.week); + const timestamp = dated.getTime() / 1000; + + const url = `${salix.url}worker/${args.workerId}/time-control?timestamp=${timestamp}`; + ctx.args.url = url; + + Self.sendTemplate(ctx, 'weekly-hour-record'); + + return models.WorkerTimeControl.updateWorkerTimeControlMail(ctx, myOptions); }; + + function getMondayDateFromYearWeek(yearNumber, weekNumber) { + const yearStart = new Date(yearNumber, 0, 1); + const firstMonday = new Date(yearStart.getTime() + ((7 - yearStart.getDay() + 1) % 7) * 86400000); + const firstMondayWeekNumber = getWeekNumber(firstMonday); + + if (firstMondayWeekNumber > 1) + firstMonday.setDate(firstMonday.getDate() + 7); + + firstMonday.setDate(firstMonday.getDate() + (weekNumber - 1) * 7); + + return firstMonday; + } + + function getWeekNumber(date) { + const firstDayOfYear = new Date(date.getFullYear(), 0, 1); + const daysPassed = (date - firstDayOfYear) / 86400000; + return Math.ceil((daysPassed + firstDayOfYear.getDay() + 1) / 7); + } }; diff --git a/modules/worker/back/model-config.json b/modules/worker/back/model-config.json index 63fc65827..145934700 100644 --- a/modules/worker/back/model-config.json +++ b/modules/worker/back/model-config.json @@ -53,6 +53,9 @@ "Worker": { "dataSource": "vn" }, + "WorkerObservation": { + "dataSource": "vn" + }, "WorkerConfig": { "dataSource": "vn" }, diff --git a/modules/worker/back/models/worker-observation.js b/modules/worker/back/models/worker-observation.js new file mode 100644 index 000000000..cccc2cfbd --- /dev/null +++ b/modules/worker/back/models/worker-observation.js @@ -0,0 +1,12 @@ +module.exports = function(Self) { + Self.validatesPresenceOf('text', { + message: 'Description cannot be blank' + }); + + Self.observe('before save', async function(ctx) { + ctx.instance.created = new Date(); + let token = ctx.options.accessToken; + let userId = token && token.userId; + ctx.instance.userFk = userId; + }); +}; diff --git a/modules/worker/back/models/worker-observation.json b/modules/worker/back/models/worker-observation.json new file mode 100644 index 000000000..90eb35837 --- /dev/null +++ b/modules/worker/back/models/worker-observation.json @@ -0,0 +1,39 @@ +{ + "name": "WorkerObservation", + "base": "VnModel", + "options": { + "mysql": { + "table": "workerObservation" + } + }, + "properties": { + "id": { + "id": true, + "type": "number" + }, + "workerFk": { + "type": "number" + }, + "userFk": { + "type": "number" + }, + "text": { + "type": "string" + }, + "created": { + "type": "date" + } + }, + "relations": { + "worker": { + "type": "belongsTo", + "model": "Worker", + "foreignKey": "workerFk" + }, + "user":{ + "type": "belongsTo", + "model": "Account", + "foreignKey": "userFk" + } + } +} diff --git a/modules/worker/back/models/worker-time-control-mail.json b/modules/worker/back/models/worker-time-control-mail.json index 78b99881d..87eae9217 100644 --- a/modules/worker/back/models/worker-time-control-mail.json +++ b/modules/worker/back/models/worker-time-control-mail.json @@ -28,6 +28,9 @@ }, "reason": { "type": "string" + }, + "sendedCounter": { + "type": "number" } }, "acls": [ diff --git a/modules/worker/back/models/worker-time-control.js b/modules/worker/back/models/worker-time-control.js index 5b13e17f2..d5da680cf 100644 --- a/modules/worker/back/models/worker-time-control.js +++ b/modules/worker/back/models/worker-time-control.js @@ -9,6 +9,7 @@ module.exports = Self => { require('../methods/worker-time-control/updateWorkerTimeControlMail')(Self); require('../methods/worker-time-control/weeklyHourRecordEmail')(Self); require('../methods/worker-time-control/getMailStates')(Self); + require('../methods/worker-time-control/resendWeeklyHourEmail')(Self); Self.rewriteDbError(function(err) { if (err.code === 'ER_DUP_ENTRY') diff --git a/modules/worker/front/index.js b/modules/worker/front/index.js index 657f6a8c6..8fad2c0df 100644 --- a/modules/worker/front/index.js +++ b/modules/worker/front/index.js @@ -18,3 +18,6 @@ import './log'; import './dms/index'; import './dms/create'; import './dms/edit'; +import './note/index'; +import './note/create'; + diff --git a/modules/worker/front/locale/es.yml b/modules/worker/front/locale/es.yml index b5bcfefa4..a25377122 100644 --- a/modules/worker/front/locale/es.yml +++ b/modules/worker/front/locale/es.yml @@ -31,3 +31,5 @@ Deallocate PDA: Desasignar PDA PDA deallocated: PDA desasignada PDA allocated: PDA asignada New PDA: Nueva PDA +Notes: Notas +New note: Nueva nota diff --git a/modules/worker/front/note/create/index.html b/modules/worker/front/note/create/index.html new file mode 100644 index 000000000..d09fc2da5 --- /dev/null +++ b/modules/worker/front/note/create/index.html @@ -0,0 +1,30 @@ + + +
+ + + + + + + + + + + + +
\ No newline at end of file diff --git a/modules/worker/front/note/create/index.js b/modules/worker/front/note/create/index.js new file mode 100644 index 000000000..81ee247db --- /dev/null +++ b/modules/worker/front/note/create/index.js @@ -0,0 +1,21 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + constructor($element, $) { + super($element, $); + this.note = { + workerFk: parseInt(this.$params.id), + text: null + }; + } + + cancel() { + this.$state.go('worker.card.note.index', {id: this.$params.id}); + } +} + +ngModule.vnComponent('vnNoteWorkerCreate', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/worker/front/note/create/index.spec.js b/modules/worker/front/note/create/index.spec.js new file mode 100644 index 000000000..d900c8ee0 --- /dev/null +++ b/modules/worker/front/note/create/index.spec.js @@ -0,0 +1,22 @@ +import './index'; + +describe('Worker', () => { + describe('Component vnNoteWorkerCreate', () => { + let $state; + let controller; + + beforeEach(ngModule('worker')); + + beforeEach(inject(($componentController, _$state_) => { + $state = _$state_; + $state.params.id = '1234'; + const $element = angular.element(''); + controller = $componentController('vnNoteWorkerCreate', {$element, $state}); + })); + + it('should define workerFk using $state.params.id', () => { + expect(controller.note.workerFk).toBe(1234); + expect(controller.note.worker).toBe(undefined); + }); + }); +}); diff --git a/modules/worker/front/note/create/locale/es.yml b/modules/worker/front/note/create/locale/es.yml new file mode 100644 index 000000000..bfe773f48 --- /dev/null +++ b/modules/worker/front/note/create/locale/es.yml @@ -0,0 +1,2 @@ +New note: Nueva nota +Note: Nota \ No newline at end of file diff --git a/modules/worker/front/note/index/index.html b/modules/worker/front/note/index/index.html new file mode 100644 index 000000000..9f5c27008 --- /dev/null +++ b/modules/worker/front/note/index/index.html @@ -0,0 +1,32 @@ + + + + +
+ + {{::note.user.nickname}} + {{::note.created | date:'dd/MM/yyyy HH:mm'}} + + + {{::note.text}} + +
+
+
+ + + diff --git a/modules/worker/front/note/index/index.js b/modules/worker/front/note/index/index.js new file mode 100644 index 000000000..d20971413 --- /dev/null +++ b/modules/worker/front/note/index/index.js @@ -0,0 +1,22 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; +import './style.scss'; + +export default class Controller extends Section { + constructor($element, $) { + super($element, $); + this.filter = { + order: 'created DESC', + }; + } +} + +Controller.$inject = ['$element', '$scope']; + +ngModule.vnComponent('vnWorkerNote', { + template: require('./index.html'), + controller: Controller, + bindings: { + worker: '<' + } +}); diff --git a/modules/worker/front/note/index/style.scss b/modules/worker/front/note/index/style.scss new file mode 100644 index 000000000..5ff6baf4f --- /dev/null +++ b/modules/worker/front/note/index/style.scss @@ -0,0 +1,5 @@ +vn-worker-note { + .note:last-child { + margin-bottom: 0; + } +} \ No newline at end of file diff --git a/modules/worker/front/routes.json b/modules/worker/front/routes.json index 64b98bfca..64cb186d6 100644 --- a/modules/worker/front/routes.json +++ b/modules/worker/front/routes.json @@ -11,6 +11,7 @@ ], "card": [ {"state": "worker.card.basicData", "icon": "settings"}, + {"state": "worker.card.note.index", "icon": "insert_drive_file"}, {"state": "worker.card.timeControl", "icon": "access_time"}, {"state": "worker.card.calendar", "icon": "icon-calendar"}, {"state": "worker.card.pda", "icon": "phone_android"}, @@ -72,6 +73,24 @@ "component": "vn-worker-log", "description": "Log", "acl": ["salesAssistant"] + }, { + "url": "/note", + "state": "worker.card.note", + "component": "ui-view", + "abstract": true + }, { + "url": "/index", + "state": "worker.card.note.index", + "component": "vn-worker-note", + "description": "Notes", + "params": { + "worker": "$ctrl.worker" + } + }, { + "url": "/create", + "state": "worker.card.note.create", + "component": "vn-note-worker-create", + "description": "New note" }, { "url": "/pbx", "state": "worker.card.pbx", diff --git a/modules/worker/front/time-control/index.html b/modules/worker/front/time-control/index.html index bd7e68b89..044ea4038 100644 --- a/modules/worker/front/time-control/index.html +++ b/modules/worker/front/time-control/index.html @@ -204,7 +204,7 @@ vn-id="sendEmailConfirmation" on-accept="$ctrl.resendEmail()" message="Send time control email"> - + Are you sure you want to send it? diff --git a/modules/worker/front/time-control/index.js b/modules/worker/front/time-control/index.js index 9ed454d31..85ddcedfe 100644 --- a/modules/worker/front/time-control/index.js +++ b/modules/worker/front/time-control/index.js @@ -303,7 +303,10 @@ class Controller extends Section { const query = `WorkerTimeControls/${this.worker.id}/addTimeEntry`; this.$http.post(query, entry) - .then(() => this.fetchHours()); + .then(() => { + this.fetchHours(); + this.getMailStates(this.date); + }); } catch (e) { this.vnApp.showError(this.$t(e.message)); return false; @@ -324,6 +327,7 @@ class Controller extends Section { this.$http.post(`WorkerTimeControls/${entryId}/deleteTimeEntry`).then(() => { this.fetchHours(); + this.getMailStates(this.date); this.vnApp.showSuccess(this.$t('Entry removed')); }); } @@ -395,23 +399,24 @@ class Controller extends Section { this.$http.post(query, {direction: entry.direction}) .then(() => this.vnApp.showSuccess(this.$t('Data saved!'))) .then(() => this.$.editEntry.hide()) - .then(() => this.fetchHours()); + .then(() => this.fetchHours()) + .then(() => this.getMailStates(this.date)); } catch (e) { this.vnApp.showError(this.$t(e.message)); } } resendEmail() { - const timestamp = this.date.getTime() / 1000; - const url = `${window.location.origin}/#!/worker/${this.worker.id}/time-control?timestamp=${timestamp}`; const params = { recipient: this.worker.user.emailUser.email, week: this.weekNumber, year: this.date.getFullYear(), - url: url, + workerId: this.worker.id, + state: 'SENDED' }; this.$http.post(`WorkerTimeControls/weekly-hour-hecord-email`, params) .then(() => { + this.getMailStates(this.date); this.vnApp.showSuccess(this.$t('Email sended')); }); } diff --git a/modules/worker/front/time-control/index.spec.js b/modules/worker/front/time-control/index.spec.js index 0f9b48f6b..94f9d3d48 100644 --- a/modules/worker/front/time-control/index.spec.js +++ b/modules/worker/front/time-control/index.spec.js @@ -120,6 +120,13 @@ describe('Component vnWorkerTimeControl', () => { describe('save() ', () => { it(`should make a query an then call to the fetchHours() method`, () => { + const today = Date.vnNew(); + + jest.spyOn(controller, 'getWeekData').mockReturnThis(); + jest.spyOn(controller, 'getMailStates').mockReturnThis(); + + controller.$.model = {applyFilter: jest.fn().mockReturnValue(Promise.resolve())}; + controller.date = today; controller.fetchHours = jest.fn(); controller.selectedRow = {id: 1, timed: Date.vnNew(), direction: 'in'}; controller.$.editEntry = { @@ -240,7 +247,9 @@ describe('Component vnWorkerTimeControl', () => { describe('resendEmail() ', () => { it(`should make a query an then call showSuccess method`, () => { const today = Date.vnNew(); + jest.spyOn(controller, 'getWeekData').mockReturnThis(); + jest.spyOn(controller, 'getMailStates').mockReturnThis(); jest.spyOn(controller.vnApp, 'showSuccess'); controller.$.model = {applyFilter: jest.fn().mockReturnValue(Promise.resolve())}; diff --git a/package.json b/package.json index 9af083934..5ab8991fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salix-back", - "version": "23.12.01", + "version": "23.14.01", "author": "Verdnatura Levante SL", "description": "Salix backend", "license": "GPL-3.0",