diff --git a/back/methods/account/change-password.js b/back/methods/account/change-password.js index 25b63b9a8..c0956b193 100644 --- a/back/methods/account/change-password.js +++ b/back/methods/account/change-password.js @@ -5,17 +5,17 @@ module.exports = Self => { accepts: [ { arg: 'id', - type: 'Number', + type: 'number', description: 'The user id', http: {source: 'path'} }, { arg: 'oldPassword', - type: 'String', + type: 'string', description: 'The old password', required: true }, { arg: 'newPassword', - type: 'String', + type: 'string', description: 'The new password', required: true } diff --git a/back/methods/chat/send.js b/back/methods/chat/send.js index fcb49f4b8..c5c8feead 100644 --- a/back/methods/chat/send.js +++ b/back/methods/chat/send.js @@ -1,7 +1,6 @@ -const axios = require('axios'); module.exports = Self => { Self.remoteMethodCtx('send', { - description: 'Send a RocketChat message', + description: 'Creates a direct message in the chat model for a user or a channel', accessType: 'WRITE', accepts: [{ arg: 'to', @@ -31,39 +30,19 @@ module.exports = Self => { const recipient = to.replace('@', ''); if (sender.name != recipient) { - await sendMessage(sender, to, message); + await models.Chat.create({ + senderFk: sender.id, + recipient: to, + dated: new Date(), + checkUserStatus: 0, + message: message, + status: 0, + attempts: 0 + }); return true; } return false; }; - - async function sendMessage(sender, channel, message) { - if (process.env.NODE_ENV !== 'production') { - return new Promise(resolve => { - return resolve({ - statusCode: 200, - message: 'Fake notification sent' - }); - }); - } - - const login = await Self.getServiceAuth(); - const avatar = `${login.host}/avatar/${sender.name}`; - - const options = { - headers: { - 'X-Auth-Token': login.auth.token, - 'X-User-Id': login.auth.userId - }, - }; - - return axios.post(`${login.api}/chat.postMessage`, { - 'channel': channel, - 'avatar': avatar, - 'alias': sender.nickname, - 'text': message - }, options); - } }; diff --git a/back/methods/chat/sendCheckingPresence.js b/back/methods/chat/sendCheckingPresence.js index 6560240c6..3bc022429 100644 --- a/back/methods/chat/sendCheckingPresence.js +++ b/back/methods/chat/sendCheckingPresence.js @@ -1,8 +1,6 @@ -const axios = require('axios'); - module.exports = Self => { Self.remoteMethodCtx('sendCheckingPresence', { - description: 'Sends a RocketChat message to a connected user or department channel', + description: 'Creates a message in the chat model checking the user status', accessType: 'WRITE', accepts: [{ arg: 'workerId', @@ -36,6 +34,7 @@ module.exports = Self => { const models = Self.app.models; const userId = ctx.req.accessToken.userId; + const sender = await models.Account.findById(userId); const recipient = await models.Account.findById(recipientId, null, myOptions); // Prevent sending messages to yourself @@ -44,54 +43,16 @@ module.exports = Self => { if (!recipient) throw new Error(`Could not send message "${message}" to worker id ${recipientId} from user ${userId}`); - const {data} = await Self.getUserStatus(recipient.name); - if (data) { - if (data.status === 'offline' || data.status === 'busy') { - // Send message to department room - const workerDepartment = await models.WorkerDepartment.findById(recipientId, { - include: { - relation: 'department' - } - }, myOptions); - const department = workerDepartment && workerDepartment.department(); - const channelName = department && department.chatName; + await models.Chat.create({ + senderFk: sender.id, + recipient: `@${recipient.name}`, + dated: new Date(), + checkUserStatus: 1, + message: message, + status: 0, + attempts: 0 + }); - if (channelName) - return Self.send(ctx, `#${channelName}`, `@${recipient.name} ➔ ${message}`); - else - return Self.send(ctx, `@${recipient.name}`, message); - } else - return Self.send(ctx, `@${recipient.name}`, message); - } - }; - - /** - * Returns the current user status on Rocketchat - * - * @param {string} username - The recipient user name - * @return {Promise} - The request promise - */ - Self.getUserStatus = async function getUserStatus(username) { - if (process.env.NODE_ENV !== 'production') { - return new Promise(resolve => { - return resolve({ - data: { - status: 'online' - } - }); - }); - } - - const login = await Self.getServiceAuth(); - - const options = { - params: {username}, - headers: { - 'X-Auth-Token': login.auth.token, - 'X-User-Id': login.auth.userId - }, - }; - - return axios.get(`${login.api}/users.getStatus`, options); + return true; }; }; diff --git a/back/methods/chat/sendQueued.js b/back/methods/chat/sendQueued.js new file mode 100644 index 000000000..c34642c7e --- /dev/null +++ b/back/methods/chat/sendQueued.js @@ -0,0 +1,168 @@ +const axios = require('axios'); +module.exports = Self => { + Self.remoteMethodCtx('sendQueued', { + description: 'Send a RocketChat message', + accessType: 'WRITE', + accepts: [], + returns: { + type: 'object', + root: true + }, + http: { + path: `/sendQueued`, + verb: 'POST' + } + }); + + Self.sendQueued = async() => { + const models = Self.app.models; + const maxAttempts = 3; + const sentStatus = 1; + const errorStatus = 2; + + const chats = await models.Chat.find({ + where: { + status: {neq: sentStatus}, + attempts: {lt: maxAttempts} + } + }); + + for (let chat of chats) { + if (chat.checkUserStatus) { + try { + await Self.sendCheckingUserStatus(chat); + await updateChat(chat, sentStatus); + } catch (error) { + await updateChat(chat, errorStatus); + } + } else { + try { + await Self.sendMessage(chat.senderFk, chat.recipient, chat.message); + await updateChat(chat, sentStatus); + } catch (error) { + await updateChat(chat, errorStatus); + } + } + } + }; + + /** + * Check user status in Rocket + * + * @param {object} chat - The sender id + * @return {Promise} - The request promise + */ + Self.sendCheckingUserStatus = async function sendCheckingUserStatus(chat) { + const models = Self.app.models; + + const recipientName = chat.recipient.slice(1); + const recipient = await models.Account.findOne({ + where: { + name: recipientName + } + }); + + const {data} = await Self.getUserStatus(recipient.name); + if (data) { + if (data.status === 'offline' || data.status === 'busy') { + // Send message to department room + const workerDepartment = await models.WorkerDepartment.findById(recipient.id, { + include: { + relation: 'department' + } + }); + const department = workerDepartment && workerDepartment.department(); + const channelName = department && department.chatName; + + if (channelName) + return Self.sendMessage(chat.senderFk, `#${channelName}`, `@${recipient.name} ➔ ${message}`); + else + return Self.sendMessage(chat.senderFk, `@${recipient.name}`, chat.message); + } else + return Self.sendMessage(chat.senderFk, `@${recipient.name}`, chat.message); + } + }; + + /** + * Send a rocket message + * + * @param {object} senderFk - The sender id + * @param {string} recipient - The user (@) or channel (#) to send the message + * @param {string} message - The message to send + * @return {Promise} - The request promise + */ + Self.sendMessage = async function sendMessage(senderFk, recipient, message) { + if (process.env.NODE_ENV !== 'production') { + return new Promise(resolve => { + return resolve({ + statusCode: 200, + message: 'Fake notification sent' + }); + }); + } + + const models = Self.app.models; + const sender = await models.Account.findById(senderFk); + + const login = await Self.getServiceAuth(); + const avatar = `${login.host}/avatar/${sender.name}`; + + const options = { + headers: { + 'X-Auth-Token': login.auth.token, + 'X-User-Id': login.auth.userId + }, + }; + + return axios.post(`${login.api}/chat.postMessage`, { + 'channel': recipient, + 'avatar': avatar, + 'alias': sender.nickname, + 'text': message + }, options); + }; + + /** + * Update status and attempts of a chat + * + * @param {object} chat - The chat + * @param {string} status - The new status + * @return {Promise} - The request promise + */ + async function updateChat(chat, status) { + return chat.updateAttributes({ + status: status, + attempts: ++chat.attempts + }); + } + + /** + * Returns the current user status on Rocketchat + * + * @param {string} username - The recipient user name + * @return {Promise} - The request promise + */ + Self.getUserStatus = async function getUserStatus(username) { + if (process.env.NODE_ENV !== 'production') { + return new Promise(resolve => { + return resolve({ + data: { + status: 'online' + } + }); + }); + } + + const login = await Self.getServiceAuth(); + + const options = { + params: {username}, + headers: { + 'X-Auth-Token': login.auth.token, + 'X-User-Id': login.auth.userId + }, + }; + + return axios.get(`${login.api}/users.getStatus`, options); + }; +}; diff --git a/back/methods/chat/spec/send.spec.js b/back/methods/chat/spec/send.spec.js index 634e1d420..dd07a1342 100644 --- a/back/methods/chat/spec/send.spec.js +++ b/back/methods/chat/spec/send.spec.js @@ -1,14 +1,14 @@ const app = require('vn-loopback/server/server'); describe('Chat send()', () => { - it('should return a "Fake notification sent" as response', async() => { + it('should return true as response', async() => { let ctx = {req: {accessToken: {userId: 1}}}; let response = await app.models.Chat.send(ctx, '@salesPerson', 'I changed something'); expect(response).toEqual(true); }); - it('should retrun false as response', async() => { + it('should return false as response', async() => { let ctx = {req: {accessToken: {userId: 18}}}; let response = await app.models.Chat.send(ctx, '@salesPerson', 'I changed something'); diff --git a/back/methods/chat/spec/sendCheckingPresence.spec.js b/back/methods/chat/spec/sendCheckingPresence.spec.js index 712e7f947..5d1a1b3dd 100644 --- a/back/methods/chat/spec/sendCheckingPresence.spec.js +++ b/back/methods/chat/spec/sendCheckingPresence.spec.js @@ -1,58 +1,21 @@ const models = require('vn-loopback/server/server').models; describe('Chat sendCheckingPresence()', () => { - const today = new Date(); - today.setHours(6, 0); - const ctx = {req: {accessToken: {userId: 1}}}; - const chatModel = models.Chat; - const departmentId = 23; - const workerId = 1107; + it('should return true as response', async() => { + const workerId = 1107; - it(`should call to send() method with "@HankPym" as recipient argument`, async() => { - spyOn(chatModel, 'send').and.callThrough(); - spyOn(chatModel, 'getUserStatus').and.returnValue( - new Promise(resolve => { - return resolve({ - data: { - status: 'online' - } - }); - }) - ); + let ctx = {req: {accessToken: {userId: 1}}}; + let response = await models.Chat.sendCheckingPresence(ctx, workerId, 'I changed something'); - await chatModel.sendCheckingPresence(ctx, workerId, 'I changed something'); - - expect(chatModel.send).toHaveBeenCalledWith(ctx, '@HankPym', 'I changed something'); + expect(response).toEqual(true); }); - it(`should call to send() method with "#cooler" as recipient argument`, async() => { - spyOn(chatModel, 'send').and.callThrough(); - spyOn(chatModel, 'getUserStatus').and.returnValue( - new Promise(resolve => { - return resolve({ - data: { - status: 'offline' - } - }); - }) - ); + it('should return false as response', async() => { + const salesPersonId = 18; - const tx = await models.Claim.beginTransaction({}); + let ctx = {req: {accessToken: {userId: 18}}}; + let response = await models.Chat.sendCheckingPresence(ctx, salesPersonId, 'I changed something'); - try { - const options = {transaction: tx}; - - const department = await models.Department.findById(departmentId, null, options); - await department.updateAttribute('chatName', 'cooler'); - - await chatModel.sendCheckingPresence(ctx, workerId, 'I changed something'); - - expect(chatModel.send).toHaveBeenCalledWith(ctx, '#cooler', '@HankPym ➔ I changed something'); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } + expect(response).toEqual(false); }); }); diff --git a/back/methods/chat/spec/sendQueued.spec.js b/back/methods/chat/spec/sendQueued.spec.js new file mode 100644 index 000000000..bbf5a73c7 --- /dev/null +++ b/back/methods/chat/spec/sendQueued.spec.js @@ -0,0 +1,41 @@ +const models = require('vn-loopback/server/server').models; + +describe('Chat sendCheckingPresence()', () => { + const today = new Date(); + today.setHours(6, 0); + const chatModel = models.Chat; + + it(`should call to sendCheckingUserStatus()`, async() => { + spyOn(chatModel, 'sendCheckingUserStatus').and.callThrough(); + + const chat = { + checkUserStatus: 1, + status: 0, + attempts: 0 + }; + + await chatModel.destroyAll(); + await chatModel.create(chat); + + await chatModel.sendQueued(); + + expect(chatModel.sendCheckingUserStatus).toHaveBeenCalled(); + }); + + it(`should call to sendMessage() method`, async() => { + spyOn(chatModel, 'sendMessage').and.callThrough(); + + const chat = { + checkUserStatus: 0, + status: 0, + attempts: 0 + }; + + await chatModel.destroyAll(); + await chatModel.create(chat); + + await chatModel.sendQueued(); + + expect(chatModel.sendMessage).toHaveBeenCalled(); + }); +}); diff --git a/back/methods/dms/deleteTrashFiles.js b/back/methods/dms/deleteTrashFiles.js index 9d16e9d81..716e03311 100644 --- a/back/methods/dms/deleteTrashFiles.js +++ b/back/methods/dms/deleteTrashFiles.js @@ -11,11 +11,11 @@ module.exports = Self => { }, http: { path: `/deleteTrashFiles`, - verb: 'POST' + verb: 'GET' } }); - Self.deleteTrashFiles = async(options) => { + Self.deleteTrashFiles = async options => { const tx = await Self.beginTransaction({}); const myOptions = {}; @@ -47,10 +47,9 @@ module.exports = Self => { await dms.destroy(myOptions); } if (tx) await tx.commit(); - } catch (e) { if (tx) await tx.rollback(); - + throw e; } }; diff --git a/back/methods/edi/updateData.js b/back/methods/edi/updateData.js index abd1c7a77..3dae2d47d 100644 --- a/back/methods/edi/updateData.js +++ b/back/methods/edi/updateData.js @@ -19,89 +19,186 @@ module.exports = Self => { Self.updateData = async() => { const models = Self.app.models; + // Get files checksum + const files = await Self.rawSql('SELECT name, checksum, keyValue FROM edi.fileConfig'); + + const updatableFiles = []; + for (const file of files) { + const fileChecksum = await getChecksum(file); + + if (file.checksum != fileChecksum) { + updatableFiles.push({ + name: file.name, + checksum: fileChecksum + }); + } else + console.debug(`File already updated, skipping...`); + } + + if (updatableFiles.length === 0) + return false; + + // Download files const container = await models.TempContainer.container('edi'); const tempPath = path.join(container.client.root, container.name); - // Temporary file clean - await fs.rmdir(`${tempPath}/*`, {recursive: true}); - fs.emptyDirSync(`${tempPath}`); - - const [ftpConfig] = await Self.rawSql('SELECT host, user, password FROM edi.ftpConfig'); - console.debug(`Openning FTP connection to ${ftpConfig.host}...\n`); - - const FtpClient = require('ftps'); - const ftpClient = new FtpClient({ - host: ftpConfig.host, - username: ftpConfig.user, - password: ftpConfig.password, - procotol: 'ftp' - }); - - const files = await Self.rawSql('SELECT fileName, toTable, file, updated FROM edi.fileConfig'); - let remoteFile; let tempDir; let tempFile; - for (const file of files) { + + const fileNames = updatableFiles.map(file => file.name); + + const tables = await Self.rawSql(` + SELECT fileName, toTable, file + FROM edi.tableConfig + WHERE file IN (?)`, [fileNames]); + + for (const table of tables) { + const fileName = table.file; + + console.debug(`Downloading file ${fileName}...`); + + remoteFile = `codes/${fileName}.ZIP`; + tempDir = `${tempPath}/${fileName}`; + tempFile = `${tempPath}/${fileName}.zip`; + try { - const fileName = file.file; - - console.debug(`Downloading file ${fileName}...`); - - remoteFile = `codes/${fileName}.ZIP`; - tempDir = `${tempPath}/${fileName}`; - tempFile = `${tempPath}/${fileName}.zip`; - - // if (fs.existsSync(tempFile)) - // await fs.unlink(tempFile); - - // if (fs.existsSync(tempDir)) - // await fs.rmdir(tempDir, {recursive: true}); - - await extractFile({ - ftpClient: ftpClient, - file: file, - paths: { - remoteFile: remoteFile, - tempDir: tempDir, - tempFile: tempFile - } - }); + await fs.readFile(tempFile); } catch (error) { - if (fs.existsSync(tempFile)) - await fs.unlink(tempFile); - - console.error(error); + if (error.code === 'ENOENT') { + const downloadOutput = await downloadFile(remoteFile, tempFile); + if (downloadOutput.error) + continue; + } } + + console.debug(`Extracting file ${fileName}...`); + await extractFile(tempFile, tempDir); + + console.debug(`Updating table ${table.toTable}...`); + await dumpData(tempDir, table); + } + + // Update files checksum + for (const file of updatableFiles) { + await Self.rawSql(` + UPDATE edi.fileConfig + SET checksum = ? + WHERE name = ?`, + [file.checksum, file.name]); + } + + // Clean files + try { + await fs.remove(tempPath); + } catch (error) { + if (error.code !== 'ENOENT') + throw e; } return true; }; - async function extractFile({ftpClient, file, paths}) { - // Download the zip file - ftpClient.get(paths.remoteFile, paths.tempFile); + let ftpClient; + async function getFtpClient() { + if (!ftpClient) { + const [ftpConfig] = await Self.rawSql('SELECT host, user, password FROM edi.ftpConfig'); + console.debug(`Openning FTP connection to ${ftpConfig.host}...\n`); - // Execute download command - ftpClient.exec(async(err, response) => { - if (response.error) { - console.debug(`Error downloading file... ${response.error}`); - return; - } + const FtpClient = require('ftps'); - const AdmZip = require('adm-zip'); - const zip = new AdmZip(paths.tempFile); - const entries = zip.getEntries(); + ftpClient = new FtpClient({ + host: ftpConfig.host, + username: ftpConfig.user, + password: ftpConfig.password, + procotol: 'ftp' + }); + } - zip.extractAllTo(paths.tempDir, false); + return ftpClient; + } - await dumpData({file, entries, paths}); + async function getChecksum(file) { + const ftpClient = await getFtpClient(); + console.debug(`Checking checksum for file ${file.name}...`); + + ftpClient.cat(`codes/${file.name}.txt`); + + const response = await new Promise((resolve, reject) => { + ftpClient.exec((err, response) => { + if (response.error) { + console.debug(`Error downloading checksum file... ${response.error}`); + reject(err); + } + + resolve(response); + }); + }); + + if (response && response.data) { + const fileContents = response.data; + const rows = fileContents.split('\n'); + const row = rows[4]; + const columns = row.split(/\s+/); + + let fileChecksum; + if (file.keyValue) + fileChecksum = columns[1]; + + if (!file.keyValue) + fileChecksum = columns[0]; + + return fileChecksum; + } + } + + async function downloadFile(remoteFile, tempFile) { + const ftpClient = await getFtpClient(); + + ftpClient.get(remoteFile, tempFile); + + return new Promise((resolve, reject) => { + ftpClient.exec((err, response) => { + if (response.error) { + console.debug(`Error downloading file... ${response.error}`); + reject(err); + } + + resolve(response); + }); }); } - async function dumpData({file, entries, paths}) { - const toTable = file.toTable; - const baseName = file.fileName; + async function extractFile(tempFile, tempDir) { + const JSZip = require('jszip'); + + try { + await fs.mkdir(tempDir); + } catch (error) { + if (error.code !== 'EEXIST') + throw e; + } + + const fileStream = await fs.readFile(tempFile); + if (fileStream) { + const zip = new JSZip(); + const zipContents = await zip.loadAsync(fileStream); + + if (!zipContents) return; + + const fileNames = Object.keys(zipContents.files); + + for (const fileName of fileNames) { + const fileContent = await zip.file(fileName).async('nodebuffer'); + const dest = path.join(tempDir, fileName); + await fs.writeFile(dest, fileContent); + } + } + } + + async function dumpData(tempDir, table) { + const toTable = table.toTable; + const baseName = table.fileName; const firstEntry = entries[0]; const entryName = firstEntry.entryName; @@ -136,20 +233,22 @@ module.exports = Self => { const tableName = `edi.${toTable}`; await Self.rawSql(`DELETE FROM ??`, [tableName], options); - for (const zipEntry of entries) { - const entryName = zipEntry.entryName; - console.log(`Dumping data from file ${entryName}...`); + const dirFiles = await fs.readdir(tempDir); + const files = dirFiles.filter(file => file.startsWith(baseName)); + + for (const file of files) { + console.log(`Dumping data from file ${file}...`); + const templatePath = path.join(__dirname, `./sql/${toTable}.sql`); - const sqlTemplate = fs.readFileSync(templatePath, 'utf8'); + const sqlTemplate = await fs.readFile(templatePath, 'utf8'); + const filePath = path.join(tempDir, file); - const rawPath = path.join(paths.tempDir, entryName); - - await Self.rawSql(sqlTemplate, [rawPath], options); + await Self.rawSql(sqlTemplate, [filePath], options); await Self.rawSql(` - UPDATE edi.fileConfig - SET updated = ? - WHERE fileName = ? - `, [lastUpdated, baseName], options); + UPDATE edi.tableConfig + SET updated = ? + WHERE fileName = ? + `, [new Date(), baseName], options); } tx.commit(); diff --git a/back/model-config.json b/back/model-config.json index 4ce11b99d..343210383 100644 --- a/back/model-config.json +++ b/back/model-config.json @@ -68,6 +68,12 @@ "Language": { "dataSource": "vn" }, + "MachineWorker": { + "dataSource": "vn" + }, + "MobileAppVersionControl": { + "dataSource": "vn" + }, "Module": { "dataSource": "vn" }, diff --git a/back/models/accounting-type.json b/back/models/accounting-type.json index be08ac533..086be9d13 100644 --- a/back/models/accounting-type.json +++ b/back/models/accounting-type.json @@ -28,6 +28,9 @@ }, "maxAmount": { "type": "number" + }, + "daysInFuture": { + "type": "number" } }, "acls": [{ diff --git a/back/models/app-version-control.json b/back/models/app-version-control.json new file mode 100644 index 000000000..46c53be3c --- /dev/null +++ b/back/models/app-version-control.json @@ -0,0 +1,24 @@ +{ + "name": "MobileAppVersionControl", + "base": "VnModel", + "options": { + "mysql": { + "table": "vn.mobileAppVersionControl" + } + }, + "properties": { + "id": { + "type": "number", + "id": true + }, + "appName": { + "type": "string" + }, + "version": { + "type": "string" + }, + "isVersionCritical": { + "type": "boolean" + } + } +} diff --git a/back/models/chat.js b/back/models/chat.js index 7d8468aae..95a1e2c29 100644 --- a/back/models/chat.js +++ b/back/models/chat.js @@ -3,4 +3,5 @@ module.exports = Self => { require('../methods/chat/send')(Self); require('../methods/chat/sendCheckingPresence')(Self); require('../methods/chat/notifyIssues')(Self); + require('../methods/chat/sendQueued')(Self); }; diff --git a/back/models/chat.json b/back/models/chat.json index 697d8c181..8fc3a6304 100644 --- a/back/models/chat.json +++ b/back/models/chat.json @@ -1,6 +1,39 @@ { "name": "Chat", "base": "VnModel", + "options": { + "mysql": { + "table": "chat" + } + }, + "properties": { + "id": { + "id": true, + "type": "number", + "description": "Identifier" + }, + "senderFk": { + "type": "number" + }, + "recipient": { + "type": "string" + }, + "dated": { + "type": "date" + }, + "checkUserStatus": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + }, + "attempts": { + "type": "number" + } + }, "acls": [{ "property": "validations", "accessType": "EXECUTE", diff --git a/back/models/machine-worker.json b/back/models/machine-worker.json new file mode 100644 index 000000000..2244a533f --- /dev/null +++ b/back/models/machine-worker.json @@ -0,0 +1,33 @@ +{ + "name": "MachineWorker", + "base": "VnModel", + "options": { + "mysql": { + "table": "vn.machineWorker" + } + }, + "properties": { + "id": { + "type": "number", + "id": true + }, + "workerFk": { + "type": "number" + }, + "machineFk": { + "type": "number" + }, + "inTime": { + "type": "date", + "mysql": { + "columnName": "inTimed" + } + }, + "outTime": { + "type": "date", + "mysql": { + "columnName": "outTimed" + } + } + } +} diff --git a/db/changes/10451-april/00-ticket_doRefund.sql b/db/changes/10451-april/00-ticket_doRefund.sql deleted file mode 100644 index 5540ff8cf..000000000 --- a/db/changes/10451-april/00-ticket_doRefund.sql +++ /dev/null @@ -1,113 +0,0 @@ -DROP PROCEDURE IF EXISTS vn.ticket_doRefund; - -DELIMITER $$ -$$ -CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_doRefund`(IN vOriginTicket INT, OUT vNewTicket INT) -BEGIN - - DECLARE vDone BIT DEFAULT 0; - DECLARE vCustomer MEDIUMINT; - DECLARE vWarehouse TINYINT; - DECLARE vCompany MEDIUMINT; - DECLARE vAddress MEDIUMINT; - DECLARE vRefundAgencyMode INT; - DECLARE vItemFk INT; - DECLARE vQuantity DECIMAL (10,2); - DECLARE vConcept VARCHAR(50); - DECLARE vPrice DECIMAL (10,2); - DECLARE vDiscount TINYINT; - DECLARE vSaleNew INT; - DECLARE vSaleMain INT; - DECLARE vZoneFk INT; - DECLARE vDescription VARCHAR(50); - DECLARE vTaxClassFk INT; - DECLARE vTicketServiceTypeFk INT; - - DECLARE cSales CURSOR FOR - SELECT * - FROM tmp.sale; - - DECLARE cTicketServices CURSOR FOR - SELECT * - FROM tmp.ticketService; - - DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = 1; - - SELECT id INTO vRefundAgencyMode - FROM agencyMode WHERE `name` = 'ABONO'; - - SELECT clientFk, warehouseFk, companyFk, addressFk - INTO vCustomer, vWarehouse, vCompany, vAddress - FROM ticket - WHERE id = vOriginTicket; - - SELECT id INTO vZoneFk - FROM zone WHERE agencyModeFk = vRefundAgencyMode - LIMIT 1; - - INSERT INTO vn.ticket ( - clientFk, - shipped, - addressFk, - agencyModeFk, - nickname, - warehouseFk, - companyFk, - landed, - zoneFk - ) - SELECT - vCustomer, - CURDATE(), - vAddress, - vRefundAgencyMode, - a.nickname, - vWarehouse, - vCompany, - CURDATE(), - vZoneFk - FROM address a - WHERE a.id = vAddress; - - SET vNewTicket = LAST_INSERT_ID(); - - SET vDone := 0; - OPEN cSales; - FETCH cSales INTO vSaleMain, vItemFk, vQuantity, vConcept, vPrice, vDiscount; - - WHILE NOT vDone DO - - INSERT INTO vn.sale(ticketFk, itemFk, quantity, concept, price, discount) - VALUES( vNewTicket, vItemFk, vQuantity, vConcept, vPrice, vDiscount ); - - SET vSaleNew = LAST_INSERT_ID(); - - INSERT INTO vn.saleComponent(saleFk,componentFk,`value`) - SELECT vSaleNew,componentFk,`value` - FROM vn.saleComponent - WHERE saleFk = vSaleMain; - - FETCH cSales INTO vSaleMain, vItemFk, vQuantity, vConcept, vPrice, vDiscount; - - END WHILE; - CLOSE cSales; - - SET vDone := 0; - OPEN cTicketServices; - FETCH cTicketServices INTO vDescription, vQuantity, vPrice, vTaxClassFk, vTicketServiceTypeFk; - - WHILE NOT vDone DO - - INSERT INTO vn.ticketService(description, quantity, price, taxClassFk, ticketFk, ticketServiceTypeFk) - VALUES(vDescription, vQuantity, vPrice, vTaxClassFk, vNewTicket, vTicketServiceTypeFk); - - FETCH cTicketServices INTO vDescription, vQuantity, vPrice, vTaxClassFk, vTicketServiceTypeFk; - - END WHILE; - CLOSE cTicketServices; - - INSERT INTO vn.ticketRefund(refundTicketFk, originalTicketFk) - VALUES(vNewTicket, vOriginTicket); - -END$$ -DELIMITER ; diff --git a/db/changes/10470-family/00-accountingType.sql b/db/changes/10470-family/00-accountingType.sql new file mode 100644 index 000000000..964027e3a --- /dev/null +++ b/db/changes/10470-family/00-accountingType.sql @@ -0,0 +1,3 @@ +ALTER TABLE `vn`.`accountingType` ADD daysInFuture INT NULL; +ALTER TABLE `vn`.`accountingType` MODIFY COLUMN daysInFuture int(11) DEFAULT 0 NULL; +UPDATE `vn`.`accountingType` SET daysInFuture=1 WHERE id=8; \ No newline at end of file diff --git a/db/changes/10470-family/00-aclItemType.sql b/db/changes/10470-family/00-aclItemType.sql new file mode 100644 index 000000000..836a69dfd --- /dev/null +++ b/db/changes/10470-family/00-aclItemType.sql @@ -0,0 +1,4 @@ +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES + ('ItemType', '*', 'READ', 'ALLOW', 'ROLE', 'employee'), + ('ItemType', '*', 'WRITE', 'ALLOW', 'ROLE', 'buyer'); \ No newline at end of file diff --git a/db/changes/10470-family/00-aclMdb.sql b/db/changes/10470-family/00-aclMdb.sql new file mode 100644 index 000000000..b02ddc451 --- /dev/null +++ b/db/changes/10470-family/00-aclMdb.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS `vn`.`mdbBranch` ( + `name` VARCHAR(255), + PRIMARY KEY(`name`) +); + +CREATE TABLE IF NOT EXISTS `vn`.`mdbVersion` ( + `app` VARCHAR(255) NOT NULL, + `branchFk` VARCHAR(255) NOT NULL, + `version` INT, + CONSTRAINT `mdbVersion_branchFk` FOREIGN KEY (`branchFk`) REFERENCES `vn`.`mdbBranch` (`name`) ON DELETE CASCADE ON UPDATE CASCADE +); + +INSERT IGNORE INTO `salix`.`ACL` (`id`, `model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES(318, 'MdbVersion', '*', '*', 'ALLOW', 'ROLE', 'developer'); diff --git a/db/changes/10470-family/00-chat.sql b/db/changes/10470-family/00-chat.sql new file mode 100644 index 000000000..d4a8f068a --- /dev/null +++ b/db/changes/10470-family/00-chat.sql @@ -0,0 +1,13 @@ +CREATE TABLE `vn`.`chat` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `senderFk` int(10) unsigned DEFAULT NULL, + `recipient` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL, + `dated` date DEFAULT NULL, + `checkUserStatus` tinyint(1) DEFAULT NULL, + `message` varchar(200) COLLATE utf8_unicode_ci DEFAULT NULL, + `status` tinyint(1) DEFAULT NULL, + `attempts` int(1) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `chat_FK` (`senderFk`), + CONSTRAINT `chat_FK` FOREIGN KEY (`senderFk`) REFERENCES `account`.`user` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; \ No newline at end of file diff --git a/db/changes/10470-family/00-creditInsurance.sql b/db/changes/10470-family/00-creditInsurance.sql new file mode 100644 index 000000000..9d4db470b --- /dev/null +++ b/db/changes/10470-family/00-creditInsurance.sql @@ -0,0 +1,8 @@ +ALTER TABLE `vn`.`creditInsurance` ADD creditClassificationFk int(11) NULL; + +UPDATE `vn`.`creditInsurance` AS `destiny` + SET `destiny`.`creditClassificationFk`= (SELECT creditClassification FROM `vn`.`creditInsurance` AS `origin` WHERE `origin`.id = `destiny`.id); + +ALTER TABLE `vn`.`creditInsurance` + ADD CONSTRAINT `creditInsurance_creditClassificationFk` FOREIGN KEY (`creditClassificationFk`) + REFERENCES `vn`.`creditClassification` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; \ No newline at end of file diff --git a/db/changes/10470-family/00-defaultViewConfig.sql b/db/changes/10470-family/00-defaultViewConfig.sql new file mode 100644 index 000000000..d423599b1 --- /dev/null +++ b/db/changes/10470-family/00-defaultViewConfig.sql @@ -0,0 +1,3 @@ +INSERT INTO `salix`.`defaultViewConfig` (tableCode, columns) +VALUES ('clientsDetail', '{"id":true,"phone":true,"city":true,"socialName":true,"salesPersonFk":true,"email":true,"name":false,"fi":false,"credit":false,"creditInsurance":false,"mobile":false,"street":false,"countryFk":false,"provinceFk":false,"postcode":false,"created":false,"businessTypeFk":false,"payMethodFk":false,"sageTaxTypeFk":false,"sageTransactionTypeFk":false,"isActive":false,"isVies":false,"isTaxDataChecked":false,"isEqualizated":false,"isFreezed":false,"hasToInvoice":false,"hasToInvoiceByAddress":false,"isToBeMailed":false,"hasLcr":false,"hasCoreVnl":false,"hasSepaVnl":false}'); + diff --git a/db/changes/10470-family/00-ticket_doRefund.sql b/db/changes/10470-family/00-ticket_doRefund.sql new file mode 100644 index 000000000..f4ecf29d7 --- /dev/null +++ b/db/changes/10470-family/00-ticket_doRefund.sql @@ -0,0 +1,127 @@ +DROP PROCEDURE IF EXISTS `vn`.`ticket_doRefund`; + +DELIMITER $$ +$$ +CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_doRefund`(OUT vNewTicket INT) +BEGIN +/** + * Crea un ticket de abono a partir de tmp.sale y/o tmp.ticketService + * + * @return vNewTicket + */ + DECLARE vDone BIT DEFAULT 0; + DECLARE vClientFk MEDIUMINT; + DECLARE vWarehouse TINYINT; + DECLARE vCompany MEDIUMINT; + DECLARE vAddress MEDIUMINT; + DECLARE vRefundAgencyMode INT; + DECLARE vItemFk INT; + DECLARE vQuantity DECIMAL (10,2); + DECLARE vConcept VARCHAR(50); + DECLARE vPrice DECIMAL (10,2); + DECLARE vDiscount TINYINT; + DECLARE vSaleNew INT; + DECLARE vSaleMain INT; + DECLARE vZoneFk INT; + DECLARE vDescription VARCHAR(50); + DECLARE vTaxClassFk INT; + DECLARE vTicketServiceTypeFk INT; + DECLARE vOriginTicket INT; + + DECLARE cSales CURSOR FOR + SELECT s.id, s.itemFk, - s.quantity, s.concept, s.price, s.discount + FROM tmp.sale s; + + DECLARE cTicketServices CURSOR FOR + SELECT ts.description, - ts.quantity, ts.price, ts.taxClassFk, ts.ticketServiceTypeFk + FROM tmp.ticketService ts; + + DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE; + + SELECT sub.ticketFk INTO vOriginTicket + FROM ( + SELECT s.ticketFk + FROM tmp.sale s + UNION ALL + SELECT ts.ticketFk + FROM tmp.ticketService ts + ) sub + LIMIT 1; + + SELECT id INTO vRefundAgencyMode + FROM agencyMode WHERE `name` = 'ABONO'; + + SELECT clientFk, warehouseFk, companyFk, addressFk + INTO vClientFk, vWarehouse, vCompany, vAddress + FROM ticket + WHERE id = vOriginTicket; + + SELECT id INTO vZoneFk + FROM zone WHERE agencyModeFk = vRefundAgencyMode + LIMIT 1; + + INSERT INTO vn.ticket ( + clientFk, + shipped, + addressFk, + agencyModeFk, + nickname, + warehouseFk, + companyFk, + landed, + zoneFk + ) + SELECT + vClientFk, + CURDATE(), + vAddress, + vRefundAgencyMode, + a.nickname, + vWarehouse, + vCompany, + CURDATE(), + vZoneFk + FROM address a + WHERE a.id = vAddress; + + SET vNewTicket = LAST_INSERT_ID(); + + SET vDone := FALSE; + OPEN cSales; + FETCH cSales INTO vSaleMain, vItemFk, vQuantity, vConcept, vPrice, vDiscount; + + WHILE NOT vDone DO + + INSERT INTO vn.sale(ticketFk, itemFk, quantity, concept, price, discount) + VALUES( vNewTicket, vItemFk, vQuantity, vConcept, vPrice, vDiscount ); + + SET vSaleNew = LAST_INSERT_ID(); + + INSERT INTO vn.saleComponent(saleFk,componentFk,`value`) + SELECT vSaleNew,componentFk,`value` + FROM vn.saleComponent + WHERE saleFk = vSaleMain; + + FETCH cSales INTO vSaleMain, vItemFk, vQuantity, vConcept, vPrice, vDiscount; + + END WHILE; + CLOSE cSales; + + SET vDone := FALSE; + OPEN cTicketServices; + FETCH cTicketServices INTO vDescription, vQuantity, vPrice, vTaxClassFk, vTicketServiceTypeFk; + + WHILE NOT vDone DO + + INSERT INTO vn.ticketService(description, quantity, price, taxClassFk, ticketFk, ticketServiceTypeFk) + VALUES(vDescription, vQuantity, vPrice, vTaxClassFk, vNewTicket, vTicketServiceTypeFk); + + FETCH cTicketServices INTO vDescription, vQuantity, vPrice, vTaxClassFk, vTicketServiceTypeFk; + + END WHILE; + CLOSE cTicketServices; + + INSERT INTO vn.ticketRefund(refundTicketFk, originalTicketFk) + VALUES(vNewTicket, vOriginTicket); +END$$ +DELIMITER ; diff --git a/db/changes/10470-family/01-creditInsuranceTriggers.sql b/db/changes/10470-family/01-creditInsuranceTriggers.sql new file mode 100644 index 000000000..53ba3ba11 --- /dev/null +++ b/db/changes/10470-family/01-creditInsuranceTriggers.sql @@ -0,0 +1,11 @@ +DELIMITER $$ +$$ +CREATE DEFINER=`root`@`localhost` TRIGGER `vn`.`creditInsurance_beforeInsert` + BEFORE INSERT ON `creditInsurance` + FOR EACH ROW +BEGIN + IF NEW.creditClassificationFk THEN + SET NEW.creditClassification = NEW.creditClassificationFk; + END IF; +END$$ +DELIMITER ; \ No newline at end of file diff --git a/db/changes/10470-family/01-tableConfig.sql b/db/changes/10470-family/01-tableConfig.sql new file mode 100644 index 000000000..685981d90 --- /dev/null +++ b/db/changes/10470-family/01-tableConfig.sql @@ -0,0 +1 @@ +RENAME TABLE `edi`.`fileConfig` to `edi`.`tableConfig`; \ No newline at end of file diff --git a/db/changes/10470-family/02-fileConfig.sql b/db/changes/10470-family/02-fileConfig.sql new file mode 100644 index 000000000..3109a4616 --- /dev/null +++ b/db/changes/10470-family/02-fileConfig.sql @@ -0,0 +1,22 @@ +CREATE TABLE `edi`.`fileConfig` +( + name varchar(25) NOT NULL, + checksum text NULL, + keyValue tinyint(1) default true NOT NULL, + constraint fileConfig_pk + primary key (name) +); + +create unique index fileConfig_name_uindex + on `edi`.`fileConfig` (name); + + +INSERT INTO `edi`.`fileConfig` (name, checksum, keyValue) +VALUES ('FEC010104', null, 0); + +INSERT INTO `edi`.`fileConfig` (name, checksum, keyValue) +VALUES ('VBN020101', null, 1); + +INSERT INTO `edi`.`fileConfig` (name, checksum, keyValue) +VALUES ('florecompc2', null, 1); + diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 4300f52f7..86fadd90b 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -99,13 +99,19 @@ INSERT INTO `account`.`mailForward`(`account`, `forwardTo`) VALUES (1, 'employee@domain.local'); -INSERT INTO `vn`.`worker`(`id`, `code`, `firstName`, `lastName`, `userFk`,`bossFk`, `phone`) +INSERT INTO `vn`.`printer` (`id`, `name`, `path`, `isLabeler`) VALUES - (1106, 'LGN', 'David Charles', 'Haller', 1106, 19, 432978106), - (1107, 'ANT', 'Hank' , 'Pym' , 1107, 19, 432978107), - (1108, 'DCX', 'Charles' , 'Xavier', 1108, 19, 432978108), - (1109, 'HLK', 'Bruce' , 'Banner', 1109, 19, 432978109), - (1110, 'JJJ', 'Jessica' , 'Jones' , 1110, 19, 432978110); + (1, 'printer1', 'path1', 0), + (2, 'printer2', 'path2', 1); + + +INSERT INTO `vn`.`worker`(`id`, `code`, `firstName`, `lastName`, `userFk`,`bossFk`, `phone`, `sectorFk`, `labelerFk`) + VALUES + (1106, 'LGN', 'David Charles', 'Haller', 1106, 19, 432978106, NULL, NULL), + (1107, 'ANT', 'Hank' , 'Pym' , 1107, 19, 432978107, NULL, 1), + (1108, 'DCX', 'Charles' , 'Xavier', 1108, 19, 432978108, 1, NULL), + (1109, 'HLK', 'Bruce' , 'Banner', 1109, 19, 432978109, 1, 2), + (1110, 'JJJ', 'Jessica' , 'Jones' , 1110, 19, 432978110, 2, 1); INSERT INTO `vn`.`currency`(`id`, `code`, `name`, `ratio`) VALUES @@ -113,7 +119,7 @@ INSERT INTO `vn`.`currency`(`id`, `code`, `name`, `ratio`) (2, 'USD', 'Dollar USA', 1.4), (3, 'GBP', 'Libra', 1), (4, 'JPY', 'Yen Japones', 1); - + INSERT INTO `vn`.`country`(`id`, `country`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`, `continentFk`, `hasDailyInvoice`, `CEE`) VALUES (1, 'España', 1, 'ES', 1, 24, 4, 0, 1), @@ -156,22 +162,23 @@ INSERT INTO `vn`.`shelving` (`code`, `parkingFk`, `isPrinted`, `priority`, `park ('HEJ', 2, 0, 1, 0, 1106), ('UXN', 1, 0, 1, 0, 1106); -INSERT INTO `vn`.`accountingType`(`id`, `description`, `receiptDescription`,`code`, `maxAmount`) +INSERT INTO `vn`.`accountingType`(`id`, `description`, `receiptDescription`,`code`, `maxAmount`, `daysInFuture`) VALUES - (1, 'CC y Polizas de crédito', NULL, NULL, NULL), - (2, 'Cash', 'Cash', 'cash', 1000), - (3, 'Credit card', 'Credit Card', 'creditCard', NULL), - (4, 'Finalcial lines', NULL, NULL, NULL), - (5, 'Other products', NULL, NULL, NULL), - (6, 'Loans', NULL, NULL, NULL), - (7, 'Leasing', NULL, NULL, NULL), - (8, 'Compensations', 'Compensations', 'compensation', NULL); + (1, 'CC and credit policies', 'Transfers', 'wireTransfer', NULL, 1), + (2, 'Cash', 'Cash', 'cash', 1000, 0), + (3, 'Credit card', 'Credit Card', 'creditCard', NULL, 0), + (4, 'Finalcial lines', NULL, NULL, NULL, 0), + (5, 'Other products', NULL, NULL, NULL, 0), + (6, 'Loans', NULL, NULL, NULL, 0), + (7, 'Leasing', NULL, NULL, NULL, 0), + (8, 'Compensations', 'Compensations', 'compensation', NULL, 0); INSERT INTO `vn`.`bank`(`id`, `bank`, `account`, `cash`, `entityFk`, `isActive`, `currencyFk`) VALUES (1, 'Pay on receipt', '5720000001', 3, 0, 1, 1), (2, 'Cash', '5700000001', 2, 0, 1, 1), (3, 'Compensation', '4000000000', 8, 0, 1, 1), + (4, 'Transfers', '4000000001', 1, 0, 1, 1), (3117, 'Caixa Rural d''Algemesi', '5720000000', 8, 3117, 1, 1); @@ -451,7 +458,7 @@ INSERT INTO `vn`.`creditClassification`(`id`, `client`, `dateStart`, `dateEnd`) (4, 1104, CURDATE(), CURDATE()), (5, 1105, CURDATE(), CURDATE()); -INSERT INTO `vn`.`creditInsurance`(`id`, `creditClassification`, `credit`, `creationDate`, `grade`) +INSERT INTO `vn`.`creditInsurance`(`id`, `creditClassificationFk`, `credit`, `creationDate`, `grade`) VALUES (1, 1, 3000, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), NULL), (2, 2, 6000, DATE_ADD(CURDATE(), INTERVAL -2 MONTH), NULL), @@ -744,14 +751,19 @@ INSERT INTO `vn`.`itemCategory`(`id`, `name`, `display`, `color`, `icon`, `code` (7, 'Accessories', 1, NULL, 'icon-accessory', 'accessory'), (8, 'Fruit', 1, NULL, 'icon-fruit', 'fruit'); -INSERT INTO `vn`.`itemType`(`id`, `code`, `name`, `categoryFk`, `warehouseFk`, `life`,`workerFk`, `isPackaging`) +INSERT INTO `vn`.`temperature`(`code`, `name`, `description`) VALUES - (1, 'CRI', 'Crisantemo', 2, 1, 31, 35, 0), - (2, 'ITG', 'Anthurium', 1, 1, 31, 35, 0), - (3, 'WPN', 'Paniculata', 2, 1, 31, 35, 0), - (4, 'PRT', 'Delivery ports', 3, 1, NULL, 35, 1), - (5, 'CON', 'Container', 3, 1, NULL, 35, 1), - (6, 'ALS', 'Alstroemeria', 1, 1, 31, 16, 0); + ('warm', 'Warm', 'Warm'), + ('cool', 'Cool', 'Cool'); + +INSERT INTO `vn`.`itemType`(`id`, `code`, `name`, `categoryFk`, `warehouseFk`, `life`,`workerFk`, `isPackaging`, `temperatureFk`) + VALUES + (1, 'CRI', 'Crisantemo', 2, 1, 31, 35, 0, 'cool'), + (2, 'ITG', 'Anthurium', 1, 1, 31, 35, 0, 'cool'), + (3, 'WPN', 'Paniculata', 2, 1, 31, 35, 0, 'cool'), + (4, 'PRT', 'Delivery ports', 3, 1, NULL, 35, 1, 'warm'), + (5, 'CON', 'Container', 3, 1, NULL, 35, 1, 'warm'), + (6, 'ALS', 'Alstroemeria', 1, 1, 31, 16, 0, 'warm'); INSERT INTO `vn`.`ink`(`id`, `name`, `picture`, `showOrder`, `hex`) VALUES @@ -809,25 +821,25 @@ INSERT INTO `vn`.`itemFamily`(`code`, `description`) ('VT', 'Sales'); INSERT INTO `vn`.`item`(`id`, `typeFk`, `size`, `inkFk`, `stems`, `originFk`, `description`, `producerFk`, `intrastatFk`, `expenceFk`, - `comment`, `relevancy`, `image`, `subName`, `minPrice`, `stars`, `family`, `isFloramondo`, `genericFk`, `itemPackingTypeFk`, `hasMinPrice`) + `comment`, `relevancy`, `image`, `subName`, `minPrice`, `stars`, `family`, `isFloramondo`, `genericFk`, `itemPackingTypeFk`, `hasMinPrice`, `packingShelve`) VALUES - (1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 2000000000, NULL, 0, '1', NULL, 0, 1, 'VT', 0, NULL, 'V', 0), - (2, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 2000000000, NULL, 0, '2', NULL, 0, 2, 'VT', 0, NULL, 'H', 0), - (3, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 4751000000, NULL, 0, '3', NULL, 0, 5, 'VT', 0, NULL, NULL, 0), - (4, 1, 60, 'YEL', 1, 1, 'Increases block', 1, 05080000, 4751000000, NULL, 0, '4', NULL, 0, 3, 'VT', 0, NULL, NULL, 0), - (5, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 4751000000, NULL, 0, '5', NULL, 0, 3, 'VT', 0, NULL, NULL, 0), - (6, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '6', NULL, 0, 4, 'VT', 0, NULL, NULL, 0), - (7, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '7', NULL, 0, 4, 'VT', 0, NULL, NULL, 0), - (8, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 2000000000, NULL, 0, '8', NULL, 0, 5, 'VT', 0, NULL, NULL, 0), - (9, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 2000000000, NULL, 0, '9', NULL, 0, 4, 'VT', 1, NULL, NULL, 0), - (10, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 4751000000, NULL, 0, '10', NULL, 0, 4, 'VT', 0, NULL, NULL, 0), - (11, 1, 60, 'YEL', 1, 1, NULL, 1, 05080000, 4751000000, NULL, 0, '11', NULL, 0, 4, 'VT', 0, NULL, NULL, 0), - (12, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 4751000000, NULL, 0, '12', NULL, 0, 3, 'VT', 0, NULL, NULL, 0), - (13, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '13', NULL, 1, 2, 'VT', 1, NULL, NULL, 1), - (14, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 4, 'VT', 1, NULL, NULL, 0), - (15, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'EMB', 0, NULL, NULL, 0), - (16, 6, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'EMB', 0, NULL, NULL, 0), - (71, 6, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'VT', 0, NULL, NULL, 0); + (1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 2000000000, NULL, 0, '1', NULL, 0, 1, 'VT', 0, NULL, 'V', 0, 15), + (2, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 2000000000, NULL, 0, '2', NULL, 0, 2, 'VT', 0, NULL, 'H', 0, 10), + (3, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 4751000000, NULL, 0, '3', NULL, 0, 5, 'VT', 0, NULL, NULL, 0, 5), + (4, 1, 60, 'YEL', 1, 1, 'Increases block', 1, 05080000, 4751000000, NULL, 0, '4', NULL, 0, 3, 'VT', 0, NULL, NULL, 0, NULL), + (5, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 4751000000, NULL, 0, '5', NULL, 0, 3, 'VT', 0, NULL, NULL, 0, NULL), + (6, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '6', NULL, 0, 4, 'VT', 0, NULL, NULL, 0, NULL), + (7, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '7', NULL, 0, 4, 'VT', 0, NULL, NULL, 0, NULL), + (8, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 2000000000, NULL, 0, '8', NULL, 0, 5, 'VT', 0, NULL, NULL, 0, NULL), + (9, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 2000000000, NULL, 0, '9', NULL, 0, 4, 'VT', 1, NULL, NULL, 0, NULL), + (10, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 4751000000, NULL, 0, '10', NULL, 0, 4, 'VT', 0, NULL, NULL, 0, NULL), + (11, 1, 60, 'YEL', 1, 1, NULL, 1, 05080000, 4751000000, NULL, 0, '11', NULL, 0, 4, 'VT', 0, NULL, NULL, 0, NULL), + (12, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 4751000000, NULL, 0, '12', NULL, 0, 3, 'VT', 0, NULL, NULL, 0, NULL), + (13, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '13', NULL, 1, 2, 'VT', 1, NULL, NULL, 1, NULL), + (14, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 4, 'VT', 1, NULL, NULL, 0, NULL), + (15, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'EMB', 0, NULL, NULL, 0, NULL), + (16, 6, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'EMB', 0, NULL, NULL, 0, NULL), + (71, 6, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'VT', 0, NULL, NULL, 0, NULL); -- Update the taxClass after insert of the items UPDATE `vn`.`itemTaxCountry` SET `taxClassFk` = 2 @@ -2298,11 +2310,6 @@ INSERT INTO `vn`.`workerTimeControlParams` (`id`, `dayBreak`, `weekBreak`, `week (1, 43200, 129600, 734400, 43200, 50400, 259200, 1296000, 36000); INSERT IGNORE INTO `vn`.`greugeConfig` (`id`, `freightPickUpPrice`) VALUES ('1', '11'); - -INSERT INTO `vn`.`temperature`(`code`, `name`, `description`) - VALUES - ('warm', 'Warm', 'Warm'), - ('cool', 'Cool', 'Cool'); INSERT INTO `vn`.`thermograph`(`id`, `model`) VALUES @@ -2551,4 +2558,37 @@ INSERT INTO `vn`.`supplierAgencyTerm` (`agencyFk`, `supplierFk`, `minimumPackage (2, 1, 60, 0.00, 0.00, NULL, 0, 5.00, 33), (3, 2, 0, 15.00, 0.00, NULL, 0, 0.00, 0), (4, 2, 0, 20.00, 0.00, NULL, 0, 0.00, 0), - (5, 442, 0, 0.00, 3.05, NULL, 0, 0.00, 0); + (5, 442, 0, 0.00, 3.05, NULL, 0, 0.00, 0); + +INSERT INTO `vn`.`chat` (`senderFk`, `recipient`, `dated`, `checkUserStatus`, `message`, `status`, `attempts`) + VALUES + (1101, '@PetterParker', CURDATE(), 1, 'First test message', 0, 0), + (1101, '@PetterParker', CURDATE(), 0, 'Second test message', 0, 0); + + +INSERT INTO `vn`.`mobileAppVersionControl` (`appName`, `version`, `isVersionCritical`) + VALUES + ('delivery', '9.2', 0), + ('warehouse', '8.1', 0); + +INSERT INTO `vn`.`machine` (`plate`, `maker`, `model`, `warehouseFk`, `departmentFk`, `type`, `use`, `productionYear`, `workerFk`, `companyFk`) + VALUES + ('RE-001', 'STILL', 'LTX-20', 60, 23, 'ELECTRIC TOW', 'Drag cars', 2020, 103, 442), + ('RE-002', 'STILL', 'LTX-20', 60, 23, 'ELECTRIC TOW', 'Drag cars', 2020, 103, 442); + +INSERT INTO `vn`.`machineWorker` (`workerFk`, `machineFk`, `inTimed`, `outTimed`) + VALUES + (1106, 1, CURDATE(), CURDATE()), + (1106, 1, DATE_ADD(CURDATE(), INTERVAL + 1 DAY), DATE_ADD(CURDATE(), INTERVAL +1 DAY)), + (1106, 2, CURDATE(), NULL), + (1106, 2, DATE_ADD(CURDATE(), INTERVAL + 1 DAY), DATE_ADD(CURDATE(), INTERVAL +1 DAY)); + +INSERT INTO `vn`.`mdbBranch` (`name`) + VALUES + ('test'), + ('master'); + +INSERT INTO `vn`.`mdbVersion` (`app`, `branchFk`, `version`) + VALUES + ('tpv', 'test', '1'), + ('lab', 'master', '1'); \ No newline at end of file diff --git a/db/dump/structure.sql b/db/dump/structure.sql index 1ad73243b..67763066f 100644 --- a/db/dump/structure.sql +++ b/db/dump/structure.sql @@ -31067,7 +31067,7 @@ CREATE TABLE `entry` ( `dated` datetime NOT NULL, `ref` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL, `isBooked` tinyint(1) NOT NULL DEFAULT '0', - `isExcludedFromAvailable` tinyint(1) NOT NULL DEFAULT '0', + `isExcludedFromAvailable` tinyint(1) NOT NULL DEFAULT 0, `notes` longtext COLLATE utf8_unicode_ci, `isConfirmed` tinyint(1) NOT NULL DEFAULT '0', `isOrdered` tinyint(1) NOT NULL DEFAULT '0', @@ -37460,6 +37460,31 @@ SET character_set_client = utf8; ) ENGINE=MyISAM */; SET character_set_client = @saved_cs_client; +-- +-- Temporary table structure for view `printer` +-- + +DROP TABLE IF EXISTS `printer`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `printer` ( + `id` tinyint(3) unsigned NOT NULL, + `name` varchar(50) COLLATE utf8mb3_unicode_ci DEFAULT NULL, + `path` varchar(50) COLLATE utf8mb3_unicode_ci DEFAULT NULL, + `modelFk` varchar(50) COLLATE utf8mb3_unicode_ci DEFAULT NULL, + `macWifi` varchar(20) COLLATE utf8mb3_unicode_ci DEFAULT NULL, + `ipAddress` varchar(15) COLLATE utf8mb3_unicode_ci DEFAULT NULL, + `reference` varchar(50) COLLATE utf8mb3_unicode_ci DEFAULT NULL, + `isLabeler` tinyint(1) DEFAULT 0 COMMENT 'Indica si es impresora de etiquetas', + PRIMARY KEY (`id`), + UNIQUE KEY `printer_UN` (`reference`), + UNIQUE KEY `printer_UN1` (`macWifi`), + UNIQUE KEY `printer_UN2` (`name`), + KEY `printer_FK` (`modelFk`), + CONSTRAINT `printer_FK` FOREIGN KEY (`modelFk`) REFERENCES `printerModel` (`code`) ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `printingQueueCheck` -- @@ -82023,7 +82048,7 @@ BEGIN * Devuelve los tickets y la cantidad de lineas de venta que se pueden adelantar. * * @param vDated Fecha de los tickets que se quieren adelantar. - * @param vWarehouseFk Almacén + * @param vWarehouseFk AlmacénitemEntryIn */ DECLARE vDateInventory DATE; DECLARE vDateToAdvance DATE; diff --git a/db/export-structure.sh b/db/export-structure.sh index 388231306..9b23f43ac 100755 --- a/db/export-structure.sh +++ b/db/export-structure.sh @@ -60,7 +60,6 @@ IGNORETABLES=( --ignore-table=vn.plantpassportAuthority__ --ignore-table=vn.preparationException --ignore-table=vn.priceFixed__ - --ignore-table=vn.printer --ignore-table=vn.printingQueue --ignore-table=vn.printServerQueue__ --ignore-table=vn.promissoryNote diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 57acfda81..609145019 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -101,6 +101,47 @@ export default { email: 'vn-user-mail-forwarding vn-textfield[ng-model="data.forwardTo"]', save: 'vn-user-mail-forwarding vn-submit' }, + accountAcl: { + addAcl: 'vn-acl-index button vn-icon[icon="add"]', + thirdAcl: 'vn-acl-index vn-list> a:nth-child(3)', + deleteThirdAcl: 'vn-acl-index vn-list > a:nth-child(3) > vn-item-section > vn-icon-button[icon="delete"]', + role: 'vn-acl-create vn-autocomplete[ng-model="$ctrl.acl.principalId"]', + model: 'vn-acl-create vn-autocomplete[ng-model="$ctrl.acl.model"]', + property: 'vn-acl-create vn-autocomplete[ng-model="$ctrl.acl.property"]', + accessType: 'vn-acl-create vn-autocomplete[ng-model="$ctrl.acl.accessType"]', + permission: 'vn-acl-create vn-autocomplete[ng-model="$ctrl.acl.permission"]', + save: 'vn-acl-create vn-submit' + }, + accountConnections: { + firstConnection: 'vn-connections vn-list > a:nth-child(1)', + deleteFirstConnection: 'vn-connections vn-list > a:nth-child(1) > vn-item-section > vn-icon-button[icon="exit_to_app"]' + }, + accountAccounts: { + syncRoles: 'vn-account-accounts vn-button[label="Synchronize roles"]', + syncUser: 'vn-account-accounts vn-button[label="Synchronize user"]', + syncAll: 'vn-account-accounts vn-button[label="Synchronize all"]', + syncUserName: 'vn-textfield[ng-model="$ctrl.syncUser"]', + syncUserPassword: 'vn-textfield[ng-model="$ctrl.syncPassword"]', + buttonAccept: 'button[response="accept"]' + }, + accountLdap: { + checkEnable: 'vn-account-ldap vn-check[ng-model="watcher.hasData"]', + server: 'vn-account-ldap vn-textfield[ng-model="$ctrl.config.server"]', + rdn: 'vn-account-ldap vn-textfield[ng-model="$ctrl.config.rdn"]', + password: 'vn-account-ldap vn-textfield[ng-model="$ctrl.config.password"]', + userDn: 'vn-account-ldap vn-textfield[ng-model="$ctrl.config.userDn"]', + groupDn: 'vn-account-ldap vn-textfield[ng-model="$ctrl.config.groupDn"]', + save: 'vn-account-ldap vn-submit' + }, + accountSamba: { + checkEnable: 'vn-account-samba vn-check[ng-model="watcher.hasData"]', + adDomain: 'vn-account-samba vn-textfield[ng-model="$ctrl.config.adDomain"]', + adController: 'vn-account-samba vn-textfield[ng-model="$ctrl.config.adController"]', + adUser: 'vn-account-samba vn-textfield[ng-model="$ctrl.config.adUser"]', + adPassword: 'vn-account-samba vn-textfield[ng-model="$ctrl.config.adPassword"]', + verifyCert: 'vn-account-samba vn-check[ng-model="$ctrl.config.verifyCert"]', + save: 'vn-account-samba vn-submit' + }, clientsIndex: { createClientButton: `vn-float-button` }, diff --git a/e2e/paths/14-account/04_acl.spec.js b/e2e/paths/14-account/04_acl.spec.js new file mode 100644 index 000000000..c400dbfb2 --- /dev/null +++ b/e2e/paths/14-account/04_acl.spec.js @@ -0,0 +1,60 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Account ACL path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('developer', 'account'); + await page.accessToSection('account.acl'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should go to create new acl', async() => { + await page.waitToClick(selectors.accountAcl.addAcl); + await page.waitForState('account.acl.create'); + }); + + it('should create new acl', async() => { + await page.autocompleteSearch(selectors.accountAcl.role, 'sysadmin'); + await page.autocompleteSearch(selectors.accountAcl.model, 'UserAccount'); + await page.autocompleteSearch(selectors.accountAcl.accessType, '*'); + await page.autocompleteSearch(selectors.accountAcl.permission, 'ALLOW'); + await page.waitToClick(selectors.accountAcl.save); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should navigate to edit', async() => { + await page.doSearch(); + await page.waitToClick(selectors.accountAcl.thirdAcl); + await page.waitForState('account.acl.edit'); + }); + + it('should edit the third acl', async() => { + await page.autocompleteSearch(selectors.accountAcl.model, 'Supplier'); + await page.autocompleteSearch(selectors.accountAcl.accessType, 'READ'); + await page.waitToClick(selectors.accountAcl.save); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should delete the third result', async() => { + const result = await page.waitToGetProperty(selectors.accountAcl.thirdAcl, 'innerText'); + await page.waitToClick(selectors.accountAcl.deleteThirdAcl); + await page.waitToClick(selectors.globalItems.acceptButton); + const message = await page.waitForSnackbar(); + const newResult = await page.waitToGetProperty(selectors.accountAcl.thirdAcl, 'innerText'); + + expect(message.text).toContain('ACL removed'); + expect(result).not.toEqual(newResult); + }); +}); diff --git a/e2e/paths/14-account/05_connections.spec.js b/e2e/paths/14-account/05_connections.spec.js new file mode 100644 index 000000000..89b286101 --- /dev/null +++ b/e2e/paths/14-account/05_connections.spec.js @@ -0,0 +1,33 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Account Connections path', () => { + let browser; + let page; + const account = 'sysadmin'; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule(account, 'account'); + await page.accessToSection('account.connections'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should check this is the last connection', async() => { + const firstResult = await page.waitToGetProperty(selectors.accountConnections.firstConnection, 'innerText'); + + expect(firstResult).toContain(account); + }); + + it('should kill this connection and then get redirected to the login page', async() => { + await page.waitToClick(selectors.accountConnections.deleteFirstConnection); + await page.waitToClick(selectors.globalItems.acceptButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Your session has expired, please login again'); + }); +}); diff --git a/e2e/paths/14-account/06_accounts.spec.js b/e2e/paths/14-account/06_accounts.spec.js new file mode 100644 index 000000000..83893e0cf --- /dev/null +++ b/e2e/paths/14-account/06_accounts.spec.js @@ -0,0 +1,49 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Account Accounts path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('sysadmin', 'account'); + await page.accessToSection('account.accounts'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should sync roles', async() => { + await page.waitToClick(selectors.accountAccounts.syncRoles); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Roles synchronized!'); + }); + + it('should sync user', async() => { + await page.waitToClick(selectors.accountAccounts.syncUser); + await page.write(selectors.accountAccounts.syncUserName, 'sysadmin'); + await page.write(selectors.accountAccounts.syncUserPassword, 'nightmare'); + + await page.waitToClick(selectors.accountAccounts.buttonAccept); + + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('User synchronized!'); + }); + + it('should relogin', async() => { + await page.loginAndModule('sysadmin', 'account'); + await page.accessToSection('account.accounts'); + }); + + it('should sync all', async() => { + await page.waitToClick(selectors.accountAccounts.syncAll); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Synchronizing in the background'); + }); +}); diff --git a/e2e/paths/14-account/07_ldap.spec.js b/e2e/paths/14-account/07_ldap.spec.js new file mode 100644 index 000000000..a3b8137d3 --- /dev/null +++ b/e2e/paths/14-account/07_ldap.spec.js @@ -0,0 +1,32 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Account LDAP path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('sysadmin', 'account'); + await page.accessToSection('account.ldap'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should set data and save', async() => { + await page.waitToClick(selectors.accountLdap.checkEnable); + await page.write(selectors.accountLdap.server, '1234'); + await page.write(selectors.accountLdap.rdn, '1234'); + await page.write(selectors.accountLdap.password, 'nightmare'); + await page.write(selectors.accountLdap.userDn, 'sysadmin'); + await page.write(selectors.accountLdap.groupDn, '1234'); + await page.waitToClick(selectors.accountLdap.save); + + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); +}); diff --git a/e2e/paths/14-account/08_samba.spec.js b/e2e/paths/14-account/08_samba.spec.js new file mode 100644 index 000000000..c3db026dc --- /dev/null +++ b/e2e/paths/14-account/08_samba.spec.js @@ -0,0 +1,32 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Account Samba path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('sysadmin', 'account'); + await page.accessToSection('account.samba'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should set data and save', async() => { + await page.waitToClick(selectors.accountSamba.checkEnable); + await page.write(selectors.accountSamba.adDomain, '1234'); + await page.write(selectors.accountSamba.adController, '1234'); + await page.write(selectors.accountSamba.adUser, 'nightmare'); + await page.write(selectors.accountSamba.adPassword, 'sysadmin'); + await page.waitToClick(selectors.accountSamba.verifyCert); + await page.waitToClick(selectors.accountSamba.save); + + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); +}); diff --git a/front/core/components/smart-table/index.js b/front/core/components/smart-table/index.js index 81d8d103e..401541c2c 100644 --- a/front/core/components/smart-table/index.js +++ b/front/core/components/smart-table/index.js @@ -318,6 +318,8 @@ export default class SmartTable extends Component { for (let column of columns) { const field = column.getAttribute('field'); const cell = document.createElement('td'); + cell.setAttribute('centered', ''); + if (field) { let input; let options; @@ -331,6 +333,15 @@ export default class SmartTable extends Component { continue; } + input = this.$compile(` + `)(this.$inputsScope); + if (options && options.autocomplete) { let props = ``; @@ -346,16 +357,29 @@ export default class SmartTable extends Component { on-change="$ctrl.searchByColumn('${field}')" clear-disabled="true" />`)(this.$inputsScope); - } else { + } + + if (options && options.checkbox) { input = this.$compile(` - `)(this.$inputsScope); } + + if (options && options.datepicker) { + input = this.$compile(` + `)(this.$inputsScope); + } + cell.appendChild(input[0]); } searchRow.appendChild(cell); @@ -372,13 +396,12 @@ export default class SmartTable extends Component { searchByColumn(field) { const searchCriteria = this.$inputsScope.searchProps[field]; - const emptySearch = searchCriteria == '' || null; + const emptySearch = searchCriteria === '' || searchCriteria == null; const filters = this.filterSanitizer(field); if (filters && filters.userFilter) this.model.userFilter = filters.userFilter; - if (!emptySearch) this.addFilter(field, this.$inputsScope.searchProps[field]); else this.model.refresh(); diff --git a/front/core/styles/icons/salixfont.css b/front/core/styles/icons/salixfont.css index 6b2482bee..c500f976d 100644 --- a/front/core/styles/icons/salixfont.css +++ b/front/core/styles/icons/salixfont.css @@ -85,7 +85,6 @@ } .icon-bucket:before { content: "\e97a"; - color: #000; } .icon-buscaman:before { content: "\e93b"; @@ -95,32 +94,26 @@ } .icon-calc_volum .path1:before { content: "\e915"; - color: rgb(0, 0, 0); } .icon-calc_volum .path2:before { content: "\e916"; margin-left: -1em; - color: rgb(0, 0, 0); } .icon-calc_volum .path3:before { content: "\e917"; margin-left: -1em; - color: rgb(0, 0, 0); } .icon-calc_volum .path4:before { content: "\e918"; margin-left: -1em; - color: rgb(0, 0, 0); } .icon-calc_volum .path5:before { content: "\e919"; margin-left: -1em; - color: rgb(0, 0, 0); } .icon-calc_volum .path6:before { content: "\e91a"; margin-left: -1em; - color: rgb(255, 255, 255); } .icon-calendar:before { content: "\e93d"; diff --git a/loopback/locale/es.json b/loopback/locale/es.json index a44ba2da8..9e2b8989b 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -224,5 +224,7 @@ "The agency is already assigned to another autonomous": "La agencia ya está asignada a otro autónomo", "date in the future": "Fecha en el futuro", "reference duplicated": "Referencia duplicada", - "This ticket is already a refund": "Este ticket ya es un abono" + "This ticket is already a refund": "Este ticket ya es un abono", + "isWithoutNegatives": "isWithoutNegatives", + "routeFk": "routeFk" } \ No newline at end of file diff --git a/loopback/server/datasources.json b/loopback/server/datasources.json index f51beeb19..5dade9c2e 100644 --- a/loopback/server/datasources.json +++ b/loopback/server/datasources.json @@ -98,5 +98,15 @@ "image/jpg", "video/mp4" ] + }, + "accessStorage": { + "name": "accessStorage", + "connector": "loopback-component-storage", + "provider": "filesystem", + "root": "./storage/access", + "maxFileSize": "524288000", + "allowedContentTypes": [ + "application/x-7z-compressed" + ] } } \ No newline at end of file diff --git a/modules/account/front/role/summary/index.js b/modules/account/front/role/summary/index.js index 0a08fe8b2..4f321fa98 100644 --- a/modules/account/front/role/summary/index.js +++ b/modules/account/front/role/summary/index.js @@ -6,7 +6,6 @@ class Controller extends Component { this._role = value; this.$.summary = null; if (!value) return; - this.$http.get(`Roles/${value.id}`) .then(res => this.$.summary = res.data); } diff --git a/modules/claim/front/summary/index.html b/modules/claim/front/summary/index.html index 282c55b00..5d90da516 100644 --- a/modules/claim/front/summary/index.html +++ b/modules/claim/front/summary/index.html @@ -1,5 +1,6 @@ @@ -106,8 +107,13 @@
+ zoom-image="{{$ctrl.getImagePath(photo.dmsFk)}}" + ng-if="photo.dms.contentType != 'video/mp4'">
+
diff --git a/modules/claim/front/summary/index.js b/modules/claim/front/summary/index.js index 721f51846..7cd4805e9 100644 --- a/modules/claim/front/summary/index.js +++ b/modules/claim/front/summary/index.js @@ -6,6 +6,13 @@ class Controller extends Summary { constructor($element, $, vnFile) { super($element, $); this.vnFile = vnFile; + this.filter = { + include: [ + { + relation: 'dms' + } + ] + }; } $onChanges() { diff --git a/modules/claim/front/summary/style.scss b/modules/claim/front/summary/style.scss index e81213658..5b4e32f7a 100644 --- a/modules/claim/front/summary/style.scss +++ b/modules/claim/front/summary/style.scss @@ -10,4 +10,19 @@ vn-claim-summary { vn-textarea *{ height: 80px; } + + .video { + width: 100%; + height: 100%; + object-fit: cover; + cursor: pointer; + box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), + 0 3px 1px -2px rgba(0,0,0,.2), + 0 1px 5px 0 rgba(0,0,0,.12); + border: 2px solid transparent; + + } + .video:hover { + border: 2px solid $color-primary + } } \ No newline at end of file diff --git a/modules/client/back/methods/client/extendedListFilter.js b/modules/client/back/methods/client/extendedListFilter.js new file mode 100644 index 000000000..8e02cd413 --- /dev/null +++ b/modules/client/back/methods/client/extendedListFilter.js @@ -0,0 +1,159 @@ + +const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; +const buildFilter = require('vn-loopback/util/filter').buildFilter; +const mergeFilters = require('vn-loopback/util/filter').mergeFilters; + +module.exports = Self => { + Self.remoteMethodCtx('extendedListFilter', { + description: 'Find all clients matched by the filter', + accessType: 'READ', + accepts: [ + { + arg: 'filter', + type: 'object', + }, + { + arg: 'search', + type: 'string', + description: `If it's and integer searchs by id, otherwise it searchs by name`, + }, + { + arg: 'name', + type: 'string', + description: 'The client name', + }, + { + arg: 'salesPersonFk', + type: 'number', + }, + { + arg: 'fi', + type: 'string', + description: 'The client fiscal id', + }, + { + arg: 'socialName', + type: 'string', + }, + { + arg: 'city', + type: 'string', + }, + { + arg: 'postcode', + type: 'string', + }, + { + arg: 'provinceFk', + type: 'number', + }, + { + arg: 'email', + type: 'string', + }, + { + arg: 'phone', + type: 'string', + }, + ], + returns: { + type: ['object'], + root: true + }, + http: { + path: `/extendedListFilter`, + verb: 'GET' + } + }); + + Self.extendedListFilter = async(ctx, filter, options) => { + const conn = Self.dataSource.connector; + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + const where = buildFilter(ctx.args, (param, value) => { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? {'c.id': {inq: value}} + : {'c.name': {like: `%${value}%`}}; + case 'name': + case 'salesPersonFk': + case 'fi': + case 'socialName': + case 'city': + case 'postcode': + case 'provinceFk': + case 'email': + case 'phone': + param = `c.${param}`; + return {[param]: value}; + } + }); + + filter = mergeFilters(filter, {where}); + + const stmts = []; + const stmt = new ParameterizedSQL( + `SELECT + c.id, + c.name, + c.socialName, + c.fi, + c.credit, + c.creditInsurance, + c.phone, + c.mobile, + c.street, + c.city, + c.postcode, + c.email, + c.created, + c.isActive, + c.isVies, + c.isTaxDataChecked, + c.isEqualizated, + c.isFreezed, + c.hasToInvoice, + c.hasToInvoiceByAddress, + c.isToBeMailed, + c.hasSepaVnl, + c.hasLcr, + c.hasCoreVnl, + ct.id AS countryFk, + ct.country, + p.id AS provinceFk, + p.name AS province, + u.id AS salesPersonFk, + u.name AS salesPerson, + bt.code AS businessTypeFk, + bt.description AS businessType, + pm.id AS payMethodFk, + pm.name AS payMethod, + sti.CodigoIva AS sageTaxTypeFk, + sti.Iva AS sageTaxType, + stt.CodigoTransaccion AS sageTransactionTypeFk, + stt.Transaccion AS sageTransactionType + FROM client c + LEFT JOIN account.user u ON u.id = c.salesPersonFk + LEFT JOIN country ct ON ct.id = c.countryFk + LEFT JOIN province p ON p.id = c.provinceFk + LEFT JOIN businessType bt ON bt.code = c.businessTypeFk + LEFT JOIN payMethod pm ON pm.id = c.payMethodFk + LEFT JOIN sage.TiposIva sti ON sti.CodigoIva = c.taxTypeSageFk + LEFT JOIN sage.TiposTransacciones stt ON stt.CodigoTransaccion = c.transactionTypeSageFk + ` + ); + + stmt.merge(conn.makeWhere(filter.where)); + stmt.merge(conn.makePagination(filter)); + + const clientsIndex = stmts.push(stmt) - 1; + const sql = ParameterizedSQL.join(stmts, ';'); + const result = await conn.executeStmt(sql, myOptions); + + return clientsIndex === 0 ? result : result[clientsIndex]; + }; +}; diff --git a/modules/client/back/methods/client/sendSms.js b/modules/client/back/methods/client/sendSms.js index cc11d17be..9d6a12416 100644 --- a/modules/client/back/methods/client/sendSms.js +++ b/modules/client/back/methods/client/sendSms.js @@ -39,7 +39,7 @@ module.exports = Self => { const userId = ctx.req.accessToken.userId; - const sms = await models.Sms.send(ctx, id, destination, message); + const sms = await models.Sms.send(ctx, destination, message); const logRecord = { originFk: id, userFk: userId, diff --git a/modules/client/back/methods/client/specs/extendedListFilter.spec.js b/modules/client/back/methods/client/specs/extendedListFilter.spec.js new file mode 100644 index 000000000..907c03ef9 --- /dev/null +++ b/modules/client/back/methods/client/specs/extendedListFilter.spec.js @@ -0,0 +1,180 @@ +const { models } = require('vn-loopback/server/server'); + +describe('client extendedListFilter()', () => { + it('should return the clients matching the filter with a limit of 20 rows', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {}}; + const filter = {limit: '20'}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + expect(result.length).toEqual(20); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the client "Bruce Wayne" matching the search argument with his name', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {search: 'Bruce Wayne'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const firstResult = result[0]; + + expect(result.length).toEqual(1); + expect(firstResult.name).toEqual('Bruce Wayne'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the client "Bruce Wayne" matching the search argument with his id', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {search: '1101'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const firstResult = result[0]; + + expect(result.length).toEqual(1); + expect(firstResult.name).toEqual('Bruce Wayne'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the client "Bruce Wayne" matching the name argument', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {name: 'Bruce Wayne'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const firstResult = result[0]; + + expect(result.length).toEqual(1); + expect(firstResult.name).toEqual('Bruce Wayne'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the clients matching the "salesPersonFk" argument', async() => { + const tx = await models.Client.beginTransaction({}); + const salesPersonId = 18; + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {salesPersonFk: salesPersonId}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const randomIndex = Math.floor(Math.random() * result.length); + const randomResultClient = result[randomIndex]; + + expect(result.length).toBeGreaterThanOrEqual(5); + expect(randomResultClient.salesPersonFk).toEqual(salesPersonId); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the clients matching the "fi" argument', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {fi: '251628698'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const firstClient = result[0]; + + expect(result.length).toEqual(1); + expect(firstClient.name).toEqual('Max Eisenhardt'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the clients matching the "city" argument', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {city: 'Silla'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const randomIndex = Math.floor(Math.random() * result.length); + const randomResultClient = result[randomIndex]; + + expect(result.length).toBeGreaterThanOrEqual(20); + expect(randomResultClient.city.toLowerCase()).toEqual('silla'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the clients matching the "postcode" argument', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {postcode: '46460'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const randomIndex = Math.floor(Math.random() * result.length); + const randomResultClient = result[randomIndex]; + + expect(result.length).toBeGreaterThanOrEqual(20); + expect(randomResultClient.postcode).toEqual('46460'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); +}); diff --git a/modules/client/back/methods/client/specs/updatePortfolio.spec.js b/modules/client/back/methods/client/specs/updatePortfolio.spec.js index f56555c08..bf681eb2e 100644 --- a/modules/client/back/methods/client/specs/updatePortfolio.spec.js +++ b/modules/client/back/methods/client/specs/updatePortfolio.spec.js @@ -1,21 +1,39 @@ const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); describe('Client updatePortfolio', () => { - const clientId = 1108; + const activeCtx = { + accessToken: {userId: 9}, + http: { + req: { + headers: {origin: 'http://localhost'}, + [`__`]: value => { + return value; + } + } + } + }; + + beforeAll(() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + }); + it('should update the portfolioWeight when the salesPerson of a client changes', async() => { + const clientId = 1108; const salesPersonId = 18; const tx = await models.Client.beginTransaction({}); try { const options = {transaction: tx}; - const expectedResult = 841.63; - const clientQuery = `UPDATE vn.client SET salesPersonFk = ${salesPersonId} WHERE id = ${clientId}; `; - await models.Client.rawSql(clientQuery); + const client = await models.Client.findById(clientId, null, options); + await client.updateAttribute('salesPersonFk', salesPersonId, options); - await models.Client.updatePortfolio(); + await models.Client.updatePortfolio(options); const portfolioQuery = `SELECT portfolioWeight FROM bs.salesPerson WHERE workerFk = ${salesPersonId}; `; const [salesPerson] = await models.Client.rawSql(portfolioQuery, null, options); @@ -30,21 +48,21 @@ describe('Client updatePortfolio', () => { }); it('should keep the same portfolioWeight when a salesperson is unassigned of a client', async() => { - pending('task 3817'); + const clientId = 1107; const salesPersonId = 19; const tx = await models.Client.beginTransaction({}); try { const options = {transaction: tx}; - const expectedResult = 34.40; - await models.Client.rawSql(`UPDATE vn.client SET salesPersonFk = NULL WHERE id = ${clientId}; `); + const client = await models.Client.findById(clientId, null, options); + await client.updateAttribute('salesPersonFk', null, options); await models.Client.updatePortfolio(); const portfolioQuery = `SELECT portfolioWeight FROM bs.salesPerson WHERE workerFk = ${salesPersonId}; `; - const [salesPerson] = await models.Client.rawSql(portfolioQuery, null, options); + const [salesPerson] = await models.Client.rawSql(portfolioQuery); expect(salesPerson.portfolioWeight).toEqual(expectedResult); diff --git a/modules/client/back/methods/client/updatePortfolio.js b/modules/client/back/methods/client/updatePortfolio.js index 3d522f6c8..809a84636 100644 --- a/modules/client/back/methods/client/updatePortfolio.js +++ b/modules/client/back/methods/client/updatePortfolio.js @@ -13,8 +13,13 @@ module.exports = function(Self) { } }); - Self.updatePortfolio = async() => { + Self.updatePortfolio = async options => { + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + query = `CALL bs.salesPerson_updatePortfolio()`; - return await Self.rawSql(query); + return Self.rawSql(query, null, myOptions); }; }; diff --git a/modules/client/back/methods/credit-classification/createWithInsurance.js b/modules/client/back/methods/credit-classification/createWithInsurance.js index fb0a7222b..d4110676c 100644 --- a/modules/client/back/methods/credit-classification/createWithInsurance.js +++ b/modules/client/back/methods/credit-classification/createWithInsurance.js @@ -38,7 +38,7 @@ module.exports = Self => { }, myOptions); await models.CreditInsurance.create({ - creditClassification: newClassification.id, + creditClassificationFk: newClassification.id, credit: data.credit, grade: data.grade }, myOptions); diff --git a/modules/client/back/methods/sms/send.js b/modules/client/back/methods/sms/send.js index 08daf83a1..94b2b6c27 100644 --- a/modules/client/back/methods/sms/send.js +++ b/modules/client/back/methods/sms/send.js @@ -6,10 +6,6 @@ module.exports = Self => { description: 'Sends SMS to a destination phone', accessType: 'WRITE', accepts: [ - { - arg: 'destinationFk', - type: 'integer' - }, { arg: 'destination', type: 'string', @@ -31,7 +27,7 @@ module.exports = Self => { } }); - Self.send = async(ctx, destinationFk, destination, message) => { + Self.send = async(ctx, destination, message) => { const userId = ctx.req.accessToken.userId; const smsConfig = await Self.app.models.SmsConfig.findOne(); @@ -68,7 +64,6 @@ module.exports = Self => { const newSms = { senderFk: userId, - destinationFk: destinationFk || null, destination: destination, message: message, status: error diff --git a/modules/client/back/methods/sms/send.spec.js b/modules/client/back/methods/sms/send.spec.js index 7ca78b214..8eee85bd6 100644 --- a/modules/client/back/methods/sms/send.spec.js +++ b/modules/client/back/methods/sms/send.spec.js @@ -3,7 +3,7 @@ const app = require('vn-loopback/server/server'); describe('sms send()', () => { it('should not return status error', async() => { const ctx = {req: {accessToken: {userId: 1}}}; - const result = await app.models.Sms.send(ctx, 1105, '123456789', 'My SMS Body'); + const result = await app.models.Sms.send(ctx, '123456789', 'My SMS Body'); expect(result.status).toBeUndefined(); }); diff --git a/modules/client/back/models/client.js b/modules/client/back/models/client.js index fb41fb973..90a9b9e23 100644 --- a/modules/client/back/models/client.js +++ b/modules/client/back/models/client.js @@ -31,6 +31,7 @@ module.exports = Self => { require('../methods/client/createReceipt')(Self); require('../methods/client/updatePortfolio')(Self); require('../methods/client/checkDuplicated')(Self); + require('../methods/client/extendedListFilter')(Self); // Validations diff --git a/modules/client/back/models/client.json b/modules/client/back/models/client.json index b9951e8bb..1426152a4 100644 --- a/modules/client/back/models/client.json +++ b/modules/client/back/models/client.json @@ -136,6 +136,9 @@ "mysql": { "columnName": "businessTypeFk" } + }, + "salesPersonFk": { + "type": "number" } }, "relations": { diff --git a/modules/client/back/models/credit-classification.json b/modules/client/back/models/credit-classification.json index 543f8359e..c38d5cd45 100644 --- a/modules/client/back/models/credit-classification.json +++ b/modules/client/back/models/credit-classification.json @@ -36,7 +36,7 @@ "insurances": { "type": "hasMany", "model": "CreditInsurance", - "foreignKey": "creditClassification" + "foreignKey": "creditClassificationFk" } } } \ No newline at end of file diff --git a/modules/client/back/models/credit-insurance.js b/modules/client/back/models/credit-insurance.js index a2e8091e1..6f656d382 100644 --- a/modules/client/back/models/credit-insurance.js +++ b/modules/client/back/models/credit-insurance.js @@ -24,7 +24,7 @@ module.exports = function(Self) { let filter = { fields: ['grade'], where: { - creditClassification: this.creditClassification + creditClassificationFk: this.creditClassificationFk }, order: 'created DESC' }; diff --git a/modules/client/back/models/credit-insurance.json b/modules/client/back/models/credit-insurance.json index db4154978..08b2e3d60 100644 --- a/modules/client/back/models/credit-insurance.json +++ b/modules/client/back/models/credit-insurance.json @@ -30,7 +30,7 @@ "classification": { "type": "belongsTo", "model": "CreditClassification", - "foreignKey": "creditClassification" + "foreignKey": "creditClassificationFk" } }, "scope": { diff --git a/modules/client/front/balance/create/index.js b/modules/client/front/balance/create/index.js index 454e5e44d..c6a6e7ff9 100644 --- a/modules/client/front/balance/create/index.js +++ b/modules/client/front/balance/create/index.js @@ -6,12 +6,7 @@ class Controller extends Dialog { super($element, $, $transclude); this.vnReport = vnReport; - - const tomorrow = new Date(); - tomorrow.setDate(tomorrow.getDate() + 1); - this.receipt = { - payed: tomorrow - }; + this.receipt = {}; } set payed(value) { @@ -72,6 +67,10 @@ class Controller extends Dialog { `${accountingType && accountingType.receiptDescription}`; } this.maxAmount = accountingType && accountingType.maxAmount; + + this.receipt.payed = new Date(); + if (accountingType.daysInFuture) + this.receipt.payed.setDate(this.receipt.payed.getDate() + accountingType.daysInFuture); } } diff --git a/modules/client/front/billing-data/index.html b/modules/client/front/billing-data/index.html index 06d7d7d73..33a237ad7 100644 --- a/modules/client/front/billing-data/index.html +++ b/modules/client/front/billing-data/index.html @@ -54,14 +54,7 @@ show-field="bic" vn-acl="salesAssistant" disabled="$ctrl.ibanCountry == 'ES'"> - - - {{bic}} - -
{{name}}
-
-
-
+ {{bic}} {{name}} diff --git a/modules/client/front/extended-list/index.html b/modules/client/front/extended-list/index.html new file mode 100644 index 000000000..b45a0bc5f --- /dev/null +++ b/modules/client/front/extended-list/index.html @@ -0,0 +1,319 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Identifier + + Name + + Social name + + Tax number + + Salesperson + + Credit + + Credit insurance + + Phone + + Mobile + + Street + + Country + + Province + + City + + Postcode + + Email + + Created + + Business type + + Billing data + + Sage tax type + + Sage tr. type + + Active + + Vies + + Verified data + + Is equalizated + + Freezed + + Invoice + + Invoice by address + + Mailing + + Received LCR + + Received core VNL + + Received B2B VNL +
+ + + + + + + {{::client.id}} + + {{::client.name}}{{::client.socialName}}{{::client.fi}} + + {{::client.salesPerson | dashIfEmpty}} + + {{::client.credit}}{{::client.creditInsurance | dashIfEmpty}}{{::client.phone | dashIfEmpty}}{{::client.mobile | dashIfEmpty}}{{::client.street | dashIfEmpty}}{{::client.country | dashIfEmpty}}{{::client.province | dashIfEmpty}}{{::client.city | dashIfEmpty}}{{::client.postcode | dashIfEmpty}}{{::client.email | dashIfEmpty}}{{::client.created | date:'dd/MM/yyyy'}}{{::client.businessType | dashIfEmpty}}{{::client.payMethod | dashIfEmpty}}{{::client.sageTaxType | dashIfEmpty}}{{::client.sageTransactionType | dashIfEmpty}} + + {{ ::client.isActive ? 'Yes' : 'No' | translate}} + + + + {{ ::client.isVies ? 'Yes' : 'No' | translate}} + + + + {{ ::client.isTaxDataChecked ? 'Yes' : 'No' | translate}} + + + + {{ ::client.isEqualizated ? 'Yes' : 'No' | translate}} + + + + {{ ::client.isFreezed ? 'Yes' : 'No' | translate}} + + + + {{ ::client.hasToInvoice ? 'Yes' : 'No' | translate}} + + + + {{ ::client.hasToInvoiceByAddress ? 'Yes' : 'No' | translate}} + + + + {{ ::client.isToBeMailed ? 'Yes' : 'No' | translate}} + + + + {{ ::client.hasLcr ? 'Yes' : 'No' | translate}} + + + + {{ ::client.hasCoreVnl ? 'Yes' : 'No' | translate}} + + + + {{ ::client.hasSepaVnl ? 'Yes' : 'No' | translate}} + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + Filter by selection + + + Exclude selection + + + Remove filter + + + Remove all filters + + + \ No newline at end of file diff --git a/modules/client/front/extended-list/index.js b/modules/client/front/extended-list/index.js new file mode 100644 index 000000000..8eed48d01 --- /dev/null +++ b/modules/client/front/extended-list/index.js @@ -0,0 +1,184 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; +import './style.scss'; + +class Controller extends Section { + constructor($element, $) { + super($element, $); + + this.smartTableOptions = { + activeButtons: { + search: true, + shownColumns: true, + }, + columns: [ + { + field: 'socialName', + autocomplete: { + url: 'Clients', + showField: 'socialName', + valueField: 'socialName', + } + }, + { + field: 'created', + datepicker: true + }, + { + field: 'countryFk', + autocomplete: { + url: 'Countries', + showField: 'country', + } + }, + { + field: 'provinceFk', + autocomplete: { + url: 'Provinces' + } + }, + { + field: 'salesPersonFk', + autocomplete: { + url: 'Workers/activeWithInheritedRole', + where: `{role: 'salesPerson'}`, + searchFunction: '{firstName: $search}', + showField: 'nickname', + valueField: 'id', + } + }, + { + field: 'businessTypeFk', + autocomplete: { + url: 'BusinessTypes', + valueField: 'code', + showField: 'description', + } + }, + { + field: 'payMethodFk', + autocomplete: { + url: 'PayMethods', + } + }, + { + field: 'sageTaxTypeFk', + autocomplete: { + url: 'SageTaxTypes', + showField: 'vat', + } + }, + { + field: 'sageTransactionTypeFk', + autocomplete: { + url: 'SageTransactionTypes', + showField: 'transaction', + } + }, + { + field: 'isActive', + checkbox: true + }, + { + field: 'isVies', + checkbox: true + }, + { + field: 'isTaxDataChecked', + checkbox: true + }, + { + field: 'isEqualizated', + checkbox: true + }, + { + field: 'isFreezed', + checkbox: true + }, + { + field: 'hasToInvoice', + checkbox: true + }, + { + field: 'hasToInvoiceByAddress', + checkbox: true + }, + { + field: 'isToBeMailed', + checkbox: true + }, + { + field: 'hasSepaVnl', + checkbox: true + }, + { + field: 'hasLcr', + checkbox: true + }, + { + field: 'hasCoreVnl', + checkbox: true + } + ] + }; + } + + exprBuilder(param, value) { + switch (param) { + case 'created': + return {'c.created': { + between: this.dateRange(value)} + }; + case 'id': + case 'name': + case 'socialName': + case 'fi': + case 'credit': + case 'creditInsurance': + case 'phone': + case 'mobile': + case 'street': + case 'city': + case 'postcode': + case 'email': + case 'isActive': + case 'isVies': + case 'isTaxDataChecked': + case 'isEqualizated': + case 'isFreezed': + case 'hasToInvoice': + case 'hasToInvoiceByAddress': + case 'isToBeMailed': + case 'hasSepaVnl': + case 'hasLcr': + case 'hasCoreVnl': + case 'countryFk': + case 'provinceFk': + case 'salesPersonFk': + case 'businessTypeFk': + case 'payMethodFk': + case 'sageTaxTypeFk': + case 'sageTransactionTypeFk': + return {[`c.${param}`]: 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]; + } + + preview(client) { + this.clientSelected = client; + this.$.preview.show(); + } +} + +ngModule.vnComponent('vnClientExtendedList', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/client/front/extended-list/locale/es.yml b/modules/client/front/extended-list/locale/es.yml new file mode 100644 index 000000000..ea56f3af2 --- /dev/null +++ b/modules/client/front/extended-list/locale/es.yml @@ -0,0 +1,3 @@ +Mailing: Env. emails +Sage tr. type: Tipo tr. sage +Yes: Sí \ No newline at end of file diff --git a/modules/client/front/extended-list/style.scss b/modules/client/front/extended-list/style.scss new file mode 100644 index 000000000..7625b5d16 --- /dev/null +++ b/modules/client/front/extended-list/style.scss @@ -0,0 +1,6 @@ +@import "variables"; + +vn-chip.success, +vn-chip.alert { + color: $color-font-bg +} \ No newline at end of file diff --git a/modules/client/front/index.js b/modules/client/front/index.js index ea732beea..a5782c789 100644 --- a/modules/client/front/index.js +++ b/modules/client/front/index.js @@ -47,3 +47,4 @@ import './consumption-search-panel'; import './defaulter'; import './notification'; import './unpaid'; +import './extended-list'; diff --git a/modules/client/front/locale/es.yml b/modules/client/front/locale/es.yml index 4eb99318c..de4b91e0b 100644 --- a/modules/client/front/locale/es.yml +++ b/modules/client/front/locale/es.yml @@ -33,6 +33,7 @@ Search client by id or name: Buscar clientes por identificador o nombre # Sections Clients: Clientes +Extended list: Listado extendido Defaulter: Morosos New client: Nuevo cliente Fiscal data: Datos fiscales diff --git a/modules/client/front/routes.json b/modules/client/front/routes.json index 293243470..6616443bb 100644 --- a/modules/client/front/routes.json +++ b/modules/client/front/routes.json @@ -7,6 +7,7 @@ "menus": { "main": [ {"state": "client.index", "icon": "person"}, + {"state": "client.extendedList", "icon": "person"}, {"state": "client.notification", "icon": "campaign"}, {"state": "client.defaulter", "icon": "icon-defaulter"} ], @@ -381,6 +382,12 @@ "component": "vn-client-unpaid", "acl": ["administrative"], "description": "Unpaid" + }, + { + "url": "/extended-list", + "state": "client.extendedList", + "component": "vn-client-extended-list", + "description": "Extended list" } ] } diff --git a/modules/client/front/search-panel/locale/es.yml b/modules/client/front/search-panel/locale/es.yml index 93d2faf53..b0d0649c8 100644 --- a/modules/client/front/search-panel/locale/es.yml +++ b/modules/client/front/search-panel/locale/es.yml @@ -1,7 +1,7 @@ Client id: Id cliente Tax number: NIF/CIF Name: Nombre -Social name: Razon social +Social name: Razón social Town/City: Ciudad Postcode: Código postal Email: E-mail diff --git a/modules/invoiceOut/front/descriptor-menu/index.html b/modules/invoiceOut/front/descriptor-menu/index.html index 859486ab1..ef4c9a62e 100644 --- a/modules/invoiceOut/front/descriptor-menu/index.html +++ b/modules/invoiceOut/front/descriptor-menu/index.html @@ -76,6 +76,13 @@ translate> Show CITES letter + + Refund + + + diff --git a/modules/invoiceOut/front/descriptor-menu/index.js b/modules/invoiceOut/front/descriptor-menu/index.js index 7738845f9..b884e50cb 100644 --- a/modules/invoiceOut/front/descriptor-menu/index.js +++ b/modules/invoiceOut/front/descriptor-menu/index.js @@ -116,6 +116,35 @@ class Controller extends Section { invoiceId: this.id }); } + + async refundInvoiceOut() { + let filter = { + where: {refFk: this.invoiceOut.ref} + }; + const tickets = await this.$http.get('Tickets', {filter}); + this.tickets = tickets.data; + this.ticketsIds = []; + for (let ticket of this.tickets) + this.ticketsIds.push(ticket.id); + + filter = { + where: {ticketFk: {inq: this.ticketsIds}} + }; + const sales = await this.$http.get('Sales', {filter}); + this.sales = sales.data; + + const ticketServices = await this.$http.get('TicketServices', {filter}); + this.services = ticketServices.data; + + const params = { + sales: this.sales, + services: this.services + }; + const query = `Sales/refund`; + return this.$http.post(query, params).then(res => { + this.$state.go('ticket.card.sale', {id: res.data}); + }); + } } Controller.$inject = ['$element', '$scope', 'vnReport', 'vnEmail']; diff --git a/modules/invoiceOut/front/descriptor-menu/index.spec.js b/modules/invoiceOut/front/descriptor-menu/index.spec.js index fced12e0d..c84c97a57 100644 --- a/modules/invoiceOut/front/descriptor-menu/index.spec.js +++ b/modules/invoiceOut/front/descriptor-menu/index.spec.js @@ -122,4 +122,34 @@ describe('vnInvoiceOutDescriptorMenu', () => { expect(controller.vnApp.showMessage).toHaveBeenCalled(); }); }); + + // #4084 review with Juan + xdescribe('refundInvoiceOut()', () => { + it('should make a query and go to ticket.card.sale', () => { + controller.$state.go = jest.fn(); + + const invoiceOut = { + id: 1, + ref: 'T1111111' + }; + controller.invoiceOut = invoiceOut; + const tickets = [{id: 1}]; + const sales = [{id: 1}]; + const services = [{id: 2}]; + + $httpBackend.expectGET(`Tickets`).respond(tickets); + $httpBackend.expectGET(`Sales`).respond(sales); + $httpBackend.expectGET(`TicketServices`).respond(services); + + const expectedParams = { + sales: sales, + services: services + }; + $httpBackend.expectPOST(`Sales/refund`, expectedParams).respond(); + controller.refundInvoiceOut(); + $httpBackend.flush(); + + expect(controller.$state.go).toHaveBeenCalledWith('ticket.card.sale', {id: undefined}); + }); + }); }); diff --git a/modules/invoiceOut/front/descriptor-menu/locale/es.yml b/modules/invoiceOut/front/descriptor-menu/locale/es.yml index a76f6aad3..8949f1f91 100644 --- a/modules/invoiceOut/front/descriptor-menu/locale/es.yml +++ b/modules/invoiceOut/front/descriptor-menu/locale/es.yml @@ -12,6 +12,8 @@ Are you sure you want to delete this invoice?: Estas seguro de eliminar esta fac Are you sure you want to clone this invoice?: Estas seguro de clonar esta factura? InvoiceOut booked: Factura asentada Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura? +Are you sure you want to refund this invoice?: Estas seguro de querer abonar esta factura? +Create a single ticket with all the content of the current invoice: Crear un ticket unico con todo el contenido de la factura actual Regenerate PDF invoice: Regenerar PDF factura The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado The email can't be empty: El correo no puede estar vacío diff --git a/modules/item/back/methods/item-image-queue/downloadImages.js b/modules/item/back/methods/item-image-queue/downloadImages.js index 05b223598..ec8244f49 100644 --- a/modules/item/back/methods/item-image-queue/downloadImages.js +++ b/modules/item/back/methods/item-image-queue/downloadImages.js @@ -12,7 +12,7 @@ module.exports = Self => { }, http: { path: `/downloadImages`, - verb: 'POST' + verb: 'GET' } }); diff --git a/modules/item/back/models/item-type.json b/modules/item/back/models/item-type.json index cb9d5ace8..843d9877f 100644 --- a/modules/item/back/models/item-type.json +++ b/modules/item/back/models/item-type.json @@ -21,8 +21,11 @@ "life": { "type": "number" }, - "isPackaging": { - "type": "boolean" + "promo": { + "type": "number" + }, + "isUnconventionalSize": { + "type": "number" } }, "relations": { @@ -40,6 +43,16 @@ "type": "belongsTo", "model": "ItemCategory", "foreignKey": "categoryFk" + }, + "itemPackingType": { + "type": "belongsTo", + "model": "ItemPackingType", + "foreignKey": "itemPackingTypeFk" + }, + "temperature": { + "type": "belongsTo", + "model": "Temperature", + "foreignKey": "temperatureFk" } }, "acls": [ diff --git a/modules/item/back/models/item.json b/modules/item/back/models/item.json index efde2690f..01b6ba093 100644 --- a/modules/item/back/models/item.json +++ b/modules/item/back/models/item.json @@ -140,6 +140,9 @@ }, "isFloramondo": { "type": "boolean" + }, + "packingShelve": { + "type": "number" } }, "relations": { diff --git a/modules/item/front/index.js b/modules/item/front/index.js index c328b1c8d..6a8d1b3b7 100644 --- a/modules/item/front/index.js +++ b/modules/item/front/index.js @@ -23,4 +23,4 @@ import './waste/index/'; import './waste/detail'; import './fixed-price'; import './fixed-price-search-panel'; - +import './item-type'; diff --git a/modules/item/front/item-type/basic-data/index.html b/modules/item/front/item-type/basic-data/index.html new file mode 100644 index 000000000..1417a05ab --- /dev/null +++ b/modules/item/front/item-type/basic-data/index.html @@ -0,0 +1,62 @@ + + +
+ + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/modules/item/front/item-type/basic-data/index.js b/modules/item/front/item-type/basic-data/index.js new file mode 100644 index 000000000..ec280fdf8 --- /dev/null +++ b/modules/item/front/item-type/basic-data/index.js @@ -0,0 +1,12 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section {} + +ngModule.component('vnItemTypeBasicData', { + template: require('./index.html'), + controller: Controller, + bindings: { + itemType: '<' + } +}); diff --git a/modules/item/front/item-type/card/index.html b/modules/item/front/item-type/card/index.html new file mode 100644 index 000000000..80af6088e --- /dev/null +++ b/modules/item/front/item-type/card/index.html @@ -0,0 +1,5 @@ + + + + + diff --git a/modules/item/front/item-type/card/index.js b/modules/item/front/item-type/card/index.js new file mode 100644 index 000000000..fa6b37340 --- /dev/null +++ b/modules/item/front/item-type/card/index.js @@ -0,0 +1,23 @@ +import ngModule from '../../module'; +import ModuleCard from 'salix/components/module-card'; + +class Controller extends ModuleCard { + reload() { + const filter = { + include: [ + {relation: 'worker'}, + {relation: 'category'}, + {relation: 'itemPackingType'}, + {relation: 'temperature'} + ] + }; + + this.$http.get(`ItemTypes/${this.$params.id}`, {filter}) + .then(res => this.itemType = res.data); + } +} + +ngModule.vnComponent('vnItemTypeCard', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/item/front/item-type/card/index.spec.js b/modules/item/front/item-type/card/index.spec.js new file mode 100644 index 000000000..ab2314bb9 --- /dev/null +++ b/modules/item/front/item-type/card/index.spec.js @@ -0,0 +1,27 @@ +import './index'; + +describe('component vnItemTypeCard', () => { + let controller; + let $httpBackend; + + beforeEach(ngModule('item')); + + beforeEach(inject(($componentController, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + controller = $componentController('vnItemTypeCard', {$element: null}); + })); + + describe('reload()', () => { + it('should reload the controller data', () => { + controller.$params.id = 1; + + const itemType = {id: 1}; + + $httpBackend.expectGET('ItemTypes/1').respond(itemType); + controller.reload(); + $httpBackend.flush(); + + expect(controller.itemType).toEqual(itemType); + }); + }); +}); diff --git a/modules/item/front/item-type/create/index.html b/modules/item/front/item-type/create/index.html new file mode 100644 index 000000000..44cb5183d --- /dev/null +++ b/modules/item/front/item-type/create/index.html @@ -0,0 +1,62 @@ + + +
+ + + + + + + + + + + + + + + + + + + + +
diff --git a/modules/item/front/item-type/create/index.js b/modules/item/front/item-type/create/index.js new file mode 100644 index 000000000..ccf7744be --- /dev/null +++ b/modules/item/front/item-type/create/index.js @@ -0,0 +1,15 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + onSubmit() { + return this.$.watcher.submit().then(res => + this.$state.go('item.itemType.card.basicData', {id: res.data.id}) + ); + } +} + +ngModule.component('vnItemTypeCreate', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/item/front/item-type/create/index.spec.js b/modules/item/front/item-type/create/index.spec.js new file mode 100644 index 000000000..4b000df9a --- /dev/null +++ b/modules/item/front/item-type/create/index.spec.js @@ -0,0 +1,34 @@ +import './index'; + +describe('component vnItemTypeCreate', () => { + let $scope; + let $state; + let controller; + + beforeEach(ngModule('item')); + + beforeEach(inject(($componentController, $rootScope, _$state_) => { + $scope = $rootScope.$new(); + $state = _$state_; + $scope.watcher = { + submit: () => { + return { + then: callback => { + callback({data: {id: '1234'}}); + } + }; + } + }; + const $element = angular.element(''); + controller = $componentController('vnItemTypeCreate', {$element, $scope}); + })); + + describe('onSubmit()', () => { + it(`should call submit() on the watcher then expect a callback`, () => { + jest.spyOn($state, 'go'); + controller.onSubmit(); + + expect(controller.$state.go).toHaveBeenCalledWith('item.itemType.card.basicData', {id: '1234'}); + }); + }); +}); diff --git a/modules/item/front/item-type/descriptor/index.html b/modules/item/front/item-type/descriptor/index.html new file mode 100644 index 000000000..5a0e8ed49 --- /dev/null +++ b/modules/item/front/item-type/descriptor/index.html @@ -0,0 +1,25 @@ + + +
+ + + + + + + + +
+
+
\ No newline at end of file diff --git a/modules/item/front/item-type/descriptor/index.js b/modules/item/front/item-type/descriptor/index.js new file mode 100644 index 000000000..9322c599a --- /dev/null +++ b/modules/item/front/item-type/descriptor/index.js @@ -0,0 +1,20 @@ +import ngModule from '../../module'; +import Descriptor from 'salix/components/descriptor'; + +class Controller extends Descriptor { + get itemType() { + return this.entity; + } + + set itemType(value) { + this.entity = value; + } +} + +ngModule.component('vnItemTypeDescriptor', { + template: require('./index.html'), + controller: Controller, + bindings: { + itemType: '<' + } +}); diff --git a/modules/item/front/item-type/index.js b/modules/item/front/item-type/index.js new file mode 100644 index 000000000..5dcbe4097 --- /dev/null +++ b/modules/item/front/item-type/index.js @@ -0,0 +1,8 @@ +import './main'; +import './index/'; +import './summary'; +import './card'; +import './descriptor'; +import './create'; +import './basic-data'; +import './search-panel'; diff --git a/modules/item/front/item-type/index/index.html b/modules/item/front/item-type/index/index.html new file mode 100644 index 000000000..50b9eb172 --- /dev/null +++ b/modules/item/front/item-type/index/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/item/front/item-type/index/index.js b/modules/item/front/item-type/index/index.js new file mode 100644 index 000000000..54ecba997 --- /dev/null +++ b/modules/item/front/item-type/index/index.js @@ -0,0 +1,14 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + preview(itemType) { + this.selectedItemType = itemType; + this.$.summary.show(); + } +} + +ngModule.component('vnItemTypeIndex', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/item/front/item-type/index/index.spec.js b/modules/item/front/item-type/index/index.spec.js new file mode 100644 index 000000000..887c03f7f --- /dev/null +++ b/modules/item/front/item-type/index/index.spec.js @@ -0,0 +1,34 @@ +import './index'; + +describe('Item', () => { + describe('Component vnItemTypeIndex', () => { + let controller; + let $window; + + beforeEach(ngModule('item')); + + beforeEach(inject(($componentController, _$window_) => { + $window = _$window_; + const $element = angular.element(''); + controller = $componentController('vnItemTypeIndex', {$element}); + })); + + describe('preview()', () => { + it('should show the dialog summary', () => { + controller.$.summary = {show: () => {}}; + jest.spyOn(controller.$.summary, 'show'); + + const itemType = {id: 1}; + + const event = new MouseEvent('click', { + view: $window, + bubbles: true, + cancelable: true + }); + controller.preview(event, itemType); + + expect(controller.$.summary.show).toHaveBeenCalledWith(); + }); + }); + }); +}); diff --git a/modules/item/front/item-type/index/locale/es.yml b/modules/item/front/item-type/index/locale/es.yml new file mode 100644 index 000000000..1a71ff212 --- /dev/null +++ b/modules/item/front/item-type/index/locale/es.yml @@ -0,0 +1,2 @@ +Item Type: Familia +New itemType: Nueva familia \ No newline at end of file diff --git a/modules/item/front/item-type/main/index.html b/modules/item/front/item-type/main/index.html new file mode 100644 index 000000000..faba696c0 --- /dev/null +++ b/modules/item/front/item-type/main/index.html @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/modules/item/front/item-type/main/index.js b/modules/item/front/item-type/main/index.js new file mode 100644 index 000000000..0dea00abb --- /dev/null +++ b/modules/item/front/item-type/main/index.js @@ -0,0 +1,24 @@ +import ngModule from '../../module'; +import ModuleMain from 'salix/components/module-main'; + +export default class ItemType extends ModuleMain { + exprBuilder(param, value) { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? {id: value} + : {or: [ + {name: {like: `%${value}%`}}, + {code: {like: `%${value}%`}} + ]}; + case 'name': + case 'code': + return {[param]: {like: `%${value}%`}}; + } + } +} + +ngModule.vnComponent('vnItemType', { + controller: ItemType, + template: require('./index.html') +}); diff --git a/modules/item/front/item-type/main/index.spec.js b/modules/item/front/item-type/main/index.spec.js new file mode 100644 index 000000000..dcb14ec0e --- /dev/null +++ b/modules/item/front/item-type/main/index.spec.js @@ -0,0 +1,31 @@ +import './index'; + +describe('Item', () => { + describe('Component vnItemType', () => { + let controller; + + beforeEach(ngModule('item')); + + beforeEach(inject($componentController => { + const $element = angular.element(''); + controller = $componentController('vnItemType', {$element}); + })); + + describe('exprBuilder()', () => { + it('should return a filter based on a search by id', () => { + const filter = controller.exprBuilder('search', '123'); + + expect(filter).toEqual({id: '123'}); + }); + + it('should return a filter based on a search by name or code', () => { + const filter = controller.exprBuilder('search', 'Alstroemeria'); + + expect(filter).toEqual({or: [ + {name: {like: '%Alstroemeria%'}}, + {code: {like: '%Alstroemeria%'}}, + ]}); + }); + }); + }); +}); diff --git a/modules/item/front/item-type/main/locale/es.yml b/modules/item/front/item-type/main/locale/es.yml new file mode 100644 index 000000000..7aceac46f --- /dev/null +++ b/modules/item/front/item-type/main/locale/es.yml @@ -0,0 +1 @@ +Search itemType by id, name or code: Buscar familia por id, nombre o código \ No newline at end of file diff --git a/modules/item/front/item-type/search-panel/index.html b/modules/item/front/item-type/search-panel/index.html new file mode 100644 index 000000000..4aa762900 --- /dev/null +++ b/modules/item/front/item-type/search-panel/index.html @@ -0,0 +1,22 @@ +
+
+ + + + + + + + + + + +
+
\ No newline at end of file diff --git a/modules/item/front/item-type/search-panel/index.js b/modules/item/front/item-type/search-panel/index.js new file mode 100644 index 000000000..17a439c39 --- /dev/null +++ b/modules/item/front/item-type/search-panel/index.js @@ -0,0 +1,7 @@ +import ngModule from '../../module'; +import SearchPanel from 'core/components/searchbar/search-panel'; + +ngModule.component('vnItemTypeSearchPanel', { + template: require('./index.html'), + controller: SearchPanel +}); diff --git a/modules/item/front/item-type/summary/index.html b/modules/item/front/item-type/summary/index.html new file mode 100644 index 000000000..d003c8f38 --- /dev/null +++ b/modules/item/front/item-type/summary/index.html @@ -0,0 +1,50 @@ + +
+ {{summary.id}} - {{summary.name}} - {{summary.worker.firstName}} {{summary.worker.lastName}} +
+ + +

Basic data

+ + + + + + + + + + + + + + + + + + + + +
+
+
\ No newline at end of file diff --git a/modules/item/front/item-type/summary/index.js b/modules/item/front/item-type/summary/index.js new file mode 100644 index 000000000..7645de8b1 --- /dev/null +++ b/modules/item/front/item-type/summary/index.js @@ -0,0 +1,33 @@ +import ngModule from '../../module'; +import Component from 'core/lib/component'; + +class Controller extends Component { + set itemType(value) { + this._itemType = value; + this.$.summary = null; + if (!value) return; + + const filter = { + include: [ + {relation: 'worker'}, + {relation: 'category'}, + {relation: 'itemPackingType'}, + {relation: 'temperature'} + ] + }; + this.$http.get(`ItemTypes/${value.id}`, {filter}) + .then(res => this.$.summary = res.data); + } + + get itemType() { + return this._itemType; + } +} + +ngModule.component('vnItemTypeSummary', { + template: require('./index.html'), + controller: Controller, + bindings: { + itemType: '<' + } +}); diff --git a/modules/item/front/item-type/summary/locale/es.yml b/modules/item/front/item-type/summary/locale/es.yml new file mode 100644 index 000000000..8f4cef70f --- /dev/null +++ b/modules/item/front/item-type/summary/locale/es.yml @@ -0,0 +1,4 @@ +Life: Vida +Promo: Promoción +Item packing type: Tipo de embalaje +Is unconventional size: Es de tamaño poco convencional \ No newline at end of file diff --git a/modules/item/front/routes.json b/modules/item/front/routes.json index 9e21e1697..5743d0ce7 100644 --- a/modules/item/front/routes.json +++ b/modules/item/front/routes.json @@ -9,7 +9,8 @@ {"state": "item.index", "icon": "icon-item"}, {"state": "item.request", "icon": "icon-buyrequest"}, {"state": "item.waste.index", "icon": "icon-claims"}, - {"state": "item.fixedPrice", "icon": "icon-fixedPrice"} + {"state": "item.fixedPrice", "icon": "icon-fixedPrice"}, + {"state": "item.itemType", "icon": "contact_support"} ], "card": [ {"state": "item.card.basicData", "icon": "settings"}, @@ -20,6 +21,9 @@ {"state": "item.card.diary", "icon": "icon-transaction"}, {"state": "item.card.last-entries", "icon": "icon-regentry"}, {"state": "item.card.log", "icon": "history"} + ], + "itemType": [ + {"state": "item.itemType.card.basicData", "icon": "settings"} ] }, "keybindings": [ @@ -169,6 +173,47 @@ "component": "vn-fixed-price", "description": "Fixed prices", "acl": ["buyer"] + }, + { + "url" : "/item-type?q", + "state": "item.itemType", + "component": "vn-item-type", + "description": "Item Type", + "acl": ["buyer"] + }, + { + "url": "/create", + "state": "item.itemType.create", + "component": "vn-item-type-create", + "description": "New itemType", + "acl": ["buyer"] + }, + { + "url": "/:id", + "state": "item.itemType.card", + "component": "vn-item-type-card", + "abstract": true, + "description": "Detail" + }, + { + "url": "/summary", + "state": "item.itemType.card.summary", + "component": "vn-item-type-summary", + "description": "Summary", + "params": { + "item-type": "$ctrl.itemType" + }, + "acl": ["buyer"] + }, + { + "url": "/basic-data", + "state": "item.itemType.card.basicData", + "component": "vn-item-type-basic-data", + "description": "Basic data", + "params": { + "item-type": "$ctrl.itemType" + }, + "acl": ["buyer"] } ] } \ No newline at end of file diff --git a/modules/mdb/back/methods/mdbVersion/upload.js b/modules/mdb/back/methods/mdbVersion/upload.js new file mode 100644 index 000000000..3d54c0250 --- /dev/null +++ b/modules/mdb/back/methods/mdbVersion/upload.js @@ -0,0 +1,121 @@ +const fs = require('fs-extra'); +const path = require('path'); +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.remoteMethodCtx('upload', { + description: 'Upload and attach a access file', + accepts: [ + { + arg: 'appName', + type: 'string', + required: true, + description: 'The app name' + }, + { + arg: 'newVersion', + type: 'number', + required: true, + description: `The new version number` + }, + { + arg: 'branch', + type: 'string', + required: true, + description: `The branch name` + } + ], + returns: { + type: ['object'], + root: true + }, + http: { + path: `/upload`, + verb: 'POST' + } + }); + + Self.upload = async(ctx, appName, newVersion, branch, options) => { + const models = Self.app.models; + const myOptions = {}; + + const TempContainer = models.TempContainer; + const AccessContainer = models.AccessContainer; + const fileOptions = {}; + + let tx; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + let srcFile; + try { + const tempContainer = await TempContainer.container('access'); + const uploaded = await TempContainer.upload(tempContainer.name, ctx.req, ctx.result, fileOptions); + const files = Object.values(uploaded.files).map(file => { + return file[0]; + }); + const uploadedFile = files[0]; + + const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name); + srcFile = path.join(file.client.root, file.container, file.name); + + const accessContainer = await AccessContainer.container('.archive'); + const destinationFile = path.join( + accessContainer.client.root, accessContainer.name, appName, `${newVersion}.7z`); + + if (process.env.NODE_ENV == 'test') + await fs.unlink(srcFile); + else { + await fs.move(srcFile, destinationFile, { + overwrite: true + }); + await fs.chmod(destinationFile, 0o644); + + const existBranch = await models.MdbBranch.findOne({ + where: {name: branch} + }); + + if (!existBranch) + throw new UserError('Not exist this branch'); + + const branchPath = path.join(accessContainer.client.root, 'branches', branch); + await fs.mkdir(branchPath, {recursive: true}); + + const destinationBranch = path.join(branchPath, `${appName}.7z`); + const destinationRoot = path.join(accessContainer.client.root, `${appName}.7z`); + try { + await fs.unlink(destinationBranch); + } catch (e) {} + await fs.symlink(destinationFile, destinationBranch); + + if (branch == 'master') { + try { + await fs.unlink(destinationRoot); + } catch (e) {} + await fs.symlink(destinationFile, destinationRoot); + } + } + + await models.MdbVersion.upsert({ + app: appName, + branchFk: branch, + version: newVersion + }); + + if (tx) await tx.commit(); + } catch (e) { + if (tx) await tx.rollback(); + + if (fs.existsSync(srcFile)) + await fs.unlink(srcFile); + + throw e; + } + }; +}; diff --git a/modules/mdb/back/model-config.json b/modules/mdb/back/model-config.json new file mode 100644 index 000000000..d5be8de87 --- /dev/null +++ b/modules/mdb/back/model-config.json @@ -0,0 +1,11 @@ +{ + "MdbBranch": { + "dataSource": "vn" + }, + "MdbVersion": { + "dataSource": "vn" + }, + "AccessContainer": { + "dataSource": "accessStorage" + } +} diff --git a/modules/mdb/back/models/mdb-container.json b/modules/mdb/back/models/mdb-container.json new file mode 100644 index 000000000..a927b30f1 --- /dev/null +++ b/modules/mdb/back/models/mdb-container.json @@ -0,0 +1,10 @@ +{ + "name": "AccessContainer", + "base": "Container", + "acls": [{ + "accessType": "*", + "principalType": "ROLE", + "principalId": "developer", + "permission": "ALLOW" + }] +} \ No newline at end of file diff --git a/modules/mdb/back/models/mdbBranch.json b/modules/mdb/back/models/mdbBranch.json new file mode 100644 index 000000000..486dfaf25 --- /dev/null +++ b/modules/mdb/back/models/mdbBranch.json @@ -0,0 +1,16 @@ +{ + "name": "MdbBranch", + "base": "VnModel", + "options": { + "mysql": { + "table": "mdbBranch" + } + }, + "properties": { + "name": { + "id": true, + "type": "string", + "description": "Identifier" + } + } +} \ No newline at end of file diff --git a/modules/mdb/back/models/mdbVersion.js b/modules/mdb/back/models/mdbVersion.js new file mode 100644 index 000000000..b36ee2a60 --- /dev/null +++ b/modules/mdb/back/models/mdbVersion.js @@ -0,0 +1,3 @@ +module.exports = Self => { + require('../methods/mdbVersion/upload')(Self); +}; diff --git a/modules/mdb/back/models/mdbVersion.json b/modules/mdb/back/models/mdbVersion.json new file mode 100644 index 000000000..02635ff8a --- /dev/null +++ b/modules/mdb/back/models/mdbVersion.json @@ -0,0 +1,26 @@ +{ + "name": "MdbVersion", + "base": "VnModel", + "options": { + "mysql": { + "table": "mdbVersion" + } + }, + "properties": { + "app": { + "type": "string", + "description": "The app name", + "id": true + }, + "version": { + "type": "number" + } + }, + "relations": { + "branch": { + "type": "belongsTo", + "model": "MdbBranch", + "foreignKey": "branchFk" + } + } +} \ No newline at end of file diff --git a/modules/monitor/front/index/tickets/index.js b/modules/monitor/front/index/tickets/index.js index 0770d8634..91d9079d8 100644 --- a/modules/monitor/front/index/tickets/index.js +++ b/modules/monitor/front/index/tickets/index.js @@ -53,7 +53,7 @@ export default class Controller extends Section { }, { field: 'shippedDate', - searchable: false + datepicker: true }, { field: 'theoreticalHour', diff --git a/modules/supplier/front/account/index.html b/modules/supplier/front/account/index.html index add62cd01..a0b58c737 100644 --- a/modules/supplier/front/account/index.html +++ b/modules/supplier/front/account/index.html @@ -25,8 +25,13 @@ label="Bank entity" ng-model="supplierAccount.bankEntityFk" url="BankEntities" - show-field="name" + fields="['name']" + initial-data="supplierAccount.bankEntityFk" + search-function="{or: [{bic: {like: $search +'%'}}, {name: {like: '%'+ $search +'%'}}]}" + value-field="id" + show-field="bic" rule> + {{bic}} {{name}} { Self.remoteMethodCtx('refund', { - description: 'Create ticket with the selected lines changing the sign to the quantites', + description: 'Create ticket refund with lines and services changing the sign to the quantites', accessType: 'WRITE', accepts: [{ arg: 'sales', description: 'The sales', type: ['object'], - required: true + required: false }, { - arg: 'ticketId', - type: 'number', - required: true, - description: 'The ticket id' + arg: 'services', + type: ['object'], + required: false, + description: 'The services' }], returns: { type: 'number', @@ -26,7 +26,7 @@ module.exports = Self => { } }); - Self.refund = async(ctx, sales, ticketId, options) => { + Self.refund = async(ctx, sales, services, options) => { const myOptions = {}; let tx; @@ -39,7 +39,6 @@ module.exports = Self => { } try { - const salesIds = []; const userId = ctx.req.accessToken.userId; const isClaimManager = await Self.app.models.Account.hasRole(userId, 'claimManager'); @@ -49,39 +48,47 @@ module.exports = Self => { if (!hasValidRole) throw new UserError(`You don't have privileges to create refund`); - for (let sale of sales) - salesIds.push(sale.id); + const salesIds = []; + if (sales) { + for (let sale of sales) + salesIds.push(sale.id); + } else + salesIds.push(null); + + const servicesIds = []; + if (services && services.length) { + for (let service of services) + servicesIds.push(service.id); + } else + servicesIds.push(null); const query = ` DROP TEMPORARY TABLE IF EXISTS tmp.sale; DROP TEMPORARY TABLE IF EXISTS tmp.ticketService; CREATE TEMPORARY TABLE tmp.sale - SELECT s.id, s.itemFk, - s.quantity, s.concept, s.price, s.discount + SELECT s.id, s.itemFk, s.quantity, s.concept, s.price, s.discount, s.ticketFk FROM sale s WHERE s.id IN (?); - CREATE TEMPORARY TABLE tmp.ticketService( - description VARCHAR(50), - quantity DECIMAL (10,2), - price DECIMAL (10,2), - taxClassFk INT, - ticketServiceTypeFk INT - ); - - CALL vn.ticket_doRefund(?, @newTicket); + CREATE TEMPORARY TABLE tmp.ticketService + SELECT ts.description, ts.quantity, ts.price, ts.taxClassFk, ts.ticketServiceTypeFk, ts.ticketFk + FROM ticketService ts + WHERE ts.id IN (?); + + CALL vn.ticket_doRefund(@newTicket); DROP TEMPORARY TABLE tmp.sale; DROP TEMPORARY TABLE tmp.ticketService;`; - await Self.rawSql(query, [salesIds, ticketId], myOptions); + await Self.rawSql(query, [salesIds, servicesIds], myOptions); const [newTicket] = await Self.rawSql('SELECT @newTicket id', null, myOptions); - ticketId = newTicket.id; + const newTicketId = newTicket.id; if (tx) await tx.commit(); - return ticketId; + return newTicketId; } catch (e) { if (tx) await tx.rollback(); throw e; diff --git a/modules/ticket/back/methods/sale/refundAll.js b/modules/ticket/back/methods/sale/refundAll.js deleted file mode 100644 index 6fcd27f0a..000000000 --- a/modules/ticket/back/methods/sale/refundAll.js +++ /dev/null @@ -1,78 +0,0 @@ -const UserError = require('vn-loopback/util/user-error'); - -module.exports = Self => { - Self.remoteMethodCtx('refundAll', { - description: 'Create ticket with all lines and services changing the sign to the quantites', - accessType: 'WRITE', - accepts: [{ - arg: 'ticketId', - type: 'number', - required: true, - description: 'The ticket id' - }], - returns: { - type: 'number', - root: true - }, - http: { - path: `/refundAll`, - verb: 'post' - } - }); - - Self.refundAll = async(ctx, ticketId, options) => { - const myOptions = {}; - let tx; - - if (typeof options == 'object') - Object.assign(myOptions, options); - - if (!myOptions.transaction) { - tx = await Self.beginTransaction({}); - myOptions.transaction = tx; - } - - try { - const userId = ctx.req.accessToken.userId; - - const isClaimManager = await Self.app.models.Account.hasRole(userId, 'claimManager'); - const isSalesAssistant = await Self.app.models.Account.hasRole(userId, 'salesAssistant'); - const hasValidRole = isClaimManager || isSalesAssistant; - - if (!hasValidRole) - throw new UserError(`You don't have privileges to create refund`); - - const query = ` - DROP TEMPORARY TABLE IF EXISTS tmp.sale; - DROP TEMPORARY TABLE IF EXISTS tmp.ticketService; - - CREATE TEMPORARY TABLE tmp.sale - SELECT s.id, s.itemFk, - s.quantity, s.concept, s.price, s.discount - FROM sale s - JOIN ticket t ON t.id = s.ticketFk - WHERE t.id IN (?); - - CREATE TEMPORARY TABLE tmp.ticketService - SELECT ts.description, - ts.quantity, ts.price, ts.taxClassFk, ts.ticketServiceTypeFk - FROM ticketService ts - WHERE ts.ticketFk IN (?); - - CALL vn.ticket_doRefund(?, @newTicket); - - DROP TEMPORARY TABLE tmp.sale; - DROP TEMPORARY TABLE tmp.ticketService;`; - - await Self.rawSql(query, [ticketId, ticketId, ticketId], myOptions); - - const [newTicket] = await Self.rawSql('SELECT @newTicket id', null, myOptions); - ticketId = newTicket.id; - - if (tx) await tx.commit(); - - return ticketId; - } catch (e) { - if (tx) await tx.rollback(); - throw e; - } - }; -}; diff --git a/modules/ticket/back/methods/sale/specs/refund.spec.js b/modules/ticket/back/methods/sale/specs/refund.spec.js index 40fd6c17e..5cb353055 100644 --- a/modules/ticket/back/methods/sale/specs/refund.spec.js +++ b/modules/ticket/back/methods/sale/specs/refund.spec.js @@ -1,20 +1,20 @@ const models = require('vn-loopback/server/server').models; describe('sale refund()', () => { + const sales = [ + {id: 7, ticketFk: 11}, + {id: 8, ticketFk: 11} + ]; + const services = [{id: 1}]; + it('should create ticket with the selected lines changing the sign to the quantites', async() => { const tx = await models.Sale.beginTransaction({}); const ctx = {req: {accessToken: {userId: 9}}}; - const ticketId = 11; - const sales = [ - {id: 7, ticketFk: 11}, - {id: 8, ticketFk: 11} - ]; - try { const options = {transaction: tx}; - const response = await models.Sale.refund(ctx, sales, ticketId, options); + const response = await models.Sale.refund(ctx, sales, services, options); const [newTicketId] = await models.Sale.rawSql('SELECT MAX(t.id) id FROM vn.ticket t;', null, options); expect(response).toEqual(newTicketId.id); @@ -30,17 +30,12 @@ describe('sale refund()', () => { const tx = await models.Sale.beginTransaction({}); const ctx = {req: {accessToken: {userId: 1}}}; - const ticketId = 11; - const sales = [ - {id: 7, ticketFk: 11} - ]; - let error; try { const options = {transaction: tx}; - await models.Sale.refund(ctx, sales, ticketId, options); + await models.Sale.refund(ctx, sales, services, options); await tx.rollback(); } catch (e) { diff --git a/modules/ticket/back/methods/ticket/sendSms.js b/modules/ticket/back/methods/ticket/sendSms.js index efe8ff206..a0adcae07 100644 --- a/modules/ticket/back/methods/ticket/sendSms.js +++ b/modules/ticket/back/methods/ticket/sendSms.js @@ -45,7 +45,7 @@ module.exports = Self => { const userId = ctx.req.accessToken.userId; try { - const sms = await Self.app.models.Sms.send(ctx, id, destination, message); + const sms = await Self.app.models.Sms.send(ctx, destination, message); const logRecord = { originFk: id, userFk: userId, diff --git a/modules/ticket/back/models/sale.js b/modules/ticket/back/models/sale.js index 2652aded2..2a4457263 100644 --- a/modules/ticket/back/models/sale.js +++ b/modules/ticket/back/models/sale.js @@ -7,7 +7,6 @@ module.exports = Self => { require('../methods/sale/updateConcept')(Self); require('../methods/sale/recalculatePrice')(Self); require('../methods/sale/refund')(Self); - require('../methods/sale/refundAll')(Self); require('../methods/sale/canEdit')(Self); Self.validatesPresenceOf('concept', { diff --git a/modules/ticket/front/descriptor-menu/index.html b/modules/ticket/front/descriptor-menu/index.html index c99575d42..1dcfd669f 100644 --- a/modules/ticket/front/descriptor-menu/index.html +++ b/modules/ticket/front/descriptor-menu/index.html @@ -302,7 +302,7 @@ \ No newline at end of file diff --git a/modules/ticket/front/descriptor-menu/index.js b/modules/ticket/front/descriptor-menu/index.js index 1c80a6f9d..c6388654e 100644 --- a/modules/ticket/front/descriptor-menu/index.js +++ b/modules/ticket/front/descriptor-menu/index.js @@ -273,9 +273,21 @@ class Controller extends Section { .then(() => this.vnApp.showSuccess(this.$t('Data saved!'))); } - refundAll() { - const params = {ticketId: this.id}; - const query = `Sales/refundAll`; + async refund() { + const filter = { + where: {ticketFk: this.id} + }; + const sales = await this.$http.get('Sales', {filter}); + this.sales = sales.data; + + const ticketServices = await this.$http.get('TicketServices', {filter}); + this.services = ticketServices.data; + + const params = { + sales: this.sales, + services: this.services + }; + const query = `Sales/refund`; return this.$http.post(query, params).then(res => { this.$state.go('ticket.card.sale', {id: res.data}); }); diff --git a/modules/ticket/front/descriptor-menu/index.spec.js b/modules/ticket/front/descriptor-menu/index.spec.js index 75f3522ae..af377d8ea 100644 --- a/modules/ticket/front/descriptor-menu/index.spec.js +++ b/modules/ticket/front/descriptor-menu/index.spec.js @@ -262,16 +262,27 @@ describe('Ticket Component vnTicketDescriptorMenu', () => { }); }); - describe('refundAll()', () => { + // #4084 review with Juan + xdescribe('refund()', () => { it('should make a query and go to ticket.card.sale', () => { - jest.spyOn(controller.$state, 'go').mockReturnValue(); - const expectedParams = {ticketId: ticket.id}; + controller.$state.go = jest.fn(); - $httpBackend.expect('POST', `Sales/refundAll`, expectedParams).respond({ticketId: 16}); - controller.refundAll(); + controller._id = ticket.id; + const sales = [{id: 1}]; + const services = [{id: 2}]; + + $httpBackend.expectGET(`Sales`).respond(sales); + $httpBackend.expectGET(`TicketServices`).respond(services); + + const expectedParams = { + sales: sales, + services: services + }; + $httpBackend.expectPOST(`Sales/refund`, expectedParams).respond(); + controller.refund(); $httpBackend.flush(); - expect(controller.$state.go).toHaveBeenCalledWith('ticket.card.sale', {id: {ticketId: ticket.id}}); + expect(controller.$state.go).toHaveBeenCalledWith('ticket.card.sale', {id: undefined}); }); }); diff --git a/modules/ticket/front/sale/index.js b/modules/ticket/front/sale/index.js index bb8a81bc4..987333e28 100644 --- a/modules/ticket/front/sale/index.js +++ b/modules/ticket/front/sale/index.js @@ -483,7 +483,7 @@ class Controller extends Section { const sales = this.selectedValidSales(); if (!sales) return; - const params = {sales: sales, ticketId: this.ticket.id}; + const params = {sales: sales}; const query = `Sales/refund`; this.resetChanges(); this.$http.post(query, params).then(res => { diff --git a/modules/ticket/front/services/index.html b/modules/ticket/front/services/index.html index 13fd84b00..bb5505ce6 100644 --- a/modules/ticket/front/services/index.html +++ b/modules/ticket/front/services/index.html @@ -42,7 +42,7 @@ =14" }, "dependencies": { - "adm-zip": "^0.5.9", "axios": "^0.25.0", "bmp-js": "^0.1.0", "compression": "^1.7.3", @@ -23,6 +22,7 @@ "i18n": "^0.8.4", "image-type": "^4.1.0", "imap": "^0.8.19", + "jszip": "^3.10.0", "ldapjs": "^2.2.0", "loopback": "^3.26.0", "loopback-boot": "3.3.1", diff --git a/storage/access/.keep b/storage/access/.keep new file mode 100644 index 000000000..8e2556896 --- /dev/null +++ b/storage/access/.keep @@ -0,0 +1 @@ +Forces tmp folder creation! \ No newline at end of file